Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F996038
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
57 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php
index 93903506..c65d48f1 100644
--- a/src/repository/api/ArcanistMercurialAPI.php
+++ b/src/repository/api/ArcanistMercurialAPI.php
@@ -1,898 +1,922 @@
<?php
/**
* Interfaces with the Mercurial working copies.
*
* @group workingcopy
*/
final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
private $branch;
private $localCommitInfo;
private $includeDirectoryStateInDiffs;
private $rawDiffCache = array();
+ private $supportsRebase;
+ private $supportsPhases;
+
protected function buildLocalFuture(array $argv) {
// Mercurial has a "defaults" feature which basically breaks automation by
// allowing the user to add random flags to any command. This feature is
// "deprecated" and "a bad idea" that you should "forget ... existed"
// according to project lead Matt Mackall:
//
// http://markmail.org/message/hl3d6eprubmkkqh5
//
// There is an HGPLAIN environmental variable which enables "plain mode"
// and hopefully disables this stuff.
if (phutil_is_windows()) {
$argv[0] = 'set HGPLAIN=1 & hg '.$argv[0];
} else {
$argv[0] = 'HGPLAIN=1 hg '.$argv[0];
}
$future = newv('ExecFuture', $argv);
$future->setCWD($this->getPath());
return $future;
}
public function execPassthru($pattern /* , ... */) {
$args = func_get_args();
if (phutil_is_windows()) {
$args[0] = 'set HGPLAIN=1 & hg '.$args[0];
} else {
$args[0] = 'HGPLAIN=1 hg '.$args[0];
}
return call_user_func_array("phutil_passthru", $args);
}
public function getSourceControlSystemName() {
return 'hg';
}
public function getMetadataPath() {
return $this->getPath('.hg');
}
public function getSourceControlBaseRevision() {
return $this->getCanonicalRevisionName($this->getBaseCommit());
}
public function getCanonicalRevisionName($string) {
list($stdout) = $this->execxLocal(
'log -l 1 --template %s -r %s --',
'{node}',
$string);
return $stdout;
}
public function getSourceControlPath() {
return '/';
}
public function getBranchName() {
if (!$this->branch) {
list($stdout) = $this->execxLocal('branch');
$this->branch = trim($stdout);
}
return $this->branch;
}
public function didReloadCommitRange() {
$this->localCommitInfo = null;
}
protected function buildBaseCommit($symbolic_commit) {
if ($symbolic_commit !== null) {
try {
$commit = $this->getCanonicalRevisionName(
hgsprintf('ancestor(%s,.)', $symbolic_commit));
} catch (Exception $ex) {
throw new ArcanistUsageException(
"Commit '{$symbolic_commit}' is not a valid Mercurial commit ".
"identifier.");
}
$this->setBaseCommitExplanation("it is the greatest common ancestor of ".
"the working directory and the commit you specified explicitly.");
return $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;
}
// Mercurial 2.1 and up have phases which indicate if something is
// published or not. To find which revs are outgoing, it's much
// faster to check the phase instead of actually checking the server.
- list($err) = $this->execManualLocal('help phase');
- if (!$err) {
+ if (!$this->supportsPhases()) {
list($err, $stdout) = $this->execManualLocal(
'log --branch %s -r %s --style default',
$this->getBranchName(),
'draft()');
} else {
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() {
if ($this->localCommitInfo === null) {
$base_commit = $this->getBaseCommit();
list($info) = $this->execxLocal(
"log --template '%C' --rev %s --branch %s --",
"{node}\1{rev}\1{author|emailuser}\1{author|email}\1".
"{date|rfc822date}\1{branch}\1{tag}\1{parents}\1{desc}\2",
hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
$this->getBranchName());
$logs = array_filter(explode("\2", $info));
$last_node = null;
$futures = array();
$commits = array();
foreach ($logs as $log) {
list($node, $rev, $author, $author_email, $date, $branch, $tag,
$parents, $desc) = explode("\1", $log, 9);
// NOTE: If a commit has only one parent, {parents} returns empty.
// If it has two parents, {parents} returns revs and short hashes, not
// full hashes. Try to avoid making calls to "hg parents" because it's
// relatively expensive.
$commit_parents = null;
if (!$parents) {
if ($last_node) {
$commit_parents = array($last_node);
}
}
if (!$commit_parents) {
// We didn't get a cheap hit on previous commit, so do the full-cost
// "hg parents" call. We can run these in parallel, at least.
$futures[$node] = $this->execFutureLocal(
"parents --template='{node}\\n' --rev %s",
$node);
}
$commits[$node] = array(
'author' => $author,
'time' => strtotime($date),
'branch' => $branch,
'tag' => $tag,
'commit' => $node,
'rev' => $node, // TODO: Remove eventually.
'local' => $rev,
'parents' => $commit_parents,
'summary' => head(explode("\n", $desc)),
'message' => $desc,
'authorEmail' => $author_email,
);
$last_node = $node;
}
foreach (Futures($futures)->limit(4) as $node => $future) {
list($parents) = $future->resolvex();
$parents = array_filter(explode("\n", $parents));
$commits[$node]['parents'] = $parents;
}
// Put commits in newest-first order, to be consistent with Git and the
// expected order of "hg log" and "git log" under normal circumstances.
// The order of ancestors() is oldest-first.
$commits = array_reverse($commits);
$this->localCommitInfo = $commits;
}
return $this->localCommitInfo;
}
public function getAllFiles() {
// TODO: Handle paths with newlines.
$future = $this->buildLocalFuture(array('manifest'));
return new LinesOfALargeExecFuture($future);
}
public function getChangedFiles($since_commit) {
list($stdout) = $this->execxLocal(
'status --rev %s',
$since_commit);
return ArcanistMercurialParser::parseMercurialStatus($stdout);
}
public function getBlame($path) {
list($stdout) = $this->execxLocal(
'annotate -u -v -c --rev %s -- %s',
$this->getBaseCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
$matches = null;
$ok = preg_match('/^\s*([^:]+?) [a-f0-9]{12}: (.*)$/', $line, $matches);
if (!$ok) {
throw new Exception("Unable to parse Mercurial blame line: {$line}");
}
$revision = $matches[2];
$author = trim($matches[1]);
$blame[] = array($author, $revision);
}
return $blame;
}
protected function buildUncommittedStatus() {
list($stdout) = $this->execxLocal('status');
$results = new PhutilArrayWithDefaultValue();
$working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
foreach ($working_status as $path => $mask) {
if (!($mask & ArcanistRepositoryAPI::FLAG_UNTRACKED)) {
// Mark tracked files as uncommitted.
$mask |= self::FLAG_UNCOMMITTED;
}
$results[$path] |= $mask;
}
return $results->toArray();
}
protected function buildCommitRangeStatus() {
// TODO: Possibly we should use "hg status --rev X --rev ." for this
// instead, but we must run "hg diff" later anyway in most cases, so
// building and caching it shouldn't hurt us.
$diff = $this->getFullMercurialDiff();
if (!$diff) {
return array();
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
$status_map = array();
foreach ($changes as $change) {
$flags = 0;
switch ($change->getType()) {
case ArcanistDiffChangeType::TYPE_ADD:
case ArcanistDiffChangeType::TYPE_MOVE_HERE:
case ArcanistDiffChangeType::TYPE_COPY_HERE:
$flags |= self::FLAG_ADDED;
break;
case ArcanistDiffChangeType::TYPE_CHANGE:
case ArcanistDiffChangeType::TYPE_COPY_AWAY: // Check for changes?
$flags |= self::FLAG_MODIFIED;
break;
case ArcanistDiffChangeType::TYPE_DELETE:
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
case ArcanistDiffChangeType::TYPE_MULTICOPY:
$flags |= self::FLAG_DELETED;
break;
}
$status_map[$change->getCurrentPath()] = $flags;
}
return $status_map;
}
protected function didReloadWorkingCopy() {
// Diffs are against ".", so we need to drop the cache if we change the
// working copy.
$this->rawDiffCache = array();
$this->branch = null;
}
private function getDiffOptions() {
$options = array(
'--git',
'-U'.$this->getDiffLinesOfContext(),
);
return implode(' ', $options);
}
public function getRawDiffText($path) {
$options = $this->getDiffOptions();
// NOTE: In Mercurial, "--rev x" means "diff between x and the working
// copy state", while "--rev x..." means "diff between x and the working
// copy commit" (i.e., from 'x' to '.'). The latter excludes any dirty
// changes in the working copy.
$range = $this->getBaseCommit();
if (!$this->includeDirectoryStateInDiffs) {
$range .= '...';
}
$raw_diff_cache_key = $options.' '.$range.' '.$path;
if (idx($this->rawDiffCache, $raw_diff_cache_key)) {
return idx($this->rawDiffCache, $raw_diff_cache_key);
}
list($stdout) = $this->execxLocal(
'diff %C --rev %s -- %s',
$options,
$range,
$path);
$this->rawDiffCache[$raw_diff_cache_key] = $stdout;
return $stdout;
}
public function getFullMercurialDiff() {
return $this->getRawDiffText('');
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getBaseCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision(
$path,
$this->getWorkingCopyRevision());
}
private function getFileDataAtRevision($path, $revision) {
list($err, $stdout) = $this->execManualLocal(
'cat --rev %s -- %s',
$revision,
$path);
if ($err) {
// Assume this is "no file at revision", i.e. a deleted or added file.
return null;
} else {
return $stdout;
}
}
public function getWorkingCopyRevision() {
return '.';
}
public function isHistoryDefaultImmutable() {
return true;
}
public function supportsAmend() {
list($err, $stdout) = $this->execManualLocal('help commit');
if ($err) {
return false;
} else {
return (strpos($stdout, "amend") !== false);
}
}
+ public function supportsRebase() {
+ if ($this->supportsRebase === null) {
+ list ($err) = $this->execManualLocal("help rebase");
+ $this->supportsRebase = $err === 0;
+ }
+
+ return $this->supportsRebase;
+ }
+
+ public function supportsPhases() {
+ if ($this->supportsPhases === null) {
+ list ($err) = $this->execManualLocal("help phase");
+ $this->supportsPhases = $err === 0;
+ }
+
+ return $this->supportsPhases;
+ }
+
public function supportsCommitRanges() {
return true;
}
public function supportsLocalCommits() {
return true;
}
public function getAllBranches() {
list($branch_info) = $this->execxLocal('bookmarks');
$matches = null;
preg_match_all(
'/^\s*(\*?)\s*(.+)\s(\S+)$/m',
$branch_info,
$matches,
PREG_SET_ORDER);
$return = array();
foreach ($matches as $match) {
list(, $current, $name) = $match;
$return[] = array(
'current' => (bool)$current,
'name' => rtrim($name),
);
}
return $return;
}
public function hasLocalCommit($commit) {
try {
$this->getCanonicalRevisionName($commit);
return true;
} catch (Exception $ex) {
return false;
}
}
public function getCommitMessage($commit) {
list($message) = $this->execxLocal(
'log --template={desc} --rev %s',
$commit);
return $message;
}
public function getAllLocalChanges() {
$diff = $this->getFullMercurialDiff();
if (!strlen(trim($diff))) {
return array();
}
$parser = new ArcanistDiffParser();
return $parser->parseDiff($diff);
}
public function supportsLocalBranchMerge() {
return true;
}
public function performLocalBranchMerge($branch, $message) {
if ($branch) {
$err = phutil_passthru(
'(cd %s && HGPLAIN=1 hg merge --rev %s && hg commit -m %s)',
$this->getPath(),
$branch,
$message);
} else {
$err = phutil_passthru(
'(cd %s && HGPLAIN=1 hg merge && hg commit -m %s)',
$this->getPath(),
$message);
}
if ($err) {
throw new ArcanistUsageException("Merge failed!");
}
}
public function getFinalizedRevisionMessage() {
return "You may now push this commit upstream, as appropriate (e.g. with ".
"'hg push' or by printing and faxing it).";
}
public function getCommitMessageLog() {
$base_commit = $this->getBaseCommit();
list($stdout) = $this->execxLocal(
"log --template '{node}\\2{desc}\\1' --rev %s --branch %s --",
hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
$this->getBranchName());
$map = array();
$logs = explode("\1", trim($stdout));
foreach (array_filter($logs) as $log) {
list($node, $desc) = explode("\2", $log);
$map[$node] = $desc;
}
return array_reverse($map);
}
public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query) {
$messages = $this->getCommitMessageLog();
$parser = new ArcanistDiffParser();
// First, try to find revisions by explicit revision IDs in commit messages.
$reason_map = array();
$revision_ids = array();
foreach ($messages as $node_id => $message) {
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
if ($object->getRevisionID()) {
$revision_ids[] = $object->getRevisionID();
$reason_map[$object->getRevisionID()] = $node_id;
}
}
if ($revision_ids) {
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'ids' => $revision_ids,
));
foreach ($results as $key => $result) {
$hash = substr($reason_map[$result['id']], 0, 16);
$results[$key]['why'] =
"Commit message for '{$hash}' has explicit 'Differential Revision'.";
}
return $results;
}
// Try to find revisions by hash.
$hashes = array();
foreach ($this->getLocalCommitInformation() as $commit) {
$hashes[] = array('hgcm', $commit['commit']);
}
if ($hashes) {
// NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working
// copy with dirty changes, there may be no local commits.
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'commitHashes' => $hashes,
));
foreach ($results as $key => $hash) {
$results[$key]['why'] =
"A mercurial commit hash in the commit range is already attached ".
"to the Differential revision.";
}
return $results;
}
return array();
}
public function updateWorkingCopy() {
$this->execxLocal('up');
$this->reloadWorkingCopy();
}
private function getMercurialConfig($key, $default = null) {
list($stdout) = $this->execxLocal('showconfig %s', $key);
if ($stdout == '') {
return $default;
}
return rtrim($stdout);
}
public function getAuthor() {
return $this->getMercurialConfig('ui.username');
}
public function addToCommit(array $paths) {
$this->execxLocal(
'addremove -- %Ls',
$paths);
$this->reloadWorkingCopy();
}
public function doCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal(
'commit -l %s',
$tmp_file);
$this->reloadWorkingCopy();
}
public function amendCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal(
'commit --amend -l %s',
$tmp_file);
$this->reloadWorkingCopy();
}
public function setIncludeDirectoryStateInDiffs($include) {
$this->includeDirectoryStateInDiffs = $include;
return $this;
}
public function getCommitSummary($commit) {
if ($commit == 'null') {
return '(The Empty Void)';
}
list($summary) = $this->execxLocal(
'log --template {desc} --limit 1 --rev %s',
$commit);
$summary = head(explode("\n", $summary));
return trim($summary);
}
public function resolveBaseCommitRule($rule, $source) {
list($type, $name) = explode(':', $rule, 2);
// NOTE: This function MUST return node hashes or symbolic commits (like
// branch names or the word "tip"), not revsets. This includes ".^" and
// similar, which a revset, not a symbolic commit identifier. If you return
// a revset it will be escaped later and looked up literally.
switch ($type) {
case 'hg':
$matches = null;
if (preg_match('/^gca\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'log --template={node} --rev %s',
sprintf('ancestor(., %s)', $matches[1]));
if (!$err) {
$this->setBaseCommitExplanation(
"it is the greatest common ancestor of '{$matches[1]}' and ., as".
"specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return trim($merge_base);
}
} else {
list($err) = $this->execManualLocal(
'id -r %s',
$name);
if (!$err) {
$this->setBaseCommitExplanation(
"it is specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return $name;
}
}
break;
case 'arc':
switch ($name) {
case 'empty':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'null';
case 'outgoing':
list($err, $outgoing_base) = $this->execManualLocal(
'log --template={node} --rev %s',
'limit(reverse(ancestors(.) - outgoing()), 1)'
);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the first ancestor of the working copy that is not ".
"outgoing, and it matched the rule {$rule} in your {$source} ".
"'base' configuration.");
return trim($outgoing_base);
}
case 'amended':
$text = $this->getCommitMessage('.');
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$text);
if ($message->getRevisionID()) {
$this->setBaseCommitExplanation(
"'.' has been amended with 'Differential Revision:', ".
"as specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
// NOTE: This should be safe because Mercurial doesn't support
// amend until 2.2.
return $this->getCanonicalRevisionName('.^');
}
break;
case 'bookmark':
$revset =
'limit('.
' sort('.
' (ancestors(.) and bookmark() - .) or'.
' (ancestors(.) - outgoing()), '.
' -rev),'.
'1)';
list($err, $bookmark_base) = $this->execManualLocal(
'log --template={node} --rev %s',
$revset);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the first ancestor of . that either has a bookmark, or ".
"is already in the remote and it matched the rule {$rule} in ".
"your {$source} 'base' configuration");
return trim($bookmark_base);
}
break;
case 'this':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return $this->getCanonicalRevisionName('.^');
}
break;
default:
return null;
}
return null;
}
+ public function isHgSubversionRepo() {
+ return file_exists($this->getPath('.hg/svn'));
+ }
+
public function getSubversionInfo() {
$info = array();
$base_path = null;
$revision = null;
list($err, $raw_info) = $this->execManualLocal('svn info');
if (!$err) {
foreach (explode("\n", trim($raw_info)) as $line) {
list($key, $value) = explode(': ', $line, 2);
switch ($key) {
case 'URL':
$info['base_path'] = $value;
$base_path = $value;
break;
case 'Repository UUID':
$info['uuid'] = $value;
break;
case 'Revision':
$revision = $value;
break;
default:
break;
}
}
if ($base_path && $revision) {
$info['base_revision'] = $base_path.'@'.$revision;
}
}
return $info;
}
public function getActiveBookmark() {
$bookmarks = $this->getBookmarks();
foreach ($bookmarks as $bookmark) {
if ($bookmark['is_active']) {
return $bookmark['name'];
}
}
return null;
}
public function isBookmark($name) {
$bookmarks = $this->getBookmarks();
foreach ($bookmarks as $bookmark) {
if ($bookmark['name'] === $name) {
return true;
}
}
return false;
}
public function isBranch($name) {
$branches = $this->getBranches();
foreach ($branches as $branch) {
if ($branch['name'] === $name) {
return true;
}
}
return false;
}
public function getBranches() {
$branches = array();
list($raw_output) = $this->execxLocal('branches');
$raw_output = trim($raw_output);
foreach (explode("\n", $raw_output) as $line) {
// example line: default 0:a5ead76cdf85 (inactive)
list($name, $rev_line) = $this->splitBranchOrBookmarkLine($line);
// strip off the '(inactive)' bit if it exists
$rev_parts = explode(' ', $rev_line);
$revision = $rev_parts[0];
$branches[] = array(
'name' => $name,
'revision' => $revision);
}
return $branches;
}
public function getBookmarks() {
$bookmarks = array();
list($raw_output) = $this->execxLocal('bookmarks');
$raw_output = trim($raw_output);
if ($raw_output !== 'no bookmarks set') {
foreach (explode("\n", $raw_output) as $line) {
// example line: * mybook 2:6b274d49be97
list($name, $revision) = $this->splitBranchOrBookmarkLine($line);
$is_active = false;
if ('*' === $name[0]) {
$is_active = true;
$name = substr($name, 2);
}
$bookmarks[] = array(
'is_active' => $is_active,
'name' => $name,
'revision' => $revision);
}
}
return $bookmarks;
}
private function splitBranchOrBookmarkLine($line) {
// branches and bookmarks are printed in the format:
// default 0:a5ead76cdf85 (inactive)
// * mybook 2:6b274d49be97
// this code divides the name half from the revision half
// it does not parse the * and (inactive) bits
$colon_index = strrpos($line, ':');
$before_colon = substr($line, 0, $colon_index);
$start_rev_index = strrpos($before_colon, ' ');
$name = substr($line, 0, $start_rev_index);
$rev = substr($line, $start_rev_index);
return array(trim($name), trim($rev));
}
}
diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php
index 31492dbe..6a1e8667 100644
--- a/src/workflow/ArcanistLandWorkflow.php
+++ b/src/workflow/ArcanistLandWorkflow.php
@@ -1,878 +1,893 @@
<?php
/**
* Lands a branch by rebasing, merging and amending it.
*
* @group workflow
*/
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
private $isGit;
private $isGitSvn;
private $isHg;
private $isHgSvn;
private $oldBranch;
private $branch;
private $onto;
private $ontoRemoteBranch;
private $remote;
private $useSquash;
private $keepBranch;
private $shouldUpdateWithRebase;
private $revision;
private $messageFile;
public function getWorkflowName() {
return 'land';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**land** [__options__] [__branch__] [--onto __master__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, hg
Land an accepted change (currently sitting in local feature branch
__branch__) onto __master__ and push it to the remote. Then, delete
the feature branch. If you omit __branch__, the current branch will
be used.
In mutable repositories, this will perform a --squash merge (the
entire branch will be represented by one commit on __master__). In
immutable repositories (or when --merge is provided), it will perform
a --no-ff merge (the branch will always be merged into __master__ with
a merge commit).
Under hg, bookmarks can be landed the same way as branches.
EOTEXT
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresConduit() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getArguments() {
return array(
'onto' => array(
'param' => 'master',
'help' => "Land feature branch onto a branch other than the default ".
"('master' in git, 'default' in hg). You can change the ".
"default by setting 'arc.land.onto.default' with ".
"`arc set-config` or for the entire project in .arcconfig.",
),
'hold' => array(
'help' => "Prepare the change to be pushed, but do not actually ".
"push it.",
),
'keep-branch' => array(
'help' => "Keep the feature branch after pushing changes to the ".
"remote (by default, it is deleted).",
),
'remote' => array(
'param' => 'origin',
'help' => "Push to a remote other than the default ('origin' in git).",
),
'merge' => array(
'help' => 'Perform a --no-ff merge, not a --squash merge. If the '.
'project is marked as having an immutable history, this is '.
'the default behavior.',
'supports' => array(
'git',
),
'nosupport' => array(
'hg' => 'Use the --squash strategy when landing in mercurial.',
),
),
'squash' => array(
'help' => 'Perform a --squash merge, not a --no-ff merge. If the '.
'project is marked as having a mutable history, this is '.
'the default behavior.',
'conflicts' => array(
'merge' => '--merge and --squash are conflicting merge strategies.',
),
),
'delete-remote' => array(
'help' => 'Delete the feature branch in the remote after '.
'landing it.',
'conflicts' => array(
'keep-branch' => true,
),
),
'update-with-rebase' => array(
'help' => 'When updating the feature branch, use rebase intead of '.
'merge. This might make things work better in some cases.',
'conflicts' => array(
'merge' => 'The --merge strategy does not update the feature branch.',
),
),
'revision' => array(
'param' => 'id',
'help' => 'Use the message from a specific revision, rather than '.
'inferring the revision based on branch content.',
),
'*' => 'branch',
);
}
public function run() {
$this->readArguments();
$this->validate();
try {
$this->pullFromRemote();
} catch (Exception $ex) {
$this->restoreBranch();
throw $ex;
}
$this->checkoutBranch();
$this->findRevision();
if ($this->useSquash) {
$this->rebase();
$this->squash();
} else {
$this->merge();
}
$this->push();
if (!$this->keepBranch) {
$this->cleanupBranch();
}
if ($this->oldBranch != $this->onto) {
// If we were on some branch A and the user ran "arc land B",
// switch back to A.
if ($this->keepBranch || $this->oldBranch != $this->branch) {
$this->restoreBranch();
}
}
echo "Done.\n";
return 0;
}
private function readArguments() {
$repository_api = $this->getRepositoryAPI();
$this->isGit = $repository_api instanceof ArcanistGitAPI;
$this->isHg = $repository_api instanceof ArcanistMercurialAPI;
if (!$this->isGit && !$this->isHg) {
throw new ArcanistUsageException(
"'arc land' only supports git and mercurial.");
}
if ($this->isGit) {
$repository = $this->loadProjectRepository();
$this->isGitSvn = (idx($repository, 'vcs') == 'svn');
}
if ($this->isHg) {
- list ($err) = $repository_api->execManualLocal('svn info');
- $this->isHgSvn = !$err;
+ $this->isHgSvn = $repository_api->isHgSubversionRepo();
}
$branch = $this->getArgument('branch');
if (empty($branch)) {
$branch = $this->getBranchOrBookmark();
if ($branch) {
echo "Landing current branch '{$branch}'.\n";
$branch = array($branch);
}
}
if (count($branch) !== 1) {
throw new ArcanistUsageException(
"Specify exactly one branch to land changes from.");
}
$this->branch = head($branch);
$this->keepBranch = $this->getArgument('keep-branch');
$this->shouldUpdateWithRebase = $this->getArgument('update-with-rebase');
$onto_default = $this->isGit ? 'master' : 'default';
$onto_default = nonempty(
$this->getWorkingCopy()->getConfigFromAnySource('arc.land.onto.default'),
$onto_default);
$this->onto = $this->getArgument('onto', $onto_default);
$remote_default = $this->isGit ? 'origin' : '';
$this->remote = $this->getArgument('remote', $remote_default);
if ($this->getArgument('merge')) {
$this->useSquash = false;
} else if ($this->getArgument('squash')) {
$this->useSquash = true;
} else {
$this->useSquash = !$this->isHistoryImmutable();
}
$this->ontoRemoteBranch = $this->onto;
if ($this->isGitSvn) {
$this->ontoRemoteBranch = 'trunk';
} else if ($this->isGit) {
$this->ontoRemoteBranch = $this->remote.'/'.$this->onto;
}
$this->oldBranch = $this->getBranchOrBookmark();
}
private function validate() {
$repository_api = $this->getRepositoryAPI();
if ($this->onto == $this->branch) {
$message =
"You can not land a branch onto itself -- you are trying to land ".
"'{$this->branch}' onto '{$this->onto}'. For more information on ".
"how to push changes, see 'Pushing and Closing Revisions' in ".
"'Arcanist User Guide: arc diff' in the documentation.";
if (!$this->isHistoryImmutable()) {
$message .= " You may be able to 'arc amend' instead.";
}
throw new ArcanistUsageException($message);
}
if ($this->isHg) {
if ($this->useSquash) {
- list ($err) = $repository_api->execManualLocal("rebase --help");
- if ($err) {
+ if (!$repository_api->supportsRebase()) {
throw new ArcanistUsageException(
"You must enable the rebase extension to use ".
"the --squash strategy.");
}
}
if ($repository_api->isBookmark($this->branch) &&
!$repository_api->isBookmark($this->onto)) {
throw new ArcanistUsageException(
"Source {$this->branch} is a bookmark but destination ".
"{$this->onto} is not a bookmark. When landing a bookmark, ".
"the destination must also be a bookmark. Use --onto to specify ".
"a bookmark, or set arc.land.onto.default in .arcconfig.");
}
if ($repository_api->isBranch($this->branch) &&
!$repository_api->isBranch($this->onto)) {
throw new ArcanistUsageException(
"Source {$this->branch} is a branch but destination {$this->onto} ".
"is not a branch. When landing a branch, the destination must also ".
"be a branch. Use --onto to specify a branch, or set ".
"arc.land.onto.default in .arcconfig.");
}
}
if ($this->isGit) {
list($err) = $repository_api->execManualLocal(
'rev-parse --verify %s',
$this->branch);
if ($err) {
throw new ArcanistUsageException(
"Branch '{$this->branch}' does not exist.");
}
}
$this->requireCleanWorkingCopy();
}
private function checkoutBranch() {
$repository_api = $this->getRepositoryAPI();
- $repository_api->execxLocal(
- 'checkout %s',
- $this->branch);
+ if ($this->getBranchOrBookmark() != $this->branch) {
+ $repository_api->execxLocal(
+ 'checkout %s',
+ $this->branch);
+ }
echo phutil_console_format(
"Switched to branch **%s**. Identifying and merging...\n",
$this->branch);
}
private function findRevision() {
$repository_api = $this->getRepositoryAPI();
$this->parseBaseCommitArgument(array($this->ontoRemoteBranch));
$revision_id = $this->getArgument('revision');
if ($revision_id) {
$revision_id = $this->normalizeRevisionID($revision_id);
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => array($revision_id),
));
if (!$revisions) {
throw new ArcanistUsageException("No such revision 'D{$revision_id}'!");
}
} else {
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
));
}
if (!count($revisions)) {
throw new ArcanistUsageException(
"arc can not identify which revision exists on branch ".
"'{$this->branch}'. Update the revision with recent changes ".
"to synchronize the branch name and hashes, or use 'arc amend' ".
"to amend the commit message at HEAD, or use '--revision <id>' ".
"to select a revision explicitly.");
} else if (count($revisions) > 1) {
$message =
"There are multiple revisions on feature branch '{$this->branch}' ".
"which are not present on '{$this->onto}':\n\n".
$this->renderRevisionList($revisions)."\n".
"Separate these revisions onto different branches, or use ".
"'--revision <id>' to select one.";
throw new ArcanistUsageException($message);
}
$this->revision = head($revisions);
$rev_status = $this->revision['status'];
$rev_id = $this->revision['id'];
$rev_title = $this->revision['title'];
if ($rev_status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
$ok = phutil_console_confirm(
"Revision 'D{$rev_id}: {$rev_title}' has not been ".
"accepted. Continue anyway?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
$message = $this->getConduit()->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $rev_id,
));
$this->messageFile = new TempFile();
Filesystem::writeFile($this->messageFile, $message);
echo "Landing revision 'D{$rev_id}: ".
"{$rev_title}'...\n";
}
private function pullFromRemote() {
$repository_api = $this->getRepositoryAPI();
- $repository_api->execxLocal('checkout %s', $this->onto);
-
- echo phutil_console_format(
- "Switched to branch **%s**. Updating branch...\n",
- $this->onto);
$local_ahead_of_remote = false;
if ($this->isGit) {
+ $repository_api->execxLocal('checkout %s', $this->onto);
+
+ echo phutil_console_format(
+ "Switched to branch **%s**. Updating branch...\n",
+ $this->onto);
+
try {
$repository_api->execxLocal('pull --ff-only --no-stat');
} catch (CommandException $ex) {
if (!$this->isGitSvn) {
throw $ex;
}
list($out) = $repository_api->execxLocal(
'log %s..%s',
$this->ontoRemoteBranch,
$this->onto);
if (strlen(trim($out))) {
$local_ahead_of_remote = true;
} else {
$repository_api->execxLocal('svn rebase');
}
}
} else if ($this->isHg) {
- // execManual instead of execx because outgoing returns
- // code 1 when there is nothing outgoing
- list($err, $out) = $repository_api->execManualLocal(
- 'outgoing -r %s',
+ echo phutil_console_format(
+ "Updating **%s**...\n",
$this->onto);
- // $err === 0 means something is outgoing
- if ($err === 0) {
- $local_ahead_of_remote = true;
+ if ($repository_api->supportsPhases()) {
+ list($out) = $repository_api->execxLocal(
+ 'log -r %s --template {phase}', $this->onto);
+ if ($out != 'public') {
+ $local_ahead_of_remote = true;
+ }
} else {
+ // execManual instead of execx because outgoing returns
+ // code 1 when there is nothing outgoing
+ list($err, $out) = $repository_api->execManualLocal(
+ 'outgoing -r %s',
+ $this->onto);
+
+ // $err === 0 means something is outgoing
+ if ($err === 0) {
+ $local_ahead_of_remote = true;
+ }
+ }
+
+ if (!$local_ahead_of_remote) {
try {
- $repository_api->execxLocal('pull -u');
+ $repository_api->execxLocal('pull');
} catch (CommandException $ex) {
$err = $ex->getError();
$stdout = $ex->getStdOut();
// Copied from: PhabricatorRepositoryPullLocalDaemon.php
// NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the
// behavior of "hg pull" to return 1 in case of a successful pull
// with no changes. This behavior has been reverted, but users who
// updated between Feb 1, 2012 and Mar 1, 2012 will have the
// erroring version. Do a dumb test against stdout to check for this
// possibility.
// See: https://github.com/facebook/phabricator/issues/101/
// NOTE: Mercurial has translated versions, which translate this error
// string. In a translated version, the string will be something else,
// like "aucun changement trouve". There didn't seem to be an easy way
// to handle this (there are hard ways but this is not a common
// problem and only creates log spam, not application failures).
// Assume English.
// TODO: Remove this once we're far enough in the future that
// deployment of 2.1 is exceedingly rare?
if ($err == 1 && preg_match('/no changes found/', $stdout)) {
return;
} else {
throw $ex;
}
}
}
}
if ($local_ahead_of_remote) {
throw new ArcanistUsageException(
"Local branch '{$this->onto}' is ahead of remote branch ".
"'{$this->ontoRemoteBranch}', so landing a feature branch ".
"would push additional changes. Push or reset the changes ".
"in '{$this->onto}' before running 'arc land'.");
}
}
private function rebase() {
$repository_api = $this->getRepositoryAPI();
chdir($repository_api->getPath());
if ($this->isGit) {
if ($this->shouldUpdateWithRebase) {
$err = phutil_passthru('git rebase %s', $this->onto);
if ($err) {
throw new ArcanistUsageException(
"'git rebase {$this->onto}' failed. ".
"You can abort with 'git rebase --abort', ".
"or resolve conflicts and use 'git rebase ".
"--continue' to continue forward. After resolving the rebase, ".
"run 'arc land' again.");
}
} else {
$err = phutil_passthru(
'git merge %s -m %s',
$this->onto,
"Automatic merge by 'arc land'");
if ($err) {
throw new ArcanistUsageException(
"'git merge {$this->onto}' failed. ".
"To continue: resolve the conflicts, commit the changes, then run ".
"'arc land' again. To abort: run 'git merge --abort'.");
}
}
} else if ($this->isHg) {
$onto_tip = $repository_api->getCanonicalRevisionName($this->onto);
$common_ancestor = $repository_api->getCanonicalRevisionName(
hgsprintf("ancestor(%s, %s)",
$this->onto,
$this->branch));
// Only rebase if the local branch is not at the tip of the onto branch.
if ($onto_tip != $common_ancestor) {
// keep branch here so later we can decide whether to remove it
$err = $repository_api->execPassthru(
'rebase -d %s --keepbranches',
$this->onto);
if ($err) {
throw new ArcanistUsageException(
"'hg rebase {$this->onto}' failed. ".
"You can abort with 'hg rebase --abort', ".
"or resolve conflicts and use 'hg rebase ".
"--continue' to continue forward. After resolving the rebase, ".
"run 'arc land' again.");
}
}
}
$repository_api->reloadWorkingCopy();
}
private function squash() {
$repository_api = $this->getRepositoryAPI();
- $repository_api->execxLocal('checkout %s', $this->onto);
if ($this->isGit) {
+ $repository_api->execxLocal('checkout %s', $this->onto);
$repository_api->execxLocal(
'merge --squash --ff-only %s',
$this->branch);
} else if ($this->isHg) {
// The hg code is a little more complex than git's because we
// need to handle the case where the landing branch has child branches:
// -a--------b master
// \
// w--x mybranch
// \--y subbranch1
// \--z subbranch2
//
// arc land --branch mybranch --onto master :
// -a--b--wx master
// \--y subbranch1
// \--z subbranch2
$branch_rev_id = $repository_api->getCanonicalRevisionName($this->branch);
// At this point $this->onto has been pulled from remote and
// $this->branch has been rebased on top of onto(by the rebase()
// function). So we're guaranteed to have onto as an ancestor of branch
// when we use first((onto::branch)-onto) below.
$branch_root = $repository_api->getCanonicalRevisionName(
hgsprintf("first((%s::%s)-%s)",
$this->onto,
$this->branch,
$this->onto));
$branch_range = hgsprintf(
"(%s::%s)",
$branch_root,
$this->branch);
if (!$this->keepBranch) {
$this->handleAlternateBranches($branch_root, $branch_range);
}
// Collapse just the landing branch onto master.
// Leave its children on the original branch.
$repository_api->execxLocal(
'rebase --collapse --keep --logfile %s -r %s -d %s',
$this->messageFile,
$branch_range,
$this->onto);
if ($repository_api->isBookmark($this->branch)) {
// a bug in mercurial means bookmarks end up on the revision prior
// to the collapse when using --collapse with --keep,
// so we manually move them to the correct spots
// see: http://bz.selenic.com/show_bug.cgi?id=3716
$repository_api->execxLocal(
'bookmark -f %s',
$this->onto);
$repository_api->execxLocal(
'bookmark -f %s -r %s',
$this->branch,
$branch_rev_id);
}
// check if the branch had children
list($output) = $repository_api->execxLocal(
"log -r %s --template '{node}\\n'",
hgsprintf("children(%s)", $this->branch));
$child_branch_roots = phutil_split_lines($output, false);
$child_branch_roots = array_filter($child_branch_roots);
if ($child_branch_roots) {
// move the branch's children onto the collapsed commit
foreach ($child_branch_roots as $child_root) {
$repository_api->execxLocal(
'rebase -d %s -s %s --keep --keepbranches',
$this->onto,
$child_root);
}
}
// delete the old branch if necessary
if (!$this->keepBranch) {
$repository_api->execxLocal(
'--config extensions.mq= strip -r %s',
$branch_root);
if ($repository_api->isBookmark($this->branch)) {
$repository_api->execxLocal(
'bookmark -d %s',
$this->branch);
}
}
// All the rebases may have moved us to another branch
// so we move back.
$repository_api->execxLocal('checkout %s', $this->onto);
}
}
/**
* Detect alternate branches and prompt the user for how to handle
* them. An alternate branch is a branch that forks from the landing
* branch prior to the landing branch tip.
*
* In a situation like this:
* -a--------b master
* \
* w--x landingbranch
* \ \-- g subbranch
* \--y altbranch1
* \--z altbranch2
*
* y and z are alternate branches and will get deleted by the squash,
* so we need to detect them and ask the user what they want to do.
*
* @param string The revision id of the landing branch's root commit.
* @param string The revset specifying all the commits in the landing branch.
* @return void
*/
private function handleAlternateBranches($branch_root, $branch_range) {
$repository_api = $this->getRepositoryAPI();
// Using the tree in the doccomment, the revset below resolves as follows:
// 1. roots(descendants(w) - descendants(x) - (w::x))
// 2. roots({x,g,y,z} - {g} - {w,x})
// 3. roots({y,z})
// 4. {y,z}
$alt_branch_revset = hgsprintf(
'roots(descendants(%s)-descendants(%s)-%R)',
$branch_root,
$this->branch,
$branch_range);
list($alt_branches) = $repository_api->execxLocal(
"log --template '{node}\n' -r %s",
$alt_branch_revset);
$alt_branches = phutil_split_lines($alt_branches, false);
$alt_branches = array_filter($alt_branches);
$alt_count = count($alt_branches);
if ($alt_count > 0) {
$input = phutil_console_prompt(
"Branch {$this->branch} has {$alt_count} branch(s) forking off of it ".
"that would be deleted during a squash. Would you like to keep a ".
"non-squashed copy, rebase them on top of {$this->branch}, or abort ".
"and deal with them yourself? (k)eep, (r)ebase, (a)bort:");
if ($input == 'k' || $input == 'keep') {
$this->keepBranch = true;
} else if ($input == 'r' || $input == 'rebase') {
foreach ($alt_branches as $alt_branch) {
$repository_api->execxLocal(
'rebase --keep --keepbranches -d %s -s %s',
$this->branch,
$alt_branch);
}
} else if ($input == 'a' || $input == 'abort') {
$branch_string = implode("\n", $alt_branches);
echo "\nRemove the branches starting at these revision and ".
"run arc land again:\n{$branch_string}\n\n";
throw new ArcanistUserAbortException();
} else {
throw new ArcanistUsageException("Invalid choice. Aborting arc land.");
}
}
}
private function merge() {
$repository_api = $this->getRepositoryAPI();
// In immutable histories, do a --no-ff merge to force a merge commit with
// the right message.
$repository_api->execxLocal('checkout %s', $this->onto);
chdir($repository_api->getPath());
if ($this->isGit) {
$err = phutil_passthru(
'git merge --no-ff --no-commit %s',
$this->branch);
if ($err) {
throw new ArcanistUsageException(
"'git merge' failed. Your working copy has been left in a partially ".
"merged state. You can: abort with 'git merge --abort'; or follow ".
"the instructions to complete the merge.");
}
} else if ($this->isHg) {
// HG arc land currently doesn't support --merge.
// When merging a bookmark branch to a master branch that
// hasn't changed since the fork, mercurial fails to merge.
// Instead of only working in some cases, we just disable --merge
// until there is a demand for it.
// The user should never reach this line, since --merge is
// forbidden at the command line argument level.
throw new ArcanistUsageException(
"--merge is not currently supported for hg repos.");
}
}
private function push() {
$repository_api = $this->getRepositoryAPI();
if ($this->isGit) {
$repository_api->execxLocal(
'commit -F %s',
$this->messageFile);
} else if ($this->isHg) {
// hg rebase produces a commit earlier as part of rebase
if (!$this->useSquash) {
$repository_api->execxLocal(
'commit --logfile %s',
$this->messageFile);
}
}
if ($this->getArgument('hold')) {
echo phutil_console_format(
"Holding change in **%s**: it has NOT been pushed yet.\n",
$this->onto);
} else {
echo "Pushing change...\n\n";
chdir($repository_api->getPath());
if ($this->isGitSvn) {
$err = phutil_passthru('git svn dcommit');
$cmd = "git svn dcommit";
} else if ($this->isGit) {
$err = phutil_passthru(
'git push %s %s',
$this->remote,
$this->onto);
$cmd = "git push";
} else if ($this->isHgSvn) {
// hg-svn doesn't support 'push -r', so we do a normal push
// which hg-svn modifies to only push the current branch and
// ancestors.
$err = $repository_api->execPassthru(
'push %s',
$this->remote);
$cmd = "hg push";
} else if ($this->isHg) {
$err = $repository_api->execPassthru(
'push -r %s %s',
$this->onto,
$this->remote);
$cmd = "hg push";
}
if ($err) {
echo phutil_console_format("<bg:red>** PUSH FAILED! **</bg>\n");
if ($this->isGit) {
$repository_api->execxLocal('reset --hard HEAD^');
$this->restoreBranch();
throw new ArcanistUsageException(
"'{$cmd}' failed! Fix the error and run 'arc land' again.");
}
throw new ArcanistUsageException(
"'{$cmd}' failed! Fix the error and push this change manually.");
}
$mark_workflow = $this->buildChildWorkflow(
'close-revision',
array(
'--finalize',
'--quiet',
$this->revision['id'],
));
$mark_workflow->run();
echo "\n";
}
}
private function cleanupBranch() {
$repository_api = $this->getRepositoryAPI();
echo "Cleaning up feature branch...\n";
if ($this->isGit) {
list($ref) = $repository_api->execxLocal(
'rev-parse --verify %s',
$this->branch);
$ref = trim($ref);
$recovery_command = csprintf(
'git checkout -b %s %s',
$this->branch,
$ref);
echo "(Use `{$recovery_command}` if you want it back.)\n";
$repository_api->execxLocal(
'branch -D %s',
$this->branch);
}
// hg branches/bookmarks were closed earlier
if ($this->getArgument('delete-remote')) {
if ($this->isGit) {
list($err, $ref) = $repository_api->execManualLocal(
'rev-parse --verify %s/%s',
$this->remote,
$this->branch);
if ($err) {
echo "No remote feature branch to clean up.\n";
} else {
// NOTE: In Git, you delete a remote branch by pushing it with a
// colon in front of its name:
//
// git push <remote> :<branch>
echo "Cleaning up remote feature branch...\n";
$repository_api->execxLocal(
'push %s :%s',
$this->remote,
$this->branch);
}
} else if ($this->isHg) {
// named branches were closed as part of the earlier commit
// so only worry about bookmarks
if ($repository_api->isBookmark($this->branch)) {
$repository_api->execxLocal(
'push -B %s %s',
$this->branch,
$this->remote);
}
}
}
}
protected function getSupportedRevisionControlSystems() {
return array('git', 'hg');
}
private function getBranchOrBookmark() {
$repository_api = $this->getRepositoryAPI();
if ($this->isGit) {
$branch = $repository_api->getBranchName();
} else if ($this->isHg) {
$branch = $repository_api->getActiveBookmark();
if (!$branch) {
$branch = $repository_api->getBranchName();
}
}
return $branch;
}
/**
* Restore the original branch, e.g. after a successful land or a failed
* pull.
*/
private function restoreBranch() {
$repository_api = $this->getRepositoryAPI();
$repository_api->execxLocal(
'checkout %s',
$this->oldBranch);
echo phutil_console_format(
"Switched back to branch **%s**.\n",
$this->oldBranch);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Dec 24, 08:03 (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
557049
Default Alt Text
(57 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment