1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-28 17:52:42 +01:00

Basic Mercurial support for Arcanist

Summary:
There's a lot of ground left to cover but this makes "arc diff" work (on one
trivial diff) in my sandbox, at least, and supports parsing of Mercurial native
diffs (which are unified + a custom header). Piles of missing features, still.
Some of this is blocked by me not understanding the mercurial model well yet.

This is also a really good opportunity for cleanup (especially, reducing the
level of "instanceof" in the diff workflow), I'll try to do a bunch of that in
followup diffs.

Test Plan: Ran "arc diff" in a mercurial repository, got a diff out of it.
Reviewed By: aran
Reviewers: Makinde, jungejason, tuomaspelkonen, aran, codeblock
CC: aran, epriestley, codeblock, fratrik
Differential Revision: 792
This commit is contained in:
epriestley 2011-08-09 09:00:29 -07:00
parent 40b445b387
commit 268de6428c
8 changed files with 249 additions and 21 deletions

View file

@ -55,6 +55,7 @@ phutil_register_library_map(array(
'ArcanistLinterTestCase' => 'lint/linter/base/test', 'ArcanistLinterTestCase' => 'lint/linter/base/test',
'ArcanistListWorkflow' => 'workflow/list', 'ArcanistListWorkflow' => 'workflow/list',
'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed', 'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed',
'ArcanistMercurialAPI' => 'repository/api/mercurial',
'ArcanistNoEffectException' => 'exception/usage/noeffect', 'ArcanistNoEffectException' => 'exception/usage/noeffect',
'ArcanistNoEngineException' => 'exception/usage/noengine', 'ArcanistNoEngineException' => 'exception/usage/noengine',
'ArcanistNoLintLinter' => 'lint/linter/nolint', 'ArcanistNoLintLinter' => 'lint/linter/nolint',
@ -118,6 +119,7 @@ phutil_register_library_map(array(
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase', 'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistNoEffectException' => 'ArcanistUsageException', 'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException', 'ArcanistNoEngineException' => 'ArcanistUsageException',
'ArcanistNoLintLinter' => 'ArcanistLinter', 'ArcanistNoLintLinter' => 'ArcanistLinter',

View file

@ -27,6 +27,7 @@ class ArcanistDiffParser {
protected $text; protected $text;
protected $line; protected $line;
protected $isGit; protected $isGit;
protected $isMercurial;
protected $detectBinaryFiles = false; protected $detectBinaryFiles = false;
protected $changes = array(); protected $changes = array();
@ -209,6 +210,9 @@ class ArcanistDiffParser {
'(?P<binary>Binary) files '. '(?P<binary>Binary) files '.
'(?P<old>.+)\s+\d{4}-\d{2}-\d{2} and '. '(?P<old>.+)\s+\d{4}-\d{2}-\d{2} and '.
'(?P<new>.+)\s+\d{4}-\d{2}-\d{2} differ.*', '(?P<new>.+)\s+\d{4}-\d{2}-\d{2} differ.*',
// This is a normal Mercurial text change, probably from "hg diff".
'(?P<type>diff -r) (?P<hgrev>[a-f0-9]+) (?P<cur>.+)',
); );
$ok = false; $ok = false;
@ -274,6 +278,10 @@ class ArcanistDiffParser {
$line = $this->nextLine(); $line = $this->nextLine();
$this->parseChangeset($change); $this->parseChangeset($change);
break; break;
case 'diff -r':
$this->setIsMercurial(true);
$this->parseIndexHunk($change);
break;
default: default:
$this->didFailParse("Unknown diff type."); $this->didFailParse("Unknown diff type.");
} }
@ -432,8 +440,19 @@ class ArcanistDiffParser {
return $this->isGit; 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) { protected function parseIndexHunk(ArcanistDiffChange $change) {
$is_git = $this->getIsGit(); $is_git = $this->getIsGit();
$is_mercurial = $this->getIsMercurial();
$is_svn = (!$is_git && !$is_mercurial);
$line = $this->getLine(); $line = $this->getLine();
if ($is_git) { if ($is_git) {
@ -532,19 +551,27 @@ class ArcanistDiffParser {
} }
$line = $this->getLine(); $line = $this->getLine();
$ok = preg_match('/^=+$/', $line) ||
($is_git && preg_match('/^index .*$/', $line)); if ($is_svn) {
if (!$ok) { $ok = preg_match('/^=+$/', $line);
if ($is_git) { if (!$ok) {
$this->didFailParse( $this->didFailParse("Expected '=======================' divider line.");
"Expected 'index af23f...a98bc' header line.");
} else { } else {
$this->didFailParse( // Adding an empty file in SVN can produce an empty line here.
"Expected '==========================' divider line."); $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 {
$line = $this->nextLine();
} }
} }
// Adding an empty file in SVN can produce an empty line here.
$line = $this->nextNonemptyLine();
// If there are files with only whitespace changes and -b or -w are // 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 // supplied as command-line flags to `diff', svn and git both produce
@ -596,14 +623,23 @@ class ArcanistDiffParser {
protected function parseHunkTarget() { protected function parseHunkTarget() {
$line = $this->getLine(); $line = $this->getLine();
$matches = null; $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( $ok = preg_match(
'@^[-+]{3} (?:[ab]/)?(?P<path>.*?)(?:\s*\(.*\))?$@', '@^[-+]{3} (?:[ab]/)?(?P<path>.*?)'.$remainder.'$@',
$line, $line,
$matches); $matches);
if (!$ok) { if (!$ok) {
$this->didFailParse( $this->didFailParse(
"Expected hunk target '+++ path/to/file.ext (revision N)'."); "Expected hunk target '+++ path/to/file.ext (revision N)'.");
} }
$this->nextLine(); $this->nextLine();
return $matches['path']; return $matches['path'];
} }

View file

@ -65,11 +65,21 @@ abstract class ArcanistRepositoryAPI {
"any parent directory. Create an '.arcconfig' file to configure arc."); "any parent directory. Create an '.arcconfig' file to configure arc.");
} }
if (@file_exists($root.'/.svn')) { if (Filesystem::pathExists($root.'/.svn')) {
phutil_require_module('arcanist', 'repository/api/subversion'); return newv('ArcanistSubversionAPI', array($root));
return new ArcanistSubversionAPI($root);
} }
if (Filesystem::pathExists($root.'/.hg')) {
// TODO: Stabilize and remove.
file_put_contents(
'php://stderr',
phutil_console_format(
"**WARNING:** Mercurial support is largely imaginary right now.\n"));
return newv('ArcanistMercurialAPI', array($root));
}
$git_root = self::discoverGitBaseDirectory($root); $git_root = self::discoverGitBaseDirectory($root);
if ($git_root) { if ($git_root) {
if (!Filesystem::pathsAreEquivalent($root, $git_root)) { if (!Filesystem::pathsAreEquivalent($root, $git_root)) {
@ -77,16 +87,16 @@ abstract class ArcanistRepositoryAPI {
"'.arcconfig' file is located at '{$root}', but working copy root ". "'.arcconfig' file is located at '{$root}', but working copy root ".
"is '{$git_root}'. Move '.arcconfig' file to the working copy root."); "is '{$git_root}'. Move '.arcconfig' file to the working copy root.");
} }
phutil_require_module('arcanist', 'repository/api/git');
return new ArcanistGitAPI($root); return newv('ArcanistGitAPI', array($root));
} }
throw new ArcanistUsageException( throw new ArcanistUsageException(
"The current working directory is not part of a working copy for a ". "The current working directory is not part of a working copy for a ".
"supported version control system (svn or git)."); "supported version control system (svn, git or mercurial).");
} }
protected function __construct($path) { public function __construct($path) {
$this->path = $path; $this->path = $path;
} }

View file

@ -8,8 +8,10 @@
phutil_require_module('arcanist', 'exception/usage'); phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'filesystem'); phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'future/exec'); phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistRepositoryAPI.php'); phutil_require_source('ArcanistRepositoryAPI.php');

