Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F995690
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
105 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/parser/ArcanistBundle.php b/src/parser/ArcanistBundle.php
index cdb11edf..994f15db 100644
--- a/src/parser/ArcanistBundle.php
+++ b/src/parser/ArcanistBundle.php
@@ -1,797 +1,798 @@
<?php
/**
* Converts changesets between different formats.
*
* @group diff
*/
final class ArcanistBundle {
private $changes;
private $conduit;
private $blobs = array();
private $diskPath;
private $projectID;
private $baseRevision;
private $revisionID;
private $encoding;
private $loadFileDataCallback;
public function setConduit(ConduitClient $conduit) {
$this->conduit = $conduit;
return $this;
}
public function setProjectID($project_id) {
$this->projectID = $project_id;
return $this;
}
public function getProjectID() {
return $this->projectID;
}
public function setBaseRevision($base_revision) {
$this->baseRevision = $base_revision;
return $this;
}
public function setEncoding($encoding) {
$this->encoding = $encoding;
return $this;
}
public function getEncoding() {
return $this->encoding;
}
public function getBaseRevision() {
return $this->baseRevision;
}
public function setRevisionID($revision_id) {
$this->revisionID = $revision_id;
return $this;
}
public function getRevisionID() {
return $this->revisionID;
}
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 tfO %s',
$path));
list($stdout, $file_list) = $future->resolvex();
$file_list = explode("\n", trim($file_list));
if (in_array('meta.json', $file_list)) {
$future = new ExecFuture(
csprintf(
'tar xfO %s meta.json',
$path));
$meta_info = $future->resolveJSON();
$version = idx($meta_info, 'version', 0);
$project_name = idx($meta_info, 'projectName');
$base_revision = idx($meta_info, 'baseRevision');
$revision_id = idx($meta_info, 'revisionID');
$encoding = idx($meta_info, 'encoding');
// this arc bundle was probably made before we started storing meta info
} else {
$version = 0;
$project_name = null;
$base_revision = null;
$revision_id = null;
$encoding = null;
}
$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;
$obj->setProjectID($project_name);
$obj->setBaseRevision($base_revision);
$obj->setRevisionID($revision_id);
$obj->setEncoding($encoding);
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-phid'])) {
$blobs[$change['metadata']['old:binary-phid']] = null;
}
if (!empty($change['metadata']['new:binary-phid'])) {
$blobs[$change['metadata']['new:binary-phid']] = null;
}
}
foreach ($blobs as $phid => $null) {
$blobs[$phid] = $this->getBlob($phid);
}
$meta_info = array(
'version' => 3,
'projectName' => $this->getProjectID(),
'baseRevision' => $this->getBaseRevision(),
'revisionID' => $this->getRevisionID(),
'encoding' => $this->getEncoding(),
);
$dir = Filesystem::createTemporaryDirectory();
Filesystem::createDirectory($dir.'/hunks');
Filesystem::createDirectory($dir.'/blobs');
Filesystem::writeFile($dir.'/changes.json', json_encode($change_list));
Filesystem::writeFile($dir.'/meta.json', json_encode($meta_info));
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) {
$hunk_changes = $this->buildHunkChanges($change->getHunks());
if (!$hunk_changes) {
continue;
}
$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[] = PHP_EOL;
$result[] = str_repeat('=', 67);
$result[] = PHP_EOL;
if ($old_path === null) {
$old_path = '/dev/null';
}
if ($cur_path === null) {
$cur_path = '/dev/null';
}
// When the diff is used by `patch`, `patch` ignores what is listed as the
// current path and just makes changes to the file at the old path (unless
// the current path is '/dev/null'.
// If the old path and the current path aren't the same (and neither is
// /dev/null), this indicates the file was moved or copied. By listing
// both paths as the new file, `patch` will apply the diff to the new
// file.
if ($cur_path !== '/dev/null' && $old_path !== '/dev/null') {
$old_path = $cur_path;
}
$result[] = '--- '.$old_path.PHP_EOL;
$result[] = '+++ '.$cur_path.PHP_EOL;
$result[] = $hunk_changes;
}
if (!$result) {
return '';
}
$diff = implode('', $result);
return $this->convertNonUTF8Diff($diff);
}
public function toGitPatch() {
$result = array();
$changes = $this->getChanges();
$binary_sources = array();
foreach ($changes as $change) {
if (!$this->isGitBinaryChange($change)) {
continue;
}
$type = $change->getType();
if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY ||
$type == ArcanistDiffChangeType::TYPE_COPY_AWAY ||
$type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
foreach ($change->getAwayPaths() as $path) {
$binary_sources[$path] = $change;
}
}
}
foreach (array_keys($changes) as $multicopy_key) {
$multicopy_change = $changes[$multicopy_key];
$type = $multicopy_change->getType();
if ($type != ArcanistDiffChangeType::TYPE_MULTICOPY) {
continue;
}
// Decompose MULTICOPY into one MOVE_HERE and several COPY_HERE because
// we need more information than we have in order to build a delete patch
// and represent it as a bunch of COPY_HERE plus a delete. For details,
// see T419.
// Basically, MULTICOPY means there are 2 or more corresponding COPY_HERE
// changes, so find one of them arbitrarily and turn it into a MOVE_HERE.
// TODO: We might be able to do this more cleanly after T230 is resolved.
$decompose_okay = false;
foreach ($changes as $change_key => $change) {
if ($change->getType() != ArcanistDiffChangeType::TYPE_COPY_HERE) {
continue;
}
if ($change->getOldPath() != $multicopy_change->getCurrentPath()) {
continue;
}
$decompose_okay = true;
$change = clone $change;
$change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE);
$changes[$change_key] = $change;
// The multicopy is now fully represented by MOVE_HERE plus one or more
// COPY_HERE, so throw it away.
unset($changes[$multicopy_key]);
break;
}
if (!$decompose_okay) {
throw new Exception(
"Failed to decompose multicopy changeset in order to generate diff.");
}
}
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');
$is_binary = $this->isGitBinaryChange($change);
if ($is_binary) {
$old_binary = idx($binary_sources, $this->getCurrentPath($change));
$change_body = $this->buildBinaryChange($change, $old_binary);
} 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}".PHP_EOL;
if ($type == ArcanistDiffChangeType::TYPE_ADD) {
$result[] = "new file mode {$new_mode}".PHP_EOL;
}
if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE ||
$type == ArcanistDiffChangeType::TYPE_MOVE_HERE ||
- $type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
+ $type == ArcanistDiffChangeType::TYPE_COPY_AWAY ||
+ $type == ArcanistDiffChangeType::TYPE_CHANGE) {
if ($old_mode !== $new_mode) {
$result[] = "old mode {$old_mode}".PHP_EOL;
$result[] = "new mode {$new_mode}".PHP_EOL;
}
}
if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) {
$result[] = "copy from {$old_path}".PHP_EOL;
$result[] = "copy to {$cur_path}".PHP_EOL;
} else if ($type == ArcanistDiffChangeType::TYPE_MOVE_HERE) {
$result[] = "rename from {$old_path}".PHP_EOL;
$result[] = "rename to {$cur_path}".PHP_EOL;
} 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}".PHP_EOL;
}
}
if ($change_body) {
if (!$is_binary) {
$result[] = "--- {$old_target}".PHP_EOL;
$result[] = "+++ {$cur_target}".PHP_EOL;
}
$result[] = $change_body;
}
}
$diff = implode('', $result).PHP_EOL;
return $this->convertNonUTF8Diff($diff);
}
private function isGitBinaryChange(ArcanistDiffChange $change) {
$file_type = $change->getFileType();
return ($file_type == ArcanistDiffChangeType::FILE_BINARY ||
$file_type == ArcanistDiffChangeType::FILE_IMAGE);
}
private function convertNonUTF8Diff($diff) {
if ($this->encoding) {
$diff = phutil_utf8_convert($diff, $this->encoding, 'UTF-8');
}
return $diff;
}
public function getChanges() {
return $this->changes;
}
private function breakHunkIntoSmallHunks(ArcanistDiffHunk $base_hunk) {
$context = 3;
$results = array();
$lines = phutil_split_lines($base_hunk->getCorpus());
$n = count($lines);
$old_offset = $base_hunk->getOldOffset();
$new_offset = $base_hunk->getNewOffset();
$ii = 0;
$jj = 0;
while ($ii < $n) {
// Skip lines until we find the next line with changes. Note: this skips
// both ' ' (no changes) and '\' (no newline at end of file) lines. If we
// don't skip the latter, we may incorrectly generate a terminal hunk
// that has no actual change information when a file doesn't have a
// terminal newline and not changed near the end of the file. 'patch' will
// fail to apply the diff if we generate a hunk that does not actually
// contain changes.
for ($jj = $ii; $jj < $n; ++$jj) {
$char = $lines[$jj][0];
if ($char == '-' || $char == '+') {
break;
}
}
if ($jj >= $n) {
break;
}
$hunk_start = max($jj - $context, 0);
// NOTE: There are two tricky considerations here.
// We can not generate a patch with overlapping hunks, or 'git apply'
// rejects it after 1.7.3.4.
// We can not generate a patch with too much trailing context, or
// 'patch' rejects it.
// So we need to ensure that we generate disjoint hunks, but don't
// generate any hunks with too much context.
$old_lines = 0;
$new_lines = 0;
$hunk_adjust = 0;
$last_change = $jj;
$break_here = null;
for (; $jj < $n; ++$jj) {
if ($lines[$jj][0] == ' ') {
if ($jj - $last_change > $context) {
if ($break_here === null) {
// We haven't seen a change in $context lines, so this is a
// potential place to break the hunk. However, we need to keep
// looking in case there is another change fewer than $context
// lines away, in which case we have to merge the hunks.
$break_here = $jj;
}
}
if ($jj - $last_change > (($context + 1) * 2)) {
// We definitely aren't going to merge this with the next hunk, so
// break out of the loop. We'll end the hunk at $break_here.
break;
}
} else {
$break_here = null;
$last_change = $jj;
if ($lines[$jj][0] == '\\') {
// When we have a "\ No newline at end of file" line, it does not
// contribute to either hunk length.
++$hunk_adjust;
} else if ($lines[$jj][0] == '-') {
++$old_lines;
} else if ($lines[$jj][0] == '+') {
++$new_lines;
}
}
}
if ($break_here !== null) {
$jj = $break_here;
}
$hunk_length = min($jj, $n) - $hunk_start;
$count_length = ($hunk_length - $hunk_adjust);
$hunk = new ArcanistDiffHunk();
$hunk->setOldOffset($old_offset + $hunk_start - $ii);
$hunk->setNewOffset($new_offset + $hunk_start - $ii);
$hunk->setOldLength($count_length - $new_lines);
$hunk->setNewLength($count_length - $old_lines);
$corpus = array_slice($lines, $hunk_start, $hunk_length);
$corpus = implode('', $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) {
assert_instances_of($hunks, 'ArcanistDiffHunk');
$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();
// NOTE: If the length is 1 it can be omitted. Since git does this,
// we also do it so that "arc export --git" diffs are as similar to
// real git diffs as possible, which helps debug issues.
if ($o_len == 1) {
$o_head = "{$o_off}";
} else {
$o_head = "{$o_off},{$o_len}";
}
if ($n_len == 1) {
$n_head = "{$n_off}";
} else {
$n_head = "{$n_off},{$n_len}";
}
$result[] = "@@ -{$o_head} +{$n_head} @@".PHP_EOL;
$result[] = $corpus;
$last = substr($corpus, -1);
if ($last !== false && $last != "\r" && $last != "\n") {
$result[] = PHP_EOL;
}
}
}
return implode('', $result);
}
public function setLoadFileDataCallback($callback) {
$this->loadFileDataCallback = $callback;
return $this;
}
private function getBlob($phid, $name = null) {
if ($this->loadFileDataCallback) {
return call_user_func($this->loadFileDataCallback, $phid);
}
if ($this->diskPath) {
list($blob_data) = execx('tar xfO %s blobs/%s', $this->diskPath, $phid);
return $blob_data;
}
$console = PhutilConsole::getConsole();
if ($this->conduit) {
if ($name) {
$console->writeErr("Downloading binary data for '%s'...\n", $name);
} else {
$console->writeErr("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_binary) {
// In Git, when we write out a binary file move or copy, we need the
// original binary for the source and the current binary for the
// destination.
if ($old_binary) {
if ($old_binary->getOriginalFileData() !== null) {
$old_data = $old_binary->getOriginalFileData();
$old_phid = null;
} else {
$old_data = null;
$old_binary->getMetadata('old:binary-phid');
}
} else {
$old_data = $change->getOriginalFileData();
$old_phid = $change->getMetadata('old:binary-phid');
}
if ($old_data === null && $old_phid) {
$name = basename($change->getOldPath());
$old_data = $this->getBlob($old_phid, $name);
}
$old_length = strlen($old_data);
if ($old_data === null) {
$old_data = '';
$old_sha1 = str_repeat('0', 40);
} else {
$old_sha1 = sha1("blob {$old_length}\0{$old_data}");
}
$new_phid = $change->getMetadata('new:binary-phid');
$new_data = null;
if ($change->getCurrentFileData() !== null) {
$new_data = $change->getCurrentFileData();
} else if ($new_phid) {
$name = basename($change->getCurrentPath());
$new_data = $this->getBlob($new_phid, $name);
}
$new_length = strlen($new_data);
if ($new_data === null) {
$new_data = '';
$new_sha1 = str_repeat('0', 40);
} else {
$new_sha1 = sha1("blob {$new_length}\0{$new_data}");
}
$content = array();
$content[] = "index {$old_sha1}..{$new_sha1}".PHP_EOL;
$content[] = "GIT binary patch".PHP_EOL;
$content[] = "literal {$new_length}".PHP_EOL;
$content[] = $this->emitBinaryDiffBody($new_data).PHP_EOL;
$content[] = "literal {$old_length}".PHP_EOL;
$content[] = $this->emitBinaryDiffBody($old_data).PHP_EOL;
return implode('', $content);
}
private function emitBinaryDiffBody($data) {
if (!function_exists('gzcompress')) {
throw new Exception(
"This patch has binary data. The PHP zlib extension is required to ".
"apply patches with binary data to git. Install the PHP zlib ".
"extension to continue.");
}
// 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 .= self::encodeBase85($line);
$buf .= PHP_EOL;
}
return $buf;
}
public static function encodeBase85($data) {
// This is implemented awkwardly in order to closely mirror git's
// implementation in base85.c
// It is also implemeted awkwardly to work correctly on 32-bit machines.
// Broadly, this algorithm converts the binary input to printable output
// by transforming each 4 binary bytes of input to 5 printable bytes of
// output, one piece at a time.
//
// To do this, we convert the 4 bytes into a 32-bit integer, then use
// modulus and division by 85 to pick out printable bytes (85^5 is slightly
// larger than 2^32). In C, this algorithm is fairly easy to implement
// because the accumulator can be made unsigned.
//
// In PHP, there are no unsigned integers, so values larger than 2^31 break
// on 32-bit systems under modulus:
//
// $ php -r 'print (1 << 31) % 13;' # On a 32-bit machine.
// -11
//
// However, PHP's float type is an IEEE 754 64-bit double precision float,
// so we can safely store integers up to around 2^53 without loss of
// precision. To work around the lack of an unsigned type, we just use a
// double and perform the modulus with fmod().
//
// (Since PHP overflows integer operations into floats, we don't need much
// additional casting.)
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 = $val * (1 << $count);
$accum = $accum + $val;
if (--$bytes == 0) {
break;
}
}
$slice = '';
for ($count = 4; $count >= 0; $count--) {
$val = (int)fmod($accum, 85.0);
$accum = floor($accum / 85.0);
$slice .= $map[$val];
}
$buf .= strrev($slice);
}
return $buf;
}
}
diff --git a/src/parser/ArcanistDiffParser.php b/src/parser/ArcanistDiffParser.php
index 4e266973..38d74bd0 100644
--- a/src/parser/ArcanistDiffParser.php
+++ b/src/parser/ArcanistDiffParser.php
@@ -1,1275 +1,1281 @@
<?php
/**
* Parses diffs from a working copy.
*
* @group diff
*/
final class ArcanistDiffParser {
protected $repositoryAPI;
protected $text;
protected $line;
protected $lineSaved;
protected $isGit;
protected $isMercurial;
protected $detectBinaryFiles = false;
protected $tryEncoding;
protected $rawDiff;
protected $writeDiffOnFailure;
protected $changes = array();
private $forcePath;
public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) {
$this->repositoryAPI = $repository_api;
return $this;
}
public function setDetectBinaryFiles($detect) {
$this->detectBinaryFiles = $detect;
return $this;
}
public function setTryEncoding($encoding) {
$this->tryEncoding = $encoding;
return $this;
}
public function forcePath($path) {
$this->forcePath = $path;
return $this;
}
public function setChanges(array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$this->changes = mpull($changes, null, 'getCurrentPath');
return $this;
}
public function parseSubversionDiff(ArcanistSubversionAPI $api, $paths) {
$this->setRepositoryAPI($api);
$diffs = array();
foreach ($paths as $path => $status) {
if ($status & ArcanistRepositoryAPI::FLAG_UNTRACKED ||
$status & ArcanistRepositoryAPI::FLAG_CONFLICT ||
$status & ArcanistRepositoryAPI::FLAG_MISSING) {
unset($paths[$path]);
}
}
$root = null;
$from = array();
foreach ($paths as $path => $status) {
$change = $this->buildChange($path);
if ($status & ArcanistRepositoryAPI::FLAG_ADDED) {
$change->setType(ArcanistDiffChangeType::TYPE_ADD);
} else if ($status & ArcanistRepositoryAPI::FLAG_DELETED) {
$change->setType(ArcanistDiffChangeType::TYPE_DELETE);
} else {
$change->setType(ArcanistDiffChangeType::TYPE_CHANGE);
}
$is_dir = is_dir($api->getPath($path));
if ($is_dir) {
$change->setFileType(ArcanistDiffChangeType::FILE_DIRECTORY);
// We have to go hit the diff even for directories because they may
// have property changes or moves, etc.
}
$is_link = is_link($api->getPath($path));
if ($is_link) {
$change->setFileType(ArcanistDiffChangeType::FILE_SYMLINK);
}
$diff = $api->getRawDiffText($path);
if ($diff) {
$this->parseDiff($diff);
}
$info = $api->getSVNInfo($path);
if (idx($info, 'Copied From URL')) {
if (!$root) {
$rinfo = $api->getSVNInfo('.');
$root = $rinfo['URL'].'/';
}
$cpath = $info['Copied From URL'];
$root_len = strlen($root);
if (!strncmp($cpath, $root, $root_len)) {
$cpath = substr($cpath, $root_len);
// The user can "svn cp /path/to/file@12345 x", which pulls a file out
// of version history at a specific revision. If we just use the path,
// we'll collide with possible changes to that path in the working
// copy below. In particular, "svn cp"-ing a path which no longer
// exists somewhere in the working copy and then adding that path
// gets us to the "origin change type" branches below with a
// TYPE_ADD state on the path. To avoid this, append the origin
// revision to the path so we'll necessarily generate a new change.
// TODO: In theory, you could have an '@' in your path and this could
// cause a collision, e.g. two files named 'f' and 'f@12345'. This is
// at least somewhat the user's fault, though.
if ($info['Copied From Rev']) {
if ($info['Copied From Rev'] != $info['Revision']) {
$cpath .= '@'.$info['Copied From Rev'];
}
}
$change->setOldPath($cpath);
$from[$path] = $cpath;
}
}
$type = $change->getType();
if (($type === ArcanistDiffChangeType::TYPE_MOVE_AWAY ||
$type === ArcanistDiffChangeType::TYPE_DELETE) &&
idx($info, 'Node Kind') === 'directory') {
$change->setFileType(ArcanistDiffChangeType::FILE_DIRECTORY);
}
}
foreach ($paths as $path => $status) {
$change = $this->buildChange($path);
if (empty($from[$path])) {
continue;
}
if (empty($this->changes[$from[$path]])) {
if ($change->getType() == ArcanistDiffChangeType::TYPE_COPY_HERE) {
// If the origin path wasn't changed (or isn't included in this diff)
// and we only copied it, don't generate a changeset for it. This
// keeps us out of trouble when we go to 'arc commit' and need to
// figure out which files should be included in the commit list.
continue;
}
}
$origin = $this->buildChange($from[$path]);
$origin->addAwayPath($change->getCurrentPath());
$type = $origin->getType();
switch ($type) {
case ArcanistDiffChangeType::TYPE_MULTICOPY:
case ArcanistDiffChangeType::TYPE_COPY_AWAY:
// "Add" is possible if you do some bizarre tricks with svn:ignore and
// "svn copy"'ing URLs straight from the repository; you can end up with
// a file that is a copy of itself. See T271.
case ArcanistDiffChangeType::TYPE_ADD:
break;
case ArcanistDiffChangeType::TYPE_DELETE:
$origin->setType(ArcanistDiffChangeType::TYPE_MOVE_AWAY);
break;
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
$origin->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
break;
case ArcanistDiffChangeType::TYPE_CHANGE:
$origin->setType(ArcanistDiffChangeType::TYPE_COPY_AWAY);
break;
default:
throw new Exception("Bad origin state {$type}.");
}
$type = $origin->getType();
switch ($type) {
case ArcanistDiffChangeType::TYPE_MULTICOPY:
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
$change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE);
break;
case ArcanistDiffChangeType::TYPE_ADD:
case ArcanistDiffChangeType::TYPE_COPY_AWAY:
$change->setType(ArcanistDiffChangeType::TYPE_COPY_HERE);
break;
default:
throw new Exception("Bad origin state {$type}.");
}
}
return $this->changes;
}
public function parseDiff($diff) {
$this->didStartParse($diff);
if ($this->getLine() === null) {
$this->didFailParse("Can't parse an empty diff!");
}
do {
$patterns = array(
// This is a normal SVN text change, probably from "svn diff".
'(?P<type>Index): (?P<cur>.+)',
// This is an SVN property change, probably from "svn diff".
'(?P<type>Property changes on): (?P<cur>.+)',
// This is a git commit message, probably from "git show".
'(?P<type>commit) (?P<hash>[a-f0-9]+)(?: \(.*\))?',
// This is a git diff, probably from "git show" or "git diff".
// Note that the filenames may appear quoted.
'(?P<type>diff --git) (?P<oldnew>.*)',
// This is a unified diff, probably from "diff -u" or synthetic diffing.
'(?P<type>---) (?P<old>.+)\s+\d{4}-\d{2}-\d{2}.*',
'(?P<binary>Binary) files '.
'(?P<old>.+)\s+\d{4}-\d{2}-\d{2} and '.
'(?P<new>.+)\s+\d{4}-\d{2}-\d{2} differ.*',
// This is a normal Mercurial text change, probably from "hg diff". It
// may have two "-r" blocks if it came from "hg diff -r x:y".
'(?P<type>diff -r) (?P<hgrev>[a-f0-9]+) (?:-r [a-f0-9]+ )?(?P<cur>.+)',
);
$line = $this->getLineTrimmed();
$match = null;
$ok = $this->tryMatchHeader($patterns, $line, $match);
$failed_parse = false;
if (!$ok && $this->isFirstNonEmptyLine()) {
// 'hg export' command creates so called "extended diff" that
// contains some meta information and comment at the beginning
// (isFirstNonEmptyLine() to check for beginning). Actual mercurial
// code detects where comment ends and unified diff starts by
// searching "diff -r" in the text.
$this->saveLine();
$line = $this->nextLineThatLooksLikeDiffStart();
if (!$this->tryMatchHeader($patterns, $line, $match)) {
// Restore line before guessing to display correct error.
$this->restoreLine();
$failed_parse = true;
}
} else if (!$ok) {
$failed_parse = true;
}
if ($failed_parse) {
$this->didFailParse(
"Expected a hunk header, like 'Index: /path/to/file.ext' (svn), ".
"'Property changes on: /path/to/file.ext' (svn properties), ".
"'commit 59bcc3ad6775562f845953cf01624225' (git show), ".
"'diff --git' (git diff), '--- filename' (unified diff), or " .
"'diff -r' (hg diff or patch).");
}
if (isset($match['type'])) {
if ($match['type'] == 'diff --git') {
list($old, $new) = self::splitGitDiffPaths($match['oldnew']);
$match['old'] = $old;
$match['cur'] = $new;
}
}
$change = $this->buildChange(idx($match, 'cur'));
if (isset($match['old'])) {
$change->setOldPath($match['old']);
}
if (isset($match['hash'])) {
$change->setCommitHash($match['hash']);
}
if (isset($match['binary'])) {
$change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
$line = $this->nextNonemptyLine();
continue;
}
$line = $this->nextLine();
switch ($match['type']) {
case 'Index':
$this->parseIndexHunk($change);
break;
case 'Property changes on':
$this->parsePropertyHunk($change);
break;
case 'diff --git':
$this->setIsGit(true);
$this->parseIndexHunk($change);
break;
case 'commit':
$this->setIsGit(true);
$this->parseCommitMessage($change);
break;
case '---':
$ok = preg_match(
'@^(?:\+\+\+) (.*)\s+\d{4}-\d{2}-\d{2}.*$@',
$line,
$match);
if (!$ok) {
$this->didFailParse("Expected '+++ filename' in unified diff.");
}
$change->setCurrentPath($match[1]);
$line = $this->nextLine();
$this->parseChangeset($change);
break;
case 'diff -r':
$this->setIsMercurial(true);
$this->parseIndexHunk($change);
break;
default:
$this->didFailParse("Unknown diff type.");
break;
}
} while ($this->getLine() !== null);
$this->didFinishParse();
$this->loadSyntheticData();
return $this->changes;
}
protected function tryMatchHeader($patterns, $line, &$match) {
foreach ($patterns as $pattern) {
if (preg_match('@^'.$pattern.'$@', $line, $match)) {
return true;
}
}
return false;
}
protected function parseCommitMessage(ArcanistDiffChange $change) {
$change->setType(ArcanistDiffChangeType::TYPE_MESSAGE);
$message = array();
$line = $this->getLine();
if (preg_match('/^Merge: /', $line)) {
$this->nextLine();
}
$line = $this->getLine();
if (!preg_match('/^Author: /', $line)) {
$this->didFailParse("Expected 'Author:'.");
}
$line = $this->nextLine();
if (!preg_match('/^Date: /', $line)) {
$this->didFailParse("Expected 'Date:'.");
}
while (($line = $this->nextLineTrimmed()) !== null) {
if (strlen($line) && $line[0] != ' ') {
break;
}
// Strip leading spaces from Git commit messages. Note that empty lines
// are represented as just "\n"; don't touch those.
$message[] = preg_replace('/^ /', '', $this->getLine());
}
$message = rtrim(implode('', $message), "\r\n");
$change->setMetadata('message', $message);
}
/**
* Parse an SVN property change hunk. These hunks are ambiguous so just sort
* of try to get it mostly right. It's entirely possible to foil this parser
* (or any other parser) with a carefully constructed property change.
*/
protected function parsePropertyHunk(ArcanistDiffChange $change) {
$line = $this->getLine();
if (!preg_match('/^_+$/', $line)) {
$this->didFailParse("Expected '______________________'.");
}
$line = $this->nextLine();
while ($line !== null) {
$done = preg_match('/^(Index|Property changes on):/', $line);
if ($done) {
break;
}
$matches = null;
$ok = preg_match('/^(Modified|Added|Deleted): (.*)$/', $line, $matches);
if (!$ok) {
$this->didFailParse("Expected 'Added', 'Deleted', or 'Modified'.");
}
$op = $matches[1];
$prop = $matches[2];
list($old, $new) = $this->parseSVNPropertyChange($op, $prop);
if ($old !== null) {
$change->setOldProperty($prop, $old);
}
if ($new !== null) {
$change->setNewProperty($prop, $new);
}
$line = $this->getLine();
}
}
private function parseSVNPropertyChange($op, $prop) {
$old = array();
$new = array();
$target = null;
$line = $this->nextLine();
$prop_index = 2;
while ($line !== null) {
$done = preg_match(
'/^(Modified|Added|Deleted|Index|Property changes on):/',
$line);
if ($done) {
break;
}
$trimline = ltrim($line);
if ($trimline && $trimline[0] == '#') {
// in svn1.7, a line like ## -0,0 +1 ## is put between the Added: line
// and the line with the property change. If we have such a line, we'll
// just ignore it (:
$line = $this->nextLine();
$prop_index = 1;
$trimline = ltrim($line);
}
if ($trimline && $trimline[0] == '+') {
if ($op == 'Deleted') {
$this->didFailParse('Unexpected "+" section in property deletion.');
}
$target = 'new';
$line = substr($trimline, $prop_index);
} else if ($trimline && $trimline[0] == '-') {
if ($op == 'Added') {
$this->didFailParse('Unexpected "-" section in property addition.');
}
$target = 'old';
$line = substr($trimline, $prop_index);
} else if (!strncmp($trimline, 'Merged', 6)) {
if ($op == 'Added') {
$target = 'new';
} else {
// These can appear on merges. No idea how to interpret this (unclear
// what the old / new values are) and it's of dubious usefulness so
// just throw it away until someone complains.
$target = null;
}
$line = $trimline;
}
if ($target == 'new') {
$new[] = $line;
} else if ($target == 'old') {
$old[] = $line;
}
$line = $this->nextLine();
}
$old = rtrim(implode('', $old));
$new = rtrim(implode('', $new));
if (!strlen($old)) {
$old = null;
}
if (!strlen($new)) {
$new = null;
}
return array($old, $new);
}
protected function setIsGit($git) {
if ($this->isGit !== null && $this->isGit != $git) {
throw new Exception("Git status has changed!");
}
$this->isGit = $git;
return $this;
}
protected function getIsGit() {
return $this->isGit;
}
public function setIsMercurial($is_mercurial) {
$this->isMercurial = $is_mercurial;
return $this;
}
public function getIsMercurial() {
return $this->isMercurial;
}
protected function parseIndexHunk(ArcanistDiffChange $change) {
$is_git = $this->getIsGit();
$is_mercurial = $this->getIsMercurial();
$is_svn = (!$is_git && !$is_mercurial);
$move_source = null;
$line = $this->getLine();
if ($is_git) {
do {
$patterns = array(
'(?P<new>new) file mode (?P<newmode>\d+)',
'(?P<deleted>deleted) file mode (?P<oldmode>\d+)',
// These occur when someone uses `chmod` on a file.
'old mode (?P<oldmode>\d+)',
'new mode (?P<newmode>\d+)',
// These occur when you `mv` a file and git figures it out.
'similarity index ',
'rename from (?P<old>.*)',
'(?P<move>rename) to (?P<cur>.*)',
'copy from (?P<old>.*)',
'(?P<copy>copy) to (?P<cur>.*)'
);
$ok = false;
$match = null;
foreach ($patterns as $pattern) {
$ok = preg_match('@^'.$pattern.'@', $line, $match);
if ($ok) {
break;
}
}
if (!$ok) {
if ($line === null ||
preg_match('/^(diff --git|commit) /', $line)) {
// In this case, there are ONLY file mode changes, or this is a
// pure move. If it's a move, flag these changesets so we can build
// synthetic changes later, enabling us to show file contents in
// Differential -- git only gives us a block like this:
//
// diff --git a/README b/READYOU
// similarity index 100%
// rename from README
// rename to READYOU
//
// ...i.e., there is no associated diff.
- $change->setNeedsSyntheticGitHunks(true);
- if ($move_source) {
- $move_source->setNeedsSyntheticGitHunks(true);
+ // This allows us to distinguish between property changes only
+ // and actual moves. For property changes only, we can't currently
+ // build a synthetic diff correctly, so just skip it.
+ // TODO: Build synthetic diffs for property changes, too.
+ if ($change->getType() != ArcanistDiffChangeType::TYPE_CHANGE) {
+ $change->setNeedsSyntheticGitHunks(true);
+ if ($move_source) {
+ $move_source->setNeedsSyntheticGitHunks(true);
+ }
}
return;
}
break;
}
if (!empty($match['oldmode'])) {
$change->setOldProperty('unix:filemode', $match['oldmode']);
}
if (!empty($match['newmode'])) {
$change->setNewProperty('unix:filemode', $match['newmode']);
}
if (!empty($match['deleted'])) {
$change->setType(ArcanistDiffChangeType::TYPE_DELETE);
}
if (!empty($match['new'])) {
// If you replace a symlink with a normal file, git renders the change
// as a "delete" of the symlink plus an "add" of the new file. We
// prefer to represent this as a change.
if ($change->getType() == ArcanistDiffChangeType::TYPE_DELETE) {
$change->setType(ArcanistDiffChangeType::TYPE_CHANGE);
} else {
$change->setType(ArcanistDiffChangeType::TYPE_ADD);
}
}
if (!empty($match['old'])) {
$match['old'] = self::unescapeFilename($match['old']);
$change->setOldPath($match['old']);
}
if (!empty($match['cur'])) {
$match['cur'] = self::unescapeFilename($match['cur']);
$change->setCurrentPath($match['cur']);
}
if (!empty($match['copy'])) {
$change->setType(ArcanistDiffChangeType::TYPE_COPY_HERE);
$old = $this->buildChange($change->getOldPath());
$type = $old->getType();
if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) {
$old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
} else {
$old->setType(ArcanistDiffChangeType::TYPE_COPY_AWAY);
}
$old->addAwayPath($change->getCurrentPath());
}
if (!empty($match['move'])) {
$change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE);
$old = $this->buildChange($change->getOldPath());
$type = $old->getType();
if ($type == ArcanistDiffChangeType::TYPE_MULTICOPY) {
// Great, no change.
} else if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) {
$old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
} else if ($type == ArcanistDiffChangeType::TYPE_COPY_AWAY) {
$old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY);
} else {
$old->setType(ArcanistDiffChangeType::TYPE_MOVE_AWAY);
}
// We'll reference this above.
$move_source = $old;
$old->addAwayPath($change->getCurrentPath());
}
$line = $this->nextNonemptyLine();
} while (true);
}
$line = $this->getLine();
if ($is_svn) {
$ok = preg_match('/^=+\s*$/', $line);
if (!$ok) {
$this->didFailParse("Expected '=======================' divider line.");
} else {
// Adding an empty file in SVN can produce an empty line here.
$line = $this->nextNonemptyLine();
}
} else if ($is_git) {
$ok = preg_match('/^index .*$/', $line);
if (!$ok) {
// TODO: "hg diff -g" diffs ("mercurial git-style diffs") do not include
// this line, so we can't parse them if we fail on it. Maybe introduce
// a flag saying "parse this diff using relaxed git-style diff rules"?
// $this->didFailParse("Expected 'index af23f...a98bc' header line.");
} else {
// NOTE: In the git case, where this patch is the last change in the
// file, we may have a final terminal newline. Skip over it so that
// we'll hit the '$line === null' block below. This is covered by the
// 'git-empty-file.gitdiff' test case.
$line = $this->nextNonemptyLine();
}
}
// If there are files with only whitespace changes and -b or -w are
// supplied as command-line flags to `diff', svn and git both produce
// changes without any body.
if ($line === null ||
preg_match(
'/^(Index:|Property changes on:|diff --git|commit) /',
$line)) {
return;
}
$is_binary_add = preg_match(
'/^Cannot display: file marked as a binary type\.$/',
rtrim($line));
if ($is_binary_add) {
$this->nextLine(); // Cannot display: file marked as a binary type.
$this->nextNonemptyLine(); // svn:mime-type = application/octet-stream
$this->markBinary($change);
return;
}
// We can get this in git, or in SVN when a file exists in the repository
// WITHOUT a binary mime-type and is changed and given a binary mime-type.
$is_binary_diff = preg_match(
'/^Binary files .* and .* differ$/',
rtrim($line));
if ($is_binary_diff) {
$this->nextNonemptyLine(); // Binary files x and y differ
$this->markBinary($change);
return;
}
// This occurs under "hg diff --git" when a binary file is removed. See
// test case "hg-binary-delete.hgdiff". (I believe it never occurs under
// git, which reports the "files X and /dev/null differ" string above. Git
// can not apply these patches.)
$is_hg_binary_delete = preg_match(
'/^Binary file .* has changed$/',
rtrim($line));
if ($is_hg_binary_delete) {
$this->nextNonemptyLine();
$this->markBinary($change);
return;
}
// With "git diff --binary" (not a normal mode, but one users may explicitly
// invoke and then, e.g., copy-paste into the web console) or "hg diff
// --git" (normal under hg workflows), we may encounter a literal binary
// patch.
$is_git_binary_patch = preg_match(
'/^GIT binary patch$/',
rtrim($line));
if ($is_git_binary_patch) {
$this->nextLine();
$this->parseGitBinaryPatch();
$line = $this->getLine();
if (preg_match('/^literal/', $line)) {
// We may have old/new binaries (change) or just a new binary (hg add).
// If there are two blocks, parse both.
$this->parseGitBinaryPatch();
}
$this->markBinary($change);
return;
}
if ($is_git) {
// "git diff -b" ignores whitespace, but has an empty hunk target
if (preg_match('@^diff --git .*$@', $line)) {
$this->nextLine();
return null;
}
}
$old_file = $this->parseHunkTarget();
$new_file = $this->parseHunkTarget();
$change->setOldPath($old_file);
$this->parseChangeset($change);
}
private function parseGitBinaryPatch() {
// TODO: We could decode the patches, but it's a giant mess so don't bother
// for now. We'll pick up the data from the working copy in the common
// case ("arc diff").
$line = $this->getLine();
if (!preg_match('/^literal /', $line)) {
$this->didFailParse("Expected 'literal NNNN' to start git binary patch.");
}
do {
$line = $this->nextLineTrimmed();
if ($line === '' || $line === null) {
// Some versions of Mercurial apparently omit the terminal newline,
// although it's unclear if Git will ever do this. In either case,
// rely on the base85 check for sanity.
$this->nextNonemptyLine();
return;
} else if (!preg_match('/^[a-zA-Z]/', $line)) {
$this->didFailParse("Expected base85 line length character (a-zA-Z).");
}
} while (true);
}
protected function parseHunkTarget() {
$line = $this->getLine();
$matches = null;
$remainder = '(?:\s*\(.*\))?';
if ($this->getIsMercurial()) {
// Something like "Fri Aug 26 01:20:50 2005 -0700", don't bother trying
// to parse it.
$remainder = '\t.*';
}
$ok = preg_match(
'@^[-+]{3} (?:[ab]/)?(?P<path>.*?)'.$remainder.'$@',
$line,
$matches);
if (!$ok) {
$this->didFailParse(
"Expected hunk target '+++ path/to/file.ext (revision N)'.");
}
$this->nextLine();
return $matches['path'];
}
protected function markBinary(ArcanistDiffChange $change) {
$change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
return $this;
}
protected function parseChangeset(ArcanistDiffChange $change) {
$all_changes = array();
do {
$hunk = new ArcanistDiffHunk();
$line = $this->getLineTrimmed();
$real = array();
// In the case where only one line is changed, the length is omitted.
// The final group is for git, which appends a guess at the function
// context to the diff.
$matches = null;
$ok = preg_match(
'/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*?)?$/U',
$line,
$matches);
if (!$ok) {
// It's possible we hit the style of an svn1.7 property change.
// This is a 4-line Index block, followed by an empty line, followed
// by a "Property changes on:" section similar to svn1.6.
if ($line == '') {
$line = $this->nextNonemptyLine();
$ok = preg_match('/^Property changes on:/', $line);
if (!$ok) {
$this->didFailParse("Confused by empty line");
}
$line = $this->nextLine();
return $this->parsePropertyHunk($change);
}
$this->didFailParse("Expected hunk header '@@ -NN,NN +NN,NN @@'.");
}
$hunk->setOldOffset($matches[1]);
$hunk->setNewOffset($matches[3]);
// Cover for the cases where length wasn't present (implying one line).
$old_len = idx($matches, 2);
if (!strlen($old_len)) {
$old_len = 1;
}
$new_len = idx($matches, 4);
if (!strlen($new_len)) {
$new_len = 1;
}
$hunk->setOldLength($old_len);
$hunk->setNewLength($new_len);
$add = 0;
$del = 0;
$advance = false;
while ((($line = $this->nextLine()) !== null)) {
if (strlen($line)) {
$char = $line[0];
} else {
$char = '~';
}
switch ($char) {
case '\\':
if (!preg_match('@\\ No newline at end of file@', $line)) {
$this->didFailParse(
"Expected '\ No newline at end of file'.");
}
if ($new_len) {
$real[] = $line;
$hunk->setIsMissingOldNewline(true);
} else {
$real[] = $line;
$hunk->setIsMissingNewNewline(true);
}
if (!$new_len) {
$advance = true;
break 2;
}
break;
case '+':
if (!$new_len) {
break 2;
}
++$add;
--$new_len;
$real[] = $line;
break;
case '-':
if (!$old_len) {
break 2;
}
++$del;
--$old_len;
$real[] = $line;
break;
case ' ':
if (!$old_len && !$new_len) {
break 2;
}
--$old_len;
--$new_len;
$real[] = $line;
break;
case "\r":
case "\n":
case '~':
$advance = true;
break 2;
default:
break 2;
}
}
if ($old_len != 0 || $new_len != 0) {
$this->didFailParse("Found the wrong number of hunk lines.");
}
$corpus = implode('', $real);
$is_binary = false;
if ($this->detectBinaryFiles) {
$is_binary = !phutil_is_utf8($corpus);
$try_encoding = $this->tryEncoding;
if ($is_binary && $try_encoding) {
$is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
if (!$is_binary) {
$corpus = phutil_utf8_convert($corpus, 'UTF-8', $try_encoding);
if (!phutil_is_utf8($corpus)) {
throw new Exception(
"Failed to convert a hunk from '{$try_encoding}' to UTF-8. ".
"Check that the specified encoding is correct.");
}
}
}
}
if ($is_binary) {
// SVN happily treats binary files which aren't marked with the right
// mime type as text files. Detect that junk here and mark the file
// binary. We'll catch stuff with unicode too, but that's verboten
// anyway. If there are too many false positives with this we might
// need to make it threshold-triggered instead of triggering on any
// unprintable byte.
$change->setFileType(ArcanistDiffChangeType::FILE_BINARY);
} else {
$hunk->setCorpus($corpus);
$hunk->setAddLines($add);
$hunk->setDelLines($del);
$change->addHunk($hunk);
}
if ($advance) {
$line = $this->nextNonemptyLine();
}
} while (preg_match('/^@@ /', $line));
}
protected function buildChange($path = null) {
$change = null;
if ($path !== null) {
if (!empty($this->changes[$path])) {
return $this->changes[$path];
}
}
if ($this->forcePath) {
return $this->changes[$this->forcePath];
}
$change = new ArcanistDiffChange();
if ($path !== null) {
$change->setCurrentPath($path);
$this->changes[$path] = $change;
} else {
$this->changes[] = $change;
}
return $change;
}
protected function didStartParse($text) {
$this->rawDiff = $text;
// Eat leading whitespace. This may happen if the first change in the diff
// is an SVN property change.
$text = ltrim($text);
// Try to strip ANSI color codes from colorized diffs. ANSI color codes
// might be present in two cases:
//
// - You piped a colorized diff into 'arc --raw' or similar (normally
// we're able to disable colorization on diffs we control the generation
// of).
// - You're diffing a file which actually contains ANSI color codes.
//
// The former is vastly more likely, but we try to distinguish between the
// two cases by testing for a color code at the beginning of a line. If
// we find one, we know it's a colorized diff (since the beginning of the
// line should be "+", "-" or " " if the code is in the diff text).
//
// While it's possible a diff might be colorized and fail this test, it's
// unlikely, and it covers hg's color extension which seems to be the most
// stubborn about colorizing text despite stdout not being a TTY.
//
// We might incorrectly strip color codes from a colorized diff of a text
// file with color codes inside it, but this case is stupid and pathological
// and you've dug your own grave.
$ansi_color_pattern = '\x1B\[[\d;]*m';
if (preg_match('/^'.$ansi_color_pattern.'/m', $text)) {
$text = preg_replace('/'.$ansi_color_pattern.'/', '', $text);
}
$this->text = phutil_split_lines($text);
$this->line = 0;
}
protected function getLine() {
if ($this->text === null) {
throw new Exception("Not parsing!");
}
if (isset($this->text[$this->line])) {
return $this->text[$this->line];
}
return null;
}
protected function getLineTrimmed() {
$line = $this->getLine();
if ($line !== null) {
$line = trim($line, "\r\n");
}
return $line;
}
protected function nextLine() {
$this->line++;
return $this->getLine();
}
protected function nextLineTrimmed() {
$line = $this->nextLine();
if ($line !== null) {
$line = trim($line, "\r\n");
}
return $line;
}
protected function nextNonemptyLine() {
while (($line = $this->nextLine()) !== null) {
if (strlen(trim($line)) !== 0) {
break;
}
}
return $this->getLine();
}
protected function nextLineThatLooksLikeDiffStart() {
while (($line = $this->nextLine()) !== null) {
if (preg_match('/^\s*diff\s+-r/', $line)) {
break;
}
}
return $this->getLine();
}
protected function saveLine() {
$this->lineSaved = $this->line;
}
protected function restoreLine() {
$this->line = $this->lineSaved;
}
protected function isFirstNonEmptyLine() {
$count = count($this->text);
for ($i = 0; $i < $count; $i++) {
if (strlen(trim($this->text[$i])) != 0) {
return ($i == $this->line);
}
}
// Entire file is empty.
return false;
}
protected function didFinishParse() {
$this->text = null;
}
public function setWriteDiffOnFailure($write) {
$this->writeDiffOnFailure = $write;
return $this;
}
protected function didFailParse($message) {
$context = 5;
$min = max(0, $this->line - $context);
$max = min($this->line + $context, count($this->text) - 1);
$context = '';
for ($ii = $min; $ii <= $max; $ii++) {
$context .= sprintf(
"%8.8s %6.6s %s",
($ii == $this->line) ? '>>> ' : '',
$ii + 1,
$this->text[$ii]);
}
$out = array();
$out[] = "Diff Parse Exception: {$message}";
if ($this->writeDiffOnFailure) {
$temp = new TempFile();
$temp->setPreserveFile(true);
Filesystem::writeFile($temp, $this->rawDiff);
$out[] = "Raw input file was written to: ".(string)$temp;
}
$out[] = $context;
$out = implode("\n\n", $out);
throw new Exception($out);
}
/**
* Unescape escaped filenames, e.g. from "git diff".
*/
private static function unescapeFilename($name) {
if (preg_match('/^".+"$/', $name)) {
return stripcslashes(substr($name, 1, -1));
} else {
return $name;
}
}
private function loadSyntheticData() {
if (!$this->changes) {
return;
}
$repository_api = $this->repositoryAPI;
if (!$repository_api) {
return;
}
$changes = $this->changes;
foreach ($changes as $change) {
$path = $change->getCurrentPath();
// Certain types of changes (moves and copies) don't contain change data
// when expressed in raw "git diff" form. Augment any such diffs with
// textual data.
if ($change->getNeedsSyntheticGitHunks() &&
($repository_api instanceof ArcanistGitAPI)) {
$diff = $repository_api->getRawDiffText($path, $moves = false);
// NOTE: We're reusing the parser and it doesn't reset change state
// between parses because there's an oddball SVN workflow in Phabricator
// which relies on being able to inject changes.
// TODO: Fix this.
$parser = clone $this;
$parser->setChanges(array());
$raw_changes = $parser->parseDiff($diff);
foreach ($raw_changes as $raw_change) {
if ($raw_change->getCurrentPath() == $path) {
$change->setFileType($raw_change->getFileType());
foreach ($raw_change->getHunks() as $hunk) {
// Git thinks that this file has been added. But we know that it
// has been moved or copied without a change.
$hunk->setCorpus(
preg_replace('/^\+/m', ' ', $hunk->getCorpus()));
$change->addHunk($hunk);
}
break;
}
}
$change->setNeedsSyntheticGitHunks(false);
}
if ($change->getFileType() != ArcanistDiffChangeType::FILE_BINARY &&
$change->getFileType() != ArcanistDiffChangeType::FILE_IMAGE) {
continue;
}
$change->setOriginalFileData($repository_api->getOriginalFileData($path));
$change->setCurrentFileData($repository_api->getCurrentFileData($path));
}
$this->changes = $changes;
}
/**
* Strip prefixes off paths from `git diff`. By default git uses a/ and b/,
* but you can set `diff.mnemonicprefix` to get a different set of prefixes,
* or use `--no-prefix`, `--src-prefix` or `--dst-prefix` to set these to
* other arbitrary values.
*
* We strip the default and mnemonic prefixes, and trust the user knows what
* they're doing in the other cases.
*
* @param string Path to strip.
* @return string Stripped path.
*/
public static function stripGitPathPrefix($path) {
static $regex;
if ($regex === null) {
$prefixes = array(
// These are the defaults.
'a/',
'b/',
// These show up when you set "diff.mnemonicprefix".
'i/',
'c/',
'w/',
'o/',
'1/',
'2/',
);
foreach ($prefixes as $key => $prefix) {
$prefixes[$key] = preg_quote($prefix, '@');
}
$regex = '@^('.implode('|', $prefixes).')@S';
}
return preg_replace($regex, '', $path);
}
/**
* Split the paths on a "diff --git" line into old and new paths. This
* is difficult because they may be ambiguous if the files contain spaces.
*
* @param string Text from a diff line after "diff --git ".
* @return pair<string, string> Old and new paths.
*/
public static function splitGitDiffPaths($paths) {
$matches = null;
$paths = rtrim($paths, "\r\n");
$patterns = array(
// Try quoted paths, used for unicode filenames or filenames with quotes.
'@^(?P<old>"(?:\\\\.|[^"\\\\]+)+") (?P<new>"(?:\\\\.|[^"\\\\]+)+")$@',
// Try paths without spaces.
'@^(?P<old>[^ ]+) (?P<new>[^ ]+)$@',
// Try paths with well-known prefixes.
'@^(?P<old>[abicwo12]/.*) (?P<new>[abicwo12]/.*)$@',
// Try the exact same string twice in a row separated by a space.
// This can hit a false positive for moves from files like "old file old"
// to "file", but such a case combined with custom diff prefixes is
// incredibly obscure.
'@^(?P<old>.*) (?P<new>\\1)$@',
);
foreach ($patterns as $pattern) {
if (preg_match($pattern, $paths, $matches)) {
break;
}
}
if (!$matches) {
throw new Exception(
"Input diff contains ambiguous line 'diff --git {$paths}'. This line ".
"is ambiguous because there are spaces in the file names, so the ".
"parser can not determine where the file names begin and end. To ".
"resolve this ambiguity, use standard prefixes ('a/' and 'b/') when ".
"generating diffs.");
}
$old = $matches['old'];
$old = self::unescapeFilename($old);
$old = self::stripGitPathPrefix($old);
$new = $matches['new'];
$new = self::unescapeFilename($new);
$new = self::stripGitPathPrefix($new);
return array($old, $new);
}
}
diff --git a/src/parser/__tests__/ArcanistBundleTestCase.php b/src/parser/__tests__/ArcanistBundleTestCase.php
index 88301ef0..747286f1 100644
--- a/src/parser/__tests__/ArcanistBundleTestCase.php
+++ b/src/parser/__tests__/ArcanistBundleTestCase.php
@@ -1,833 +1,887 @@
<?php
final class ArcanistBundleTestCase extends ArcanistTestCase {
private function loadResource($name) {
return Filesystem::readFile($this->getResourcePath($name));
}
private function getResourcePath($name) {
return dirname(__FILE__).'/bundle/'.$name;
}
private function loadDiff($old, $new) {
list($err, $stdout) = exec_manual(
'diff --unified=65535 --label %s --label %s -- %s %s',
'file 9999-99-99',
'file 9999-99-99',
$this->getResourcePath($old),
$this->getResourcePath($new));
$this->assertEqual(
1,
$err,
"Expect `diff` to find changes between '{$old}' and '{$new}'.");
return $stdout;
}
private function loadOneChangeBundle($old, $new) {
$diff = $this->loadDiff($old, $new);
return ArcanistBundle::newFromDiff($diff);
}
/**
* Unarchive a saved git repository and apply each commit as though via
* "arc patch", verifying that the resulting tree hash is identical to the
* tree hash produced by the real commit.
*/
public function testGitRepository() {
if (phutil_is_windows()) {
$this->assertSkipped('This test is not supported under Windows.');
}
$archive = dirname(__FILE__).'/bundle.git.tgz';
$patches = dirname(__FILE__).'/patches/';
$fixture = PhutilDirectoryFixture::newFromArchive($archive);
chdir($fixture->getPath());
list($commits) = execx(
'git log --format=%s',
'%H %T %s');
$commits = explode("\n", trim($commits));
// The very first commit doesn't have a meaningful parent, so don't examine
// it.
array_pop($commits);
foreach ($commits as $commit) {
list($commit_hash, $tree_hash, $subject) = explode(' ', $commit, 3);
execx('git reset --hard %s --', $commit_hash);
$repository_api = new ArcanistGitAPI($fixture->getPath());
$repository_api->setDefaultBaseCommit();
$diff = $repository_api->getFullGitDiff();
$parser = new ArcanistDiffParser();
$parser->setRepositoryAPI($repository_api);
$changes = $parser->parseDiff($diff);
$this->makeChangeAssertions($commit_hash, $changes);
$bundle = ArcanistBundle::newFromChanges($changes);
execx('git reset --hard %s^ --', $commit_hash);
$patch = $bundle->toGitPatch();
$expect_path = $patches.'/'.$commit_hash.'.gitpatch';
$expect = null;
if (Filesystem::pathExists($expect_path)) {
$expect = Filesystem::readFile($expect_path);
}
if ($patch === $expect) {
$this->assertEqual($expect, $patch);
} else {
Filesystem::writeFile($expect_path.'.real', $patch);
throw new Exception(
"Expected patch and actual patch for {$commit_hash} differ. ".
"Wrote actual patch to '{$expect_path}.real'.");
}
try {
id(new ExecFuture('git apply --index --reject'))
->write($patch)
->resolvex();
} catch (CommandException $ex) {
$temp = new TempFile(substr($commit_hash, 0, 8).'.patch');
$temp->setPreserveFile(true);
Filesystem::writeFile($temp, $patch);
PhutilConsole::getConsole()->writeErr(
"Wrote failing patch to '%s'.\n",
$temp);
throw $ex;
}
execx('git commit -m %s', $subject);
list($result_hash) = execx('git log -n1 --format=%s', '%T');
$result_hash = trim($result_hash);
$this->assertEqual(
$tree_hash,
$result_hash,
"Commit {$commit_hash}: {$subject}");
}
}
private function makeChangeAssertions($commit, array $raw_changes) {
$changes = array();
// Verify that there are no duplicate changes, and rekey the changes on
// affected path because we don't care about the order in which the
// changes appear.
foreach ($raw_changes as $change) {
$this->assertEqual(
true,
empty($changes[$change->getCurrentPath()]),
"Unique Path: ".$change->getCurrentPath());
$changes[$change->getCurrentPath()] = $change;
}
switch ($commit) {
+ case '1830a13adf764b55743f7edc6066451898d8ffa4':
+ // "Mark koan2 as +x and edit it."
+
+ $this->assertEqual(1, count($changes));
+
+ $c = $changes['koan2'];
+ $this->assertEqual(
+ ArcanistDiffChangeType::TYPE_CHANGE,
+ $c->getType());
+
+ $this->assertEqual(
+ '100644',
+ idx($c->getOldProperties(), 'unix:filemode'));
+
+ $this->assertEqual(
+ '100755',
+ idx($c->getNewProperties(), 'unix:filemode'));
+ break;
+ case '8ecc728bcc9b482a9a91527ea471b04fc1a025cf':
+ // "Move 'text' to 'executable' and mark it +x."
+
+ $this->assertEqual(2, count($changes));
+
+ $c = $changes['executable'];
+ $this->assertEqual(
+ ArcanistDiffChangeType::TYPE_MOVE_HERE,
+ $c->getType());
+
+ $this->assertEqual(
+ '100644',
+ idx($c->getOldProperties(), 'unix:filemode'));
+
+ $this->assertEqual(
+ '100755',
+ idx($c->getNewProperties(), 'unix:filemode'));
+ break;
+ case '39c8e7dd3914edff087a6214f0cd996ad08e5b3d':
+ // "Mark koan as +x."
+ // Primarily a test against a recusive synthetic hunk construction bug.
+ $this->assertEqual(1, count($changes));
+
+ $c = $changes['koan'];
+ $this->assertEqual(
+ ArcanistDiffChangeType::TYPE_CHANGE,
+ $c->getType());
+
+ $this->assertEqual(
+ '100644',
+ idx($c->getOldProperties(), 'unix:filemode'));
+
+ $this->assertEqual(
+ '100755',
+ idx($c->getNewProperties(), 'unix:filemode'));
+ break;
case 'c573c25d1a767d270fed504cd993e78aba936338':
// "Copy a koan over text, editing the original koan."
// Git doesn't really do anything meaningful with this.
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$c = $changes['text'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
break;
case 'd26628e588cf7d16368845b121c6ac6c781e81d0':
// "Copy a koan, modifying both the source and destination."
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$c->getType());
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
break;
case 'b0c9663ecda5f666f62dad245a3a7549aac5e636':
// "Remove a koan copy."
$this->assertEqual(1, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
break;
case 'b6ecdb3b4801f3028d88ba49940a558360847dbf':
// "Copy a koan and edit the destination."
// Git does not detect this as a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '30d23787e1ecd254c884afbe37afa612f61e3904':
// "Move and edit a koan."
$this->assertEqual(2, count($changes));
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case 'c0ba9bfe3695f95c3f558bc5797eeba421d32483':
// "Remove two koans."
$this->assertEqual(2, count($changes));
$c = $changes['koan3'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
$c = $changes['koan4'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
break;
case '2658fd01d5355abe5d4c7ead3a0e7b4b3449fe77':
// "Multicopy a koan."
$this->assertEqual(3, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MULTICOPY,
$c->getType());
$c = $changes['koan3'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$c = $changes['koan4'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case '1c5fe4e2243bb19d6b3bf15896177b13768e6eb6':
// "Copy a koan."
// Git does not detect this as a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '6d9eb65a2c2b56dee64d72f59554c1cca748dd34':
// "Move a koan."
$this->assertEqual(2, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$c = $changes['koan2'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
break;
case '141452e2a775ee86409e8779dd2eda767b4fe8ab':
// "Add a koan."
$this->assertEqual(1, count($changes));
$c = $changes['koan'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
break;
case '5dec8bf28557f078d1987c4e8cfb53d08310f522':
// "Copy an image, and replace the original."
// `image_2.png` is copied to `image.png` and then replaced.
$this->assertEqual(2, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_AWAY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
'c9ec1b952480da09b393ba672d9b13da',
md5($c->getCurrentFileData()));
break;
case 'fb28468d25a5fdd063aca4ca559454c998a0af51':
// "Multicopy image."
// `image.png` is copied to `image_2.png` and `image_3.png` and then
// deleted. Git detects this as a move and an add.
$this->assertEqual(3, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MULTICOPY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_COPY_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
$c = $changes['image_3.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case 'df340e88d8aba12e8f2b8827f01f0cd9f35eb758':
// "Remove binary image."
// `image_2.png` is deleted.
$this->assertEqual(1, count($changes));
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_DELETE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
break;
case '3f5c6d735e64c25a04f83be48ef184b25b5282f0':
// "Copy binary image."
// `image_2.png` is copied to `image.png`. Git does not detect this as
// a copy without --find-copies-harder.
$this->assertEqual(1, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case 'b454edb3bb29890ee5b3af5ef66ce6a24d15d882':
// "Move binary image."
// `image.png` is moved to `image_2.png`.
$this->assertEqual(2, count($changes));
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_AWAY,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getOriginalFileData()));
$this->assertEqual(
null,
$c->getCurrentFileData());
$c = $changes['image_2.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_MOVE_HERE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case '5de5f3dfda1b7db2eb054e57699f05aaf1f4483e':
// "Add a binary image."
// `image.png` is added.
$c = $changes['image.png'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_ADD,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_BINARY,
$c->getFileType());
$this->assertEqual(
null,
$c->getOriginalFileData());
$this->assertEqual(
'8645053452b2cc2f955ef3944ac0831a',
md5($c->getCurrentFileData()));
break;
case '176a4c2c3fd88b2d598ce41a55d9c3958be9fd2d':
// "Convert \r\n newlines to \n newlines."
case 'a73b28e139296d23ade768f2346038318b331f94':
// "Add text with \r\n newlines."
case '337ccec314075a2bdb4a912ef467d35d04a713e4':
// "Convert \n newlines to \r\n newlines.";
case '6d5e64a4a7a6a036c53b1d087184cb2c70099f2c':
// "Remove tabs."
case '49395994a1a8a06287e40a3b318be4349e8e0288':
// "Add tabs."
case 'a5a53c424f3c2a7e85f6aee35e834c8ec5b3dbe3':
// "Add trailing newline."
case 'd53dc614090c6c7d6d023e170877d7f611f18f5a':
// "Remove trailing newline."
case 'f19fb9fa1385c01b53bdb6d8842dd154e47151ec':
// "Edit a text file."
$this->assertEqual(1, count($changes));
$c = $changes['text'];
$this->assertEqual(
ArcanistDiffChangeType::TYPE_CHANGE,
$c->getType());
$this->assertEqual(
ArcanistDiffChangeType::FILE_TEXT,
$c->getFileType());
break;
case '228d7be4840313ed805c25c15bba0f7b188af3e6':
// "Add a text file."
// This commit is never reached because we skip the 0th commit junk.
$this->assertEqual(true, "This is never reached.");
break;
default:
throw new Exception(
"Commit {$commit} has no change assertions!");
}
}
public function testTrailingContext() {
// Diffs need to generate without extra trailing context, or 'patch' will
// choke on them.
$this->assertEqual(
$this->loadResource('trailing-context.diff'),
$this->loadOneChangeBundle(
'trailing-context.old',
'trailing-context.new')->toUnifiedDiff());
}
public function testDisjointHunks() {
// Diffs need to generate without overlapping hunks.
$this->assertEqual(
$this->loadResource('disjoint-hunks.diff'),
$this->loadOneChangeBundle(
'disjoint-hunks.old',
'disjoint-hunks.new')->toUnifiedDiff());
}
public function testNonlocalTrailingNewline() {
// Diffs without changes near the end of the file should not generate a
// bogus, change-free hunk if the file has no trailing newline.
$this->assertEqual(
$this->loadResource('trailing-newline.diff'),
$this->loadOneChangeBundle(
'trailing-newline.old',
'trailing-newline.new')->toUnifiedDiff());
}
public function testEncodeBase85() {
$data = '';
for ($ii = 0; $ii <= 255; $ii++) {
$data .= chr($ii);
}
for ($ii = 255; $ii >= 0; $ii--) {
$data .= chr($ii);
}
$expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect1.txt');
$expect = trim($expect);
$this->assertEqual(
$expect,
ArcanistBundle::encodeBase85($data));
// This is just a large block of random binary data, it has no special
// significance.
$data =
"\x56\x4c\xb3\x63\xe5\x4a\x9f\x03\xa3\x4c\xdd\x5d\x85\x86\x10".
"\x30\x3f\xc1\x28\x51\xd8\xb2\x1a\xc3\x79\x15\x85\x31\x66\xf9".
"\x8e\xe1\x20\x8f\x12\xa1\x94\x0e\xbf\xb6\x9c\xb5\xc0\x15\x43".
"\x3d\xad\xed\x00\x3c\x16\xfa\x76\x2f\xed\x99\x3a\x78\x3e\xd1".
"\x91\xf8\xb0\xca\xb9\x29\xfe\xd4\x0f\x16\x70\x19\xad\xd9\x42".
"\x15\xb4\x8f\xd6\x8f\x80\x62\xe9\x48\x77\x9f\x38\x6d\x3f\xd6".
"\x0e\x40\x68\x68\x93\xae\x75\x6d\x7f\x75\x9c\x80\x69\x94\x22".
"\x87\xb6\xc0\x62\x6b\xab\x49\xb8\x91\xe9\x96\xbf\x04\xc2\x50".
"\x30\xae\xea\xc1\x70\x8e\x91\xd0\xb6\xec\x56\x14\x78\xd5\x8a".
"\x8c\x52\xd1\x3c\xde\x65\x21\xec\x93\xab\xcf\x7e\xf5\xfd\x6d".
"\x2d\x69\xb9\x2e\xa3\x42\x7b\x4d\xa5\xfb\x28\x6d\x74\xa3\x7b".
"\x3a\xc5\x34\x7c\x63\xa9\xf9\x8e\x34\x14\x42\xb0\xf1\x0e\xe2".
"\xd0\xd2\x04\x81\xff\x62\xd5\xd9\x46\x3b\x36\x88\x8a\x93\x55".
"\x02\x2c\xff\x9f\x48\xd6\x7a\xcb\xbf\x6a\x33\xaa\x6b\x08\x4c".
"\x96\x98\x89\x53\x56\xb4\xb3\x9b\x06\xb1\xa0\x13\x69\xfa\x6a".
"\xa8\x0d\x6a\xda\xb2\x6f\x62\x0b\xa8\xf6\x59\x29\x46\x7d\x04".
"\x44\xeb\x90\x6f\xd7\xc7\xb6\xca\xc5\xeb\xde\x10\x9b\xbd\xf2".
"\x66\x8e\xd0\x0b\xda\x8c\xeb\x90\x73\x73\x33\xe7\x6f\x26\x57".
"\x4e\xfc\x95\xe0\xfc\x62\x93\xa7\x28\xe6\x0c\x46\x73\xdd\x01".
"\xce\x43\x9b\x4e\x16\x74\x5b\x36\x92\x5a\x66\x4c\xe3\x9e\x90".
"\x2d\x9a\x1a\x3d\x69\x39\x67\x04\xd6\xf8\x5f\x45\xee\xbb\xd4".
"\x63\xcf\x8c\x9b\x31\x69\x98\x1a\x98\x57\x4b\xa9\x49\xf6\x1b".
"\x76\x28\xd7\xe3\x8f\x63\x95\x5b\x06\xe2\xa8\x66\x60\xf9\x49".
"\x4e\x40\x53\x32\x9b\x74\x36\xc0\x56\xf4\x33\xec\x83\xd2\x2c".
"\x69\x60\x55\x11\x3b\x4f\xd6\x0a\xf6\x04\x38\x75\xb6\xc2\x82".
"\x4d\xfa\x83\x56\xba\x35\x42\xc3\xcb\xdc\x28\xf4\x69\x48\xa9".
"\xe0\x51\x41\x79\x66\xfe\x61\xd1\xf2\x9f\x7b\xde\xc4\x3e\x8f".
"\x8f\xb6\x9c\x0a\x74\xf8\x71\x03\x37\x37\x30\x8d\x2a\x6a\xc9".
"\x51\xa1\xe2\x34\xe5\x42\xdb\x4f\x61\x4e\x16\xfc\x23\x72\x12".
"\x46\x53\x12\x82\x3e\x44\x63\x23\x82\xaa\xab\x7e\x8d\x70\x66".
"\xf1\x94\x86\x02\xc5\x3e\x9c\x79\x17\x1e\x9f\x13\x89\x3d\x25".
"\x45\xc9\x3b\x1e\xa0\x1a\x03\x20\x1c\x81\x6b\xfc\xb5\xc9\xe2".
"\xda\xb1\x87\x34\xa0\xb2\x72\x36\x68\x12\x05\x53\x7c\x68\x6b".
"\x1e\x2a\x56\x2a\x7e\x7f\xd0\x9c\x13\xa9\xb2\x4c\xe6\x8a\x65".
"\xd7\x67\xad\xf3\xf3\x2b\x9c\xe8\x10\x07\x8a\xe2\x20\x67\xe4".
"\x51\x47\xc1\x22\x91\x05\x22\x39\x1a\xef\x54\xd2\x8a\x88\x55".
"\x3f\x83\xba\x73\xd4\x95\xc7\xb8\xa2\xfd\x4d\x4e\x5d\xff\xdd".
"\xaf\x1a\xc2\x7e\xb5\xfa\x86\x5f\x93\x38\x5d\xca\x9a\x5a\x7e".
"\xb7\x47\xd5\x5c\x6b\xf3\x32\x03\x11\x44\xe9\x49\x12\x40\x82".
"\x67\x7d\x2a\x5a\x61\x81\xbd\x24\xaa\xd7\x7c\xc9\xcf\xaf\xb0".
"\x3e\xb0\x43\xcd\xce\x21\xe4\x1b\x5a\xd6\x40\xf5\x0e\x23\xef".
"\x70\xf4\xc6\xd2\xd7\x36\xd7\x20\xda\x8d\x39\x46\xea\xfc\x78".
"\x55\xa2\x02\xd6\x77\x21\xc8\x97\x1e\xdf\x45\xde\x93\xa7\x74".
"\xd8\x59\x10\x24\x8a\xe8\xcd\xe9\x00\xb5\x4e\xe6\x49\xb0\xde".
"\x14\x1a\x5d\xdd\x38\x47\xb0\xc7\x1e\xec\x7c\x76\xc9\x21\x3c".
"\x3a\x85\x4f\x71\x97\xed\x4a\x94\x2c\x51\x48\x9c\x43\x90\x70".
"\xe9\x0e\x84\x55\xd2\xa4\x48\xfa\xfd\x54\x12\x11\xb9\x32\xfc".
"\x1d\x66\xe7\x42\xe3\x5e\x65\xf4\x3d\xea\x1a\x53\xe3\x7b\x4b".
"\xee\xdb\x74\xce\x30\xd3\x04\xcb\xda\xa4\xdd\xad\x98\x3a\x76".
"\xe8\xba\x1b\x03\x53\xed\x46\x5d\xef\xd4\x34\xc2\x8d\xef\xae".
"\x51\x35\x0f\x4d\x40\xaa\x3a\xdb\x50\x1a\xbe\x5f\x8b\xb8\x24".
"\x40\x19\x8f\x8a\x6b\x44\x4f\x9b\xe0\xf4\x9c\x4b\xc4\x23\x37".
"\xf0\xb3\xe1\x58\x9d\x0e\xd9\xa9\xf7\x3e\x86\x43\x9b\x5b\x90".
"\x3c\xc0\x20\xa0\xc5\x86\x4f\xc6\xcb\xb5\xcb\xd4\x88\xc6\x72".
"\x57\xa7\x57\x2c\x34\x26\x91\x44\x15\xa8\xf4\x88\xca\x74\x56".
"\x9e\x12\x6c\xdf\x52\xef\xc0\xb4\x5c\x16\xe8\xaa\xf7\xb6\xf3".
"\x7c\xda\xcd\x42\xf9\x1c\x40\x88\x44\x68\x4f\x1b\x5a\x7b\x8f".
"\xc3\x47\x48\xd3\xf3\xe5\xf5\x66\x35\x48\xbe\x64\xdf\xfe\x35".
"\xf1\xc3\xe4\xa8\xfc\x86\xfb\x69\x20\xc9\xf4\x16\x96\xc1\x7a".
"\x51\x14\x77\xa4\x6e\x13\xe8\x59\x35\x24\xf1\xe5\xfe\xe9\x98".
"\x0d\xd1\xe8\xce\x9c\x7f\xf8\x3b\x79\x39\x3a\x1d\xa3\x77\xef".
"\x4f\x4b\x59\x73\x03\xb3\xfe\xae\x70\x2a\x3a\xf0\x79\x9d\x7e".
"\x9b\xaa\xb1\x18\xf9\x43\x69\xf3\x55\x46\xad\x38\xa2\xf1\xcb".
"\xce\x37\xa9\x88\x20\x38\xea\x19\x29\x95\x8c\x75\x06\x9d\x1d".
"\x9e\xf2\xb7\x64\x98\x21\x36\x90\x92\xf8\xb8\x89\x1e\x5c\x5d".
"\x09\x3b\x52\xc5\x6a\x87\x7e\x46\xca\x8c\xdf\xe7\xca\xa9\x7b".
"\x11\x63\x0f\x9e\x42\x9a\x3e\xe0\x8b\x80\x9e\x91\x76\x88\x9a".
"\xa1\xe2\x96\xae\xfb\x18\x39\xdc\x92\x99\x34\xfd\x98\x20\xa8".
"\x89\x61\x2c\x26\xe0\xb8\x83\xa7\xe7\x50\x42\x8f\xfc\x36\x66".
"\x6b\x25\xc5\x6d\xb4\x31\xe1\x4d\x0f\x2e\xf8\x44\xe2\xb6\x6a".
"\x6d\xfe\x83\x9e\x2c\x07\x2f\x15\x41\xf3\xe7\xa6\x18\x2b\x84".
"\x7e\xeb\x43\xcc\xbb\xdb\xa9\x54\x5c\xbc\x59\x6a\xdc\x26\x2a".
"\xf4\x59\xa7\x75\xa4\xac\xed\x73\x8f\x16\x43\x0d\x97\x10\x2c".
"\x70\xef\x9e\xb2\xc9\xdf\xe6\xa7\x9b\x08\x79\xa3\xf7\x99\xf5".
"\x59\xe4\xd5\x89\x10\xe5\xc9\xf7\xe7\x29\x72\x06\xc6\x54\xc3".
"\xcd\xd0\xff\x69\xf8\xdf\x19\xf2\x66\x1c\x69\x40\xbc\x97\xf1".
"\x49\x5e\x78\x62\x52\x46\x7f\xcf\x44\x50\x8b\x5f\xe7\xa8\xeb".
"\xd5\x84\x24\x81\xc0\x2c\x65\xf7\x95\xbd\xf2\x8e\x43\xfb\x6a".
"\x49\x3c\x6a\xe5\x2a\x39\xf0\xfa\x89\x59\x5f\x39\x75\xb4\x6f".
"\x04\xf1\xe0\x2c\xcd\x77\x34\xec\x6b\x45\x16\xe3\x18\x24\x05".
"\xb9\x68\xc1\x4e\x71\x4b\xff\x88\x18\xea\x0d\x56\x49\x55\xdf".
"\xe5\xb0\x59\xdb\x74\x9e\x0b\x38\x03\x9f\x10\x6f\xd9\x34\x07".
"\x44\x29\x08\xb1\xd4\x77\xc6\x84\x0d\xbb\xb5\xd5\x09\x05\x19".
"\x01\x62\x29\x45\x52\x1d\xc6\x4f\x25\x78\x7e\xbc\xae\x07\xb3".
"\xd4\xe0\x19\x91\x03\xd6\x8d\x2f\x00\xc9\xb2\x66\x3b\x4e\x3d".
"\x75\xf7\x23\x9a\x3e\xa4\xd5\x7f\x75\x47\xd0\xbc\xc3\xc8\x2a".
"\xdc\x85\x09\x6c\x0c\x90\x38\xd8\xef\xcf\xf4\x7a\x1b\xc7\x76".
"\xe0\xdb\x81\xa8\x1b\x2b\x8d\xd4\x36\x90\x76\xde\x8a\x90\xc8".
"\x5b\x05\x00\xeb\xb3\x20\xce\x6e\x5c\xb9\x35\x3d\x95\x3a\x79".
"\x4a\x60\xeb\x23\x11\xfb\x90\x2d\xf6\xb7\x05\x4a\x43\x41\x79".
"\x51\xaa\xe6\x90\x0a\x71\x87\x80\xbe\xb0\x89\x0f\xd3\x84\x19".
"\xce\x6c\xf9\xbb\x1b\x15\x4d\x0f\x33\x65\xf7\x9e\x3a\xd9\x8c".
"\x02\x43\xcf\xdf\xb2\x60\xc1\x4c\xe9\xa5\x3c\xaf\xfa\x41\x2d".
"\xb9\x1f\x45\x32\xcb\x39\x2f\x94\xae\x44\x6d\x69\xc1\xc9\x57".
"\x8c\xe5\xf4\xa4\x3a\xb6\x70\x61\xf9\xbb\x41\xdc\x78\xf0\xf7".
"\xbf\xa8\x8e\xe3\x77\x51\xce\x25\x2f\xdf\x27\x6b\x07\x30\x9f".
"\xce\xdb\x59\x58\xaa\xb2\x2e\xdc\x90\x92\x82\x55\xfe\x25\x36".
"\x49\x7f\x6d\x2d\x39\x51\xef\x3d\xc8\xa3\x87\x0b\xe7\xf2\xac".
"\x90\xa0\x1d\xd8\xc7\xea\x93\x53\x3b\x21\x84\x2e\x52\x6c\xfb".
"\x4f\x31\xda\xd1\xea\x45\x3e\xdc\xeb\x52\x81\x8c\x2b\xf4\x2a".
"\xbc\x01\xc4\xe7\x68\x36\x9c\xd5\x2d\xc1\x61\xcb\x9a\x5f\x18".
"\x00\x6a\xc8\x9a\x4e\xfd\x31\x5b\xce\x90\x4e\x45\xff\x7f\xea".
"\xb2\x26\xad\xc1\x3a\x21\xa9\xe8\x7c\x14\xae\x81\x1e\xbe\xa3".
"\x6d\xda\x92\x1b\xeb\xf2\x69\x76\x3e\xf1\x2b\xf7\x1a\x45\xd5".
"\xb3\x81\xb1\xbe\x80\x7f\x24\xba\x0e\xd5\x68\x34\x3f\x1a\x29".
"\x15\x0e\xc2\x26\x62\x0c\xaa\xa9\x20\x4c\x61\x65\x49\x07\xbe".
"\x69\xf4\xc9\xec\x2f\x1c\xfa\x59\x2e\x72\xc0\x17\xc5\x4c\xfa".
"\xba\x2f\x64\xab\xa9\xb4\xcb\xdc\xcb\x25\x5f\xcf\x0c\x87\xcc".
"\xf0\x36\x2b\xce\x81\x5a\x22\x85\xa0\x50\x50\x97\x8e\xda\x36".
"\x80\x74\xb5\x1e\x02\x3f\xd7\xc8\x29\x11\xeb\x1d\x3d\x74\x9f".
"\x26\x1a\xa4\x3d\xf9\x0e\xf0\x2d\x5c\xa9\x43\xbf\x51\x6c\x8d".
"\xe6\x78\xe0\x67\x57\xf0\xc8\x0e\x97\x9c\x57\x23\x30\xac\x63".
"\xdf\x46\x98\xa4\xaf\x4e\xa7\xe5\xac\x31\xbd\xeb\x6a\xa0\xb0".
"\xe4\x94\x7e\x51\xf6\x89\x81\x3e\xab\x4f\x64\xb7\xc5\x51\x71".
"\xcd\x74\x02\xa9\x02\x99\x5c\xab\x0e\x14\x47\x3b\x04\xc1\x9b".
"\x59\x1a\x93\x92\x4c\x71\x20\x5f\x6e\xd3\xf3\xa7\x47\x1b\x39".
"\x3e\x73\x69\xe2\xec\xcb\x52\xb3\x5c\x7a\x95\x25\x3f\x16\x98".
"\x60\xa8\xa2\x5d\xc4\x5a\x67\xe4\x11\x06\x06\xf9\x7a\xb4\x14".
"\xe0\xbc\x7b\x13\x1d\x0f\xf2\xca\x0b\xd4\xaa\x71\x35\x3e\xd6".
"\x2e\x2e\x5d\x7b\x15\xc9\x23\x1a\xa9\x24\x31\x48\xd4\xcf\x4a".
"\xf4\x32\x17\x9b\x1d\x4b\xfe\x49\x69\xd6\xc0\x8f\xb9\xdb\x72".
"\x52\x2c\xe8\xf3\xc4\xfc\x46\xf5\xb8\x1b\x05\x06\xcf\xcc\x23".
"\x34\xbf\x25\x6a\xea\x3c\xc7\x64\xd4\xd5\xb3\x67\xed\x24\x27".
"\xd3\x67\xc1\xbd\x9f\x7b\x7d\x19\x04\x5c\xd1\x96\x7e\xa5\xc7".
"\xbb\xb2\x84\x68\x98\x38\x11\x90\xfb\x62\x15\xfd\xe6\xb7\x24".
"\x77\xb2\x78\xc7\x73\x91\xc9\x60\x1d\x91\x6d\x04\x2b\x41\xe9".
"\xc9\xfa\xe4\x98\x54\x83\x9a\x6e\x76\x8c\x21\xf9\x91\x38\x1f".
"\xdc\xfe\x13\x09\x30\xd7\x53\x63\x62\xba\xe3\x2c\x70\xd5\xfc".
"\x78\x35\x36\x79\x5d\xb6\x0e\x35\x3d\x46\x87\xfb\xf5\x64\x1f".
"\x3e\xfd\x2f\x1c\xbb\xed\x95\x2d\xd6\x63\xdc\xa7\x6a\x39\x8f".
"\xbd\xcb\x79\x95\xe9\x45\xbf\xe4\x3e\x05\x55\x00\xdb\x33\x28".
"\x3a\x6c\xe2\x35\xbb\xac\x70\x52\x2b\xac\x4e\x11\x44\x58\x16".
"\x21\xb4\xae\x0d\x6a\xb9\xdc\x85\x5d\x90\x11\x26\x85\xdb\xc3".
"\xf0\x38\x6f\x8a\xff\x12\xf0\xc9\x9e\xf0\xfc\xae\x94\x11\x4d".
"\xce\x96\x29\x09\x6c\xf4\x2a\x6c\xda\x1e\x4c\x4a\xa2\x96\x5a".
"\xef\xc6\x38\x5c\x60\xa2\x28\x13\x58\x73\x96\xde\x59\x2a\x57".
"\x64\x6c\x14\x94\x8a\x2e\x8e\x21\x3f\xa2\x43\xde\xf6\x2d\x23".
"\x74\x5c\xbd\x7a\x10\xdb\x17\xa8\x93\xd0\x74\x86\x9d\x33\x07".
"\x48\xee\xac\x18\x6d\x64\x61\x7b\x61\x2b\xa4\xa2\xab\x99\x59".
"\xbe\x19\xd7\x19\x41\x1e\x61\x87\xad\x40\x5b\x69\x8c\x32\xf5".
"\xb6\x49\xbe\x1f\xad\xd8\x0f\x3e\xd9\x62\xac\x3a\x76\xde\x32".
"\xa3\xb2\x41\x95\xad\x17\x23\xab\xa1\x37\x9c\xab\x73\x79\x70".
"\xd6\x66\x0d\x6e\x4d\x8b\xa0\xac\xe3\x44\x1e\x0a\xee\xf0\x74".
"\x64\xd8\x44\xd1\x6c\xa6\xd5\x36\x2e\xd9\x55\x6e\x90\x63\xb7".
"\xf7\x8e\xc6\x28\xa3\x40\x00\x60\x9a\x3c\xfe\xff\x03\x30\x11".
"\x18\x92\x2f\x5b\x23\xe1\x4e\x99\xe4\x82\xc9\x51\xe2\x15\x6a".
"\x76\x5c\x67\xae\xa3\xa2\x9c\x85\x51\xe0\x44\x89\x63\xa5\x71".
"\x99\xbc\x2d\x9c\xab\x9a\xfb\x20\x37\x58\xd6\x2d\x8b\x7d\x42".
"\x13\x35\x44\x4c\x11\x97\x66\x27\x17\xac\x44\xe8\x6a\x03\x78".
"\xa2\x88\xc6\x36\x71\x5a\x5a\x5a\x72\xa3\xe9\x72\x0c\x91\x31".
"\xfc\xae\x7b\xa0\x75\x21\x0a\xc1\x4b\x95\xcb\xe3\xc2\xee\x03".
"\x0f\xb8\xb2\x51\xc7\xc8\x9c\x8d\x6d\x3a\xe7\x4e\x2c\xaa\xeb".
"\x5e\x49\x93\xe0\x8f\xa1\x54\x93\xe7\x7c\x5d\x31\xc7\x05\x00".
"\x28\x14\x57\x47\xb3\x05\x2d\x17\x92\x28\x45\xee\x85\x3a\x59".
"\xb6\xa6\x04\xc0\x5c\x07\x1f\xe6\x5b\x36\x53\x62\x82\x64\xd5".
"\xb6\xf2\xf5\x67\x19\x11\xee\xd2\x70\xc5\x14\x63\xc1\x75\xe1".
"\x24\xe5\x01\x59\x52\x7c\x88\x17\xb4\xe0\x15\xe9\x12\x05\xcd".
"\x88\x7a\xd5\xea\x45\xc3\xbb\x65\xd4\xdd\x0d\xde\x36\x94\x98".
"\x0d\x2c\xfb\x3c\x2f\x69\xd0\x28\xe2\x85\xd9\x27\xf3\x7a\xad".
"\x50\x68\x96\x54\x5e\xeb\xbc\x2a\x74\xde\xf3\x4e\x8b\x27\x0a".
"\xcf\x4c\x60\x40\xe8\xc5\x72\xab\x8c\xfd\xe9\xab\xff\x51\xe5".
"\xd6\xea\x9e\x34\x73\xe1\xe6\xf8\x5b\xb1\x10\xf0\xf9\x2d\x23".
"\x0e\xfe\xe5\xf4\x8d\xb6\x6d\x37\x14\xed\x54\x97\x92\x5c\x68".
"\x40\x88\xf1\x43\x29\xef\x5e\x96\x77\xa2\xe8\x3c\xae\x7f\xb1".
"\x99\x17\xa7\x0c\x6f\xe2\x43\x32\x9b\x14\x43\xf2\x15\x6b\x13".
"\x10\x68\x56\x0b\xaa\x06\x2e\xc0\xf8\xde\x9e\x54\x9d\xba\xff".
"\x76\x26\x6d\x5e\x9e\x88\x3a\x2b\x9b\x20\x43\xb9\x1a\x0e\x58".
"\x65\xec\xdb\x9e\x97\xb8\xfb\x03\x6c\xb0\x7f\xa2\xf1\xf4\x27".
"\x24\x21\x47\x51\x21\x40\x45\x28\x71\xf7\xa1\x6b\xbe\x0e\xc8".
"\x3f\x9b\xda\x62\x9d\x73\xf7\x5f\x70\x6c\xba\x1e\xeb\x16\x5c".
"\x2e\x44\x0a\x22\x02\x6c\xbe\xb9\x69\x93\xfd\xa5\x33\x26\x64".
"\x24\x6c\xc2\x3d\x2f\xf3\xd1\x97\xde\x60\x43\x1c\x0d\x1b\x94".
"\xb3\x48\x45\x7c\xd5\xd0\x71\x4d\xad\xbf\xa4\x0a\x22\x27\x04".
"\x38\x84\x19\x66\x63\xf0\xf3\xfc\xb0\xf3\x1d\xea\xba\xb9\xe4".
"\xe5\x80\xed\xe3\xf1\x78\x24\xc3\x25\x27\x71\x81\xc2\xec\x54".
"\xed\xcc\x63\xf7\x39\xcd\x83\xdf\x32\x88\xc0\x3b\xd4\x62\xb8".
"\xea\x34\xd8\xcf\xbc\x3a\x89\x38\x64\x60\x44\xde\xb6\x76\x59".
"\xb1\x95\x6a\x26\x08\xf0\xf4\x71\x25\x8b\xf8\x81\xdd\x0d\x2f".
"\x8c\xe2\x70\xc2\x96\xc2\xd8\x9b\xe4\x3f\xec\x8b\xfd\xbd\xc9".
"\x36\x33\xb7\xbc\x59\x37\x19\x09\x30\x5e\xef\x67\xae\x67\x48".
"\x72\x0b\xf4\x2a\x82\xff\xcb\xd7\xd9\x9d\x6d\x7c\xa6\x20\x42".
"\x50\x2b\x0a\x2f\x45\x99\x5b\x76\x6d\x99\x39\xa9\xb6\x32\x06".
"\x11\xf8\x19\xd1\x3f\xc0\xd6\x1f\x67\xfa\xd5\xae\x7a\x71\x8c".
"\xbc\x3d\xb4\x5f\x5c\x81\x7c\xa1\x39\x70\x0a\x17\x24\xb7\x22".
"\x86\x50\xd8\x1f\xc8\x6c\x59\x9a\xdc\xf0\x71\x01\xda\xd8\x53".
"\x98\x1c\x73\x36\xf1\x09\x86\xc9\xa7\x26\x25\xc0\x03\x3e\x13".
"\x4e\x29\xeb\xf0\x8d\xe3\x38\x03\x54\xee\x37\xfb\x51\x2e\xb4".
"\xf6\x12\x1f\xb2\x8c\x66\x75\x00\x30\x5b\xef\x59\xf9\x63\xa9".
"\x74\x07\x91\xe4\x9c\xb7\xc9\x89\xd9\xa9\x51\x93\xcb\xb1\xa7".
"\x64\x08\x79\x8f\xb4\x6d\x09\xd7\xc5\xbf\x0a\xdb\x50\xe0\x1c".
"\x83\xca\xf8\xcf\xa7\x81\xbb\x0b\xe6\xcf\x1b\x0e\x0a\xe0\xcd".
"\x68\xe2\xde\xc4\x2d\xba\x55\xc7\xc7\x1e\x6c\x5e\xca\x9b\x20".
"\x75\x96\x94\x92\x84\xec\xf5\x22\x25\x78\x67\xcd\xbe\x01\xfe".
"\x53\xa5\xcc\x6a\x40\x33\x83\xa4\x7a\x44\x93\x0b\xf9\x4c\xb2".
"\x95\xb6\x7e\x4b\xa4\xc8\x86\xfe\x8a\xf1\x77\x40\x56\x13\xc1".
"\x31\x2c\x8c\x4a\xa8\x89\x61\x0c\x39\x33\x78\x8c\xd5\x50\x3b".
"\x89\xc3\xd3\x80\x1c\xa7\xb6\x36\xc2\x00\x8d\x0a\x7f\xcc\xd3".
"\x20\x74\x60\x70\x36\x7d\xda\xdc\xc4\x49\x04\xf0\xe6\x6c\xd1".
"\xbe\xcb\xfb\xf1\xa2\xd6\xd4\xe4\x97\x3f\x35\x09\x5b\xda\x06".
"\x6b\x6d\x86\x53\x23\x0c\x26\x51\x2a\x15\xaa\xe2\x73\xfb\xc7".
"\x41\x54\xdc\x5d\x99\x0b\x0a\x1e\xd4\xdb\x70\xa3\x8e\xfd\x5b".
"\xf0\xa8\x3e\x9b\xff\x57\x98\xbc\xd9\x2a\x56\xd3\x19\xf9\x0b".
"\xd9\x67\x0f\x10\x9c\x23\xe5\x6b\x12\xc6\xb6\x4b\xd1\x0c\xe9".
"\x45\x36\xdf\x54\x6f\xcc\xfe\xb5\xcc\xb9\xfe\xde\xc8\xb5\xc9".
"\x04\x59\x61\x75\x1e\x72\x37\x54\xfd\xc6\xc3\x7e\x74\xae\x55".
"\x31\x6a\xbc\x8a\xd8\x45\x91\xe2\x8d\x20\x97\x71\xe7\x55\xd6".
"\x8a\xb8\x82\x2a\x27\x4f\xdc\x53\x89\x28\xf7\x3a\xfe\x07\xef".
"\x60\xb2\x32\x7c\xbc\x13\xc4\x3d\xda\xd7\xfb\xb8\x61\x7d\x69".
"\xae\x0e\x9a\x71\xd6\x00\x26\x97\xff\xdb\xe6\xbe\x45\x7a\xb5".
"\x00\x31\xfd\x70\xcc\xd7\x34\x88\xe4\x05\x61\xf5\x72\x1d\x14".
"\xf0\x7e\x90\xdb\x0e\xc7\xda\xd4\xf3\x99\xd4\x60\xd9\xa7\xc8".
"\x5b\x33\x34\xb5\x23\x74\x2c\x5f\x6b\x56\x95\x9c\x1b\x2a\xac".
"\xf9\xfe\x46\xc3\xf1\x9b\x24\x7e\x4b\xca\x25\x58\x41\x10\x63".
"\xe8\xe7\x68\xda\xcc\xb6\x4d\x5b\x8f\xc9\xa9\x31\xeb\x5c\x2a".
"\xcf\x9d\x89\xd5\x51\x93\x80\x30\xf4\xc9\x2c\x8c\xb8\x8c\x62".
"\xd6\x33\xbd\x95\x9f\xfa\x19\xf2\x48\x28\x09\x73\xc9\x53\x61".
"\x94\x3a\x62\x68\x6c\xc6\xd6\x0a\xb4\xae\x27\x96\xfb\x29\xd7".
"\x46\x67\x11\x7a\xe8\x3a\x9a\x3f\xf4\x9a\x75\xed\x24\x67\x45".
"\x79\xdc\x8b\x19\xf2\xef\x57\xaa\xc7\x84\xff\x9d\x2d\xc3\xa8".
"\x85\x54\xb7\x9d\xe1\xd6\x2b\xe9\x31\x9d\x6c\xb8\x4e\x76\x50".
"\x80\x44\x46\x8f\x5e\x7e\x20\xaa\xa0\x8a\x36\x6b\xef\xd1\x75".
"\xf8\x3f\x20\xdd\x09\x73\xbf\xa5\xf7\xb4\x87\xb2\x44\xc0\x0f".
"\x10\xc0\x95\x2e\x8a\x42\xfa\xc3\x49\x17\xb9\xb5\x1a\xc3\x80".
"\x93\x0c\xd8\xe3\xcd\xa4\x38\x61\x7a\x22\x73\x8e\x32\x8f\x55".
"\x9c\x91\x08\xd9\x65\xa9\x02\x28\xc6\x59\xc8\x51\x32\x20\x48".
"\xea\x2c\xae\x0e\xa6\x35\x5b\xe2\x63\xf9\xf2\x9d\x5f\xe3\x45".
"\xdc\x41\xba\xfb\x40\xcc\x8d\xde\x6c\x3d\x50\x97\x9d\x83\xa0".
"\xda\x41\x61\xba\xaf\xf8\x74\xd2\x21\x7b\x09\xcc\x83\xe1\x08".
"\x01\x04\x42\xce\xcb\xec\x1d\x6b\xb7\x6f\x0f\x4b\xd4\x53\x90".
"\x55\x3b\xcf\x9f\x93\xb8\xad\xce\x5f\x13\x83\xb3\x89\x6f\x5a".
"\x1b\xa4\xf5\x95\x4b\xb4\x22\x22\x1d\x35\xaa\xfa\xc7\x14\x8c".
"\xcd\x50\x66\x14\x47\xff\x67\xb2\xf8\x12\x09\xb3\x8a\xe5\x7d".
"\xb8\xc9\xe4\x89\xf7\xa4\xb5\x70\xfa\x2d\xeb\x95\x89\xec\xbb".
"\x49\x59\xd2\xc1\x6d\x0e\x06\xe4\x5e\xd5\x13\x13\x0d\x72\x6e".
"\xf0\x6d\xa9\xd5\xe7\x54\x68\x35\xcd\xd0\xd5\xa6\xe5\xb2\xe4".
"\xb1\x19\xe4\xf1\xe3\x8a\x56\x4c\x3b\x3d\xb8\x03\xfe\x22\x2f".
"\xc6\xdc\x88\x7b\xca\x5c\xc6\xdd\x17\x34\x08\x22\xf0\x17\x61".
"\x0e\x60\x9c\xb4\x27\x57\x30\x6e\xb8\x4f\xdd\x25\x7b\xef\x9e".
"\x8e\x88\x6b\xd8\x10\x23\xc2\x44\x53\x73\x64\x8f\x40\x22\xe1".
"\xe8\xa2\xb0\x3f\x8a\x07\x66\xcd\x64\x4f\x9c\x1e\x89\x76\x04".
"\x6d\xab\xc2\xbb\x16\x85\x80\x01\xa5\xb1\xe2\x12\x04\x2e\x39".
"\x87\x8c\xee\xbc\xfb\x07\x6d\x03\x4c\x3a\xa5\x7b\x95\xd9\xd7".
"\xd6\xee\x2b\xe9\xcb\xe6\xec\xa8\x84\x6a\x42\xf9\xb2\x25\xc8".
"\xf3\x6a\xaa\x34\x3b\xd9\x72\xd9\x70\x81\x3b\xd4\x5e\x66\x97".
"\x1b\xe6\x2b\x88\x71\x82\xa3\x8a\x98\xb0\x16\xd9\xbb\x97\x8b".
"\x57\x79\x41\x56\x6e\xc2\x8f\xdf\xfa\x5b\xc7\x68\x5b\xb8\x09".
"\x41\x31\x7c\x19\xe1\x95\x2e\x05\x4c\xac\x38\x81\xda\xb3\x8b".
"\x3e\x1c\x79\x9a\x31\xac\x3e\x3d\x6d\xab\xf3\x5a\x5e\xc7\x6e".
"\x8e\x39\xcd\x7b\x6f\x62\xee\xb9\x73\xdd\x82\x42\x6f\x09\xe4".
"\xc3\xae\x92\xe8\x18\x99\xa0\x5e\xa2\x12\xf4\xe2\xe0\xe6\x95".
"\x58\x3a\x45\xad\xfe\x23\x79\x5f\x82\xce\x95\x88\x73\xeb\x46".
"\xc8\x00\xac\xc3\x2a\xdc\x7e\xab\x9b\xf8\xbb\x46\x5c\xa8\x46".
"\xbc\xfd\x99\xae\x4c\xa7\x77\xeb\x7c\x58\xbf\xbb\x52\x68\x62".
"\x3d\x0b\x79\x64\x38\x65\xa7\xcb\x7b\xe9\xb2\x33\xb5\x59\x52".
"\x7b\x17\xb4\x02\x2b\x07\x0d\x3a\x11\x57\x92\xa5\x22\x2b\xbc".
"\xe6\x97\x05\x12\x05\xe7\x91\xe3\xfa\xae\x15\xbe\x20\xe5\x5c".
"\x71\x24\x80\x85\xc9\x66\xc1\x53\x5c\x8f\x08\xd4\x52\xe1\x10".
"\xb6\xd6\x20\x08\x01\x79\x33\x9f\x1b\xbd\xa0\xab\x7c\xb1\xd9".
"\xdc\xca\x44\x22\x49\xb7\xb7\x3d\x84\xac\x92\xf4\xfa\x0a\xc9".
"\xc5\xb2\x42\x2b\x9a\x63\xbb\x8a\x82\x04\x2f\xf7\xe9\x30\x05".
"\x67\x32\xd1\x41\x1a\x69\x6e\xb9\xf8\x5f\x6d\xb7\xe5\x4e\x85".
"\x21\xfa\x16\x8a\x44\xfd\xf6\xd9\xa2\x5f\x68\x2b\xf3\xe2\x3c".
"\x8a\x69\xd2\xc1\x38\xed\x83\xef\x0d\x53\x86\x93\x32\x23\xc6".
"\x14\x0c\xb0\xb6\x6e\x77\xa4\x20\x0f\xb1\x6e\xe2\xce\xca\x6f".
"\x93\x1c\x3a\x8f\xd0\xd2\x5a\x6e\x30\xd6\x8e\x5f\x4b\xa5\xef".
"\xa9\x62\xeb\x28\xa0\x5e\x3f\xc1\xbc\x0a\x68\xab\xd7\xfa\xa2".
"\xb7\x8f\x12\xb0\x99\xbc\x93\x20\xb8\x95\x8d\xca\xc7\xa7\xd9".
"\x2e\x19\xac\x06\xb9\x4e\x56\x8e\x74\xef\x2a\x04\xd8\x75\x04".
"\x38\x2a\xc7\xa0\xa4\x89\xf3\xa4\x8a\xd4\x2c\x2c\x58\x6f\x00".
"\x03\x23\xb8\xaf\x02\x48\x7d\x50\x46\x6f\x5a\x08\x41\xe3\x56".
"\x6d\xcb\xe2\x4f\xea\x8e\xab\x74\xcd\xf9\xef\xcf\xf9\x1e\xf1".
"\xf8\xb9\x6c\xaa\x3b\x37\xd1\x21\x42\x67\xec\xd6\x44\x55\x33".
"\xe8\x1d\xa4\x18\xf3\x73\x82\xb4\x50\x59\xc2\x34\x36\x05\xeb";
$expect = Filesystem::readFile(dirname(__FILE__).'/base85/expect2.txt');
$expect = trim($expect);
$this->assertEqual(
$expect,
ArcanistBundle::encodeBase85($data));
}
}
diff --git a/src/parser/__tests__/bundle.git.tgz b/src/parser/__tests__/bundle.git.tgz
index 9ad428c6..e3b267ef 100644
Binary files a/src/parser/__tests__/bundle.git.tgz and b/src/parser/__tests__/bundle.git.tgz differ
diff --git a/src/parser/__tests__/patches/1830a13adf764b55743f7edc6066451898d8ffa4.gitpatch b/src/parser/__tests__/patches/1830a13adf764b55743f7edc6066451898d8ffa4.gitpatch
new file mode 100644
index 00000000..be88b94e
--- /dev/null
+++ b/src/parser/__tests__/patches/1830a13adf764b55743f7edc6066451898d8ffa4.gitpatch
@@ -0,0 +1,12 @@
+diff --git a/koan2 b/koan2
+old mode 100644
+new mode 100755
+--- a/koan2
++++ b/koan2
+@@ -10,3 +10,5 @@
+ as moves if both files
+ are mostly similar. So
+ you need a lot of text.
++
++Execute!
+
diff --git a/src/parser/__tests__/patches/39c8e7dd3914edff087a6214f0cd996ad08e5b3d.gitpatch b/src/parser/__tests__/patches/39c8e7dd3914edff087a6214f0cd996ad08e5b3d.gitpatch
new file mode 100644
index 00000000..4c044b23
--- /dev/null
+++ b/src/parser/__tests__/patches/39c8e7dd3914edff087a6214f0cd996ad08e5b3d.gitpatch
@@ -0,0 +1,4 @@
+diff --git a/koan b/koan
+old mode 100644
+new mode 100755
+
diff --git a/src/parser/__tests__/patches/8ecc728bcc9b482a9a91527ea471b04fc1a025cf.gitpatch b/src/parser/__tests__/patches/8ecc728bcc9b482a9a91527ea471b04fc1a025cf.gitpatch
new file mode 100644
index 00000000..a45afaf4
--- /dev/null
+++ b/src/parser/__tests__/patches/8ecc728bcc9b482a9a91527ea471b04fc1a025cf.gitpatch
@@ -0,0 +1,6 @@
+diff --git a/text b/executable
+old mode 100644
+new mode 100755
+rename from text
+rename to executable
+
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 13:16 (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
556916
Default Alt Text
(105 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment