mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-12-11 08:06:12 +01:00
d57ea379c6
Summary: D1061 introduced a 'text file' check, but it fails under SVN for new directories. - Revert D1061 (This reverts commit b2cd18252701be2093b52652fb3d1d94c5df571e.) - Make getChangedLines() return null to indicate that the operation doesn't make sense. I think this was the intent of the code in the lint engine. - Fix a bug where running "arc lint" on a change in an SVN working copy from a subdirectory would fail. - Fix a bug where warnings with no line information were incorrectly discarded. Test Plan: - Ran "arc lint" in an SVN working copy with a new directory (no failure). - Forced FilenameLinter to always raise a warning. Added a binary file and ran "arc lint". The warning was reported for the new binary file, a new text file, and a new directory. Reviewers: jungejason, andrewjcg, nh, tuomaspelkonen, aran Reviewed By: andrewjcg CC: aran, andrewjcg, epriestley Differential Revision: 1076
505 lines
14 KiB
PHP
505 lines
14 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Copyright 2011 Facebook, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* Interfaces with Subversion working copies.
|
|
*
|
|
* @group workingcopy
|
|
*/
|
|
class ArcanistSubversionAPI extends ArcanistRepositoryAPI {
|
|
|
|
protected $svnStatus;
|
|
protected $svnBaseRevisions;
|
|
protected $svnInfo = array();
|
|
|
|
protected $svnInfoRaw = array();
|
|
protected $svnDiffRaw = array();
|
|
|
|
private $svnBaseRevisionNumber;
|
|
|
|
public function getSourceControlSystemName() {
|
|
return 'svn';
|
|
}
|
|
|
|
public function hasMergeConflicts() {
|
|
foreach ($this->getSVNStatus() as $path => $mask) {
|
|
if ($mask & self::FLAG_CONFLICT) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function getWorkingCopyStatus() {
|
|
return $this->getSVNStatus();
|
|
}
|
|
|
|
public function getSVNBaseRevisions() {
|
|
if ($this->svnBaseRevisions === null) {
|
|
$this->getSVNStatus();
|
|
}
|
|
return $this->svnBaseRevisions;
|
|
}
|
|
|
|
public function getSVNStatus($with_externals = false) {
|
|
if ($this->svnStatus === null) {
|
|
list($status) = execx('(cd %s && svn --xml status)', $this->getPath());
|
|
$xml = new SimpleXMLElement($status);
|
|
|
|
if (count($xml->target) != 1) {
|
|
throw new Exception("Expected exactly one XML status target.");
|
|
}
|
|
|
|
$externals = array();
|
|
$files = array();
|
|
|
|
$target = $xml->target[0];
|
|
$this->svnBaseRevisions = array();
|
|
foreach ($target->entry as $entry) {
|
|
$path = (string)$entry['path'];
|
|
$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}'.");
|
|
}
|
|
|
|
switch ($item) {
|
|
case 'normal':
|
|
break;
|
|
case 'external':
|
|
$mask |= self::FLAG_EXTERNALS;
|
|
$externals[] = $path;
|
|
break;
|
|
case 'unversioned':
|
|
$mask |= self::FLAG_UNTRACKED;
|
|
break;
|
|
case 'obstructed':
|
|
$mask |= self::FLAG_OBSTRUCTED;
|
|
break;
|
|
case 'missing':
|
|
$mask |= self::FLAG_MISSING;
|
|
break;
|
|
case 'added':
|
|
$mask |= self::FLAG_ADDED;
|
|
break;
|
|
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".
|
|
$mask |= self::FLAG_MODIFIED;
|
|
break;
|
|
case 'modified':
|
|
$mask |= self::FLAG_MODIFIED;
|
|
break;
|
|
case 'deleted':
|
|
$mask |= self::FLAG_DELETED;
|
|
break;
|
|
case 'conflicted':
|
|
$mask |= self::FLAG_CONFLICT;
|
|
break;
|
|
case 'incomplete':
|
|
$mask |= self::FLAG_INCOMPLETE;
|
|
break;
|
|
default:
|
|
throw new Exception("Unrecognized item status '{$item}'.");
|
|
}
|
|
|
|
// This is new in or around Subversion 1.6.
|
|
$tree_conflicts = (string)($entry->{'wc-status'}[0]['tree-conflicted']);
|
|
if ($tree_conflicts) {
|
|
$mask |= self::FLAG_CONFLICT;
|
|
}
|
|
|
|
$files[$path] = $mask;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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('/');
|
|
return $info['URL'].'@'.$this->getSVNBaseRevisionNumber();
|
|
}
|
|
|
|
public function getSVNBaseRevisionNumber() {
|
|
if ($this->svnBaseRevisionNumber) {
|
|
return $this->svnBaseRevisionNumber;
|
|
}
|
|
$info = $this->getSVNInfo('/');
|
|
return $info['Revision'];
|
|
}
|
|
|
|
public function overrideSVNBaseRevisionNumber($effective_base_revision) {
|
|
$this->svnBaseRevisionNumber = $effective_base_revision;
|
|
return $this;
|
|
}
|
|
|
|
public function getBranchName() {
|
|
return 'svn';
|
|
}
|
|
|
|
public function buildInfoFuture($path) {
|
|
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
|
|
// $ svn info working_copy # ok
|
|
// $ svn info working_link # fails
|
|
//
|
|
// Work around this by cd-ing into the directory before executing
|
|
// 'svn info'.
|
|
return new ExecFuture(
|
|
'(cd %s && svn info .)',
|
|
$this->getPath());
|
|
} 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.
|
|
return new ExecFuture(
|
|
'svn info %s@',
|
|
$this->getPath($path));
|
|
}
|
|
}
|
|
|
|
public function buildDiffFuture($path) {
|
|
// The "--depth empty" flag prevents us from picking up changes in
|
|
// 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.
|
|
return new ExecFuture(
|
|
'(cd %s; svn diff --depth empty --diff-cmd diff -x -U%d %s)',
|
|
$this->getPath(),
|
|
$this->getDiffLinesOfContext(),
|
|
$path);
|
|
}
|
|
|
|
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}'.");
|
|
}
|
|
|
|
$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',
|
|
'/^(Repository UUID): (\S+)$/m',
|
|
);
|
|
|
|
$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;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
public function getBlame($path) {
|
|
$blame = array();
|
|
|
|
list($stdout) = execx(
|
|
'(cd %s && svn blame %s)',
|
|
$this->getPath(),
|
|
$path);
|
|
|
|
$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;
|
|
}
|
|
|
|
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.
|
|
list($err, $stdout) = exec_manual(
|
|
'(cd %s && svn cat %s@)',
|
|
$this->getPath(),
|
|
$path);
|
|
if ($err) {
|
|
return null;
|
|
}
|
|
return $stdout;
|
|
}
|
|
|
|
public function getCurrentFileData($path) {
|
|
$full_path = $this->getPath($path);
|
|
if (Filesystem::pathExists($full_path)) {
|
|
return Filesystem::readFile($full_path);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function getRepositorySVNUUID() {
|
|
$info = $this->getSVNInfo('/');
|
|
return $info['Repository UUID'];
|
|
}
|
|
|
|
public function getLocalCommitInformation() {
|
|
return null;
|
|
}
|
|
|
|
public function supportsRelativeLocalCommits() {
|
|
return false;
|
|
}
|
|
|
|
public function supportsLocalBranchMerge() {
|
|
return false;
|
|
}
|
|
|
|
public function getFinalizedRevisionMessage() {
|
|
// In other VCSes we give push instructions here, but it never makes sense
|
|
// in SVN.
|
|
return "Done.";
|
|
}
|
|
|
|
}
|