Page MenuHomeSealhub

No OneTemporary

diff --git a/src/parser/bundle/ArcanistBundle.php b/src/parser/bundle/ArcanistBundle.php
index 8be13f4a..b4ca908c 100644
--- a/src/parser/bundle/ArcanistBundle.php
+++ b/src/parser/bundle/ArcanistBundle.php
@@ -1,342 +1,506 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Converts changesets between different formats.
*
* @group diff
*/
class ArcanistBundle {
private $changes;
+ private $conduit;
+ private $blobs = array();
+ private $diskPath;
+
+ public function setConduit(ConduitClient $conduit) {
+ $this->conduit = $conduit;
+ }
public static function newFromChanges(array $changes) {
$obj = new ArcanistBundle();
$obj->changes = $changes;
return $obj;
}
public static function newFromArcBundle($path) {
$path = Filesystem::resolvePath($path);
$future = new ExecFuture(
csprintf(
'tar xfO %s changes.json',
$path));
$changes = $future->resolveJSON();
foreach ($changes as $change_key => $change) {
foreach ($change['hunks'] as $key => $hunk) {
list($hunk_data) = execx('tar xfO %s hunks/%s', $path, $hunk['corpus']);
$changes[$change_key]['hunks'][$key]['corpus'] = $hunk_data;
}
}
+
foreach ($changes as $change_key => $change) {
$changes[$change_key] = ArcanistDiffChange::newFromDictionary($change);
}
$obj = new ArcanistBundle();
$obj->changes = $changes;
+ $obj->diskPath = $path;
return $obj;
}
public static function newFromDiff($data) {
$obj = new ArcanistBundle();
$parser = new ArcanistDiffParser();
$obj->changes = $parser->parseDiff($data);
return $obj;
}
private function __construct() {
}
public function writeToDisk($path) {
$changes = $this->getChanges();
$change_list = array();
foreach ($changes as $change) {
$change_list[] = $change->toDictionary();
}
$hunks = array();
foreach ($change_list as $change_key => $change) {
foreach ($change['hunks'] as $key => $hunk) {
$hunks[] = $hunk['corpus'];
$change_list[$change_key]['hunks'][$key]['corpus'] = count($hunks) - 1;
}
}
$blobs = array();
+ foreach ($change_list as $change) {
+ if (!empty($change['metadata']['old:binary-guid'])) {
+ $blobs[$change['metadata']['old:binary-guid']] = null;
+ }
+ if (!empty($change['metadata']['new:binary-guid'])) {
+ $blobs[$change['metadata']['new:binary-guid']] = null;
+ }
+ }
+ foreach ($blobs as $phid => $null) {
+ $blobs[$phid] = $this->getBlob($phid);
+ }
$dir = Filesystem::createTemporaryDirectory();
Filesystem::createDirectory($dir.'/hunks');
Filesystem::createDirectory($dir.'/blobs');
Filesystem::writeFile($dir.'/changes.json', json_encode($change_list));
foreach ($hunks as $key => $hunk) {
Filesystem::writeFile($dir.'/hunks/'.$key, $hunk);
}
foreach ($blobs as $key => $blob) {
Filesystem::writeFile($dir.'/blobs/'.$key, $blob);
}
execx(
'(cd %s; tar -czf %s *)',
$dir,
Filesystem::resolvePath($path));
Filesystem::remove($dir);
}
public function toUnifiedDiff() {
$result = array();
$changes = $this->getChanges();
foreach ($changes as $change) {
$old_path = $this->getOldPath($change);
$cur_path = $this->getCurrentPath($change);
$index_path = $cur_path;
if ($index_path === null) {
$index_path = $old_path;
}
$result[] = 'Index: '.$index_path;
$result[] = str_repeat('=', 67);
if ($old_path === null) {
$old_path = '/dev/null';
}
if ($cur_path === null) {
$cur_path = '/dev/null';
}
$result[] = '--- '.$old_path;
$result[] = '+++ '.$cur_path;
$result[] = $this->buildHunkChanges($change->getHunks());
}
return implode("\n", $result)."\n";
}
public function toGitPatch() {
$result = array();
$changes = $this->getChanges();
foreach ($changes as $change) {
$type = $change->getType();
$file_type = $change->getFileType();
if ($file_type == ArcanistDiffChangeType::FILE_DIRECTORY) {
// TODO: We should raise a FYI about this, so the user is aware
// that we omitted it, if the directory is empty or has permissions
// which git can't represent.
// Git doesn't support empty directories, so we simply ignore them. If
// the directory is nonempty, 'git apply' will create it when processing
// the changesets for files inside it.
continue;
}
if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) {
// Git will apply this in the corresponding MOVE_HERE.
continue;
}
$old_mode = idx($change->getOldProperties(), 'unix:filemode', '100644');
$new_mode = idx($change->getNewProperties(), 'unix:filemode', '100644');
- $change_body = $this->buildHunkChanges($change->getHunks());
+ $is_binary = ($file_type == ArcanistDiffChangeType::FILE_BINARY);
+
+ if ($is_binary) {
+ $change_body = $this->buildBinaryChange($change);
+ } else {
+ $change_body = $this->buildHunkChanges($change->getHunks());
+ }
if ($type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
// TODO: This is only relevant when patching old Differential diffs
// which were created prior to arc pruning TYPE_COPY_AWAY for files
// with no modifications.
if (!strlen($change_body) && ($old_mode == $new_mode)) {
continue;
}
}
$old_path = $this->getOldPath($change);
$cur_path = $this->getCurrentPath($change);
if ($old_path === null) {
$old_index = 'a/'.$cur_path;
$old_target = '/dev/null';
} else {
$old_index = 'a/'.$old_path;
$old_target = 'a/'.$old_path;
}
if ($cur_path === null) {
$cur_index = 'b/'.$old_path;
$cur_target = '/dev/null';
} else {
$cur_index = 'b/'.$cur_path;
$cur_target = 'b/'.$cur_path;
}
$result[] = "diff --git {$old_index} {$cur_index}";
if ($type == ArcanistDiffChangeType::TYPE_ADD) {
$result[] = "new file mode {$new_mode}";
}
if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE ||
$type == ArcanistDiffChangeType::TYPE_MOVE_HERE ||
$type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
if ($old_mode !== $new_mode) {
$result[] = "old mode {$old_mode}";
$result[] = "new mode {$new_mode}";
}
}
if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) {
$result[] = "copy from {$old_path}";
$result[] = "copy to {$cur_path}";
} else if ($type == ArcanistDiffChangeType::TYPE_MOVE_HERE) {
$result[] = "rename from {$old_path}";
$result[] = "rename to {$cur_path}";
} else if ($type == ArcanistDiffChangeType::TYPE_DELETE ||
$type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
$old_mode = idx($change->getOldProperties(), 'unix:filemode');
if ($old_mode) {
$result[] = "deleted file mode {$old_mode}";
}
}
- $result[] = "--- {$old_target}";
- $result[] = "+++ {$cur_target}";
+ if (!$is_binary) {
+ $result[] = "--- {$old_target}";
+ $result[] = "+++ {$cur_target}";
+ }
$result[] = $change_body;
}
return implode("\n", $result)."\n";
}
public function getChanges() {
return $this->changes;
}
private function breakHunkIntoSmallHunks(ArcanistDiffHunk $hunk) {
$context = 3;
$results = array();
$lines = explode("\n", $hunk->getCorpus());
$n = count($lines);
$old_offset = $hunk->getOldOffset();
$new_offset = $hunk->getNewOffset();
$ii = 0;
$jj = 0;
while ($ii < $n) {
for ($jj = $ii; $jj < $n && $lines[$jj][0] == ' '; ++$jj) {
// Skip lines until we find the first line with changes.
}
if ($jj >= $n) {
break;
}
$hunk_start = max($jj - $context, 0);
$old_lines = 0;
$new_lines = 0;
$last_change = $jj;
for (; $jj < $n; ++$jj) {
if ($lines[$jj][0] == ' ') {
if ($jj - $last_change > $context) {
break;
}
} else {
$last_change = $jj;
if ($lines[$jj][0] == '-') {
++$old_lines;
} else {
++$new_lines;
}
}
}
$hunk_length = min($jj, $n) - $hunk_start;
$hunk = new ArcanistDiffHunk();
$hunk->setOldOffset($old_offset + $hunk_start - $ii);
$hunk->setNewOffset($new_offset + $hunk_start - $ii);
$hunk->setOldLength($hunk_length - $new_lines);
$hunk->setNewLength($hunk_length - $old_lines);
$corpus = array_slice($lines, $hunk_start, $hunk_length);
$corpus = implode("\n", $corpus);
$hunk->setCorpus($corpus);
$results[] = $hunk;
$old_offset += ($jj - $ii) - $new_lines;
$new_offset += ($jj - $ii) - $old_lines;
$ii = $jj;
}
return $results;
}
private function getOldPath(ArcanistDiffChange $change) {
$old_path = $change->getOldPath();
$type = $change->getType();
if (!strlen($old_path) ||
$type == ArcanistDiffChangeType::TYPE_ADD) {
$old_path = null;
}
return $old_path;
}
private function getCurrentPath(ArcanistDiffChange $change) {
$cur_path = $change->getCurrentPath();
$type = $change->getType();
if (!strlen($cur_path) ||
$type == ArcanistDiffChangeType::TYPE_DELETE ||
$type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
$cur_path = null;
}
return $cur_path;
}
private function buildHunkChanges(array $hunks) {
$result = array();
foreach ($hunks as $hunk) {
$small_hunks = $this->breakHunkIntoSmallHunks($hunk);
foreach ($small_hunks as $small_hunk) {
$o_off = $small_hunk->getOldOffset();
$o_len = $small_hunk->getOldLength();
$n_off = $small_hunk->getNewOffset();
$n_len = $small_hunk->getNewLength();
$corpus = $small_hunk->getCorpus();
$result[] = "@@ -{$o_off},{$o_len} +{$n_off},{$n_len} @@";
$result[] = $corpus;
}
}
return implode("\n", $result);
}
+ private function getBlob($phid) {
+ if ($this->diskPath) {
+ list($blob_data) = execx('tar xfO %s blobs/%s', $this->diskPath, $phid);
+ return $blob_data;
+ }
+
+ if ($this->conduit) {
+ echo "Downloading binary data...\n";
+ $data_base64 = $this->conduit->callMethodSynchronous(
+ 'file.download',
+ array(
+ 'phid' => $phid,
+ ));
+ return base64_decode($data_base64);
+ }
+
+ throw new Exception("Nowhere to load blob '{$phid} from!");
+ }
+
+ private function buildBinaryChange(ArcanistDiffChange $change) {
+ $old_phid = $change->getMetadata('old:binary-guid', null);
+ $new_phid = $change->getMetadata('new:binary-guid', null);
+
+ $type = $change->getType();
+ if ($type == ArcanistDiffChangeType::TYPE_ADD) {
+ $old_null = true;
+ } else {
+ $old_null = false;
+ }
+
+ if ($type == ArcanistDiffChangeType::TYPE_DELETE) {
+ $new_null = true;
+ } else {
+ $new_null = false;
+ }
+
+ if ($old_null) {
+ $old_data = '';
+ $old_length = 0;
+ $old_sha1 = str_repeat('0', 40);
+ } else {
+ $old_data = $this->getBlob($old_phid);
+ $old_length = strlen($old_data);
+ $old_sha1 = sha1("blob {$old_length}\0{$old_data}");
+ }
+
+ if ($new_null) {
+ $new_data = '';
+ $new_length = 0;
+ $new_sha1 = str_repeat('0', 40);
+ } else {
+ $new_data = $this->getBlob($new_phid);
+ $new_length = strlen($new_data);
+ $new_sha1 = sha1("blob {$new_length}\0{$new_data}");
+ }
+
+ $content = array();
+ $content[] = "index {$old_sha1}..{$new_sha1}";
+ $content[] = "GIT binary patch";
+
+ $content[] = "literal {$new_length}";
+ $content[] = $this->emitBinaryDiffBody($new_data);
+
+ $content[] = "literal {$old_length}";
+ $content[] = $this->emitBinaryDiffBody($old_data);
+
+ return implode("\n", $content);
+ }
+
+ private function emitBinaryDiffBody($data) {
+ // See emit_binary_diff_body() in diff.c for git's implementation.
+
+ $buf = '';
+
+ $deflated = gzcompress($data);
+ $lines = str_split($deflated, 52);
+ foreach ($lines as $line) {
+ $len = strlen($line);
+ // The first character encodes the line length.
+ if ($len <= 26) {
+ $buf .= chr($len + ord('A') - 1);
+ } else {
+ $buf .= chr($len - 26 + ord('a') - 1);
+ }
+ $buf .= $this->encodeBase85($line);
+ $buf .= "\n";
+ }
+
+ $buf .= "\n";
+
+ return $buf;
+ }
+
+ private function encodeBase85($data) {
+ // This is implemented awkwardly in order to closely mirror git's
+ // implementation in base85.c
+
+ static $map = array(
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+ 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+ 'u', 'v', 'w', 'x', 'y', 'z',
+ '!', '#', '$', '%', '&', '(', ')', '*', '+', '-',
+ ';', '<', '=', '>', '?', '@', '^', '_', '`', '{',
+ '|', '}', '~',
+ );
+
+ $buf = '';
+
+ $pos = 0;
+ $bytes = strlen($data);
+ while ($bytes) {
+ $accum = '0';
+ for ($count = 24; $count >= 0; $count -= 8) {
+ $val = ord($data[$pos++]);
+ $val = bcmul($val, (string)(1 << $count));
+ $accum = bcadd($accum, $val);
+ if (--$bytes == 0) {
+ break;
+ }
+ }
+ $slice = '';
+ for ($count = 4; $count >= 0; $count--) {
+ $val = bcmod($accum, 85);
+ $accum = bcdiv($accum, 85);
+ $slice .= $map[$val];
+ }
+ $buf .= strrev($slice);
+ }
+
+ return $buf;
+ }
+
}
diff --git a/src/parser/diff/change/ArcanistDiffChange.php b/src/parser/diff/change/ArcanistDiffChange.php
index 2a2a3a7f..3b093501 100644
--- a/src/parser/diff/change/ArcanistDiffChange.php
+++ b/src/parser/diff/change/ArcanistDiffChange.php
@@ -1,242 +1,242 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Represents a change to an individual path.
*
* @group diff
*/
class ArcanistDiffChange {
protected $metadata = array();
protected $oldPath;
protected $currentPath;
protected $awayPaths = array();
protected $oldProperties = array();
protected $newProperties = array();
protected $commitHash;
protected $type = ArcanistDiffChangeType::TYPE_CHANGE;
protected $fileType = ArcanistDiffChangeType::FILE_TEXT;
protected $hunks = array();
public function toDictionary() {
$hunks = array();
foreach ($this->hunks as $hunk) {
$hunks[] = $hunk->toDictionary();
}
return array(
'metadata' => $this->metadata,
'oldPath' => $this->oldPath,
'currentPath' => $this->currentPath,
'awayPaths' => $this->awayPaths,
'oldProperties' => $this->oldProperties,
'newProperties' => $this->newProperties,
'type' => $this->type,
'fileType' => $this->fileType,
'commitHash' => $this->commitHash,
'hunks' => $hunks,
);
}
public static function newFromDictionary(array $dict) {
$hunks = array();
foreach ($dict['hunks'] as $hunk) {
$hunks[] = ArcanistDiffHunk::newFromDictionary($hunk);
}
$obj = new ArcanistDiffChange();
- $obj->metdadata = $dict['metadata'];
+ $obj->metadata = $dict['metadata'];
$obj->oldPath = $dict['oldPath'];
$obj->currentPath = $dict['currentPath'];
// TODO: The backend is shipping down some bogus data, e.g. diff 199453.
// Should probably clean this up.
$obj->awayPaths = nonempty($dict['awayPaths'], array());
$obj->oldProperties = nonempty($dict['oldProperties'], array());
$obj->newProperties = nonempty($dict['newProperties'], array());
$obj->type = $dict['type'];
$obj->fileType = $dict['fileType'];
$obj->commitHash = $dict['commitHash'];
$obj->hunks = $hunks;
return $obj;
}
public function getChangedLines($type) {
$lines = array();
foreach ($this->hunks as $hunk) {
$lines += $hunk->getChangedLines($type);
}
return $lines;
}
public function getAllMetadata() {
return $this->metadata;
}
public function setMetadata($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadata($key) {
return idx($this->metadata, $key);
}
public function setCommitHash($hash) {
$this->commitHash = $hash;
return $this;
}
public function getCommitHash() {
return $this->commitHash;
}
public function addAwayPath($path) {
$this->awayPaths[] = $path;
return $this;
}
public function getAwayPaths() {
return $this->awayPaths;
}
public function setFileType($type) {
$this->fileType = $type;
return $this;
}
public function getFileType() {
return $this->fileType;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setOldProperty($key, $value) {
$this->oldProperties[$key] = $value;
return $this;
}
public function setNewProperty($key, $value) {
$this->newProperties[$key] = $value;
return $this;
}
public function getOldProperties() {
return $this->oldProperties;
}
public function getNewProperties() {
return $this->newProperties;
}
public function setCurrentPath($path) {
$this->currentPath = $this->filterPath($path);
return $this;
}
public function getCurrentPath() {
return $this->currentPath;
}
public function setOldPath($path) {
$this->oldPath = $this->filterPath($path);
return $this;
}
public function getOldPath() {
return $this->oldPath;
}
public function addHunk(ArcanistDiffHunk $hunk) {
$this->hunks[] = $hunk;
return $this;
}
public function getHunks() {
return $this->hunks;
}
public function convertToBinaryChange() {
$this->hunks = array();
$this->setFileType(ArcanistDiffChangeType::FILE_BINARY);
return $this;
}
protected function filterPath($path) {
if ($path == '/dev/null') {
return null;
}
return $path;
}
public function renderTextSummary() {
$type = $this->getType();
$file = $this->getFileType();
$char = ArcanistDiffChangeType::getSummaryCharacterForChangeType($type);
$attr = ArcanistDiffChangeType::getShortNameForFileType($file);
if ($attr) {
$attr = '('.$attr.')';
}
$summary = array();
$summary[] = sprintf(
"%s %5.5s %s",
$char,
$attr,
$this->getCurrentPath());
if (ArcanistDiffChangeType::isOldLocationChangeType($type)) {
foreach ($this->getAwayPaths() as $path) {
$summary[] = ' to: '.$path;
}
}
if (ArcanistDiffChangeType::isNewLocationChangeType($type)) {
$summary[] = ' from: '.$this->getOldPath();
}
return implode("\n", $summary);
}
public function getSymlinkTarget() {
if ($this->getFileType() != ArcanistDiffChangeType::FILE_SYMLINK) {
throw new Exception("Not a symlink!");
}
$hunks = $this->getHunks();
$hunk = reset($hunks);
$corpus = $hunk->getCorpus();
$match = null;
if (!preg_match('/^\+(?:link )?(.*)$/m', $corpus, $match)) {
throw new Exception("Failed to extract link target!");
}
return trim($match[1]);
}
}
diff --git a/src/workflow/base/ArcanistBaseWorkflow.php b/src/workflow/base/ArcanistBaseWorkflow.php
index aa96dc0e..ba9d64fc 100644
--- a/src/workflow/base/ArcanistBaseWorkflow.php
+++ b/src/workflow/base/ArcanistBaseWorkflow.php
@@ -1,629 +1,630 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Implements a runnable command, like "arc diff" or "arc help".
*
* @group workflow
*/
class ArcanistBaseWorkflow {
private $conduit;
private $userGUID;
private $userName;
private $repositoryAPI;
private $workingCopy;
private $arguments;
private $command;
private $arcanistConfiguration;
private $parentWorkflow;
private $changeCache = array();
public function __construct() {
}
public function setArcanistConfiguration($arcanist_configuration) {
$this->arcanistConfiguration = $arcanist_configuration;
return $this;
}
public function getArcanistConfiguration() {
return $this->arcanistConfiguration;
}
public function getCommandHelp() {
return get_class($this).": Undocumented";
}
public function requiresWorkingCopy() {
return false;
}
public function requiresConduit() {
return false;
}
public function requiresAuthentication() {
return false;
}
public function requiresRepositoryAPI() {
return false;
}
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
public function setUserName($user_name) {
$this->userName = $user_name;
return $this;
}
public function getUserName() {
return $this->userName;
}
public function getArguments() {
return array();
}
private function setParentWorkflow($parent_workflow) {
$this->parentWorkflow = $parent_workflow;
return $this;
}
protected function getParentWorkflow() {
return $this->parentWorkflow;
}
public function buildChildWorkflow($command, array $argv) {
$arc_config = $this->getArcanistConfiguration();
$workflow = $arc_config->buildWorkflow($command);
$workflow->setParentWorkflow($this);
$workflow->setCommand($command);
if ($this->repositoryAPI) {
$workflow->setRepositoryAPI($this->repositoryAPI);
}
if ($this->userGUID) {
$workflow->setUserGUID($this->getUserGUID());
$workflow->setUserName($this->getUserName());
}
if ($this->conduit) {
$workflow->setConduit($this->conduit);
}
if ($this->workingCopy) {
$workflow->setWorkingCopy($this->workingCopy);
}
$workflow->setArcanistConfiguration($arc_config);
$workflow->parseArguments(array_values($argv));
return $workflow;
}
public function getArgument($key, $default = null) {
$args = $this->arguments;
if (!array_key_exists($key, $args)) {
return $default;
}
return $args[$key];
}
final public function getCompleteArgumentSpecification() {
$spec = $this->getArguments();
$arc_config = $this->getArcanistConfiguration();
$command = $this->getCommand();
$spec += $arc_config->getCustomArgumentsForCommand($command);
return $spec;
}
public function parseArguments(array $args) {
$spec = $this->getCompleteArgumentSpecification();
$dict = array();
$more_key = null;
if (!empty($spec['*'])) {
$more_key = $spec['*'];
unset($spec['*']);
$dict[$more_key] = array();
}
$short_to_long_map = array();
foreach ($spec as $long => $options) {
if (!empty($options['short'])) {
$short_to_long_map[$options['short']] = $long;
}
}
$more = array();
for ($ii = 0; $ii < count($args); $ii++) {
$arg = $args[$ii];
$arg_name = null;
$arg_key = null;
if ($arg == '--') {
$more = array_merge(
$more,
array_slice($args, $ii + 1));
break;
} else if (!strncmp($arg, '--', 2)) {
$arg_key = substr($arg, 2);
if (!array_key_exists($arg_key, $spec)) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
} else if (!strncmp($arg, '-', 1)) {
$arg_key = substr($arg, 1);
if (empty($short_to_long_map[$arg_key])) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
$arg_key = $short_to_long_map[$arg_key];
} else {
$more[] = $arg;
continue;
}
$options = $spec[$arg_key];
if (empty($options['param'])) {
$dict[$arg_key] = true;
} else {
if ($ii == count($args) - 1) {
throw new ArcanistUsageException(
"Option '{$arg}' requires a parameter.");
}
$dict[$arg_key] = $args[$ii + 1];
$ii++;
}
}
if ($more) {
if ($more_key) {
$dict[$more_key] = $more;
} else {
$example = reset($more);
throw new ArcanistUsageException(
"Unrecognized argument '{$example}'. Try 'arc help'.");
}
}
foreach ($dict as $key => $value) {
if (empty($spec[$key]['conflicts'])) {
continue;
}
foreach ($spec[$key]['conflicts'] as $conflict => $more) {
if (isset($dict[$conflict])) {
if ($more) {
$more = ': '.$more;
} else {
$more = '.';
}
// TODO: We'll always display these as long-form, when the user might
// have typed them as short form.
throw new ArcanistUsageException(
"Arguments '--{$key}' and '--{$conflict}' are mutually exclusive".
$more);
}
}
}
$this->arguments = $dict;
$this->didParseArguments();
return $this;
}
protected function didParseArguments() {
// Override this to customize workflow argument behavior.
}
public function getWorkingCopy() {
if (!$this->workingCopy) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a working copy, override ".
"requiresWorkingCopy() to return true.");
}
return $this->workingCopy;
}
public function setWorkingCopy(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
public function getConduit() {
if (!$this->conduit) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a Conduit, override ".
"requiresConduit() to return true.");
}
return $this->conduit;
}
public function setConduit(ConduitClient $conduit) {
$this->conduit = $conduit;
return $this;
}
public function getUserGUID() {
if (!$this->userGUID) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires authentication, override ".
"requiresAuthentication() to return true.");
}
return $this->userGUID;
}
public function setUserGUID($guid) {
$this->userGUID = $guid;
return $this;
}
public function setRepositoryAPI($api) {
$this->repositoryAPI = $api;
return $this;
}
public function getRepositoryAPI() {
if (!$this->repositoryAPI) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a Repository API, override ".
"requiresRepositoryAPI() to return true.");
}
return $this->repositoryAPI;
}
protected function shouldRequireCleanUntrackedFiles() {
return empty($this->arguments['allow-untracked']);
}
protected function requireCleanWorkingCopy() {
$api = $this->getRepositoryAPI();
$working_copy_desc = phutil_console_format(
" Working copy: __%s__\n\n",
$api->getPath());
$untracked = $api->getUntrackedChanges();
if ($this->shouldRequireCleanUntrackedFiles()) {
if (!empty($untracked)) {
echo "You have untracked files in this working copy.\n\n".
$working_copy_desc.
" Untracked files in working copy:\n".
" ".implode("\n ", $untracked)."\n\n";
if ($api instanceof ArcanistGitAPI) {
echo phutil_console_wrap(
"Since you don't have '.gitignore' rules for these files and have ".
"not listed them in '.git/info/exclude', you may have forgotten ".
"to 'git add' them to your commit.");
} else if ($api instanceof ArcanistSubversionAPI) {
echo phutil_console_wrap(
"Since you don't have 'svn:ignore' rules for these files, you may ".
"have forgotten to 'svn add' them.");
}
$prompt = "Do you want to continue without adding these files?";
if (!phutil_console_confirm($prompt, $default_no = false)) {
throw new ArcanistUserAbortException();
}
}
}
$incomplete = $api->getIncompleteChanges();
if ($incomplete) {
throw new ArcanistUsageException(
"You have incompletely checked out directories in this working copy. ".
"Fix them before proceeding.\n\n".
$working_copy_desc.
" Incomplete directories in working copy:\n".
" ".implode("\n ", $incomplete)."\n\n".
"You can fix these paths by running 'svn update' on them.");
}
$conflicts = $api->getMergeConflicts();
if ($conflicts) {
throw new ArcanistUsageException(
"You have merge conflicts in this working copy. Resolve merge ".
"conflicts before proceeding.\n\n".
$working_copy_desc.
" Conflicts in working copy:\n".
" ".implode("\n ", $conflicts)."\n");
}
$unstaged = $api->getUnstagedChanges();
if ($unstaged) {
throw new ArcanistUsageException(
"You have unstaged changes in this working copy. Stage and commit (or ".
"revert) them before proceeding.\n\n".
$working_copy_desc.
" Unstaged changes in working copy:\n".
" ".implode("\n ", $unstaged)."\n");
}
$uncommitted = $api->getUncommittedChanges();
if ($uncommitted) {
throw new ArcanistUsageException(
"You have uncommitted changes in this branch. Commit (or revert) them ".
"before proceeding.\n\n".
$working_copy_desc.
" Uncommitted changes in working copy\n".
" ".implode("\n ", $uncommitted)."\n");
}
}
protected function chooseRevision(
array $revision_data,
$revision_id,
$prompt = null) {
$revisions = array();
foreach ($revision_data as $data) {
$ref = ArcanistDifferentialRevisionRef::newFromDictionary($data);
$revisions[$ref->getID()] = $ref;
}
if ($revision_id) {
$revision_id = $this->normalizeRevisionID($revision_id);
if (empty($revisions[$revision_id])) {
throw new ArcanistChooseInvalidRevisionException();
}
return $revisions[$revision_id];
}
if (!count($revisions)) {
throw new ArcanistChooseNoRevisionsException();
}
$repository_api = $this->getRepositoryAPI();
$candidates = array();
$cur_path = $repository_api->getPath();
foreach ($revisions as $revision) {
$source_path = $revision->getSourcePath();
if ($source_path == $cur_path) {
$candidates[] = $revision;
}
}
if (count($candidates) == 1) {
$candidate = reset($candidates);
$revision_id = $candidate->getID();
}
if ($revision_id) {
return $revisions[$revision_id];
}
$revision_indexes = array_keys($revisions);
echo "\n";
$ii = 1;
foreach ($revisions as $revision) {
echo ' ['.$ii++.'] D'.$revision->getID().' '.$revision->getName()."\n";
}
while (true) {
$id = phutil_console_prompt($prompt);
$id = trim(strtoupper($id), 'D');
if (isset($revisions[$id])) {
return $revisions[$id];
}
if (isset($revision_indexes[$id - 1])) {
return $revisions[$revision_indexes[$id - 1]];
}
}
}
protected function loadDiffBundleFromConduit(
ConduitClient $conduit,
$diff_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'diff_id' => $diff_id,
));
}
protected function loadRevisionBundleFromConduit(
ConduitClient $conduit,
$revision_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'revision_id' => $revision_id,
));
}
private function loadBundleFromConduit(
ConduitClient $conduit,
$params) {
$future = $conduit->callMethod('differential.getdiff', $params);
$diff = $future->resolve();
$changes = array();
foreach ($diff['changes'] as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$bundle = ArcanistBundle::newFromChanges($changes);
+ $bundle->setConduit($conduit);
return $bundle;
}
protected function getChangedLines($path, $mode) {
if (is_dir($path)) {
return array();
}
$change = $this->getChange($path);
$lines = $change->getChangedLines($mode);
return array_keys($lines);
}
private function getChange($path) {
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) {
if (empty($this->changeCache[$path])) {
$diff = $repository_api->getRawDiffText($path);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
if (count($changes) != 1) {
throw new Exception("Expected exactly one change.");
}
$this->changeCache[$path] = reset($changes);
}
} else {
if (empty($this->changeCache)) {
$diff = $repository_api->getFullGitDiff();
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
foreach ($changes as $change) {
$this->changeCache[$change->getCurrentPath()] = $change;
}
}
}
if (empty($this->changeCache[$path])) {
if ($repository_api instanceof ArcanistGitAPI) {
// This can legitimately occur under git if you make a change, "git
// commit" it, and then revert the change in the working copy and run
// "arc lint".
$change = new ArcanistDiffChange();
$change->setCurrentPath($path);
return $change;
} else {
throw new Exception(
"Trying to get change for unchanged path '{$path}'!");
}
}
return $this->changeCache[$path];
}
final public function willRunWorkflow() {
$spec = $this->getCompleteArgumentSpecification();
foreach ($this->arguments as $arg => $value) {
if (empty($spec[$arg])) {
continue;
}
$options = $spec[$arg];
if (!empty($options['supports'])) {
$system_name = $this->getRepositoryAPI()->getSourceControlSystemName();
if (!in_array($system_name, $options['supports'])) {
$extended_info = null;
if (!empty($options['nosupport'][$system_name])) {
$extended_info = ' '.$options['nosupport'][$system_name];
}
throw new ArcanistUsageException(
"Option '--{$arg}' is not supported under {$system_name}.".
$extended_info);
}
}
}
}
protected function parseGitRelativeCommit(ArcanistGitAPI $api, array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException(
"Specify exactly one commit.");
}
$base = reset($argv);
if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$merge_base = $base;
} else {
list($err, $merge_base) = exec_manual(
'(cd %s; git merge-base %s HEAD)',
$api->getPath(),
$base);
if ($err) {
throw new ArcanistUsageException(
"Unable to parse git commit name '{$base}'.");
}
}
$api->setRelativeCommit(trim($merge_base));
}
protected function normalizeRevisionID($revision_id) {
return ltrim(strtoupper($revision_id), 'D');
}
protected function shouldShellComplete() {
return true;
}
protected function getShellCompletions(array $argv) {
return array();
}
protected function getSupportedRevisionControlSystems() {
return array('any');
}
protected function getPassthruArgumentsAsMap($command) {
$map = array();
foreach ($this->getCompleteArgumentSpecification() as $key => $spec) {
if (!empty($spec['passthru'][$command])) {
if (isset($this->arguments[$key])) {
$map[$key] = $this->arguments[$key];
}
}
}
return $map;
}
protected function getPassthruArgumentsAsArgv($command) {
$spec = $this->getCompleteArgumentSpecification();
$map = $this->getPassthruArgumentsAsMap($command);
$argv = array();
foreach ($map as $key => $value) {
$argv[] = '--'.$key;
if (!empty($spec[$key]['param'])) {
$argv[] = $value;
}
}
return $argv;
}
}
diff --git a/src/workflow/export/ArcanistExportWorkflow.php b/src/workflow/export/ArcanistExportWorkflow.php
index eef575af..5b56d046 100644
--- a/src/workflow/export/ArcanistExportWorkflow.php
+++ b/src/workflow/export/ArcanistExportWorkflow.php
@@ -1,222 +1,222 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Exports changes from Differential or the working copy to a file.
*
* @group workflow
*/
final class ArcanistExportWorkflow extends ArcanistBaseWorkflow {
const SOURCE_LOCAL = 'local';
const SOURCE_DIFF = 'diff';
const SOURCE_REVISION = 'revision';
const FORMAT_GIT = 'git';
const FORMAT_UNIFIED = 'unified';
const FORMAT_BUNDLE = 'arcbundle';
private $source;
private $sourceID;
private $format;
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**export** [__paths__] __format__ (svn)
**export** [__commit_range__] __format__ (git)
**export** __--revision__ __revision_id__ __format__
**export** __--diff__ __diff_id__ __format__
Supports: git, svn
Export the local changeset (or a Differential changeset) to a file,
in some __format__: git diff (__--git__), unified diff
(__--unified__), or arc bundle (__--arcbundle__ __path__) format.
EOTEXT
);
}
public function getArguments() {
return array(
'git' => array(
'help' =>
"Export change as a git patch. This format is more complete than ".
"unified, but less complete than arc bundles. These patches can be ".
"applied with 'git apply' or 'arc patch'.",
),
'unified' => array(
'help' =>
"Export change as a unified patch. This format is less complete ".
"than git patches or arc bundles. These patches can be applied with ".
"'patch' or 'arc patch'.",
),
'arcbundle' => array(
'param' => 'file',
'help' =>
"Export change as an arc bundle. This format can represent all ".
"changes. These bundles can be applied with 'arc patch'.",
),
'revision' => array(
'param' => 'revision_id',
'help' =>
"Instead of exporting changes from the working copy, export them ".
"from a Differential revision."
),
'diff' => array(
'param' => 'diff_id',
'help' =>
"Instead of exporting changes from the working copy, export them ".
"from a Differential diff."
),
'*' => 'paths',
);
}
protected function didParseArguments() {
$source = self::SOURCE_LOCAL;
$requested = 0;
if ($this->getArgument('revision')) {
$source = self::SOURCE_REVISION;
$requested++;
}
if ($this->getArgument('diff')) {
$source = self::SOURCE_DIFF;
$requested++;
}
if ($requested > 1) {
throw new ArcanistUsageException(
"Options '--revision' and '--diff' are not compatible. Choose exactly ".
"one change source.");
}
$this->source = $source;
$this->sourceID = $this->getArgument($source);
$format = null;
$requested = 0;
if ($this->getArgument('git')) {
$format = self::FORMAT_GIT;
$requested++;
}
if ($this->getArgument('unified')) {
$format = self::FORMAT_UNIFIED;
$requested++;
}
if ($this->getArgument('arcbundle')) {
$format = self::FORMAT_BUNDLE;
$requested++;
}
if ($requested === 0) {
throw new ArcanistUsageException(
"Specify one of '--git', '--unified' or '--arcbundle <path>' to ".
"choose an export format.");
} else if ($requested > 1) {
throw new ArcanistUsageException(
"Options '--git', '--unified' and '--arcbundle' are not compatible. ".
"Choose exactly one export format.");
}
$this->format = $format;
}
public function requiresConduit() {
return $this->getSource() != self::SOURCE_LOCAL;
}
public function requiresAuthentication() {
return $this->requiresConduit();
}
public function requiresRepositoryAPI() {
return $this->getSource() == self::SOURCE_LOCAL;
}
public function requiresWorkingCopy() {
return $this->getSource() == self::SOURCE_LOCAL;
}
private function getSource() {
return $this->source;
}
private function getSourceID() {
return $this->sourceID;
}
private function getFormat() {
return $this->format;
}
public function run() {
$source = $this->getSource();
switch ($source) {
case self::SOURCE_LOCAL:
$repository_api = $this->getRepositoryAPI();
$parser = new ArcanistDiffParser();
if ($repository_api instanceof ArcanistGitAPI) {
$this->parseGitRelativeCommit(
$repository_api,
$this->getArgument('paths'));
$diff = $repository_api->getFullGitDiff();
$changes = $parser->parseDiff($diff);
} else {
// TODO: paths support
$paths = $repository_api->getWorkingCopyStatus();
$changes = $parser->parseSubversionDiff(
$repository_api,
$paths);
}
$bundle = ArcanistBundle::newFromChanges($changes);
break;
case self::SOURCE_REVISION:
$bundle = $this->loadRevisionBundleFromConduit(
$this->getConduit(),
$this->getSourceID());
break;
case self::SOURCE_DIFF:
$bundle = $this->loadDiffBundleFromConduit(
$this->getConduit(),
$this->getSourceID());
break;
}
$format = $this->getFormat();
switch ($format) {
case self::FORMAT_GIT:
echo $bundle->toGitPatch();
break;
case self::FORMAT_UNIFIED:
echo $bundle->toUnifiedDiff();
break;
case self::FORMAT_BUNDLE:
$path = $this->getArgument('arcbundle');
- echo "Writing bundle to '{$path}'... ";
+ echo "Writing bundle to '{$path}'...\n";
$bundle->writeToDisk($path);
echo "done.\n";
break;
}
return 0;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 08:23 (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
556879
Default Alt Text
(45 KB)

Event Timeline