View file

@ -97,7 +97,6 @@ class ArcanistGitAPI extends ArcanistRepositoryAPI {
} }
public function getRawDiffText($path) { public function getRawDiffText($path) {
$relative_commit = $this->getRelativeCommit();
$options = $this->getDiffFullOptions(); $options = $this->getDiffFullOptions();
list($stdout) = execx( list($stdout) = execx(
"(cd %s; git diff {$options} %s -- %s)", "(cd %s; git diff {$options} %s -- %s)",

View file

@ -0,0 +1,142 @@
<?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 the Mercurial working copies.
*
* @group workingcopy
*/
class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $status;
private $base;
public function getSourceControlSystemName() {
return 'hg';
}
public function getSourceControlBaseRevision() {
list($stdout) = execx(
'(cd %s && hg id -ir %s)',
$this->getPath(),
$this->getRelativeCommit());
return $stdout;
}
public function getSourceControlPath() {
return '/';
}
public function getBranchName() {
// TODO: I have nearly no idea how hg local branches work.
list($stdout) = execx(
'(cd %s && hg branch)',
$this->getPath());
return $stdout;
}
public function getRelativeCommit() {
// TODO: This is hardcoded.
return 'tip~1';
}
public function getBlame($path) {
list($stdout) = execx(
'(cd %s && hg blame -u -v -c --rev %s -- %s)',
$this->getPath(),
$this->getRelativeCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
$matches = null;
$ok = preg_match('^/\s*([^:]+?) [a-f0-9]{12}: (.*)$/', $line, $matches);
if (!$ok) {
throw new Exception("Unable to parse Mercurial blame line: {$line}");
}
$revision = $matches[2];
$author = trim($matches[1]);
$blame[] = array($author, $revision);
}
return $blame;
}
public function getWorkingCopyStatus() {
// TODO: This is critical and not yet implemented.
return array();
}
private function getDiffOptions() {
$options = array(
'-g',
'-U'.$this->getDiffLinesOfContext(),
);
return implode(' ', $options);
}
public function getRawDiffText($path) {
$options = $this->getDiffOptions();
list($stdout) = execx(
'(cd %s && hg diff %C --rev %s --rev tip -- %s)',
$this->getPath(),
$options,
$this->getRelativeCommit(),
$path);
return $stdout;
}
public function getFullMercurialDiff() {
$options = $this->getDiffOptions();
list($stdout) = execx(
'(cd %s && hg diff %C --rev %s --rev tip --)',
$this->getPath(),
$options,
$this->getRelativeCommit());
return $stdout;
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision($path, 'tip');
}
private function getFileDataAtRevision($path, $revision) {
list($stdout) = execx(
'(cd %s && hg cat --rev %s -- %s)',
$this->getPath(),
$path);
return $stdout;
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'repository/api/base');
phutil_require_module('phutil', 'future/exec');
phutil_require_source('ArcanistMercurialAPI.php');

View file

@ -280,8 +280,12 @@ EOTEXT
if ($info['uuid']) { if ($info['uuid']) {
$repo_uuid = $info['uuid']; $repo_uuid = $info['uuid'];
} }
} else { } else if ($repository_api instanceof ArcanistSubversionAPI) {
$repo_uuid = $repository_api->getRepositorySVNUUID(); $repo_uuid = $repository_api->getRepositorySVNUUID();
} else if ($repository_api instanceof ArcanistMercurialAPI) {
// TODO: Provide this information.
} else {
throw new Exception("Unsupported repository API!");
} }
$working_copy = $this->getWorkingCopy(); $working_copy = $this->getWorkingCopy();
@ -527,10 +531,18 @@ EOTEXT
} }
protected function shouldOnlyCreateDiff() { protected function shouldOnlyCreateDiff() {
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) { if ($repository_api instanceof ArcanistSubversionAPI) {
return true; return true;
} }
if ($repository_api instanceof ArcanistMercurialAPI) {
// TODO: This is unlikely to be correct since it excludes using local
// branching in Mercurial.
return true;
}
return $this->getArgument('preview') || return $this->getArgument('preview') ||
$this->getArgument('only'); $this->getArgument('only');
} }
@ -580,11 +592,19 @@ EOTEXT
} }
} }
} else { } else if ($repository_api instanceof ArcanistGitAPI) {
$this->parseGitRelativeCommit( $this->parseGitRelativeCommit(
$repository_api, $repository_api,
$this->getArgument('paths', array())); $this->getArgument('paths', array()));
$paths = $repository_api->getWorkingCopyStatus(); $paths = $repository_api->getWorkingCopyStatus();
} else if ($repository_api instanceof ArcanistMercurialAPI) {
// TODO: Unify this and the previous block.
// TODO: Parse the relative commit.
$paths = $repository_api->getWorkingCopyStatus();
} else {
throw new Exception("Unknown VCS!");
} }
foreach ($paths as $path => $mask) { foreach ($paths as $path => $mask) {
@ -669,6 +689,9 @@ EOTEXT
} }
$changes = $parser->parseDiff($diff); $changes = $parser->parseDiff($diff);
} else if ($repository_api instanceof ArcanistMercurialAPI) {
$diff = $repository_api->getFullMercurialDiff();
$changes = $parser->parseDiff($diff);
} else { } else {
throw new Exception("Repository API is not supported."); throw new Exception("Repository API is not supported.");
} }