Changeset View
Changeset View
Standalone View
Standalone View
src/repository/api/ArcanistGitAPI.php
Show All 10 Lines | final class ArcanistGitAPI extends ArcanistRepositoryAPI { | ||||
const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16; | const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16; | ||||
/** | /** | ||||
* For the repository's initial commit, 'git diff HEAD^' and similar do | * For the repository's initial commit, 'git diff HEAD^' and similar do | ||||
* not work. Using this instead does work; it is the hash of the empty tree. | * not work. Using this instead does work; it is the hash of the empty tree. | ||||
*/ | */ | ||||
const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; | const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; | ||||
private $symbolicHeadCommit = 'HEAD'; | private $symbolicHeadCommit; | ||||
private $resolvedHeadCommit; | private $resolvedHeadCommit; | ||||
public static function newHookAPI($root) { | public static function newHookAPI($root) { | ||||
return new ArcanistGitAPI($root); | return new ArcanistGitAPI($root); | ||||
} | } | ||||
protected function buildLocalFuture(array $argv) { | protected function buildLocalFuture(array $argv) { | ||||
▲ Show 20 Lines • Show All 48 Lines • ▼ Show 20 Lines | final class ArcanistGitAPI extends ArcanistRepositoryAPI { | ||||
public function getHasCommits() { | public function getHasCommits() { | ||||
return !$this->repositoryHasNoCommits; | return !$this->repositoryHasNoCommits; | ||||
} | } | ||||
/** | /** | ||||
* Tests if a child commit is descendant of a parent commit. | * Tests if a child commit is descendant of a parent commit. | ||||
* If child and parent are the same, it returns false. | * If child and parent are the same, it returns false. | ||||
* @param child commit SHA. | * @param Child commit SHA. | ||||
* @param parent commit SHA. | * @param Parent commit SHA. | ||||
* @return bool | * @return bool True if the child is a descendant of the parent. | ||||
*/ | */ | ||||
private function isDescendant($child, $parent) { | private function isDescendant($child, $parent) { | ||||
list($common_ancestor) = | list($common_ancestor) = $this->execxLocal( | ||||
$this->execxLocal('merge-base %s %s', $child, $parent); | 'merge-base %s %s', | ||||
$child, | |||||
$parent); | |||||
$common_ancestor = trim($common_ancestor); | $common_ancestor = trim($common_ancestor); | ||||
return $common_ancestor == $parent && $common_ancestor != $child; | return ($common_ancestor == $parent) && ($common_ancestor != $child); | ||||
} | } | ||||
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->getBaseCommit() == self::GIT_MAGIC_ROOT_COMMIT) { | } else if ($this->getBaseCommit() == self::GIT_MAGIC_ROOT_COMMIT) { | ||||
// One commit. | // One commit. | ||||
$against = 'HEAD'; | $against = 'HEAD'; | ||||
} else { | } else { | ||||
// 2..N commits. We include commits reachable from HEAD which are | // 2..N commits. We include commits reachable from HEAD which are | ||||
// not reachable from the relative commit; this is consistent with | // not reachable from the base commit; this is consistent with user | ||||
// user expectations even though it is not actually the diff range. | // expectations even though it is not actually the diff range. | ||||
// Particularly: | // Particularly: | ||||
// | // | ||||
// | | // | | ||||
// D <----- master branch | // D <----- master branch | ||||
// | | // | | ||||
// C Y <- feature branch | // C Y <- feature branch | ||||
// | /| | // | /| | ||||
// B X | // B X | ||||
// | / | // | / | ||||
// A | // A | ||||
// | | // | | ||||
// | // | ||||
// If "A, B, C, D" are master, and the user is at Y, when they run | // If "A, B, C, D" are master, and the user is at Y, when they run | ||||
// "arc diff B" they want (and get) a diff of B vs Y, but they think about | // "arc diff B" they want (and get) a diff of B vs Y, but they think about | ||||
// 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. | ||||
if ($this->symbolicHeadCommit !== null) { | |||||
$base_commit = $this->getBaseCommit(); | $base_commit = $this->getBaseCommit(); | ||||
$head_commit = $this->getHeadCommit(); | $resolved_base = $this->resolveCommit($base_commit); | ||||
talshiri: getBaseCommit() returns a SHA? | |||||
Not Done Inline ActionsIt may or may not return a SHA. If invoked as arc diff X, it will return a SHA, but if a --base rule is used to figure it out it may not. epriestley: It may or may not return a SHA. If invoked as `arc diff X`, it will return a SHA, but if a `… | |||||
Not Done Inline Actionsoic talshiri: oic | |||||
if ($this->isDescendant($head_commit, $base_commit) === false) { | |||||
$head_commit = $this->symbolicHeadCommit; | |||||
$resolved_head = $this->getHeadCommit(); | |||||
if (!$this->isDescendant($resolved_head, $resolved_base)) { | |||||
// NOTE: Since the base commit will have been resolved as the | |||||
// merge-base of the specified base and the specified HEAD, we can't | |||||
// easily tell exactly what's wrong with the range. | |||||
// For example, `arc diff HEAD --head HEAD^^^` is invalid because it | |||||
// is reversed, but resolving the commit "HEAD" will compute its | |||||
// merge-base with "HEAD^^^", which is "HEAD^^^", so the range will | |||||
// appear empty. | |||||
throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
"base commit ${base_commit} is not a child of head commit ". | pht( | ||||
"${head_commit}"); | 'The specified commit range is empty, backward or invalid: the '. | ||||
'base (%s) is not an ancestor of the head (%s). You can not '. | |||||
'diff an empty or reversed commit range.', | |||||
$base_commit, | |||||
$head_commit)); | |||||
Not Done Inline ActionsThis is exactly infinite time better than my phrasing talshiri: This is exactly infinite time better than my phrasing | |||||
} | |||||
} | } | ||||
$against = csprintf('%s --not %s', | $against = csprintf( | ||||
$this->getHeadCommit(), $this->getBaseCommit()); | '%s --not %s', | ||||
$this->getHeadCommit(), | |||||
$this->getBaseCommit()); | |||||
} | } | ||||
// NOTE: Windows escaping of "%" symbols apparently is inherently broken; | // NOTE: Windows escaping of "%" symbols apparently is inherently broken; | ||||
// when passed throuhgh escapeshellarg() they are replaced with spaces. | // when passed throuhgh escapeshellarg() they are replaced with spaces. | ||||
// TODO: Learn how cmd.exe works and find some clever workaround? | // TODO: Learn how cmd.exe works and find some clever workaround? | ||||
// NOTE: If we use "%x00", output is truncated in Windows. | // NOTE: If we use "%x00", output is truncated in Windows. | ||||
▲ Show 20 Lines • Show All 47 Lines • ▼ Show 20 Lines | if ($symbolic_commit !== null) { | ||||
$symbolic_commit, | $symbolic_commit, | ||||
$this->getHeadCommit()); | $this->getHeadCommit()); | ||||
if ($err) { | if ($err) { | ||||
throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
"Unable to find any git commit named '{$symbolic_commit}' in ". | "Unable to find any git commit named '{$symbolic_commit}' in ". | ||||
"this repository."); | "this repository."); | ||||
} | } | ||||
if ($this->symbolicHeadCommit === null) { | |||||
$this->setBaseCommitExplanation( | |||||
"it is the merge-base of the explicitly specified base commit ". | |||||
"'{$symbolic_commit}' and HEAD."); | |||||
} else { | |||||
$this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
"it is the merge-base of '{$symbolic_commit}' and ". | "it is the merge-base of the explicitly specified base commit ". | ||||
"{$this->symbolicHeadCommit}, as you explicitly specified."); | "'{$symbolic_commit}' and the explicitly specified head ". | ||||
"commit '{$this->symbolicHeadCommit}'."); | |||||
} | |||||
return trim($merge_base); | return trim($merge_base); | ||||
} | } | ||||
// Detect zero-commit or one-commit repositories. There is only one | // Detect zero-commit or one-commit repositories. There is only one | ||||
// relative-commit value that makes any sense in these repositories: the | // relative-commit value that makes any sense in these repositories: the | ||||
// empty tree. | // empty tree. | ||||
list($err) = $this->execManualLocal('rev-parse --verify HEAD^'); | list($err) = $this->execManualLocal('rev-parse --verify HEAD^'); | ||||
if ($err) { | if ($err) { | ||||
▲ Show 20 Lines • Show All 115 Lines • ▼ Show 20 Lines | list($merge_base) = $this->execxLocal( | ||||
'merge-base %s HEAD', | 'merge-base %s HEAD', | ||||
$default_relative); | $default_relative); | ||||
return trim($merge_base); | return trim($merge_base); | ||||
} | } | ||||
public function getHeadCommit() { | public function getHeadCommit() { | ||||
if ($this->resolvedHeadCommit === null) { | if ($this->resolvedHeadCommit === null) { | ||||
$this->resolvedHeadCommit = | $this->resolvedHeadCommit = $this->resolveCommit( | ||||
$this->resolveCommit($this->symbolicHeadCommit); | coalesce($this->symbolicHeadCommit, 'HEAD')); | ||||
} | } | ||||
return $this->resolvedHeadCommit; | return $this->resolvedHeadCommit; | ||||
} | } | ||||
final public function setHeadCommit($symbolic_commit) { | final public function setHeadCommit($symbolic_commit) { | ||||
$this->symbolicHeadCommit = $symbolic_commit; | $this->symbolicHeadCommit = $symbolic_commit; | ||||
$this->reloadCommitRange(); | $this->reloadCommitRange(); | ||||
▲ Show 20 Lines • Show All 830 Lines • Show Last 20 Lines |
getBaseCommit() returns a SHA?