Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F995582
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
45 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 08:23 (20 h, 35 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
556879
Default Alt Text
(45 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment