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

Refactor commit ranges and base commit handling

Summary:
See D4049, D4096.

  - Move commit range storage from Mercurial and Git APIs to the base API.
  - Move caching up to the base level.
    - Store symbolic name and resolved name separately, so we can re-resolve the correct commit from the symbolic name after dirtying caches.
  - Rename `supportsRelativeLocalCommit()` to `supportsCommitRanges()` (old name wasn't very good, and not consistent with new terminology like the `--base` flag).
  - Rename `getRelativeCommit()` and `setRelativeCommit()` to `getBaseCommit()` and `setBaseCommit()`.
  - Introduce `reloadCommitRange()` and call it from `reloadWorkingCopy()`.

I think this fixes the problem in D4049, and provides a general solution for the class of problems we're running into here, with D4096. Specifically:

  - We no longer get dirty caches, as long as you call reloadWorkingCopy() after changing the working copy (or call a method which calls it for you).
  - We no longer get order-of-parsing-things problems, because setBaseCommit() reloads the appropriate caches.
  - We no longer get nasty effects from calling `requireCleanWorkingCopy()` too early.

Test Plan: This is pretty far-reaching and hard to test. Unit tests; ran various arc commands. :/

Reviewers: vrana

Reviewed By: vrana

CC: aran

Differential Revision: https://secure.phabricator.com/D4097
This commit is contained in:
epriestley 2012-12-17 12:54:08 -08:00
parent 2ae0cb797d
commit 0bf5c3603c
12 changed files with 368 additions and 345 deletions

View file

@ -7,7 +7,6 @@
*/ */
final class ArcanistGitAPI extends ArcanistRepositoryAPI { final class ArcanistGitAPI extends ArcanistRepositoryAPI {
private $relativeCommit = null;
private $repositoryHasNoCommits = false; private $repositoryHasNoCommits = false;
const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16; const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16;
@ -43,18 +42,13 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
return !$this->repositoryHasNoCommits; return !$this->repositoryHasNoCommits;
} }
public function setRelativeCommit($relative_commit) {
$this->relativeCommit = $relative_commit;
return $this;
}
public function getLocalCommitInformation() { public function getLocalCommitInformation() {
if ($this->repositoryHasNoCommits) { if ($this->repositoryHasNoCommits) {
// Zero commits. // Zero commits.
throw new Exception( throw new Exception(
"You can't get local commit information for a repository with no ". "You can't get local commit information for a repository with no ".
"commits."); "commits.");
} else if ($this->relativeCommit == self::GIT_MAGIC_ROOT_COMMIT) { } else if ($this->getBaseCommit() == self::GIT_MAGIC_ROOT_COMMIT) {
// One commit. // One commit.
$against = 'HEAD'; $against = 'HEAD';
} else { } else {
@ -79,7 +73,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
// this as being the commits X and Y. If we log "B..Y", we only show // this as being the commits X and Y. If we log "B..Y", we only show
// Y. With "Y --not B", we show X and Y. // Y. With "Y --not B", we show X and Y.
$against = csprintf('%s --not %s', 'HEAD', $this->getRelativeCommit()); $against = csprintf('%s --not %s', 'HEAD', $this->getBaseCommit());
} }
// NOTE: Windows escaping of "%" symbols apparently is inherently broken; // NOTE: Windows escaping of "%" symbols apparently is inherently broken;
@ -124,139 +118,153 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
return $commits; return $commits;
} }
public function getRelativeCommit() { protected function buildBaseCommit($symbolic_commit) {
if ($this->relativeCommit === null) { if ($symbolic_commit !== null) {
if ($symbolic_commit == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
// Detect zero-commit or one-commit repositories. There is only one
// relative-commit value that makes any sense in these repositories: the
// empty tree.
list($err) = $this->execManualLocal('rev-parse --verify HEAD^');
if ($err) {
list($err) = $this->execManualLocal('rev-parse --verify HEAD');
if ($err) {
$this->repositoryHasNoCommits = true;
}
$this->relativeCommit = self::GIT_MAGIC_ROOT_COMMIT;
if ($this->repositoryHasNoCommits) {
$this->setBaseCommitExplanation(
"the repository has no commits.");
} else {
$this->setBaseCommitExplanation(
"the repository has only one commit.");
}
return $this->relativeCommit;
}
if ($this->getBaseCommitArgumentRules() ||
$this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
$this->relativeCommit = $base;
return $this->relativeCommit;
}
$do_write = false;
$default_relative = null;
$working_copy = $this->getWorkingCopyIdentity();
if ($working_copy) {
$default_relative = $working_copy->getConfig(
'git.default-relative-commit');
$this->setBaseCommitExplanation( $this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ". "you explicitly specified the empty tree.");
"specified in 'git.default-relative-commit' in '.arcconfig'. This ". return $symbolic_commit;
"setting overrides other settings.");
} }
if (!$default_relative) { list($err, $merge_base) = $this->execManualLocal(
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$default_relative = trim($upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' (the Git upstream ".
"of the current branch) HEAD.");
}
}
if (!$default_relative) {
$default_relative = $this->readScratchFile('default-relative-commit');
$default_relative = trim($default_relative);
if ($default_relative) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in '.git/arc/default-relative-commit'.");
}
}
if (!$default_relative) {
// TODO: Remove the history lesson soon.
echo phutil_console_format(
"<bg:green>** Select a Default Commit Range **</bg>\n\n");
echo phutil_console_wrap(
"You're running a command which operates on a range of revisions ".
"(usually, from some revision to HEAD) but have not specified the ".
"revision that should determine the start of the range.\n\n".
"Previously, arc assumed you meant 'HEAD^' when you did not specify ".
"a start revision, but this behavior does not make much sense in ".
"most workflows outside of Facebook's historic git-svn workflow.\n\n".
"arc no longer assumes 'HEAD^'. You must specify a relative commit ".
"explicitly when you invoke a command (e.g., `arc diff HEAD^`, not ".
"just `arc diff`) or select a default for this working copy.\n\n".
"In most cases, the best default is 'origin/master'. You can also ".
"select 'HEAD^' to preserve the old behavior, or some other remote ".
"or branch. But you almost certainly want to select ".
"'origin/master'.\n\n".
"(Technically: the merge-base of the selected revision and HEAD is ".
"used to determine the start of the commit range.)");
$prompt = "What default do you want to use? [origin/master]";
$default = phutil_console_prompt($prompt);
if (!strlen(trim($default))) {
$default = 'origin/master';
}
$default_relative = $default;
$do_write = true;
}
list($object_type) = $this->execxLocal(
'cat-file -t %s',
$default_relative);
if (trim($object_type) !== 'commit') {
throw new Exception(
"Relative commit '{$default_relative}' is not the name of a commit!");
}
if ($do_write) {
// Don't perform this write until we've verified that the object is a
// valid commit name.
$this->writeScratchFile('default-relative-commit', $default_relative);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as you ".
"just specified.");
}
list($merge_base) = $this->execxLocal(
'merge-base %s HEAD', 'merge-base %s HEAD',
$default_relative); $symbolic_commit);
if ($err) {
throw new ArcanistUsageException(
"Unable to find any git commit named '{$symbolic_commit}' in ".
"this repository.");
}
$this->relativeCommit = trim($merge_base); $this->setBaseCommitExplanation(
"it is the merge-base of '{$symbolic_commit}' and HEAD, as you ".
"explicitly specified.");
return trim($merge_base);
} }
return $this->relativeCommit; // Detect zero-commit or one-commit repositories. There is only one
// relative-commit value that makes any sense in these repositories: the
// empty tree.
list($err) = $this->execManualLocal('rev-parse --verify HEAD^');
if ($err) {
list($err) = $this->execManualLocal('rev-parse --verify HEAD');
if ($err) {
$this->repositoryHasNoCommits = true;
}
if ($this->repositoryHasNoCommits) {
$this->setBaseCommitExplanation(
"the repository has no commits.");
} else {
$this->setBaseCommitExplanation(
"the repository has only one commit.");
}
return self::GIT_MAGIC_ROOT_COMMIT;
}
if ($this->getBaseCommitArgumentRules() ||
$this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
return $base;
}
$do_write = false;
$default_relative = null;
$working_copy = $this->getWorkingCopyIdentity();
if ($working_copy) {
$default_relative = $working_copy->getConfig(
'git.default-relative-commit');
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in 'git.default-relative-commit' in '.arcconfig'. This ".
"setting overrides other settings.");
}
if (!$default_relative) {
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$default_relative = trim($upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' (the Git upstream ".
"of the current branch) HEAD.");
}
}
if (!$default_relative) {
$default_relative = $this->readScratchFile('default-relative-commit');
$default_relative = trim($default_relative);
if ($default_relative) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in '.git/arc/default-relative-commit'.");
}
}
if (!$default_relative) {
// TODO: Remove the history lesson soon.
echo phutil_console_format(
"<bg:green>** Select a Default Commit Range **</bg>\n\n");
echo phutil_console_wrap(
"You're running a command which operates on a range of revisions ".
"(usually, from some revision to HEAD) but have not specified the ".
"revision that should determine the start of the range.\n\n".
"Previously, arc assumed you meant 'HEAD^' when you did not specify ".
"a start revision, but this behavior does not make much sense in ".
"most workflows outside of Facebook's historic git-svn workflow.\n\n".
"arc no longer assumes 'HEAD^'. You must specify a relative commit ".
"explicitly when you invoke a command (e.g., `arc diff HEAD^`, not ".
"just `arc diff`) or select a default for this working copy.\n\n".
"In most cases, the best default is 'origin/master'. You can also ".
"select 'HEAD^' to preserve the old behavior, or some other remote ".
"or branch. But you almost certainly want to select ".
"'origin/master'.\n\n".
"(Technically: the merge-base of the selected revision and HEAD is ".
"used to determine the start of the commit range.)");
$prompt = "What default do you want to use? [origin/master]";
$default = phutil_console_prompt($prompt);
if (!strlen(trim($default))) {
$default = 'origin/master';
}
$default_relative = $default;
$do_write = true;
}
list($object_type) = $this->execxLocal(
'cat-file -t %s',
$default_relative);
if (trim($object_type) !== 'commit') {
throw new Exception(
"Relative commit '{$default_relative}' is not the name of a commit!");
}
if ($do_write) {
// Don't perform this write until we've verified that the object is a
// valid commit name.
$this->writeScratchFile('default-relative-commit', $default_relative);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as you ".
"just specified.");
}
list($merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$default_relative);
return trim($merge_base);
} }
private function getDiffFullOptions($detect_moves_and_renames = true) { private function getDiffFullOptions($detect_moves_and_renames = true) {
@ -294,7 +302,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
$options = $this->getDiffFullOptions(); $options = $this->getDiffFullOptions();
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
"diff {$options} %s --", "diff {$options} %s --",
$this->getRelativeCommit()); $this->getBaseCommit());
return $stdout; return $stdout;
} }
@ -308,7 +316,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
$options = $this->getDiffFullOptions($detect_moves_and_renames); $options = $this->getDiffFullOptions($detect_moves_and_renames);
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
"diff {$options} %s -- %s", "diff {$options} %s -- %s",
$this->getRelativeCommit(), $this->getBaseCommit(),
$path); $path);
return $stdout; return $stdout;
} }
@ -336,7 +344,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
} }
public function getGitCommitLog() { public function getGitCommitLog() {
$relative = $this->getRelativeCommit(); $relative = $this->getBaseCommit();
if ($this->repositoryHasNoCommits) { if ($this->repositoryHasNoCommits) {
// No commits yet. // No commits yet.
return ''; return '';
@ -348,7 +356,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
// 2..N commits. // 2..N commits.
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
'log --first-parent --format=medium %s..HEAD', 'log --first-parent --format=medium %s..HEAD',
$this->getRelativeCommit()); $this->getBaseCommit());
} }
return $stdout; return $stdout;
} }
@ -357,14 +365,14 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
'log --format=medium -n%d %s', 'log --format=medium -n%d %s',
self::SEARCH_LENGTH_FOR_PARENT_REVISIONS, self::SEARCH_LENGTH_FOR_PARENT_REVISIONS,
$this->getRelativeCommit()); $this->getBaseCommit());
return $stdout; return $stdout;
} }
public function getSourceControlBaseRevision() { public function getSourceControlBaseRevision() {
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
'rev-parse %s', 'rev-parse %s',
$this->getRelativeCommit()); $this->getBaseCommit());
return rtrim($stdout, "\n"); return rtrim($stdout, "\n");
} }
@ -455,7 +463,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
list($stdout, $stderr) = $this->execxLocal( list($stdout, $stderr) = $this->execxLocal(
'diff %C --raw %s --', 'diff %C --raw %s --',
$this->getDiffBaseOptions(), $this->getDiffBaseOptions(),
$this->getRelativeCommit()); $this->getBaseCommit());
return $this->parseGitStatus($stdout); return $this->parseGitStatus($stdout);
} }
@ -477,6 +485,8 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
$this->execxLocal( $this->execxLocal(
'add -- %Ls', 'add -- %Ls',
$paths); $paths);
$this->reloadWorkingCopy();
return $this;
} }
public function doCommit($message) { public function doCommit($message) {
@ -497,6 +507,8 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
$this->execxLocal( $this->execxLocal(
'commit --amend --allow-empty -F %s', 'commit --amend --allow-empty -F %s',
$tmp_file); $tmp_file);
$this->reloadWorkingCopy();
return $this;
} }
public function getPreReceiveHookStatus($old_ref, $new_ref) { public function getPreReceiveHookStatus($old_ref, $new_ref) {
@ -563,7 +575,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
// TODO: 'git blame' supports --porcelain and we should probably use it. // TODO: 'git blame' supports --porcelain and we should probably use it.
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
'blame --date=iso -w -M %s -- %s', 'blame --date=iso -w -M %s -- %s',
$this->getRelativeCommit(), $this->getBaseCommit(),
$path); $path);
$blame = array(); $blame = array();
@ -597,7 +609,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
} }
public function getOriginalFileData($path) { public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit()); return $this->getFileDataAtRevision($path, $this->getBaseCommit());
} }
public function getCurrentFileData($path) { public function getCurrentFileData($path) {
@ -711,7 +723,11 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
return true; return true;
} }
public function supportsRelativeLocalCommits() { public function supportsCommitRanges() {
return true;
}
public function supportsLocalCommits() {
return true; return true;
} }
@ -726,33 +742,6 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
return true; return true;
} }
public function parseRelativeLocalCommit(array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException("Specify only one commit.");
}
$base = reset($argv);
if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$merge_base = $base;
$this->setBaseCommitExplanation(
"you explicitly specified the empty tree.");
} else {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$base);
if ($err) {
throw new ArcanistUsageException(
"Unable to find any git commit named '{$base}' in this repository.");
}
$this->setBaseCommitExplanation(
"it is the merge-base of '{$base}' and HEAD, as you explicitly ".
"specified.");
}
$this->setRelativeCommit(trim($merge_base));
}
public function getAllLocalChanges() { public function getAllLocalChanges() {
$diff = $this->getFullGitDiff(); $diff = $this->getFullGitDiff();
if (!strlen(trim($diff))) { if (!strlen(trim($diff))) {
@ -859,6 +848,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
public function updateWorkingCopy() { public function updateWorkingCopy() {
$this->execxLocal('pull'); $this->execxLocal('pull');
$this->reloadWorkingCopy();
} }
public function getCommitSummary($commit) { public function getCommitSummary($commit) {

View file

@ -7,10 +7,7 @@
*/ */
final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $base;
private $relativeCommit;
private $branch; private $branch;
private $workingCopyRevision;
private $localCommitInfo; private $localCommitInfo;
private $includeDirectoryStateInDiffs; private $includeDirectoryStateInDiffs;
private $rawDiffCache = array(); private $rawDiffCache = array();
@ -58,7 +55,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
public function getSourceControlBaseRevision() { public function getSourceControlBaseRevision() {
return $this->getCanonicalRevisionName($this->getRelativeCommit()); return $this->getCanonicalRevisionName($this->getBaseCommit());
} }
public function getCanonicalRevisionName($string) { public function getCanonicalRevisionName($string) {
@ -81,107 +78,99 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return $this->branch; return $this->branch;
} }
public function setRelativeCommit($commit) { public function didReloadCommitRange() {
try {
$commit = $this->getCanonicalRevisionName($commit);
} catch (Exception $ex) {
throw new ArcanistUsageException(
"Commit '{$commit}' is not a valid Mercurial commit identifier.");
}
$this->relativeCommit = $commit;
$this->status = null;
$this->localCommitInfo = null; $this->localCommitInfo = null;
return $this;
} }
public function getRelativeCommit() { protected function buildBaseCommit($symbolic_commit) {
if (empty($this->relativeCommit)) { if ($symbolic_commit !== null) {
try {
if ($this->getBaseCommitArgumentRules() || $commit = $this->getCanonicalRevisionName($commit);
$this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) { } catch (Exception $ex) {
$base = $this->resolveBaseCommit(); throw new ArcanistUsageException(
if (!$base) { "Commit '{$commit}' is not a valid Mercurial commit identifier.");
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
$this->relativeCommit = $base;
return $this->relativeCommit;
} }
$this->setBaseCommitExplanation("you specified it explicitly.");
list($err, $stdout) = $this->execManualLocal( return $commit;
'outgoing --branch %s --style default',
$this->getBranchName());
if (!$err) {
$logs = ArcanistMercurialParser::parseMercurialLog($stdout);
} else {
// Mercurial (in some versions?) raises an error when there's nothing
// outgoing.
$logs = array();
}
if (!$logs) {
$this->setBaseCommitExplanation(
"you have no outgoing commits, so arc assumes you intend to submit ".
"uncommitted changes in the working copy.");
// In Mercurial, we support operations against uncommitted changes.
$this->setRelativeCommit($this->getWorkingCopyRevision());
return $this->relativeCommit;
}
$outgoing_revs = ipull($logs, 'rev');
// This is essentially an implementation of a theoretical `hg merge-base`
// command.
$against = $this->getWorkingCopyRevision();
while (true) {
// NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
// new as of July 2011, so do this in a compatible way. Also, "hg log"
// and "hg outgoing" don't necessarily show parents (even if given an
// explicit template consisting of just the parents token) so we need
// to separately execute "hg parents".
list($stdout) = $this->execxLocal(
'parents --style default --rev %s',
$against);
$parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
list($p1, $p2) = array_merge($parents_logs, array(null, null));
if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
$against = $p1['rev'];
break;
} else if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
$against = $p2['rev'];
break;
} else if ($p1) {
$against = $p1['rev'];
} else {
// This is the case where you have a new repository and the entire
// thing is outgoing; Mercurial literally accepts "--rev null" as
// meaning "diff against the empty state".
$against = 'null';
break;
}
}
if ($against == 'null') {
$this->setBaseCommitExplanation(
"this is a new repository (all changes are outgoing).");
} else {
$this->setBaseCommitExplanation(
"it is the first commit reachable from the working copy state ".
"which is not outgoing.");
}
$this->setRelativeCommit($against);
} }
return $this->relativeCommit;
if ($this->getBaseCommitArgumentRules() ||
$this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
return $base;
}
list($err, $stdout) = $this->execManualLocal(
'outgoing --branch %s --style default',
$this->getBranchName());
if (!$err) {
$logs = ArcanistMercurialParser::parseMercurialLog($stdout);
} else {
// Mercurial (in some versions?) raises an error when there's nothing
// outgoing.
$logs = array();
}
if (!$logs) {
$this->setBaseCommitExplanation(
"you have no outgoing commits, so arc assumes you intend to submit ".
"uncommitted changes in the working copy.");
return $this->getWorkingCopyRevision();
}
$outgoing_revs = ipull($logs, 'rev');
// This is essentially an implementation of a theoretical `hg merge-base`
// command.
$against = $this->getWorkingCopyRevision();
while (true) {
// NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
// new as of July 2011, so do this in a compatible way. Also, "hg log"
// and "hg outgoing" don't necessarily show parents (even if given an
// explicit template consisting of just the parents token) so we need
// to separately execute "hg parents".
list($stdout) = $this->execxLocal(
'parents --style default --rev %s',
$against);
$parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
list($p1, $p2) = array_merge($parents_logs, array(null, null));
if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
$against = $p1['rev'];
break;
} else if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
$against = $p2['rev'];
break;
} else if ($p1) {
$against = $p1['rev'];
} else {
// This is the case where you have a new repository and the entire
// thing is outgoing; Mercurial literally accepts "--rev null" as
// meaning "diff against the empty state".
$against = 'null';
break;
}
}
if ($against == 'null') {
$this->setBaseCommitExplanation(
"this is a new repository (all changes are outgoing).");
} else {
$this->setBaseCommitExplanation(
"it is the first commit reachable from the working copy state ".
"which is not outgoing.");
}
return $against;
} }
public function getLocalCommitInformation() { public function getLocalCommitInformation() {
@ -190,7 +179,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
"log --template '%C' --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",
'(ancestors(.) - ancestors('.$this->getRelativeCommit().'))', '(ancestors(.) - ancestors('.$this->getBaseCommit().'))',
$this->getBranchName()); $this->getBranchName());
$logs = array_filter(explode("\2", $info)); $logs = array_filter(explode("\2", $info));
@ -271,7 +260,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
public function getBlame($path) { public function getBlame($path) {
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
'annotate -u -v -c --rev %s -- %s', 'annotate -u -v -c --rev %s -- %s',
$this->getRelativeCommit(), $this->getBaseCommit(),
$path); $path);
$blame = array(); $blame = array();
@ -355,6 +344,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
// Diffs are against ".", so we need to drop the cache if we change the // Diffs are against ".", so we need to drop the cache if we change the
// working copy. // working copy.
$this->rawDiffCache = array(); $this->rawDiffCache = array();
$this->branch = null;
} }
private function getDiffOptions() { private function getDiffOptions() {
@ -373,7 +363,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
// copy commit" (i.e., from 'x' to '.'). The latter excludes any dirty // copy commit" (i.e., from 'x' to '.'). The latter excludes any dirty
// changes in the working copy. // changes in the working copy.
$range = $this->getRelativeCommit(); $range = $this->getBaseCommit();
if (!$this->includeDirectoryStateInDiffs) { if (!$this->includeDirectoryStateInDiffs) {
$range .= '...'; $range .= '...';
} }
@ -399,7 +389,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
public function getOriginalFileData($path) { public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit()); return $this->getFileDataAtRevision($path, $this->getBaseCommit());
} }
public function getCurrentFileData($path) { public function getCurrentFileData($path) {
@ -438,7 +428,11 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
} }
} }
public function supportsRelativeLocalCommits() { public function supportsCommitRanges() {
return true;
}
public function supportsLocalCommits() {
return true; return true;
} }
@ -458,21 +452,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return $message; return $message;
} }
public function parseRelativeLocalCommit(array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException("Specify only one commit.");
}
$this->setBaseCommitExplanation("you explicitly specified it.");
// This does the "hg id" call we need to normalize/validate the revision
// identifier.
$this->setRelativeCommit(reset($argv));
}
public function getAllLocalChanges() { public function getAllLocalChanges() {
$diff = $this->getFullMercurialDiff(); $diff = $this->getFullMercurialDiff();
if (!strlen(trim($diff))) { if (!strlen(trim($diff))) {
@ -513,7 +492,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
public function getCommitMessageLog() { public function getCommitMessageLog() {
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
"log --template '{node}\\2{desc}\\1' --rev %s --branch %s --", "log --template '{node}\\2{desc}\\1' --rev %s --branch %s --",
'ancestors(.) - ancestors('.$this->getRelativeCommit().')', 'ancestors(.) - ancestors('.$this->getBaseCommit().')',
$this->getBranchName()); $this->getBranchName());
$map = array(); $map = array();
@ -593,6 +572,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
public function updateWorkingCopy() { public function updateWorkingCopy() {
$this->execxLocal('up'); $this->execxLocal('up');
$this->reloadWorkingCopy();
} }
private function getMercurialConfig($key, $default = null) { private function getMercurialConfig($key, $default = null) {
@ -611,6 +591,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
$this->execxLocal( $this->execxLocal(
'add -- %Ls', 'add -- %Ls',
$paths); $paths);
$this->reloadWorkingCopy();
} }
public function doCommit($message) { public function doCommit($message) {
@ -628,6 +609,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
$this->execxLocal( $this->execxLocal(
'commit --amend -l %s', 'commit --amend -l %s',
$tmp_file); $tmp_file);
$this->reloadWorkingCopy();
} }
public function setIncludeDirectoryStateInDiffs($include) { public function setIncludeDirectoryStateInDiffs($include) {

View file

@ -34,6 +34,9 @@ abstract class ArcanistRepositoryAPI {
private $uncommittedStatusCache; private $uncommittedStatusCache;
private $commitRangeStatusCache; private $commitRangeStatusCache;
private $symbolicBaseCommit;
private $resolvedBaseCommit;
abstract public function getSourceControlSystemName(); abstract public function getSourceControlSystemName();
public function getDiffLinesOfContext() { public function getDiffLinesOfContext() {
@ -257,6 +260,7 @@ abstract class ArcanistRepositoryAPI {
$this->commitRangeStatusCache = null; $this->commitRangeStatusCache = null;
$this->didReloadWorkingCopy(); $this->didReloadWorkingCopy();
$this->reloadCommitRange();
return $this; return $this;
} }
@ -310,7 +314,6 @@ abstract class ArcanistRepositoryAPI {
abstract public function getSourceControlPath(); abstract public function getSourceControlPath();
abstract public function isHistoryDefaultImmutable(); abstract public function isHistoryDefaultImmutable();
abstract public function supportsAmend(); abstract public function supportsAmend();
abstract public function supportsRelativeLocalCommits();
abstract public function getWorkingCopyRevision(); abstract public function getWorkingCopyRevision();
abstract public function updateWorkingCopy(); abstract public function updateWorkingCopy();
abstract public function getMetadataPath(); abstract public function getMetadataPath();
@ -334,6 +337,8 @@ abstract class ArcanistRepositoryAPI {
throw new ArcanistCapabilityNotSupportedException($this); throw new ArcanistCapabilityNotSupportedException($this);
} }
abstract public function supportsLocalCommits();
public function doCommit($message) { public function doCommit($message) {
throw new ArcanistCapabilityNotSupportedException($this); throw new ArcanistCapabilityNotSupportedException($this);
} }
@ -355,10 +360,6 @@ abstract class ArcanistRepositoryAPI {
throw new ArcanistCapabilityNotSupportedException($this); throw new ArcanistCapabilityNotSupportedException($this);
} }
public function parseRelativeLocalCommit(array $argv) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getCommitSummary($commit) { public function getCommitSummary($commit) {
throw new ArcanistCapabilityNotSupportedException($this); throw new ArcanistCapabilityNotSupportedException($this);
} }
@ -539,6 +540,47 @@ abstract class ArcanistRepositoryAPI {
/* -( Base Commits )------------------------------------------------------- */ /* -( Base Commits )------------------------------------------------------- */
abstract public function supportsCommitRanges();
final public function setBaseCommit($symbolic_commit) {
if (!$this->supportsCommitRanges()) {
throw new ArcanistCapabilityNotSupportedException($this);
}
$this->symbolicBaseCommit = $symbolic_commit;
$this->reloadCommitRange();
return $this;
}
final public function getBaseCommit() {
if (!$this->supportsCommitRanges()) {
throw new ArcanistCapabilityNotSupportedException($this);
}
if ($this->resolvedBaseCommit === null) {
$commit = $this->buildBaseCommit($this->symbolicBaseCommit);
$this->resolvedBaseCommit = $commit;
}
return $this->resolvedBaseCommit;
}
final public function reloadCommitRange() {
$this->resolvedBaseCommit = null;
$this->baseCommitExplanation = null;
$this->didReloadCommitRange();
return $this;
}
protected function didReloadCommitRange() {
return;
}
protected function buildBaseCommit($symbolic_commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function getBaseCommitExplanation() { public function getBaseCommitExplanation() {
return $this->baseCommitExplanation; return $this->baseCommitExplanation;

View file

@ -553,7 +553,11 @@ EODIFF;
return false; return false;
} }
public function supportsRelativeLocalCommits() { public function supportsCommitRanges() {
return false;
}
public function supportsLocalCommits() {
return false; return false;
} }

View file

@ -828,7 +828,7 @@ abstract class ArcanistBaseWorkflow {
if ($this->shouldAmend) { if ($this->shouldAmend) {
$commit = head($api->getLocalCommitInformation()); $commit = head($api->getLocalCommitInformation());
$api->amendCommit($commit['message']); $api->amendCommit($commit['message']);
} else if ($api->supportsRelativeLocalCommits()) { } else if ($api->supportsLocalCommits()) {
$api->doCommit(self::AUTO_COMMIT_TITLE); $api->doCommit(self::AUTO_COMMIT_TITLE);
} }
} }
@ -982,7 +982,12 @@ abstract class ArcanistBaseWorkflow {
protected function getChange($path) { protected function getChange($path) {
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) { // TODO: Very gross
$is_git = ($repository_api instanceof ArcanistGitAPI);
$is_hg = ($repository_api instanceof ArcanistMercurialAPI);
$is_svn = ($repository_api instanceof ArcanistSubversionAPI);
if ($is_svn) {
// NOTE: In SVN, we don't currently support a "get all local changes" // NOTE: In SVN, we don't currently support a "get all local changes"
// operation, so special case it. // operation, so special case it.
if (empty($this->changeCache[$path])) { if (empty($this->changeCache[$path])) {
@ -994,7 +999,7 @@ abstract class ArcanistBaseWorkflow {
} }
$this->changeCache[$path] = reset($changes); $this->changeCache[$path] = reset($changes);
} }
} else if ($repository_api->supportsRelativeLocalCommits()) { } else if ($is_git || $is_hg) {
if (empty($this->changeCache)) { if (empty($this->changeCache)) {
$changes = $repository_api->getAllLocalChanges(); $changes = $repository_api->getAllLocalChanges();
foreach ($changes as $change) { foreach ($changes as $change) {
@ -1006,7 +1011,7 @@ abstract class ArcanistBaseWorkflow {
} }
if (empty($this->changeCache[$path])) { if (empty($this->changeCache[$path])) {
if ($repository_api instanceof ArcanistGitAPI) { if ($is_git) {
// This can legitimately occur under git if you make a change, "git // This can legitimately occur under git if you make a change, "git
// commit" it, and then revert the change in the working copy and run // commit" it, and then revert the change in the working copy and run
// "arc lint". // "arc lint".
@ -1257,9 +1262,7 @@ abstract class ArcanistBaseWorkflow {
} }
} else { } else {
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
if ($rev) { $this->parseBaseCommitArgument(array($rev));
$repository_api->parseRelativeLocalCommit(array($rev));
}
$paths = $repository_api->getWorkingCopyStatus(); $paths = $repository_api->getWorkingCopyStatus();
foreach ($paths as $path => $flags) { foreach ($paths as $path => $flags) {
@ -1492,4 +1495,26 @@ abstract class ArcanistBaseWorkflow {
return $event; return $event;
} }
public function parseBaseCommitArgument(array $argv) {
if (!count($argv)) {
return;
}
$api = $this->getRepositoryAPI();
if (!$api->supportsCommitRanges()) {
throw new ArcanistUsageException(
"This version control system does not support commit ranges.");
}
if (count($argv) > 1) {
throw new ArcanistUsageException(
"Specify exactly one base commit. The end of the commit range is ".
"always the working copy state.");
}
$api->setBaseCommit(head($argv));
return $this;
}
} }

View file

@ -70,9 +70,7 @@ EOTEXT
$in_rev = $this->getArgument('rev'); $in_rev = $this->getArgument('rev');
if ($in_rev) { if ($in_rev) {
// Although selectPathsForWorkflow() may set this, we want to set it $this->parseBaseCommitArgument(array($in_rev));
// explicitly so we blame against the correct relative commit.
$repository_api->parseRelativeLocalCommit(array($in_rev));
} }
$paths = $this->selectPathsForWorkflow( $paths = $this->selectPathsForWorkflow(

View file

@ -605,16 +605,8 @@ EOTEXT
$repository_api->setBaseCommitArgumentRules( $repository_api->setBaseCommitArgumentRules(
$this->getArgument('base', '')); $this->getArgument('base', ''));
if ($repository_api->supportsRelativeLocalCommits()) { if ($repository_api->supportsCommitRanges()) {
$this->parseBaseCommitArgument($this->getArgument('paths'));
// Parse the relative commit as soon as we can, to avoid generating
// caches we need to drop later and expensive discovery operations
// (particularly in Mercurial).
$relative = $this->getArgument('paths');
if ($relative) {
$repository_api->parseRelativeLocalCommit($relative);
}
} }
} }
@ -821,10 +813,8 @@ EOTEXT
} }
} }
} else if ($repository_api->supportsRelativeLocalCommits()) {
$paths = $repository_api->getWorkingCopyStatus();
} else { } else {
throw new Exception("Unknown VCS!"); $paths = $repository_api->getWorkingCopyStatus();
} }
foreach ($paths as $path => $mask) { foreach ($paths as $path => $mask) {
@ -1275,9 +1265,9 @@ EOTEXT
$this->console->writeOut("Linting...\n"); $this->console->writeOut("Linting...\n");
try { try {
$argv = $this->getPassthruArgumentsAsArgv('lint'); $argv = $this->getPassthruArgumentsAsArgv('lint');
if ($repository_api->supportsRelativeLocalCommits()) { if ($repository_api->supportsCommitRanges()) {
$argv[] = '--rev'; $argv[] = '--rev';
$argv[] = $repository_api->getRelativeCommit(); $argv[] = $repository_api->getBaseCommit();
} }
$lint_workflow = $this->buildChildWorkflow('lint', $argv); $lint_workflow = $this->buildChildWorkflow('lint', $argv);
@ -1356,9 +1346,9 @@ EOTEXT
$this->console->writeOut("Running unit tests...\n"); $this->console->writeOut("Running unit tests...\n");
try { try {
$argv = $this->getPassthruArgumentsAsArgv('unit'); $argv = $this->getPassthruArgumentsAsArgv('unit');
if ($repository_api->supportsRelativeLocalCommits()) { if ($repository_api->supportsCommitRanges()) {
$argv[] = '--rev'; $argv[] = '--rev';
$argv[] = $repository_api->getRelativeCommit(); $argv[] = $repository_api->getBaseCommit();
} }
$unit_workflow = $this->buildChildWorkflow('unit', $argv); $unit_workflow = $this->buildChildWorkflow('unit', $argv);
$unit_result = $unit_workflow->run(); $unit_result = $unit_workflow->run();

View file

@ -177,8 +177,7 @@ EOTEXT
$parser->setRepositoryAPI($repository_api); $parser->setRepositoryAPI($repository_api);
if ($repository_api instanceof ArcanistGitAPI) { if ($repository_api instanceof ArcanistGitAPI) {
$repository_api->parseRelativeLocalCommit( $this->parseBaseCommitArgument($this->getArgument('paths'));
$this->getArgument('paths'));
$diff = $repository_api->getFullGitDiff(); $diff = $repository_api->getFullGitDiff();
$changes = $parser->parseDiff($diff); $changes = $parser->parseDiff($diff);
$authors = $this->getConduit()->callMethodSynchronous( $authors = $this->getConduit()->callMethodSynchronous(
@ -191,8 +190,7 @@ EOTEXT
$author_dict['realName'], $author_dict['realName'],
$repository_api->execxLocal('config user.email')); $repository_api->execxLocal('config user.email'));
} else if ($repository_api instanceof ArcanistMercurialAPI) { } else if ($repository_api instanceof ArcanistMercurialAPI) {
$repository_api->parseRelativeLocalCommit( $repository_api->parseBaseCommitArgument($this->getArgument('paths'));
$this->getArgument('paths'));
$diff = $repository_api->getFullMercurialDiff(); $diff = $repository_api->getFullMercurialDiff();
$changes = $parser->parseDiff($diff); $changes = $parser->parseDiff($diff);
$authors = $this->getConduit()->callMethodSynchronous( $authors = $this->getConduit()->callMethodSynchronous(

View file

@ -301,7 +301,7 @@ EOTEXT
private function findRevision() { private function findRevision() {
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
$repository_api->parseRelativeLocalCommit(array($this->ontoRemoteBranch)); $this->parseBaseCommitArgument(array($this->ontoRemoteBranch));
$revision_id = $this->getArgument('revision'); $revision_id = $this->getArgument('revision');
if ($revision_id) { if ($revision_id) {
@ -484,9 +484,7 @@ EOTEXT
} }
} }
// Now that we've rebased, the merge-base of origin/master and HEAD may $repository_api->reloadWorkingCopy();
// be different. Reparse the relative commit.
$repository_api->parseRelativeLocalCommit(array($this->ontoRemoteBranch));
} }
private function squash() { private function squash() {

View file

@ -240,8 +240,9 @@ EOTEXT
if ($this->getArgument('only-new')) { if ($this->getArgument('only-new')) {
$conduit = $this->getConduit(); $conduit = $this->getConduit();
$api = $this->getRepositoryAPI(); $api = $this->getRepositoryAPI();
$relative_commit = ($rev ? $rev : $api->resolveBaseCommit()); if ($rev) {
$api->setRelativeCommit($relative_commit); $api->setBaseCommit($rev);
}
$svn_root = id(new PhutilURI($api->getSourceControlPath()))->getPath(); $svn_root = id(new PhutilURI($api->getSourceControlPath()))->getPath();
$all_paths = array(); $all_paths = array();
@ -267,7 +268,7 @@ EOTEXT
$lint_future = $conduit->callMethod('diffusion.getlintmessages', array( $lint_future = $conduit->callMethod('diffusion.getlintmessages', array(
'arcanistProject' => $this->getWorkingCopy()->getProjectID(), 'arcanistProject' => $this->getWorkingCopy()->getProjectID(),
'branch' => '', // TODO: Tracking branch. 'branch' => '', // TODO: Tracking branch.
'commit' => $api->getRelativeCommit(), 'commit' => $api->getBaseCommit(),
'files' => array_keys($all_paths), 'files' => array_keys($all_paths),
)); ));
} }

View file

@ -885,7 +885,7 @@ EOTEXT
// Check to see if the bundle's base revision matches the working copy // Check to see if the bundle's base revision matches the working copy
// base revision // base revision
if ($repository_api->supportsRelativeLocalCommits()) { if ($repository_api->supportsLocalCommits()) {
$bundle_base_rev = $bundle->getBaseRevision(); $bundle_base_rev = $bundle->getBaseRevision();
if (empty($bundle_base_rev)) { if (empty($bundle_base_rev)) {
// this means $source is SOURCE_PATCH || SOURCE_BUNDLE w/ $version < 2 // this means $source is SOURCE_PATCH || SOURCE_BUNDLE w/ $version < 2

View file

@ -73,20 +73,15 @@ EOTEXT
$arg_commit = $this->getArgument('commit'); $arg_commit = $this->getArgument('commit');
if (count($arg_commit)) { if (count($arg_commit)) {
if (!$repository_api->supportsRelativeLocalCommits()) { $this->parseBaseCommitArgument($arg_commit);
throw new ArcanistUsageException(
"This version control system does not support relative commits.");
} else {
$repository_api->parseRelativeLocalCommit($arg_commit);
}
} }
$arg = $arg_commit ? ' '.head($arg_commit) : ''; $arg = $arg_commit ? ' '.head($arg_commit) : '';
$repository_api->setBaseCommitArgumentRules( $repository_api->setBaseCommitArgumentRules(
$this->getArgument('base', '')); $this->getArgument('base', ''));
if ($repository_api->supportsRelativeLocalCommits()) { if ($repository_api->supportsCommitRanges()) {
$relative = $repository_api->getRelativeCommit(); $relative = $repository_api->getBaseCommit();
if ($this->getArgument('show-base')) { if ($this->getArgument('show-base')) {
echo $relative."\n"; echo $relative."\n";