Changeset View
Changeset View
Standalone View
Standalone View
src/repository/api/ArcanistMercurialAPI.php
| Show First 20 Lines • Show All 74 Lines • ▼ Show 20 Lines | public function getHashFromFromSVNRevisionNumber($revision_id) { | ||||
| $matches = array(); | $matches = array(); | ||||
| $string = hgsprintf('svnrev(%s)', $revision_id); | $string = hgsprintf('svnrev(%s)', $revision_id); | ||||
| list($stdout) = $this->execxLocal( | list($stdout) = $this->execxLocal( | ||||
| 'log -l 1 --template %s -r %s --', | 'log -l 1 --template %s -r %s --', | ||||
| '{node}', | '{node}', | ||||
| $string); | $string); | ||||
| if (!$stdout) { | if (!$stdout) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| "Cannot find the HG equivalent of {$revision_id} given."); | pht('Cannot find the HG equivalent of %s given.', $revision_id)); | ||||
| } | } | ||||
| return $stdout; | return $stdout; | ||||
| } | } | ||||
| public function getSVNRevisionNumberFromHash($hash) { | public function getSVNRevisionNumberFromHash($hash) { | ||||
| $matches = array(); | $matches = array(); | ||||
| list($stdout) = $this->execxLocal( | list($stdout) = $this->execxLocal( | ||||
| 'log -r %s --template {svnrev}', $hash); | 'log -r %s --template {svnrev}', $hash); | ||||
| if (!$stdout) { | if (!$stdout) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| "Cannot find the SVN equivalent of {$hash} given."); | pht('Cannot find the SVN equivalent of %s given.', $hash)); | ||||
| } | } | ||||
| return $stdout; | return $stdout; | ||||
| } | } | ||||
| public function getSourceControlPath() { | public function getSourceControlPath() { | ||||
| return '/'; | return '/'; | ||||
| } | } | ||||
| Show All 16 Lines | if ($symbolic_commit !== null) { | ||||
| hgsprintf('ancestor(%s,.)', $symbolic_commit)); | hgsprintf('ancestor(%s,.)', $symbolic_commit)); | ||||
| } catch (Exception $ex) { | } catch (Exception $ex) { | ||||
| // Try it as a revset instead of a commit id | // Try it as a revset instead of a commit id | ||||
| try { | try { | ||||
| $commit = $this->getCanonicalRevisionName( | $commit = $this->getCanonicalRevisionName( | ||||
| hgsprintf('ancestor(%R,.)', $symbolic_commit)); | hgsprintf('ancestor(%R,.)', $symbolic_commit)); | ||||
| } catch (Exception $ex) { | } catch (Exception $ex) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| "Commit '{$symbolic_commit}' is not a valid Mercurial commit ". | pht( | ||||
| "identifier."); | "Commit '%s' is not a valid Mercurial commit identifier.", | ||||
| $symbolic_commit)); | |||||
| } | } | ||||
| } | } | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| pht( | |||||
| 'it is the greatest common ancestor of the working directory '. | 'it is the greatest common ancestor of the working directory '. | ||||
| 'and the commit you specified explicitly.'); | 'and the commit you specified explicitly.')); | ||||
| return $commit; | return $commit; | ||||
| } | } | ||||
| if ($this->getBaseCommitArgumentRules() || | if ($this->getBaseCommitArgumentRules() || | ||||
| $this->getConfigurationManager()->getConfigFromAnySource('base')) { | $this->getConfigurationManager()->getConfigFromAnySource('base')) { | ||||
| $base = $this->resolveBaseCommit(); | $base = $this->resolveBaseCommit(); | ||||
| if (!$base) { | if (!$base) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| pht( | |||||
| "None of the rules in your 'base' configuration matched a valid ". | "None of the rules in your 'base' configuration matched a valid ". | ||||
| "commit. Adjust rules or specify which commit you want to use ". | "commit. Adjust rules or specify which commit you want to use ". | ||||
| "explicitly."); | "explicitly.")); | ||||
| } | } | ||||
| return $base; | return $base; | ||||
| } | } | ||||
| // Mercurial 2.1 and up have phases which indicate if something is | // Mercurial 2.1 and up have phases which indicate if something is | ||||
| // published or not. To find which revs are outgoing, it's much | // published or not. To find which revs are outgoing, it's much | ||||
| // faster to check the phase instead of actually checking the server. | // faster to check the phase instead of actually checking the server. | ||||
| if ($this->supportsPhases()) { | if ($this->supportsPhases()) { | ||||
| Show All 12 Lines | protected function buildBaseCommit($symbolic_commit) { | ||||
| } else { | } else { | ||||
| // Mercurial (in some versions?) raises an error when there's nothing | // Mercurial (in some versions?) raises an error when there's nothing | ||||
| // outgoing. | // outgoing. | ||||
| $logs = array(); | $logs = array(); | ||||
| } | } | ||||
| if (!$logs) { | if (!$logs) { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| pht( | |||||
| 'you have no outgoing commits, so arc assumes you intend to submit '. | 'you have no outgoing commits, so arc assumes you intend to submit '. | ||||
| 'uncommitted changes in the working copy.'); | 'uncommitted changes in the working copy.')); | ||||
| return $this->getWorkingCopyRevision(); | return $this->getWorkingCopyRevision(); | ||||
| } | } | ||||
| $outgoing_revs = ipull($logs, 'rev'); | $outgoing_revs = ipull($logs, 'rev'); | ||||
| // This is essentially an implementation of a theoretical `hg merge-base` | // This is essentially an implementation of a theoretical `hg merge-base` | ||||
| // command. | // command. | ||||
| $against = $this->getWorkingCopyRevision(); | $against = $this->getWorkingCopyRevision(); | ||||
| Show All 25 Lines | while (true) { | ||||
| // meaning "diff against the empty state". | // meaning "diff against the empty state". | ||||
| $against = 'null'; | $against = 'null'; | ||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| if ($against == 'null') { | if ($against == 'null') { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| 'this is a new repository (all changes are outgoing).'); | pht('this is a new repository (all changes are outgoing).')); | ||||
| } else { | } else { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| pht( | |||||
| 'it is the first commit reachable from the working copy state '. | 'it is the first commit reachable from the working copy state '. | ||||
| 'which is not outgoing.'); | 'which is not outgoing.')); | ||||
| } | } | ||||
| return $against; | return $against; | ||||
| } | } | ||||
| public function getLocalCommitInformation() { | public function getLocalCommitInformation() { | ||||
| if ($this->localCommitInfo === null) { | if ($this->localCommitInfo === null) { | ||||
| $base_commit = $this->getBaseCommit(); | $base_commit = $this->getBaseCommit(); | ||||
| ▲ Show 20 Lines • Show All 98 Lines • ▼ Show 20 Lines | foreach ($lines as $line) { | ||||
| if (!strlen($line)) { | if (!strlen($line)) { | ||||
| continue; | continue; | ||||
| } | } | ||||
| $matches = null; | $matches = null; | ||||
| $ok = preg_match('/^\s*([^:]+?) ([a-f0-9]{12}):/', $line, $matches); | $ok = preg_match('/^\s*([^:]+?) ([a-f0-9]{12}):/', $line, $matches); | ||||
| if (!$ok) { | if (!$ok) { | ||||
| throw new Exception("Unable to parse Mercurial blame line: {$line}"); | throw new Exception( | ||||
| pht( | |||||
| 'Unable to parse Mercurial blame line: %s', | |||||
| $line)); | |||||
| } | } | ||||
| $revision = $matches[2]; | $revision = $matches[2]; | ||||
| $author = trim($matches[1]); | $author = trim($matches[1]); | ||||
| $blame[] = array($author, $revision); | $blame[] = array($author, $revision); | ||||
| } | } | ||||
| return $blame; | return $blame; | ||||
| ▲ Show 20 Lines • Show All 268 Lines • ▼ Show 20 Lines | public function performLocalBranchMerge($branch, $message) { | ||||
| } else { | } else { | ||||
| $err = phutil_passthru( | $err = phutil_passthru( | ||||
| '(cd %s && HGPLAIN=1 hg merge && hg commit -m %s)', | '(cd %s && HGPLAIN=1 hg merge && hg commit -m %s)', | ||||
| $this->getPath(), | $this->getPath(), | ||||
| $message); | $message); | ||||
| } | } | ||||
| if ($err) { | if ($err) { | ||||
| throw new ArcanistUsageException('Merge failed!'); | throw new ArcanistUsageException(pht('Merge failed!')); | ||||
| } | } | ||||
| } | } | ||||
| public function getFinalizedRevisionMessage() { | public function getFinalizedRevisionMessage() { | ||||
| return "You may now push this commit upstream, as appropriate (e.g. with ". | return pht( | ||||
| "'hg push' or by printing and faxing it)."; | "You may now push this commit upstream, as appropriate (e.g. with ". | ||||
| "'%s' or by printing and faxing it).", | |||||
| 'hg push'); | |||||
| } | } | ||||
| public function getCommitMessageLog() { | public function getCommitMessageLog() { | ||||
| $base_commit = $this->getBaseCommit(); | $base_commit = $this->getBaseCommit(); | ||||
| list($stdout) = $this->execxLocal( | list($stdout) = $this->execxLocal( | ||||
| 'log --template %s --rev %s --branch %s --', | 'log --template %s --rev %s --branch %s --', | ||||
| "{node}\1{desc}\2", | "{node}\1{desc}\2", | ||||
| hgsprintf('(%s::. - %s)', $base_commit, $base_commit), | hgsprintf('(%s::. - %s)', $base_commit, $base_commit), | ||||
| Show All 34 Lines | if ($revision_ids) { | ||||
| 'differential.query', | 'differential.query', | ||||
| $query + array( | $query + array( | ||||
| 'ids' => $revision_ids, | 'ids' => $revision_ids, | ||||
| )); | )); | ||||
| foreach ($results as $key => $result) { | foreach ($results as $key => $result) { | ||||
| $hash = substr($reason_map[$result['id']], 0, 16); | $hash = substr($reason_map[$result['id']], 0, 16); | ||||
| $results[$key]['why'] = | $results[$key]['why'] = | ||||
| "Commit message for '{$hash}' has explicit 'Differential Revision'."; | pht( | ||||
| "Commit message for '%s' has explicit 'Differential Revision'.", | |||||
| $hash); | |||||
| } | } | ||||
| return $results; | return $results; | ||||
| } | } | ||||
| // Try to find revisions by hash. | // Try to find revisions by hash. | ||||
| $hashes = array(); | $hashes = array(); | ||||
| foreach ($this->getLocalCommitInformation() as $commit) { | foreach ($this->getLocalCommitInformation() as $commit) { | ||||
| $hashes[] = array('hgcm', $commit['commit']); | $hashes[] = array('hgcm', $commit['commit']); | ||||
| } | } | ||||
| if ($hashes) { | if ($hashes) { | ||||
| // NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working | // NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working | ||||
| // copy with dirty changes, there may be no local commits. | // copy with dirty changes, there may be no local commits. | ||||
| $results = $conduit->callMethodSynchronous( | $results = $conduit->callMethodSynchronous( | ||||
| 'differential.query', | 'differential.query', | ||||
| $query + array( | $query + array( | ||||
| 'commitHashes' => $hashes, | 'commitHashes' => $hashes, | ||||
| )); | )); | ||||
| foreach ($results as $key => $hash) { | foreach ($results as $key => $hash) { | ||||
| $results[$key]['why'] = | $results[$key]['why'] = pht( | ||||
| 'A mercurial commit hash in the commit range is already attached '. | 'A mercurial commit hash in the commit range is already attached '. | ||||
| 'to the Differential revision.'; | 'to the Differential revision.'); | ||||
| } | } | ||||
| return $results; | return $results; | ||||
| } | } | ||||
| return array(); | return array(); | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 76 Lines • ▼ Show 20 Lines | try { | ||||
| } | } | ||||
| } | } | ||||
| $this->reloadWorkingCopy(); | $this->reloadWorkingCopy(); | ||||
| } | } | ||||
| public function getCommitSummary($commit) { | public function getCommitSummary($commit) { | ||||
| if ($commit == 'null') { | if ($commit == 'null') { | ||||
| return '(The Empty Void)'; | return pht('(The Empty Void)'); | ||||
| } | } | ||||
| list($summary) = $this->execxLocal( | list($summary) = $this->execxLocal( | ||||
| 'log --template {desc} --limit 1 --rev %s', | 'log --template {desc} --limit 1 --rev %s', | ||||
| $commit); | $commit); | ||||
| $summary = head(explode("\n", $summary)); | $summary = head(explode("\n", $summary)); | ||||
| return trim($summary); | return trim($summary); | ||||
| } | } | ||||
| public function backoutCommit($commit_hash) { | public function backoutCommit($commit_hash) { | ||||
| $this->execxLocal( | $this->execxLocal('backout -r %s', $commit_hash); | ||||
| 'backout -r %s', $commit_hash); | |||||
| $this->reloadWorkingCopy(); | $this->reloadWorkingCopy(); | ||||
| if (!$this->getUncommittedStatus()) { | if (!$this->getUncommittedStatus()) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| "{$commit_hash} has already been reverted."); | pht('%s has already been reverted.', $commit_hash)); | ||||
| } | } | ||||
| } | } | ||||
| public function getBackoutMessage($commit_hash) { | public function getBackoutMessage($commit_hash) { | ||||
| return 'Backed out changeset '.$commit_hash.'.'; | return pht('Backed out changeset %s,', $commit_hash); | ||||
| } | } | ||||
| public function resolveBaseCommitRule($rule, $source) { | public function resolveBaseCommitRule($rule, $source) { | ||||
| list($type, $name) = explode(':', $rule, 2); | list($type, $name) = explode(':', $rule, 2); | ||||
| // NOTE: This function MUST return node hashes or symbolic commits (like | // NOTE: This function MUST return node hashes or symbolic commits (like | ||||
| // branch names or the word "tip"), not revsets. This includes ".^" and | // branch names or the word "tip"), not revsets. This includes ".^" and | ||||
| // similar, which a revset, not a symbolic commit identifier. If you return | // similar, which a revset, not a symbolic commit identifier. If you return | ||||
| // a revset it will be escaped later and looked up literally. | // a revset it will be escaped later and looked up literally. | ||||
| switch ($type) { | switch ($type) { | ||||
| case 'hg': | case 'hg': | ||||
| $matches = null; | $matches = null; | ||||
| if (preg_match('/^gca\((.+)\)$/', $name, $matches)) { | if (preg_match('/^gca\((.+)\)$/', $name, $matches)) { | ||||
| list($err, $merge_base) = $this->execManualLocal( | list($err, $merge_base) = $this->execManualLocal( | ||||
| 'log --template={node} --rev %s', | 'log --template={node} --rev %s', | ||||
| sprintf('ancestor(., %s)', $matches[1])); | sprintf('ancestor(., %s)', $matches[1])); | ||||
| if (!$err) { | if (!$err) { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| "it is the greatest common ancestor of '{$matches[1]}' and ., as". | pht( | ||||
| " specified by '{$rule}' in your {$source} 'base' ". | "it is the greatest common ancestor of '%s' and %s, as ". | ||||
| "configuration."); | "specified by '%s' in your %s 'base' configuration.", | ||||
| $matches[1], | |||||
| '.', | |||||
| $rule, | |||||
| $source)); | |||||
| return trim($merge_base); | return trim($merge_base); | ||||
| } | } | ||||
| } else { | } else { | ||||
| list($err, $commit) = $this->execManualLocal( | list($err, $commit) = $this->execManualLocal( | ||||
| 'log --template {node} --rev %s', | 'log --template {node} --rev %s', | ||||
| hgsprintf('%s', $name)); | hgsprintf('%s', $name)); | ||||
| if ($err) { | if ($err) { | ||||
| list($err, $commit) = $this->execManualLocal( | list($err, $commit) = $this->execManualLocal( | ||||
| 'log --template {node} --rev %s', | 'log --template {node} --rev %s', | ||||
| $name); | $name); | ||||
| } | } | ||||
| if (!$err) { | if (!$err) { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| "it is specified by '{$rule}' in your {$source} 'base' ". | pht( | ||||
| "configuration."); | "it is specified by '%s' in your %s 'base' configuration.", | ||||
| $rule, | |||||
| $source)); | |||||
| return trim($commit); | return trim($commit); | ||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| case 'arc': | case 'arc': | ||||
| switch ($name) { | switch ($name) { | ||||
| case 'empty': | case 'empty': | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| "you specified '{$rule}' in your {$source} 'base' ". | pht( | ||||
| "configuration."); | "you specified '%s' in your %s 'base' configuration.", | ||||
| $rule, | |||||
| $source)); | |||||
| return 'null'; | return 'null'; | ||||
| case 'outgoing': | case 'outgoing': | ||||
| list($err, $outgoing_base) = $this->execManualLocal( | list($err, $outgoing_base) = $this->execManualLocal( | ||||
| 'log --template={node} --rev %s', | 'log --template={node} --rev %s', | ||||
| 'limit(reverse(ancestors(.) - outgoing()), 1)'); | 'limit(reverse(ancestors(.) - outgoing()), 1)'); | ||||
| if (!$err) { | if (!$err) { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| pht( | |||||
| "it is the first ancestor of the working copy that is not ". | "it is the first ancestor of the working copy that is not ". | ||||
| "outgoing, and it matched the rule {$rule} in your {$source} ". | "outgoing, and it matched the rule %s in your %s ". | ||||
| "'base' configuration."); | "'base' configuration.", | ||||
| $rule, | |||||
| $source)); | |||||
| return trim($outgoing_base); | return trim($outgoing_base); | ||||
| } | } | ||||
| case 'amended': | case 'amended': | ||||
| $text = $this->getCommitMessage('.'); | $text = $this->getCommitMessage('.'); | ||||
| $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( | $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( | ||||
| $text); | $text); | ||||
| if ($message->getRevisionID()) { | if ($message->getRevisionID()) { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| "'.' has been amended with 'Differential Revision:', ". | pht( | ||||
| "as specified by '{$rule}' in your {$source} 'base' ". | "'%s' has been amended with 'Differential Revision:', ". | ||||
| "configuration."); | "as specified by '%s' in your %s 'base' configuration.", | ||||
| '.'. | |||||
| $rule, | |||||
| $source)); | |||||
| // NOTE: This should be safe because Mercurial doesn't support | // NOTE: This should be safe because Mercurial doesn't support | ||||
| // amend until 2.2. | // amend until 2.2. | ||||
| return $this->getCanonicalRevisionName('.^'); | return $this->getCanonicalRevisionName('.^'); | ||||
| } | } | ||||
| break; | break; | ||||
| case 'bookmark': | case 'bookmark': | ||||
| $revset = | $revset = | ||||
| 'limit('. | 'limit('. | ||||
| ' sort('. | ' sort('. | ||||
| ' (ancestors(.) and bookmark() - .) or'. | ' (ancestors(.) and bookmark() - .) or'. | ||||
| ' (ancestors(.) - outgoing()), '. | ' (ancestors(.) - outgoing()), '. | ||||
| ' -rev),'. | ' -rev),'. | ||||
| '1)'; | '1)'; | ||||
| list($err, $bookmark_base) = $this->execManualLocal( | list($err, $bookmark_base) = $this->execManualLocal( | ||||
| 'log --template={node} --rev %s', | 'log --template={node} --rev %s', | ||||
| $revset); | $revset); | ||||
| if (!$err) { | if (!$err) { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| "it is the first ancestor of . that either has a bookmark, or ". | pht( | ||||
| "is already in the remote and it matched the rule {$rule} in ". | "it is the first ancestor of %s that either has a bookmark, ". | ||||
| "your {$source} 'base' configuration"); | "or is already in the remote and it matched the rule %s in ". | ||||
| "your %s 'base' configuration", | |||||
| '.', | |||||
| $rule, | |||||
| $source)); | |||||
| return trim($bookmark_base); | return trim($bookmark_base); | ||||
| } | } | ||||
| break; | break; | ||||
| case 'this': | case 'this': | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| "you specified '{$rule}' in your {$source} 'base' ". | pht( | ||||
| "configuration."); | "you specified '%s' in your %s 'base' configuration.", | ||||
| $rule, | |||||
| $source)); | |||||
| return $this->getCanonicalRevisionName('.^'); | return $this->getCanonicalRevisionName('.^'); | ||||
| default: | default: | ||||
| if (preg_match('/^nodiff\((.+)\)$/', $name, $matches)) { | if (preg_match('/^nodiff\((.+)\)$/', $name, $matches)) { | ||||
| list($results) = $this->execxLocal( | list($results) = $this->execxLocal( | ||||
| 'log --template %s --rev %s', | 'log --template %s --rev %s', | ||||
| "{node}\1{desc}\2", | "{node}\1{desc}\2", | ||||
| sprintf('ancestor(.,%s)::.^', $matches[1])); | sprintf('ancestor(.,%s)::.^', $matches[1])); | ||||
| $results = array_reverse(explode("\2", trim($results))); | $results = array_reverse(explode("\2", trim($results))); | ||||
| foreach ($results as $result) { | foreach ($results as $result) { | ||||
| if (empty($result)) { | if (empty($result)) { | ||||
| continue; | continue; | ||||
| } | } | ||||
| list($node, $desc) = explode("\1", $result, 2); | list($node, $desc) = explode("\1", $result, 2); | ||||
| $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( | $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( | ||||
| $desc); | $desc); | ||||
| if ($message->getRevisionID()) { | if ($message->getRevisionID()) { | ||||
| $this->setBaseCommitExplanation( | $this->setBaseCommitExplanation( | ||||
| "it is the first ancestor of . that has a diff ". | pht( | ||||
| "and is the gca or a descendant of the gca with ". | "it is the first ancestor of %s that has a diff and is ". | ||||
| "'{$matches[1]}', specified by '{$rule}' in your ". | "the gca or a descendant of the gca with '%s', ". | ||||
| "{$source} 'base' configuration."); | "specified by '%s' in your %s 'base' configuration.", | ||||
| '.', | |||||
| $matches[1], | |||||
| $rule, | |||||
| $source)); | |||||
| return $node; | return $node; | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| } | } | ||||
| break; | break; | ||||
| default: | default: | ||||
| ▲ Show 20 Lines • Show All 143 Lines • Show Last 20 Lines | |||||