1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-29 10:12:41 +01:00

Add hg support for arc land

Summary:
Makes arc land support hg repositories. Both bookmarks and named branches can be landed.  For the most part all the arc land options work, but there are a few caveats:

- bookmarks can only be landed on bookmarks
- branches can only be landed on branches
- landing a named branch with --merge creates a commit to close the branch before the merge.
- since mercurial doesn't start with a default master bookmark, landing a bookmark requires specifying --onto or setting arc.land.onto.default

Test Plan:
Tested arc land with all permutations of --merge, --keep-branch on both bookmark branches and named branches.  Also tested --hold, --revision, --onto, --remote.

See https://secure.phabricator.com/P619

Also tested git arc land with --merge and --keep-branch.

Reviewers: dschleimer, sid0, epriestley, bos

Reviewed By: epriestley

CC: aran, Korvin

Differential Revision: https://secure.phabricator.com/D4068
This commit is contained in:
durham 2012-12-03 17:59:12 -08:00
parent 5025c06f3d
commit aada1440ef
2 changed files with 386 additions and 88 deletions

View file

@ -39,6 +39,17 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
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';
}
@ -768,17 +779,98 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
}
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) {
$line = trim($line);
if ('*' === $line[0]) {
return idx(explode(' ', $line, 3), 1);
}
}
}
return null;
// 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));
}
}

View file

@ -7,6 +7,7 @@
*/
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
private $isGit;
private $isHg;
private $oldBranch;
private $branch;
@ -17,7 +18,7 @@ final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
private $keepBranch;
private $revision;
private $message;
private $messageFile;
public function getWorkflowName() {
return 'land';
@ -32,7 +33,7 @@ EOTEXT
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git
Supports: git, hg
Land an accepted change (currently sitting in local feature branch
__branch__) onto __master__ and push it to the remote. Then, delete
@ -44,6 +45,8 @@ EOTEXT
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
);
}
@ -68,10 +71,10 @@ EOTEXT
return array(
'onto' => array(
'param' => 'master',
'help' => "Land feature branch onto a branch other than ".
"'master' (default). You can change the default by setting ".
"'arc.land.onto.default' with `arc set-config` or for the ".
"entire project in .arcconfig.",
'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 ".
@ -83,12 +86,18 @@ EOTEXT
),
'remote' => array(
'param' => 'origin',
'help' => "Push to a remote other than 'origin' (default).",
'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 '.
@ -154,14 +163,16 @@ EOTEXT
private function readArguments() {
$repository_api = $this->getRepositoryAPI();
$this->isGit = $repository_api instanceof ArcanistGitAPI;
$this->isHg = $repository_api instanceof ArcanistMercurialAPI;
if (!$this->isGit) {
throw new ArcanistUsageException("'arc land' only supports git.");
if (!$this->isGit && !$this->isHg) {
throw new ArcanistUsageException(
"'arc land' only supports git and mercurial.");
}
$branch = $this->getArgument('branch');
if (empty($branch)) {
$branch = $repository_api->getBranchName();
$branch = $this->getBranchOrBookmark();
if ($branch) {
echo "Landing current branch '{$branch}'.\n";
@ -174,14 +185,16 @@ EOTEXT
"Specify exactly one branch to land changes from.");
}
$this->branch = head($branch);
$this->keepBranch = $this->getArgument('keep-branch');
$onto_default = $this->isGit ? 'master' : 'default';
$onto_default = nonempty(
$this->getWorkingCopy()->getConfigFromAnySource('arc.land.onto.default'),
'master');
$this->remote = $this->getArgument('remote', 'origin');
$onto_default);
$this->onto = $this->getArgument('onto', $onto_default);
$this->keepBranch = $this->getArgument('keep-branch');
$remote_default = $this->isGit ? 'origin' : '';
$this->remote = $this->getArgument('remote', $remote_default);
if ($this->getArgument('merge')) {
$this->useSquash = false;
@ -191,9 +204,12 @@ EOTEXT
$this->useSquash = !$this->isHistoryImmutable();
}
$this->ontoRemoteBranch = $this->onto;
if ($this->isGit) {
$this->ontoRemoteBranch = $this->remote.'/'.$this->onto;
}
$this->oldBranch = $repository_api->getBranchName();
$this->oldBranch = $this->getBranchOrBookmark();
}
private function validate() {
@ -211,6 +227,36 @@ EOTEXT
throw new ArcanistUsageException($message);
}
if ($this->isHg) {
if ($this->useSquash) {
list ($err) = $repository_api->execManualLocal("rebase --help");
if ($err) {
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);
@ -219,6 +265,7 @@ EOTEXT
throw new ArcanistUsageException(
"Branch '{$this->branch}' does not exist.");
}
}
$this->requireCleanWorkingCopy();
}
@ -290,12 +337,15 @@ EOTEXT
}
}
$this->message = $this->getConduit()->callMethodSynchronous(
$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";
}
@ -308,6 +358,8 @@ EOTEXT
"Switched to branch **%s**. Updating branch...\n",
$this->onto);
$local_ahead_of_remote = false;
if ($this->isGit) {
$repository_api->execxLocal('pull --ff-only');
list($out) = $repository_api->execxLocal(
@ -316,6 +368,54 @@ EOTEXT
$this->onto,
$this->onto);
if (strlen(trim($out))) {
$local_ahead_of_remote = true;
}
} 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',
$this->onto);
// $err === 0 means something is outgoing
if ($err === 0) {
$local_ahead_of_remote = true;
}
else {
try {
$repository_api->execxLocal('pull -u');
} 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 ".
@ -328,26 +428,67 @@ EOTEXT
$repository_api = $this->getRepositoryAPI();
chdir($repository_api->getPath());
if ($this->isGit) {
$err = phutil_passthru('git rebase %s', $this->onto);
}
else if ($this->isHg) {
// keep branch here so later we can decide whether to remove it
$err = $repository_api->execPassthru(
'rebase -d %s --keepbranches',
$this->onto);
}
if ($err) {
$command = $repository_api->getSourceControlSystemName();
throw new ArcanistUsageException(
"'git rebase {$this->onto}' failed. ".
"You can abort with 'git rebase --abort', ".
"or resolve conflicts and use 'git rebase ".
"'{$command} rebase {$this->onto}' failed. ".
"You can abort with '{$command} rebase --abort', ".
"or resolve conflicts and use '{$command} rebase ".
"--continue' to continue forward. After resolving the rebase, ".
"run 'arc land' again.");
}
// Now that we've rebased, the merge-base of origin/master and HEAD may
// be different. Reparse the relative commit.
$repository_api->parseRelativeLocalCommit(array($this->ontoRemoteBranch));
}
private function squash() {
$repository_api = $this->getRepositoryAPI();
$repository_api->execxLocal('checkout %s', $this->onto);
if ($this->isGit) {
$repository_api->execxLocal(
'merge --squash --ff-only %s',
$this->branch);
}
else if ($this->isHg) {
$branch_rev_id = $repository_api->getCanonicalRevisionName($this->branch);
$repository_api->execxLocal(
'rebase --collapse --logfile %s -b %s -d %s %s',
$this->messageFile,
$this->branch,
$this->onto,
$this->keepBranch ? '--keep' : '');
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);
if ($this->keepBranch) {
$repository_api->execxLocal(
'bookmark -f %s -r %s',
$this->branch,
$branch_rev_id);
}
}
}
}
private function merge() {
$repository_api = $this->getRepositoryAPI();
@ -357,6 +498,7 @@ EOTEXT
$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);
@ -368,16 +510,35 @@ EOTEXT
"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();
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $this->message);
if ($this->isGit) {
$repository_api->execxLocal(
'commit -F %s',
$tmp_file);
$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(
@ -388,10 +549,18 @@ EOTEXT
chdir($repository_api->getPath());
if ($this->isGit) {
$err = phutil_passthru(
'git push %s %s',
$this->remote,
$this->onto);
}
else if ($this->isHg) {
$err = $repository_api->execPassthru(
'push --new-branch -r %s %s',
$this->onto,
$this->remote);
}
if ($err) {
$repo_command = $repository_api->getSourceControlSystemName();
@ -415,6 +584,7 @@ EOTEXT
$repository_api = $this->getRepositoryAPI();
echo "Cleaning up feature branch...\n";
if ($this->isGit) {
list($ref) = $repository_api->execxLocal(
'rev-parse --verify %s',
$this->branch);
@ -427,8 +597,19 @@ EOTEXT
$repository_api->execxLocal(
'branch -D %s',
$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(
'bookmark -d %s',
$this->branch);
}
}
if ($this->getArgument('delete-remote')) {
if ($this->isGit) {
list($err, $ref) = $repository_api->execManualLocal(
'rev-parse --verify %s/%s',
$this->remote,
@ -450,10 +631,35 @@ EOTEXT
$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');
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;
}
}