2011-01-10 00:22:25 +01:00
|
|
|
<?php
|
|
|
|
|
2011-02-19 20:36:08 +01:00
|
|
|
/**
|
|
|
|
* Interfaces with Subversion working copies.
|
|
|
|
*
|
|
|
|
* @group workingcopy
|
|
|
|
*/
|
2012-01-31 21:07:05 +01:00
|
|
|
final class ArcanistSubversionAPI extends ArcanistRepositoryAPI {
|
2011-01-10 00:22:25 +01:00
|
|
|
|
|
|
|
protected $svnStatus;
|
|
|
|
protected $svnBaseRevisions;
|
|
|
|
protected $svnInfo = array();
|
|
|
|
|
|
|
|
protected $svnInfoRaw = array();
|
|
|
|
protected $svnDiffRaw = array();
|
2011-02-25 01:34:27 +01:00
|
|
|
|
2011-02-23 21:06:22 +01:00
|
|
|
private $svnBaseRevisionNumber;
|
2012-12-15 00:49:01 +01:00
|
|
|
private $statusPaths = array();
|
2011-01-10 00:22:25 +01:00
|
|
|
|
|
|
|
public function getSourceControlSystemName() {
|
|
|
|
return 'svn';
|
|
|
|
}
|
|
|
|
|
2012-06-12 21:39:15 +02:00
|
|
|
public function getMetadataPath() {
|
2012-06-26 02:13:29 +02:00
|
|
|
static $svn_dir = null;
|
|
|
|
if ($svn_dir === null) {
|
|
|
|
// from svn 1.7, subversion keeps a single .svn directly under
|
|
|
|
// the working copy root. However, we allow .arcconfigs that
|
|
|
|
// aren't at the working copy root.
|
|
|
|
foreach (Filesystem::walkToRoot($this->getPath()) as $parent) {
|
|
|
|
$possible_svn_dir = Filesystem::resolvePath('.svn', $parent);
|
|
|
|
if (Filesystem::pathExists($possible_svn_dir)) {
|
|
|
|
$svn_dir = $possible_svn_dir;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $svn_dir;
|
2012-06-12 21:39:15 +02:00
|
|
|
}
|
|
|
|
|
2012-03-03 01:47:34 +01:00
|
|
|
protected function buildLocalFuture(array $argv) {
|
|
|
|
|
|
|
|
$argv[0] = 'svn '.$argv[0];
|
|
|
|
|
|
|
|
$future = newv('ExecFuture', $argv);
|
|
|
|
$future->setCWD($this->getPath());
|
|
|
|
return $future;
|
|
|
|
}
|
|
|
|
|
2012-12-17 21:53:28 +01:00
|
|
|
protected function buildCommitRangeStatus() {
|
|
|
|
// In SVN, the commit range is always "uncommitted changes", so these
|
|
|
|
// statuses are equivalent.
|
|
|
|
return $this->getUncommittedStatus();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function buildUncommittedStatus() {
|
2011-01-10 00:22:25 +01:00
|
|
|
return $this->getSVNStatus();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSVNBaseRevisions() {
|
|
|
|
if ($this->svnBaseRevisions === null) {
|
|
|
|
$this->getSVNStatus();
|
|
|
|
}
|
|
|
|
return $this->svnBaseRevisions;
|
|
|
|
}
|
|
|
|
|
2012-12-15 00:49:01 +01:00
|
|
|
public function limitStatusToPaths(array $paths) {
|
|
|
|
$this->statusPaths = $paths;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
public function getSVNStatus($with_externals = false) {
|
|
|
|
if ($this->svnStatus === null) {
|
2012-12-15 00:49:01 +01:00
|
|
|
if ($this->statusPaths) {
|
|
|
|
list($status) = $this->execxLocal(
|
|
|
|
'--xml status %Ls',
|
|
|
|
$this->statusPaths);
|
|
|
|
} else {
|
|
|
|
list($status) = $this->execxLocal('--xml status');
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
$xml = new SimpleXMLElement($status);
|
|
|
|
|
|
|
|
$externals = array();
|
|
|
|
$files = array();
|
|
|
|
|
Fix `arc diff x y` in SVN
Summary:
D4186 added an "svn status --xml x y" form to getSVNStatus(), but the parser doesn't work for multiple files, since we get multiple <target /> elements in the XML output. So, curently, `arc diff` works (one target, all files) and `arc diff x` works (one target, x) but `arc diff x y` does not (more than one target, hits the exception).
$ arc diff QUACK2 QUACK3
Exception
Expected exactly one XML status target.
Test Plan: Ran `arc diff QUACK2 QUACK3` in a working copy with modified QUACK2, QUACK3. Ran `arc diff`; `arc diff QUACK2`.
Reviewers: vrana, btrahan, codeblock, JThramer
Reviewed By: codeblock
CC: aran
Differential Revision: https://secure.phabricator.com/D4372
2013-01-09 18:11:26 +01:00
|
|
|
foreach ($xml->target as $target) {
|
|
|
|
$this->svnBaseRevisions = array();
|
|
|
|
foreach ($target->entry as $entry) {
|
|
|
|
$path = (string)$entry['path'];
|
2013-01-09 21:34:37 +01:00
|
|
|
// On Windows, we get paths with backslash directory separators here.
|
|
|
|
// Normalize them to the format everything else expects and generates.
|
|
|
|
if (phutil_is_windows()) {
|
|
|
|
$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
|
|
|
|
}
|
Fix `arc diff x y` in SVN
Summary:
D4186 added an "svn status --xml x y" form to getSVNStatus(), but the parser doesn't work for multiple files, since we get multiple <target /> elements in the XML output. So, curently, `arc diff` works (one target, all files) and `arc diff x` works (one target, x) but `arc diff x y` does not (more than one target, hits the exception).
$ arc diff QUACK2 QUACK3
Exception
Expected exactly one XML status target.
Test Plan: Ran `arc diff QUACK2 QUACK3` in a working copy with modified QUACK2, QUACK3. Ran `arc diff`; `arc diff QUACK2`.
Reviewers: vrana, btrahan, codeblock, JThramer
Reviewed By: codeblock
CC: aran
Differential Revision: https://secure.phabricator.com/D4372
2013-01-09 18:11:26 +01:00
|
|
|
$mask = 0;
|
|
|
|
|
|
|
|
$props = (string)($entry->{'wc-status'}[0]['props']);
|
|
|
|
$item = (string)($entry->{'wc-status'}[0]['item']);
|
|
|
|
|
|
|
|
$base = (string)($entry->{'wc-status'}[0]['revision']);
|
|
|
|
$this->svnBaseRevisions[$path] = $base;
|
|
|
|
|
|
|
|
switch ($props) {
|
|
|
|
case 'none':
|
|
|
|
case 'normal':
|
|
|
|
break;
|
|
|
|
case 'modified':
|
|
|
|
$mask |= self::FLAG_MODIFIED;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception("Unrecognized property status '{$props}'.");
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
|
Fix `arc diff x y` in SVN
Summary:
D4186 added an "svn status --xml x y" form to getSVNStatus(), but the parser doesn't work for multiple files, since we get multiple <target /> elements in the XML output. So, curently, `arc diff` works (one target, all files) and `arc diff x` works (one target, x) but `arc diff x y` does not (more than one target, hits the exception).
$ arc diff QUACK2 QUACK3
Exception
Expected exactly one XML status target.
Test Plan: Ran `arc diff QUACK2 QUACK3` in a working copy with modified QUACK2, QUACK3. Ran `arc diff`; `arc diff QUACK2`.
Reviewers: vrana, btrahan, codeblock, JThramer
Reviewed By: codeblock
CC: aran
Differential Revision: https://secure.phabricator.com/D4372
2013-01-09 18:11:26 +01:00
|
|
|
$mask |= $this->parseSVNStatus($item);
|
|
|
|
if ($item == 'external') {
|
|
|
|
$externals[] = $path;
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
|
Fix `arc diff x y` in SVN
Summary:
D4186 added an "svn status --xml x y" form to getSVNStatus(), but the parser doesn't work for multiple files, since we get multiple <target /> elements in the XML output. So, curently, `arc diff` works (one target, all files) and `arc diff x` works (one target, x) but `arc diff x y` does not (more than one target, hits the exception).
$ arc diff QUACK2 QUACK3
Exception
Expected exactly one XML status target.
Test Plan: Ran `arc diff QUACK2 QUACK3` in a working copy with modified QUACK2, QUACK3. Ran `arc diff`; `arc diff QUACK2`.
Reviewers: vrana, btrahan, codeblock, JThramer
Reviewed By: codeblock
CC: aran
Differential Revision: https://secure.phabricator.com/D4372
2013-01-09 18:11:26 +01:00
|
|
|
// This is new in or around Subversion 1.6.
|
|
|
|
$tree_conflicts = ($entry->{'wc-status'}[0]['tree-conflicted']);
|
|
|
|
if ((string)$tree_conflicts) {
|
|
|
|
$mask |= self::FLAG_CONFLICT;
|
|
|
|
}
|
2011-03-20 23:06:55 +01:00
|
|
|
|
Fix `arc diff x y` in SVN
Summary:
D4186 added an "svn status --xml x y" form to getSVNStatus(), but the parser doesn't work for multiple files, since we get multiple <target /> elements in the XML output. So, curently, `arc diff` works (one target, all files) and `arc diff x` works (one target, x) but `arc diff x y` does not (more than one target, hits the exception).
$ arc diff QUACK2 QUACK3
Exception
Expected exactly one XML status target.
Test Plan: Ran `arc diff QUACK2 QUACK3` in a working copy with modified QUACK2, QUACK3. Ran `arc diff`; `arc diff QUACK2`.
Reviewers: vrana, btrahan, codeblock, JThramer
Reviewed By: codeblock
CC: aran
Differential Revision: https://secure.phabricator.com/D4372
2013-01-09 18:11:26 +01:00
|
|
|
$files[$path] = $mask;
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($files as $path => $mask) {
|
|
|
|
foreach ($externals as $external) {
|
|
|
|
if (!strncmp($path, $external, strlen($external))) {
|
|
|
|
$files[$path] |= self::FLAG_EXTERNALS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->svnStatus = $files;
|
|
|
|
}
|
|
|
|
|
|
|
|
$status = $this->svnStatus;
|
|
|
|
if (!$with_externals) {
|
|
|
|
foreach ($status as $path => $mask) {
|
|
|
|
if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) {
|
|
|
|
unset($status[$path]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
}
|
|
|
|
|
2012-11-09 04:29:40 +01:00
|
|
|
private function parseSVNStatus($item) {
|
|
|
|
switch ($item) {
|
|
|
|
case 'normal':
|
|
|
|
return 0;
|
|
|
|
case 'external':
|
|
|
|
return self::FLAG_EXTERNALS;
|
|
|
|
case 'unversioned':
|
|
|
|
return self::FLAG_UNTRACKED;
|
|
|
|
case 'obstructed':
|
|
|
|
return self::FLAG_OBSTRUCTED;
|
|
|
|
case 'missing':
|
|
|
|
return self::FLAG_MISSING;
|
|
|
|
case 'added':
|
|
|
|
return self::FLAG_ADDED;
|
|
|
|
case 'replaced':
|
|
|
|
// This is the result of "svn rm"-ing a file, putting another one
|
|
|
|
// in place of it, and then "svn add"-ing the new file. Just treat
|
|
|
|
// this as equivalent to "modified".
|
|
|
|
return self::FLAG_MODIFIED;
|
|
|
|
case 'modified':
|
|
|
|
return self::FLAG_MODIFIED;
|
|
|
|
case 'deleted':
|
|
|
|
return self::FLAG_DELETED;
|
|
|
|
case 'conflicted':
|
|
|
|
return self::FLAG_CONFLICT;
|
|
|
|
case 'incomplete':
|
|
|
|
return self::FLAG_INCOMPLETE;
|
|
|
|
default:
|
|
|
|
throw new Exception("Unrecognized item status '{$item}'.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-11-15 21:33:36 +01:00
|
|
|
public function addToCommit(array $paths) {
|
2013-01-23 22:53:30 +01:00
|
|
|
$add = array_filter($paths, 'Filesystem::pathExists');
|
|
|
|
if ($add) {
|
|
|
|
$this->execxLocal(
|
|
|
|
'add -- %Ls',
|
|
|
|
$add);
|
|
|
|
}
|
|
|
|
if ($add != $paths) {
|
|
|
|
$this->execxLocal(
|
|
|
|
'delete -- %Ls',
|
|
|
|
array_diff($paths, $add));
|
|
|
|
}
|
2012-11-15 21:33:36 +01:00
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
public function getSVNProperty($path, $property) {
|
|
|
|
list($stdout) = execx(
|
|
|
|
'svn propget %s %s@',
|
|
|
|
$property,
|
|
|
|
$this->getPath($path));
|
|
|
|
return trim($stdout);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSourceControlPath() {
|
|
|
|
return idx($this->getSVNInfo('/'), 'URL');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSourceControlBaseRevision() {
|
|
|
|
$info = $this->getSVNInfo('/');
|
2011-02-23 21:06:22 +01:00
|
|
|
return $info['URL'].'@'.$this->getSVNBaseRevisionNumber();
|
|
|
|
}
|
2011-02-25 01:34:27 +01:00
|
|
|
|
2012-03-09 23:40:47 +01:00
|
|
|
public function getCanonicalRevisionName($string) {
|
|
|
|
throw new ArcanistCapabilityNotSupportedException($this);
|
|
|
|
}
|
|
|
|
|
2011-02-23 21:06:22 +01:00
|
|
|
public function getSVNBaseRevisionNumber() {
|
|
|
|
if ($this->svnBaseRevisionNumber) {
|
|
|
|
return $this->svnBaseRevisionNumber;
|
|
|
|
}
|
|
|
|
$info = $this->getSVNInfo('/');
|
|
|
|
return $info['Revision'];
|
|
|
|
}
|
2011-02-25 01:34:27 +01:00
|
|
|
|
2011-02-23 21:06:22 +01:00
|
|
|
public function overrideSVNBaseRevisionNumber($effective_base_revision) {
|
|
|
|
$this->svnBaseRevisionNumber = $effective_base_revision;
|
|
|
|
return $this;
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getBranchName() {
|
|
|
|
return 'svn';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function buildInfoFuture($path) {
|
2011-01-11 23:26:21 +01:00
|
|
|
if ($path == '/') {
|
|
|
|
// When the root of a working copy is referenced by a symlink and you
|
|
|
|
// execute 'svn info' on that symlink, svn fails. This is a longstanding
|
|
|
|
// bug in svn:
|
|
|
|
//
|
|
|
|
// See http://subversion.tigris.org/issues/show_bug.cgi?id=2305
|
|
|
|
//
|
|
|
|
// To reproduce, do:
|
|
|
|
//
|
|
|
|
// $ ln -s working_copy working_link
|
2011-01-13 00:45:17 +01:00
|
|
|
// $ svn info working_copy # ok
|
2011-01-11 23:26:21 +01:00
|
|
|
// $ svn info working_link # fails
|
|
|
|
//
|
|
|
|
// Work around this by cd-ing into the directory before executing
|
|
|
|
// 'svn info'.
|
2012-07-17 02:28:13 +02:00
|
|
|
return $this->buildLocalFuture(array('info .'));
|
2011-01-11 23:26:21 +01:00
|
|
|
} else {
|
|
|
|
// Note: here and elsewhere we need to append "@" to the path because if
|
|
|
|
// a file has a literal "@" in it, everything after that will be
|
|
|
|
// interpreted as a revision. By appending "@" with no argument, SVN
|
|
|
|
// parses it properly.
|
2012-07-17 02:28:13 +02:00
|
|
|
return $this->buildLocalFuture(array('info %s@', $this->getPath($path)));
|
2011-01-11 23:26:21 +01:00
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function buildDiffFuture($path) {
|
|
|
|
// The "--depth empty" flag prevents us from picking up changes in
|
2011-11-03 21:42:19 +01:00
|
|
|
// children when we run 'diff' against a directory. Specifically, when a
|
|
|
|
// user has added or modified some directory "example/", we want to return
|
|
|
|
// ONLY changes to that directory when given it as a path. If we run
|
|
|
|
// without "--depth empty", svn will give us changes to the directory
|
|
|
|
// itself (such as property changes) and also give us changes to any
|
|
|
|
// files within the directory (basically, implicit recursion). We don't
|
|
|
|
// want that, so prevent recursive diffing.
|
2012-07-03 22:25:49 +02:00
|
|
|
$root = phutil_get_library_root('arcanist');
|
|
|
|
|
2012-07-17 02:28:13 +02:00
|
|
|
if (phutil_is_windows()) {
|
|
|
|
// TODO: Provide a binary_safe_diff script for Windows.
|
|
|
|
// TODO: Provide a diff command which can take lines of context somehow.
|
|
|
|
return $this->buildLocalFuture(
|
|
|
|
array(
|
|
|
|
'diff --depth empty %s',
|
|
|
|
$path,
|
|
|
|
));
|
|
|
|
} else {
|
|
|
|
$diff_bin = $root.'/../scripts/repository/binary_safe_diff.sh';
|
|
|
|
$diff_cmd = Filesystem::resolvePath($diff_bin);
|
|
|
|
return $this->buildLocalFuture(
|
|
|
|
array(
|
|
|
|
'diff --depth empty --diff-cmd %s -x -U%d %s',
|
|
|
|
$diff_cmd,
|
|
|
|
$this->getDiffLinesOfContext(),
|
|
|
|
$path,
|
|
|
|
));
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function primeSVNInfoResult($path, $result) {
|
|
|
|
$this->svnInfoRaw[$path] = $result;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function primeSVNDiffResult($path, $result) {
|
|
|
|
$this->svnDiffRaw[$path] = $result;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSVNInfo($path) {
|
|
|
|
|
|
|
|
if (empty($this->svnInfo[$path])) {
|
|
|
|
|
|
|
|
if (empty($this->svnInfoRaw[$path])) {
|
|
|
|
$this->svnInfoRaw[$path] = $this->buildInfoFuture($path)->resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
list($err, $stdout) = $this->svnInfoRaw[$path];
|
|
|
|
if ($err) {
|
|
|
|
throw new Exception(
|
|
|
|
"Error #{$err} executing svn info against '{$path}'.");
|
|
|
|
}
|
|
|
|
|
2012-07-17 02:28:13 +02:00
|
|
|
// TODO: Hack for Windows.
|
|
|
|
$stdout = str_replace("\r\n", "\n", $stdout);
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
$patterns = array(
|
|
|
|
'/^(URL): (\S+)$/m',
|
|
|
|
'/^(Revision): (\d+)$/m',
|
|
|
|
'/^(Last Changed Author): (\S+)$/m',
|
|
|
|
'/^(Last Changed Rev): (\d+)$/m',
|
|
|
|
'/^(Last Changed Date): (.+) \(.+\)$/m',
|
|
|
|
'/^(Copied From URL): (\S+)$/m',
|
|
|
|
'/^(Copied From Rev): (\d+)$/m',
|
2011-04-06 07:27:32 +02:00
|
|
|
'/^(Repository UUID): (\S+)$/m',
|
2012-03-14 07:42:05 +01:00
|
|
|
'/^(Node Kind): (\S+)$/m',
|
2011-01-10 00:22:25 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
$result = array();
|
|
|
|
foreach ($patterns as $pattern) {
|
|
|
|
$matches = null;
|
|
|
|
if (preg_match($pattern, $stdout, $matches)) {
|
|
|
|
$result[$matches[1]] = $matches[2];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($result['Last Changed Date'])) {
|
|
|
|
$result['Last Changed Date'] = strtotime($result['Last Changed Date']);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (empty($result)) {
|
|
|
|
throw new Exception('Unable to parse SVN info.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->svnInfo[$path] = $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->svnInfo[$path];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function getRawDiffText($path) {
|
|
|
|
$status = $this->getSVNStatus();
|
|
|
|
if (!isset($status[$path])) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$status = $status[$path];
|
|
|
|
|
|
|
|
// Build meaningful diff text for "svn copy" operations.
|
|
|
|
if ($status & ArcanistRepositoryAPI::FLAG_ADDED) {
|
|
|
|
$info = $this->getSVNInfo($path);
|
|
|
|
if (!empty($info['Copied From URL'])) {
|
|
|
|
return $this->buildSyntheticAdditionDiff(
|
|
|
|
$path,
|
|
|
|
$info['Copied From URL'],
|
|
|
|
$info['Copied From Rev']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we run "diff" on a binary file which doesn't have the "svn:mime-type"
|
|
|
|
// of "application/octet-stream", `diff' will explode in a rain of
|
|
|
|
// unhelpful hellfire as it tries to build a textual diff of the two
|
|
|
|
// files. We just fix this inline since it's pretty unambiguous.
|
|
|
|
// TODO: Move this to configuration?
|
|
|
|
$matches = null;
|
|
|
|
if (preg_match('/\.(gif|png|jpe?g|swf|pdf|ico)$/i', $path, $matches)) {
|
|
|
|
$mime = $this->getSVNProperty($path, 'svn:mime-type');
|
|
|
|
if ($mime != 'application/octet-stream') {
|
|
|
|
execx(
|
|
|
|
'svn propset svn:mime-type application/octet-stream %s',
|
|
|
|
$this->getPath($path));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (empty($this->svnDiffRaw[$path])) {
|
|
|
|
$this->svnDiffRaw[$path] = $this->buildDiffFuture($path)->resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
list($err, $stdout, $stderr) = $this->svnDiffRaw[$path];
|
|
|
|
|
|
|
|
// Note: GNU Diff returns 2 when SVN hands it binary files to diff and they
|
|
|
|
// differ. This is not an error; it is documented behavior. But SVN isn't
|
|
|
|
// happy about it. SVN will exit with code 1 and return the string below.
|
|
|
|
if ($err != 0 && $stderr !== "svn: 'diff' returned 2\n") {
|
|
|
|
throw new Exception(
|
|
|
|
"svn diff returned unexpected error code: $err\n".
|
|
|
|
"stdout: $stdout\n".
|
|
|
|
"stderr: $stderr");
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($err == 0 && empty($stdout)) {
|
|
|
|
// If there are no changes, 'diff' exits with no output, but that means
|
|
|
|
// we can not distinguish between empty and unmodified files. Build a
|
|
|
|
// synthetic "diff" without any changes in it.
|
|
|
|
return $this->buildSyntheticUnchangedDiff($path);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function buildSyntheticAdditionDiff($path, $source, $rev) {
|
|
|
|
$type = $this->getSVNProperty($path, 'svn:mime-type');
|
|
|
|
if ($type == 'application/octet-stream') {
|
|
|
|
return <<<EODIFF
|
|
|
|
Index: {$path}
|
|
|
|
===================================================================
|
|
|
|
Cannot display: file marked as a binary type.
|
|
|
|
svn:mime-type = application/octet-stream
|
|
|
|
|
|
|
|
EODIFF;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is_dir($this->getPath($path))) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = Filesystem::readFile($this->getPath($path));
|
|
|
|
list($orig) = execx('svn cat %s@%s', $source, $rev);
|
|
|
|
|
|
|
|
$src = new TempFile();
|
|
|
|
$dst = new TempFile();
|
|
|
|
Filesystem::writeFile($src, $orig);
|
|
|
|
Filesystem::writeFile($dst, $data);
|
|
|
|
|
|
|
|
list($err, $diff) = exec_manual(
|
|
|
|
'diff -L a/%s -L b/%s -U%d %s %s',
|
|
|
|
str_replace($this->getSourceControlPath().'/', '', $source),
|
|
|
|
$path,
|
|
|
|
$this->getDiffLinesOfContext(),
|
|
|
|
$src,
|
|
|
|
$dst);
|
|
|
|
|
|
|
|
if ($err == 1) { // 1 means there are differences.
|
|
|
|
return <<<EODIFF
|
|
|
|
Index: {$path}
|
|
|
|
===================================================================
|
|
|
|
{$diff}
|
|
|
|
|
|
|
|
EODIFF;
|
|
|
|
} else {
|
|
|
|
return $this->buildSyntheticUnchangedDiff($path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function buildSyntheticUnchangedDiff($path) {
|
|
|
|
$full_path = $this->getPath($path);
|
|
|
|
if (is_dir($full_path)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2012-03-14 07:42:05 +01:00
|
|
|
if (!file_exists($full_path)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
$data = Filesystem::readFile($full_path);
|
|
|
|
$lines = explode("\n", $data);
|
|
|
|
$len = count($lines);
|
|
|
|
foreach ($lines as $key => $line) {
|
|
|
|
$lines[$key] = ' '.$line;
|
|
|
|
}
|
|
|
|
$lines = implode("\n", $lines);
|
|
|
|
return <<<EODIFF
|
|
|
|
Index: {$path}
|
|
|
|
===================================================================
|
|
|
|
--- {$path} (synthetic)
|
|
|
|
+++ {$path} (synthetic)
|
|
|
|
@@ -1,{$len} +1,{$len} @@
|
|
|
|
{$lines}
|
|
|
|
|
|
|
|
EODIFF;
|
|
|
|
}
|
|
|
|
|
2012-11-09 08:22:29 +01:00
|
|
|
public function getAllFiles() {
|
|
|
|
// TODO: Handle paths with newlines.
|
|
|
|
$future = $this->buildLocalFuture(array('list -R'));
|
|
|
|
return new PhutilCallbackFilterIterator(
|
|
|
|
new LinesOfALargeExecFuture($future),
|
|
|
|
array($this, 'filterFiles'));
|
|
|
|
}
|
|
|
|
|
2012-11-09 08:26:27 +01:00
|
|
|
public function getChangedFiles($since_commit) {
|
2013-01-31 21:21:26 +01:00
|
|
|
$url = '';
|
|
|
|
$match = null;
|
|
|
|
if (preg_match('/(.*)@(.*)/', $since_commit, $match)) {
|
|
|
|
list(, $url, $since_commit) = $match;
|
|
|
|
}
|
2012-11-09 08:26:27 +01:00
|
|
|
// TODO: Handle paths with newlines.
|
|
|
|
list($stdout) = $this->execxLocal(
|
2013-01-31 21:21:26 +01:00
|
|
|
'--xml diff --revision %s:HEAD --summarize %s',
|
|
|
|
$since_commit,
|
|
|
|
$url);
|
2012-11-09 04:29:40 +01:00
|
|
|
$xml = new SimpleXMLElement($stdout);
|
|
|
|
|
2012-11-09 08:26:27 +01:00
|
|
|
$return = array();
|
2012-11-09 04:29:40 +01:00
|
|
|
foreach ($xml->paths[0]->path as $path) {
|
|
|
|
$return[(string)$path] = $this->parseSVNStatus($path['item']);
|
2012-11-09 08:26:27 +01:00
|
|
|
}
|
|
|
|
return $return;
|
|
|
|
}
|
|
|
|
|
2012-11-09 08:22:29 +01:00
|
|
|
public function filterFiles($path) {
|
|
|
|
// NOTE: SVN uses '/' also on Windows.
|
|
|
|
if ($path == '' || substr($path, -1) == '/') {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $path;
|
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
public function getBlame($path) {
|
|
|
|
$blame = array();
|
|
|
|
|
2012-07-17 02:28:13 +02:00
|
|
|
list($stdout) = $this->execxLocal('blame %s', $path);
|
2011-01-10 00:22:25 +01:00
|
|
|
|
|
|
|
$stdout = trim($stdout);
|
|
|
|
if (!strlen($stdout)) {
|
|
|
|
// Empty file.
|
|
|
|
return $blame;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (explode("\n", $stdout) as $line) {
|
|
|
|
$m = array();
|
|
|
|
if (!preg_match('/^\s*(\d+)\s+(\S+)/', $line, $m)) {
|
|
|
|
throw new Exception("Bad blame? `{$line}'");
|
|
|
|
}
|
|
|
|
$revision = $m[1];
|
|
|
|
$author = $m[2];
|
|
|
|
$blame[] = array($author, $revision);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $blame;
|
|
|
|
}
|
2011-01-13 00:45:17 +01:00
|
|
|
|
2011-01-11 22:02:38 +01:00
|
|
|
public function getOriginalFileData($path) {
|
|
|
|
// SVN issues warnings for nonexistent paths, directories, etc., but still
|
|
|
|
// returns no error code. However, for new paths in the working copy it
|
|
|
|
// fails. Assume that failure means the original file does not exist.
|
2012-07-17 02:28:13 +02:00
|
|
|
list($err, $stdout) = $this->execManualLocal('cat %s@', $path);
|
2011-01-11 22:02:38 +01:00
|
|
|
if ($err) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $stdout;
|
|
|
|
}
|
2011-01-13 00:45:17 +01:00
|
|
|
|
2011-01-11 22:02:38 +01:00
|
|
|
public function getCurrentFileData($path) {
|
|
|
|
$full_path = $this->getPath($path);
|
|
|
|
if (Filesystem::pathExists($full_path)) {
|
|
|
|
return Filesystem::readFile($full_path);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
|
2011-04-06 05:12:37 +02:00
|
|
|
public function getRepositorySVNUUID() {
|
|
|
|
$info = $this->getSVNInfo('/');
|
|
|
|
return $info['Repository UUID'];
|
|
|
|
}
|
|
|
|
|
2011-08-24 03:48:55 +02:00
|
|
|
public function getLocalCommitInformation() {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2012-06-04 21:30:50 +02:00
|
|
|
public function isHistoryDefaultImmutable() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function supportsAmend() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-12-17 21:54:08 +01:00
|
|
|
public function supportsCommitRanges() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function supportsLocalCommits() {
|
2011-09-15 03:44:54 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-03-07 22:02:53 +01:00
|
|
|
public function hasLocalCommit($commit) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2011-12-03 01:21:14 +01:00
|
|
|
public function getWorkingCopyRevision() {
|
|
|
|
return $this->getSourceControlBaseRevision();
|
|
|
|
}
|
|
|
|
|
Add an "arc merge" workflow
Summary:
This should support conservative rewrite policies in git fairly well, under an
assumed workflow of:
- Develop in local branches, never rewrite history.
- Commit with "-m" or by typing a brief, non-template commit message
describing the checkpoint.
- Provide rich information in the web console (reviewers, etc.)
- Finalize with "git checkout master && arc merge branch && git push" or some
flavor thereof.
This supports Mercurial somewhat. The major problem is that "hg merge" fails if
the local is a fastforward of the remote, at which point there's nowhere we can
throw the commit message. Oh well. Just push it and we'll do our best to link
them up based on local commit info.
I am increasingly forming an opinion that Mercurial is "saftey-scissors git".
But also maybe I have no clue what I'm doing. I just don't understand why anyone
would think it's a good idea to have a trunk consisting of ~50% known-broken
revisions, random checkpoint parts, whitespace changes, typo fixes, etc. If you
use git with branching you can avoid this by making a trunk out of merges or
with rebase/amend, but there seems to be no way to have "one commit = one idea"
in any real sense in Mercurial.
Test Plan: Execute "arc merge" in git and mercurial.
Reviewers: fratrik, Makinde, aran, jungejason, tuomaspelkonen
Reviewed By: Makinde
CC: aran, epriestley, Makinde
Differential Revision: 860
2011-08-26 01:02:03 +02:00
|
|
|
public function supportsLocalBranchMerge() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
Automatically detect when to mark revisions committed
Summary:
See D945. We have this kludgy "remote_hooks_installed" mess right now, but we
have enough information on the server nowadays to figure this out without it.
Also reduce code duplication by sharing the "mark-committed" workflow.
This causes "arc merge" to effect commit marks.
Test Plan:
In Git, Mercurial and SVN working copies ran like a million
amend/merge/commit/mark-committed commands with and without --finalize in
various states of revision completion.
This change is really hard to exhaustively test because of the number of
combinations of VCS, revision state, command, command flags, repository state
and tracking state. All the reasonable tests I could come up with worked
correctly, though.
Reviewers: Makinde, jungejason, nh, tuomaspelkonen, aran
Reviewed By: jungejason
CC: aran, jungejason
Differential Revision: 967
2011-09-27 18:37:11 +02:00
|
|
|
public function getFinalizedRevisionMessage() {
|
|
|
|
// In other VCSes we give push instructions here, but it never makes sense
|
|
|
|
// in SVN.
|
|
|
|
return "Done.";
|
|
|
|
}
|
|
|
|
|
2012-01-24 17:07:38 +01:00
|
|
|
public function loadWorkingCopyDifferentialRevisions(
|
|
|
|
ConduitClient $conduit,
|
|
|
|
array $query) {
|
|
|
|
|
|
|
|
// We don't have much to go on in SVN, look for revisions that came from
|
2012-04-11 01:06:57 +02:00
|
|
|
// this directory and belong to the same project.
|
2012-01-24 17:07:38 +01:00
|
|
|
|
2012-04-11 01:06:57 +02:00
|
|
|
$project = $this->getWorkingCopyIdentity()->getProjectID();
|
Allow 'arc' to run without '.arcconfig'
Summary:
This is mostly an onboarding thing, but also allows "arc upload", "arc download", and "arc paste" to work anywhere on the system.
- Try to read the Phabricator install URI from arc global config if we can't find ".arcconfig".
- Build a WorkingCopy anyway if we can't find ".arcconfig", as long as we can find ".svn", ".git", or ".hg".
- Make all the workflows handle "no project ID" at least somewhat gracefully.
Test Plan:
- Ran "arc diff" in .arcconfig-less Mercurial, Git, and Subversion working copies.
- Ran "arc upload" and "arc download" from my desktop.
- Ran "arc paste" from somewhere random.
- Cleared my config and hit the error, got useful instructions.
Reviewers: btrahan, csilvers
Reviewed By: csilvers
CC: aran
Differential Revision: https://secure.phabricator.com/D2424
2012-05-08 00:24:58 +02:00
|
|
|
if (!$project) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
2012-01-24 17:07:38 +01:00
|
|
|
$results = $conduit->callMethodSynchronous(
|
|
|
|
'differential.query',
|
2012-04-11 01:06:57 +02:00
|
|
|
$query + array(
|
2012-04-12 19:32:54 +02:00
|
|
|
'arcanistProjects' => array($project),
|
2012-04-11 01:06:57 +02:00
|
|
|
));
|
2012-01-24 17:07:38 +01:00
|
|
|
|
|
|
|
foreach ($results as $key => $result) {
|
|
|
|
if ($result['sourcePath'] != $this->getPath()) {
|
|
|
|
unset($results[$key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-05-11 15:07:33 +02:00
|
|
|
foreach ($results as $key => $result) {
|
|
|
|
$results[$key]['why'] =
|
|
|
|
"Matching arcanist project name and working copy directory path.";
|
|
|
|
}
|
|
|
|
|
2012-01-24 17:07:38 +01:00
|
|
|
return $results;
|
|
|
|
}
|
|
|
|
|
2012-03-16 21:40:33 +01:00
|
|
|
public function updateWorkingCopy() {
|
|
|
|
$this->execxLocal('up');
|
|
|
|
}
|
|
|
|
|
2013-01-28 23:11:31 +01:00
|
|
|
public static function escapeFileNamesForSVN(array $files) {
|
2013-02-15 23:53:25 +01:00
|
|
|
foreach ($files as $k => $file) {
|
|
|
|
$files[$k] = self::escapeFileNameForSVN($file);
|
|
|
|
}
|
|
|
|
return $files;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function escapeFileNameForSVN($file) {
|
2013-01-28 23:11:31 +01:00
|
|
|
// SVN interprets "x@1" as meaning "file x at revision 1", which is not
|
|
|
|
// intended for files named "sprite@2x.png" or similar. For files with an
|
|
|
|
// "@" in their names, escape them by adding "@" at the end, which SVN
|
|
|
|
// interprets as "at the working copy revision". There is a special case
|
|
|
|
// where ".@" means "fail with an error" instead of ". at the working copy
|
|
|
|
// revision", so avoid escaping "." into ".@".
|
2013-02-15 23:53:25 +01:00
|
|
|
|
|
|
|
if (strpos($file, '@') !== false) {
|
|
|
|
$file = $file.'@';
|
2013-01-28 23:11:31 +01:00
|
|
|
}
|
2013-02-15 23:53:25 +01:00
|
|
|
|
|
|
|
return $file;
|
2013-01-28 23:11:31 +01:00
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|