Changeset 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 $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) { | ||||
$argv[0] = 'git '.$argv[0]; | $argv[0] = 'git '.$argv[0]; | ||||
▲ Show 20 Lines • Show All 43 Lines • ▼ Show 20 Lines | public function getMetadataPath() { | ||||
} | } | ||||
return $path; | return $path; | ||||
} | } | ||||
public function getHasCommits() { | public function getHasCommits() { | ||||
return !$this->repositoryHasNoCommits; | return !$this->repositoryHasNoCommits; | ||||
} | } | ||||
/** | |||||
* Tests if a child commit is descendant of a parent commit. | |||||
epriestley: This documentation should probably be explicit about what happens when `$parent` and `$child`… | |||||
* If child and parent are the same, it returns false. | |||||
* @param child commit SHA. | |||||
* @param parent commit SHA. | |||||
* @return bool | |||||
*/ | |||||
Not Done Inline ActionsCan this be private? epriestley: Can this be private? | |||||
private function isDescendant($child, $parent) { | |||||
list($common_ancestor) = | |||||
$this->execxLocal('merge-base %s %s', $child, $parent); | |||||
Not Done Inline ActionsYou can just trim(), there won't be any \2 in the output in this case. epriestley: You can just `trim()`, there won't be any \2 in the output in this case. | |||||
$common_ancestor = trim($common_ancestor); | |||||
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. | ||||
Show All 15 Lines | if ($this->repositoryHasNoCommits) { | ||||
// 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. | ||||
$against = csprintf('%s --not %s', 'HEAD', $this->getBaseCommit()); | $base_commit = $this->getBaseCommit(); | ||||
$head_commit = $this->getHeadCommit(); | |||||
if ($this->isDescendant($head_commit, $base_commit) === false) { | |||||
throw new ArcanistUsageException( | |||||
Not Done Inline ActionsWhen an error is a user's fault, throw ArcanistUsageException instead of Exception. Generally, strings should be full sentences: use uppercase initial letters and terminal punctuation. epriestley: When an error is a user's fault, throw `ArcanistUsageException` instead of `Exception`. | |||||
"base commit ${base_commit} is not a child of head commit ". | |||||
"${head_commit}"); | |||||
Not Done Inline ActionsThis message is flipped -- "base commit {$head_commit}" should be "base commit {$base_commit}". epriestley: This message is flipped -- "base commit {$head_commit}" should be "base commit {$base_commit}". | |||||
} | |||||
$against = csprintf('%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 All 38 Lines | protected function buildBaseCommit($symbolic_commit) { | ||||
if ($symbolic_commit !== null) { | if ($symbolic_commit !== null) { | ||||
if ($symbolic_commit == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) { | if ($symbolic_commit == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) { | ||||
$this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
'you explicitly specified the empty tree.'); | 'you explicitly specified the empty tree.'); | ||||
return $symbolic_commit; | return $symbolic_commit; | ||||
} | } | ||||
list($err, $merge_base) = $this->execManualLocal( | list($err, $merge_base) = $this->execManualLocal( | ||||
'merge-base %s HEAD', | 'merge-base %s %s', | ||||
$symbolic_commit); | $symbolic_commit, | ||||
$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."); | ||||
} | } | ||||
$this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
"it is the merge-base of '{$symbolic_commit}' and HEAD, as you ". | "it is the merge-base of '{$symbolic_commit}' and ". | ||||
"explicitly specified."); | "{$this->symbolicHeadCommit}, as you explicitly specified."); | ||||
return trim($merge_base); | return trim($merge_base); | ||||
Not Done Inline ActionsThis is a little misleading because the user may not have specified the symbolic head, but the message will say they did. I think it would be better to default the symbolic head to null, have getHeadCommit() deal with resolution, and then specialize the error messages to be explicit about what was specified. epriestley: This is a little misleading because the user may not have specified the symbolic head, but the… | |||||
Not Done Inline ActionsBut the original message explicitly mentioned HEAD, which is the default here. getHeadCommit() is problematic, since it resolves it to the commit hash, which is more confusing. talshiri: But the original message explicitly mentioned HEAD, which is the default here.
`getHeadCommit… | |||||
} | } | ||||
// 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) { | ||||
list($err) = $this->execManualLocal('rev-parse --verify HEAD'); | list($err) = $this->execManualLocal('rev-parse --verify HEAD'); | ||||
▲ Show 20 Lines • Show All 112 Lines • ▼ Show 20 Lines | protected function buildBaseCommit($symbolic_commit) { | ||||
list($merge_base) = $this->execxLocal( | 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() { | |||||
if ($this->resolvedHeadCommit === null) { | |||||
$this->resolvedHeadCommit = | |||||
$this->resolveCommit($this->symbolicHeadCommit); | |||||
Not Done Inline ActionsSince this is in a final/leaf class and Git supports commit ranges, you can omit this check. epriestley: Since this is in a final/leaf class and Git supports commit ranges, you can omit this check. | |||||
} | |||||
return $this->resolvedHeadCommit; | |||||
} | |||||
final public function setHeadCommit($symbolic_commit) { | |||||
$this->symbolicHeadCommit = $symbolic_commit; | |||||
$this->reloadCommitRange(); | |||||
return $this; | |||||
} | |||||
/** | |||||
* Translates a symbolic commit (like "HEAD^") to a commit identifier. | |||||
* @param string_symbol commit. | |||||
* @return string the commit SHA. | |||||
*/ | |||||
private function resolveCommit($symbolic_commit) { | |||||
list($err, $commit_hash) = $this->execManualLocal( | |||||
'rev-parse %s', | |||||
Not Done Inline ActionsMinor nits:
epriestley: Minor nits:
- Don't put text on the `/**` line; start on the next line.
- For consistency… | |||||
$symbolic_commit); | |||||
Not Done Inline Actions$commit_hash or similar might be a better name for this variable than $merge_base? epriestley: `$commit_hash` or similar might be a better name for this variable than `$merge_base`? | |||||
if ($err) { | |||||
throw new ArcanistUsageException( | |||||
"Unable to find any git commit named '{$symbolic_commit}' in ". | |||||
"this repository."); | |||||
} | |||||
return trim($commit_hash); | |||||
} | |||||
private function getDiffFullOptions($detect_moves_and_renames = true) { | private function getDiffFullOptions($detect_moves_and_renames = true) { | ||||
$options = array( | $options = array( | ||||
self::getDiffBaseOptions(), | self::getDiffBaseOptions(), | ||||
'--no-color', | '--no-color', | ||||
'--src-prefix=a/', | '--src-prefix=a/', | ||||
'--dst-prefix=b/', | '--dst-prefix=b/', | ||||
'-U'.$this->getDiffLinesOfContext(), | '-U'.$this->getDiffLinesOfContext(), | ||||
); | ); | ||||
Show All 15 Lines | $options = array( | ||||
// an alternative textual representation. TODO: Ideally, Differential | // an alternative textual representation. TODO: Ideally, Differential | ||||
// would ship up the binaries for 'arc patch' but display the textconv | // would ship up the binaries for 'arc patch' but display the textconv | ||||
// output in the visual diff. | // output in the visual diff. | ||||
'--no-textconv', | '--no-textconv', | ||||
); | ); | ||||
return implode(' ', $options); | return implode(' ', $options); | ||||
} | } | ||||
public function getFullGitDiff() { | /** | ||||
* @param the base revision | |||||
* @param head revision. If this is null, the generated diff will include the | |||||
* working copy | |||||
*/ | |||||
public function getFullGitDiff($base, $head=null) { | |||||
$options = $this->getDiffFullOptions(); | $options = $this->getDiffFullOptions(); | ||||
$diff_revision = $base; | |||||
if ($head) { | |||||
$diff_revision .= '..'.$head; | |||||
} | |||||
list($stdout) = $this->execxLocal( | list($stdout) = $this->execxLocal( | ||||
"diff {$options} %s --", | "diff {$options} %s --", | ||||
$this->getBaseCommit()); | $diff_revision); | ||||
Not Done Inline ActionsI don't think we let you get here with a dirty working copy, but this will change the behavior if we do. epriestley: I don't think we let you get here with a dirty working copy, but this will change the behavior… | |||||
Not Done Inline ActionsOh, that's true. Should I alter this so it will diff against the passed commit if --range isn't being used? Is it ever desirable to diff against working copy? talshiri: Oh, that's true. Should I alter this so it will diff against the passed commit if --range isn't… | |||||
Not Done Inline ActionsSleeping on it, I realize we do actually diff against the dirty working copy in one case: if you run arc lint, we'll call ArcanistBaseWorkflow::getChangedLines() and end up here, expecting to use the full diff (including dirty changes in the working copy) to figure out which lines have been changed by the user, so we can filter/adjust messages (e.g., not show warnings on unchanged lines). This will alter the behavior, causing us to ignore dirty changes in the working copy. I think ArcanistGitAPI::getAllLocalChanges() can be tweaked to pass a flag to getFullGitDiff(), which can then do a one-argument diff (i.e., no end of the range). This is a little messy, but as far as I know there's no Y we can write in X..Y to mean "changes since X, including dirty changes in the working copy", so I think this ultimately needs to have two flavors of invocation anyway. This is also easy to clean up later if it turns out to not have made much sense. epriestley: Sleeping on it, I realize we do actually diff against the dirty working copy in one case: if… | |||||
Not Done Inline ActionsAh yes. Ok, let me take care of it. talshiri: Ah yes. Ok, let me take care of it. | |||||
return $stdout; | return $stdout; | ||||
} | } | ||||
/** | /** | ||||
* @param string Path to generate a diff for. | * @param string Path to generate a diff for. | ||||
* @param bool If true, detect moves and renames. Otherwise, ignore | * @param bool If true, detect moves and renames. Otherwise, ignore | ||||
* moves/renames; this is useful because it prompts git to | * moves/renames; this is useful because it prompts git to | ||||
* generate real diff text. | * generate real diff text. | ||||
▲ Show 20 Lines • Show All 48 Lines • ▼ Show 20 Lines | if ($this->repositoryHasNoCommits) { | ||||
return ''; | return ''; | ||||
} else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) { | } else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) { | ||||
// First commit. | // First commit. | ||||
list($stdout) = $this->execxLocal( | list($stdout) = $this->execxLocal( | ||||
'log --format=medium HEAD'); | 'log --format=medium HEAD'); | ||||
} else { | } else { | ||||
// 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..%s', | ||||
$this->getBaseCommit()); | $this->getBaseCommit(), | ||||
$this->getHeadCommit()); | |||||
} | } | ||||
return $stdout; | return $stdout; | ||||
} | } | ||||
public function getGitHistoryLog() { | public function getGitHistoryLog() { | ||||
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, | ||||
▲ Show 20 Lines • Show All 402 Lines • ▼ Show 20 Lines | try { | ||||
} | } | ||||
} catch (CommandException $exception) { | } catch (CommandException $exception) { | ||||
return false; | return false; | ||||
} | } | ||||
return true; | return true; | ||||
} | } | ||||
public function getAllLocalChanges() { | public function getAllLocalChanges() { | ||||
$diff = $this->getFullGitDiff(); | $diff = $this->getFullGitDiff($this->getBaseCommit()); | ||||
if (!strlen(trim($diff))) { | if (!strlen(trim($diff))) { | ||||
return array(); | return array(); | ||||
} | } | ||||
$parser = new ArcanistDiffParser(); | $parser = new ArcanistDiffParser(); | ||||
return $parser->parseDiff($diff); | return $parser->parseDiff($diff); | ||||
} | } | ||||
public function supportsLocalBranchMerge() { | public function supportsLocalBranchMerge() { | ||||
▲ Show 20 Lines • Show All 267 Lines • Show Last 20 Lines |
This documentation should probably be explicit about what happens when $parent and $child are the same. Notably, in Mercurial, a commit is a descendant of itself.