1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-26 08:42:40 +01:00

Implement "arc:amended" and fix various problems with ArcanistMercurialAPI

Summary:
  - Implement "arc:amended", a base commit DSL rule which always selects HEAD (git) or `.` (hg) if it has "differential revision:" in the commit message. This is unambiguously correct in amend workflows, and can cover holes in other rules like "git:branch-unique(*)".
  - Fix a bunch of Mercurial stuff:
    - Our use of '.' is wrong, and based on a misunderstanding on my part of the behavior of `hg diff --rev . --rev .`, which means "ignore the second --rev flag", not ". means working directory state". As far as I know there's no explicit way to say "the working copy plus all its changes".
    - The `--prune` argument to "hg log" does not support symbolic names like ".^". Use revsets instead.
    - Reduce the number of times we need to run `hg branch`.
    - We can safely use "." to mean "the working copy revision", and do not need to do "hg --debug id" or similar.
    - Generally simplify some of the nonsense in the implementation left over from me having no idea how Mercurial works.

Test Plan:
Ran "arc which" in various scenarios in a mercurial working copy. I //think// I exercised all the changes.

Ran "arc which --base arc:amended" in hg and git working copies without "Differential Revision:" in head/. (no match) and with it (matched head/.).

Reviewers: dschleimer

Reviewed By: dschleimer

CC: Makinde, tido, phleet, aran

Maniphest Tasks: T1233

Differential Revision: https://secure.phabricator.com/D2876
This commit is contained in:
epriestley 2012-07-01 11:06:05 -07:00
parent 470e2eca67
commit 563230b0fb
5 changed files with 77 additions and 74 deletions

View file

