diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -16,6 +16,9 @@ */ const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; + private $symbolicHeadCommit = 'HEAD'; + private $resolvedHeadCommit; + public static function newHookAPI($root) { return new ArcanistGitAPI($root); } @@ -106,7 +109,8 @@ // 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. - $against = csprintf('%s --not %s', 'HEAD', $this->getBaseCommit()); + $against = csprintf('%s --not %s', + $this->getHeadCommit(), $this->getBaseCommit()); } // NOTE: Windows escaping of "%" symbols apparently is inherently broken; @@ -161,8 +165,9 @@ } list($err, $merge_base) = $this->execManualLocal( - 'merge-base %s HEAD', - $symbolic_commit); + 'merge-base %s %s', + $symbolic_commit, + $this->getHeadCommit()); if ($err) { throw new ArcanistUsageException( "Unable to find any git commit named '{$symbolic_commit}' in ". @@ -170,8 +175,8 @@ } $this->setBaseCommitExplanation( - "it is the merge-base of '{$symbolic_commit}' and HEAD, as you ". - "explicitly specified."); + "it is the merge-base of '{$symbolic_commit}' and ". + "{$this->symbolicHeadCommit}, as you explicitly specified."); return trim($merge_base); } @@ -301,6 +306,43 @@ return trim($merge_base); } + public function getHeadCommit() { + if (!$this->supportsCommitRanges()) { + throw new ArcanistCapabilityNotSupportedException($this); + } + + if ($this->resolvedHeadCommit === null) { + $this->resolvedHeadCommit = + $this->resolveCommit($this->symbolicHeadCommit); + } + + return $this->resolvedHeadCommit; + } + + final public function setHeadCommit($symbolic_commit) { + $this->symbolicHeadCommit = $symbolic_commit; + $this->reloadCommitRange(); + return $this; + } + + /** Translates a symbolic commit (HEAD^) to a commit identifier + * @param $symbolic_commit + * @return string the commit SHA + */ + private function resolveCommit($symbolic_commit) { + list($err, $merge_base) = $this->execManualLocal( + 'rev-parse %s', + $symbolic_commit); + + if ($err) { + throw new ArcanistUsageException( + "Unable to find any git commit named '{$symbolic_commit}' in ". + "this repository."); + } + + return trim($merge_base); + } + private function getDiffFullOptions($detect_moves_and_renames = true) { $options = array( self::getDiffBaseOptions(), @@ -335,8 +377,9 @@ public function getFullGitDiff() { $options = $this->getDiffFullOptions(); list($stdout) = $this->execxLocal( - "diff {$options} %s --", - $this->getBaseCommit()); + "diff {$options} %s..%s --", + $this->getBaseCommit(), + $this->getHeadCommit()); return $stdout; } @@ -401,8 +444,9 @@ } else { // 2..N commits. list($stdout) = $this->execxLocal( - 'log --first-parent --format=medium %s..HEAD', - $this->getBaseCommit()); + 'log --first-parent --format=medium %s..%s', + $this->getBaseCommit(), + $this->getHeadCommit()); } return $stdout; } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -575,6 +575,10 @@ return $this; } + public function setHeadCommit($symbolic_commit) { + throw new ArcanistCapabilityNotSupportedException($this); + } + final public function getBaseCommit() { if (!$this->supportsCommitRanges()) { throw new ArcanistCapabilityNotSupportedException($this); @@ -588,6 +592,10 @@ return $this->resolvedBaseCommit; } + public function getHeadCommit() { + throw new ArcanistCapabilityNotSupportedException($this); + } + final public function reloadCommitRange() { $this->resolvedBaseCommit = null; $this->baseCommitExplanation = null; diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -395,6 +395,11 @@ ), ), '*' => 'paths', + 'range' => array( + 'param' => 'range', + 'help' => 'specify a commit range to include (BASE..HEAD)', + 'supports' => array('git'), + ) ); if (phutil_is_windows()) { @@ -433,8 +438,20 @@ array_unshift($argv, '--ansi'); } - if ($this->getRepositoryAPI()->supportsCommitRanges()) { - $this->getRepositoryAPI()->getBaseCommit(); + $repo = $this->getRepositoryAPI(); + $range = $this->getArgument('range', null); + $range_supported = $repo->supportsCommitRanges(); + if ($range) { + if (!$range_supported) { + throw new Exception('ranged are not supported'); + } + + list($base, $head) = preg_split('(\\.\\.)', $range); + $repo->setHeadCommit($head); + $repo->setBaseCommit($base); + + } else if ($range_supported) { + $repo->getBaseCommit(); } $script = phutil_get_library_root('arcanist').'/../scripts/arcanist.php'; @@ -661,7 +678,9 @@ if ($repository_api instanceof ArcanistSubversionAPI) { $repository_api->limitStatusToPaths($this->getArgument('paths')); } - $this->requireCleanWorkingCopy(); + if (!$this->getArgument('range')) { + $this->requireCleanWorkingCopy(); + } } catch (ArcanistUncommittedChangesException $ex) { if ($repository_api instanceof ArcanistMercurialAPI) { $use_dirty_changes = false; @@ -1216,7 +1235,8 @@ private function runLint() { if ($this->getArgument('nolint') || $this->getArgument('only') || - $this->isRawDiffSource()) { + $this->isRawDiffSource() || + $this->getArgument('range')) { return ArcanistLintWorkflow::RESULT_SKIP; } @@ -1297,7 +1317,8 @@ private function runUnit() { if ($this->getArgument('nounit') || $this->getArgument('only') || - $this->isRawDiffSource()) { + $this->isRawDiffSource() || + $this->getArgument('range')) { return ArcanistUnitWorkflow::RESULT_SKIP; }