@ -166,6 +166,7 @@ final class ArcanistBaseCommitParser {
case 'upstream': case 'upstream':
case 'outgoing': case 'outgoing':
case 'bookmark': case 'bookmark':
case 'amended':
return $this->api->resolveBaseCommitRule($rule, $source); return $this->api->resolveBaseCommitRule($rule, $source);
default: default:
$matches = null; $matches = null;

View file

@ -752,12 +752,12 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
"'git push', or 'git svn dcommit', or by printing and faxing it)."; "'git push', or 'git svn dcommit', or by printing and faxing it).";
} }
public function getCommitMessageForRevision($rev) { public function getCommitMessage($commit) {
list($message) = $this->execxLocal( list($message) = $this->execxLocal(
'log -n1 %s', 'log -n1 --format=%C %s --',
$rev); '%s%n%b',
$parser = new ArcanistDiffParser(); $commit);
return head($parser->parseDiff($message)); return $message;
} }
public function loadWorkingCopyDifferentialRevisions( public function loadWorkingCopyDifferentialRevisions(
@ -921,6 +921,18 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
"you specified '{$rule}' in your {$source} 'base' ". "you specified '{$rule}' in your {$source} 'base' ".
"configuration."); "configuration.");
return self::GIT_MAGIC_ROOT_COMMIT; return self::GIT_MAGIC_ROOT_COMMIT;
case 'amended':
$text = $this->getCommitMessage('HEAD');
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$text);
if ($message->getRevisionID()) {
$this->setBaseCommitExplanation(
"HEAD has been amended with 'Differential Revision:', ".
"as specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'HEAD^';
}
break;
case 'upstream': case 'upstream':
list($err, $upstream) = $this->execManualLocal( list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'"); "rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");

View file

@ -26,6 +26,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $status; private $status;
private $base; private $base;
private $relativeCommit; private $relativeCommit;
private $branch;
private $workingCopyRevision; private $workingCopyRevision;
private $localCommitInfo; private $localCommitInfo;
private $includeDirectoryStateInDiffs; private $includeDirectoryStateInDiffs;
@ -78,9 +79,11 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
public function getBranchName() { public function getBranchName() {
// TODO: I have nearly no idea how hg branches work. if (!$this->branch) {
list($stdout) = $this->execxLocal('branch'); list($stdout) = $this->execxLocal('branch');
return trim($stdout); $this->branch = trim($stdout);
}
return $this->branch;
} }
public function setRelativeCommit($commit) { public function setRelativeCommit($commit) {
@ -92,7 +95,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
$this->relativeCommit = $commit; $this->relativeCommit = $commit;
$this->dropCaches(); $this->status = null;
$this->localCommitInfo = null;
return $this; return $this;
} }
@ -114,7 +118,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
list($err, $stdout) = $this->execManualLocal( list($err, $stdout) = $this->execManualLocal(
'outgoing --branch `hg branch` --style default'); 'outgoing --branch %s --style default',
$this->getBranchName());
if (!$err) { if (!$err) {
$logs = ArcanistMercurialParser::parseMercurialLog($stdout); $logs = ArcanistMercurialParser::parseMercurialLog($stdout);
@ -187,11 +192,10 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
public function getLocalCommitInformation() { public function getLocalCommitInformation() {
if ($this->localCommitInfo === null) { if ($this->localCommitInfo === null) {
list($info) = $this->execxLocal( list($info) = $this->execxLocal(
"log --template '%C' --prune %s --rev %s --branch %s --", "log --template '%C' --rev %s --branch %s --",
"{node}\1{rev}\1{author}\1{date|rfc822date}\1". "{node}\1{rev}\1{author}\1{date|rfc822date}\1".
"{branch}\1{tag}\1{parents}\1{desc}\2", "{branch}\1{tag}\1{parents}\1{desc}\2",
$this->getRelativeCommit(), '(ancestors(.) - ancestors('.$this->getRelativeCommit().'))',
"ancestors({$this->getWorkingCopyRevision()})",
$this->getBranchName()); $this->getBranchName());
$logs = array_filter(explode("\2", $info)); $logs = array_filter(explode("\2", $info));
@ -356,26 +360,27 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
public function getRawDiffText($path) { public function getRawDiffText($path) {
$options = $this->getDiffOptions(); $options = $this->getDiffOptions();
// NOTE: In Mercurial, "--rev x" means "diff between x and the working
// copy state", while "--rev x..." means "diff between x and the working
// copy commit" (i.e., from 'x' to '.'). The latter excludes any dirty
// changes in the working copy.
$range = $this->getRelativeCommit();
if (!$this->includeDirectoryStateInDiffs) {
$range .= '...';
}
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
'diff %C --rev %s --rev %s -- %s', 'diff %C --rev %s -- %s',
$options, $options,
$this->getRelativeCommit(), $range,
$this->getDiffToRevision(),
$path); $path);
return $stdout; return $stdout;
} }
public function getFullMercurialDiff() { public function getFullMercurialDiff() {
$options = $this->getDiffOptions(); return $this->getRawDiffText('');
list($stdout) = $this->execxLocal(
'diff %C --rev %s --rev %s --',
$options,
$this->getRelativeCommit(),
$this->getDiffToRevision());
return $stdout;
} }
public function getOriginalFileData($path) { public function getOriginalFileData($path) {
@ -402,25 +407,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
public function getWorkingCopyRevision() { public function getWorkingCopyRevision() {
if ($this->workingCopyRevision === null) { return '.';
// In Mercurial, "tip" means the tip of the current branch, not what's in
// the working copy. The tip may be ahead of the working copy. We need to
// use "hg summary" to figure out what is actually in the working copy.
// For instance, "hg up 4 && arc diff" should not show commits 5 and
// above.
// Without arguments, "hg id" shows the current working directory's
// commit, and "--debug" expands it to a 40-character hash.
list($stdout) = $this->execxLocal('--debug id --id');
// Even with "--id", "hg id" will print a trailing "+" after the hash
// if the working copy is dirty (has uncommitted changes). We'll
// explicitly detect this later by calling getWorkingCopyStatus(); ignore
// it for now.
$stdout = trim($stdout);
$this->workingCopyRevision = rtrim($stdout, '+');
}
return $this->workingCopyRevision;
} }
public function isHistoryDefaultImmutable() { public function isHistoryDefaultImmutable() {
@ -449,6 +436,13 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
} }
public function getCommitMessage($commit) {
list($message) = $this->execxLocal(
'log --template={desc} --rev %s',
$commit);
return $message;
}
public function parseRelativeLocalCommit(array $argv) { public function parseRelativeLocalCommit(array $argv) {
if (count($argv) == 0) { if (count($argv) == 0) {
return; return;
@ -503,15 +497,15 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
public function getCommitMessageLog() { public function getCommitMessageLog() {
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
"log --template '{node}\\1{desc}\\0' --prune %s --rev %s --", "log --template '{node}\\2{desc}\\1' --rev %s --branch %s --",
$this->getRelativeCommit(), 'ancestors(.) - ancestors('.$this->getRelativeCommit().')',
"ancestors({$this->getWorkingCopyRevision()})"); $this->getBranchName());
$map = array(); $map = array();
$logs = explode("\0", trim($stdout)); $logs = explode("\1", trim($stdout));
foreach (array_filter($logs) as $log) { foreach (array_filter($logs) as $log) {
list($node, $desc) = explode("\1", $log); list($node, $desc) = explode("\2", $log);
$map[$node] = $desc; $map[$node] = $desc;
} }
@ -591,23 +585,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return $this; return $this;
} }
private function getDiffToRevision() {
$this->dropCaches();
if ($this->includeDirectoryStateInDiffs) {
// This is a magic Mercurial revision name which means "current
// directory state"; see lookup() in localrepo.py.
return '.';
} else {
return $this->getWorkingCopyRevision();
}
}
private function dropCaches() {
$this->status = null;
$this->localCommitInfo = null;
}
public function getCommitSummary($commit) { public function getCommitSummary($commit) {
if ($commit == 'null') { if ($commit == 'null') {
return '(The Empty Void)'; return '(The Empty Void)';
@ -670,6 +647,20 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
"'base' configuration."); "'base' configuration.");
return trim($outgoing_base); return trim($outgoing_base);
} }
case 'amended':
$text = $this->getCommitMessage('.');
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$text);
if ($message->getRevisionID()) {
$this->setBaseCommitExplanation(
"'.' has been amended with 'Differential Revision:', ".
"as specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
// NOTE: This should be safe because Mercurial doesn't support
// amend until 2.2.
return '.^';
}
break;
case 'bookmark': case 'bookmark':
$revset = $revset =
'limit('. 'limit('.

View file

@ -195,7 +195,7 @@ abstract class ArcanistRepositoryAPI {
throw new ArcanistCapabilityNotSupportedException($this); throw new ArcanistCapabilityNotSupportedException($this);
} }
public function getCommitMessageForRevision($revision) { public function getCommitMessage($commit) {
throw new ArcanistCapabilityNotSupportedException($this); throw new ArcanistCapabilityNotSupportedException($this);
} }
@ -221,17 +221,17 @@ abstract class ArcanistRepositoryAPI {
throw new ArcanistCapabilityNotSupportedException($this); throw new ArcanistCapabilityNotSupportedException($this);
} }
public function execxLocal($pattern /*, ... */) { public function execxLocal($pattern /* , ... */) {
$args = func_get_args(); $args = func_get_args();
return $this->buildLocalFuture($args)->resolvex(); return $this->buildLocalFuture($args)->resolvex();
} }
public function execManualLocal($pattern /*, ... */) { public function execManualLocal($pattern /* , ... */) {
$args = func_get_args(); $args = func_get_args();
return $this->buildLocalFuture($args)->resolve(); return $this->buildLocalFuture($args)->resolve();
} }
public function execFutureLocal($pattern /*, ... */) { public function execFutureLocal($pattern /* , ... */) {
$args = func_get_args(); $args = func_get_args();
return $this->buildLocalFuture($args); return $this->buildLocalFuture($args);
} }

View file

@ -1280,10 +1280,9 @@ EOTEXT
/** /**
* @task message * @task message
*/ */
private function getCommitMessageFromCommit($rev) { private function getCommitMessageFromCommit($commit) {
$change = $this->getRepositoryAPI()->getCommitMessageForRevision($rev); $text = $this->getRepositoryAPI()->getCommitMessage($commit);
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus( $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
$change->getMetadata('message'));
$message->pullDataFromConduit($this->getConduit()); $message->pullDataFromConduit($this->getConduit());
$this->validateCommitMessage($message); $this->validateCommitMessage($message);
return $message; return $message;