mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-10 08:52:39 +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:
parent
5025c06f3d
commit
aada1440ef
2 changed files with 386 additions and 88 deletions
|
@ -39,6 +39,17 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
||||||
return $future;
|
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() {
|
public function getSourceControlSystemName() {
|
||||||
return 'hg';
|
return 'hg';
|
||||||
}
|
}
|
||||||
|
@ -768,17 +779,98 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getActiveBookmark() {
|
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');
|
list($raw_output) = $this->execxLocal('bookmarks');
|
||||||
$raw_output = trim($raw_output);
|
$raw_output = trim($raw_output);
|
||||||
if ($raw_output !== 'no bookmarks set') {
|
if ($raw_output !== 'no bookmarks set') {
|
||||||
foreach (explode("\n", $raw_output) as $line) {
|
foreach (explode("\n", $raw_output) as $line) {
|
||||||
$line = trim($line);
|
// example line: * mybook 2:6b274d49be97
|
||||||
if ('*' === $line[0]) {
|
list($name, $revision) = $this->splitBranchOrBookmarkLine($line);
|
||||||
return idx(explode(' ', $line, 3), 1);
|
|
||||||
|
$is_active = false;
|
||||||
|
if ('*' === $name[0]) {
|
||||||
|
$is_active = true;
|
||||||
|
$name = substr($name, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bookmarks[] = array(
|
||||||
|
'is_active' => $is_active,
|
||||||
|
'name' => $name,
|
||||||
|
'revision' => $revision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
|
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
|
||||||
private $isGit;
|
private $isGit;
|
||||||
|
private $isHg;
|
||||||
|
|
||||||
private $oldBranch;
|
private $oldBranch;
|
||||||
private $branch;
|
private $branch;
|
||||||
|
@ -17,7 +18,7 @@ final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
|
||||||
private $keepBranch;
|
private $keepBranch;
|
||||||
|
|
||||||
private $revision;
|
private $revision;
|
||||||
private $message;
|
private $messageFile;
|
||||||
|
|
||||||
public function getWorkflowName() {
|
public function getWorkflowName() {
|
||||||
return 'land';
|
return 'land';
|
||||||
|
@ -32,7 +33,7 @@ EOTEXT
|
||||||
|
|
||||||
public function getCommandHelp() {
|
public function getCommandHelp() {
|
||||||
return phutil_console_format(<<<EOTEXT
|
return phutil_console_format(<<<EOTEXT
|
||||||
Supports: git
|
Supports: git, hg
|
||||||
|
|
||||||
Land an accepted change (currently sitting in local feature branch
|
Land an accepted change (currently sitting in local feature branch
|
||||||
__branch__) onto __master__ and push it to the remote. Then, delete
|
__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
|
immutable repositories (or when --merge is provided), it will perform
|
||||||
a --no-ff merge (the branch will always be merged into __master__ with
|
a --no-ff merge (the branch will always be merged into __master__ with
|
||||||
a merge commit).
|
a merge commit).
|
||||||
|
|
||||||
|
Under hg, bookmarks can be landed the same way as branches.
|
||||||
EOTEXT
|
EOTEXT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -68,10 +71,10 @@ EOTEXT
|
||||||
return array(
|
return array(
|
||||||
'onto' => array(
|
'onto' => array(
|
||||||
'param' => 'master',
|
'param' => 'master',
|
||||||
'help' => "Land feature branch onto a branch other than ".
|
'help' => "Land feature branch onto a branch other than the default ".
|
||||||
"'master' (default). You can change the default by setting ".
|
"('master' in git, 'default' in hg). You can change the ".
|
||||||
"'arc.land.onto.default' with `arc set-config` or for the ".
|
"default by setting 'arc.land.onto.default' with ".
|
||||||
"entire project in .arcconfig.",
|
"`arc set-config` or for the entire project in .arcconfig.",
|
||||||
),
|
),
|
||||||
'hold' => array(
|
'hold' => array(
|
||||||
'help' => "Prepare the change to be pushed, but do not actually ".
|
'help' => "Prepare the change to be pushed, but do not actually ".
|
||||||
|
@ -83,12 +86,18 @@ EOTEXT
|
||||||
),
|
),
|
||||||
'remote' => array(
|
'remote' => array(
|
||||||
'param' => 'origin',
|
'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(
|
'merge' => array(
|
||||||
'help' => 'Perform a --no-ff merge, not a --squash merge. If the '.
|
'help' => 'Perform a --no-ff merge, not a --squash merge. If the '.
|
||||||
'project is marked as having an immutable history, this is '.
|
'project is marked as having an immutable history, this is '.
|
||||||
'the default behavior.',
|
'the default behavior.',
|
||||||
|
'supports' => array(
|
||||||
|
'git',
|
||||||
|
),
|
||||||
|
'nosupport' => array(
|
||||||
|
'hg' => 'Use the --squash strategy when landing in mercurial.',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
'squash' => array(
|
'squash' => array(
|
||||||
'help' => 'Perform a --squash merge, not a --no-ff merge. If the '.
|
'help' => 'Perform a --squash merge, not a --no-ff merge. If the '.
|
||||||
|
@ -154,14 +163,16 @@ EOTEXT
|
||||||
private function readArguments() {
|
private function readArguments() {
|
||||||
$repository_api = $this->getRepositoryAPI();
|
$repository_api = $this->getRepositoryAPI();
|
||||||
$this->isGit = $repository_api instanceof ArcanistGitAPI;
|
$this->isGit = $repository_api instanceof ArcanistGitAPI;
|
||||||
|
$this->isHg = $repository_api instanceof ArcanistMercurialAPI;
|
||||||
|
|
||||||
if (!$this->isGit) {
|
if (!$this->isGit && !$this->isHg) {
|
||||||
throw new ArcanistUsageException("'arc land' only supports git.");
|
throw new ArcanistUsageException(
|
||||||
|
"'arc land' only supports git and mercurial.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$branch = $this->getArgument('branch');
|
$branch = $this->getArgument('branch');
|
||||||
if (empty($branch)) {
|
if (empty($branch)) {
|
||||||
$branch = $repository_api->getBranchName();
|
$branch = $this->getBranchOrBookmark();
|
||||||
|
|
||||||
if ($branch) {
|
if ($branch) {
|
||||||
echo "Landing current branch '{$branch}'.\n";
|
echo "Landing current branch '{$branch}'.\n";
|
||||||
|
@ -174,14 +185,16 @@ EOTEXT
|
||||||
"Specify exactly one branch to land changes from.");
|
"Specify exactly one branch to land changes from.");
|
||||||
}
|
}
|
||||||
$this->branch = head($branch);
|
$this->branch = head($branch);
|
||||||
|
$this->keepBranch = $this->getArgument('keep-branch');
|
||||||
|
|
||||||
|
$onto_default = $this->isGit ? 'master' : 'default';
|
||||||
$onto_default = nonempty(
|
$onto_default = nonempty(
|
||||||
$this->getWorkingCopy()->getConfigFromAnySource('arc.land.onto.default'),
|
$this->getWorkingCopy()->getConfigFromAnySource('arc.land.onto.default'),
|
||||||
'master');
|
$onto_default);
|
||||||
|
|
||||||
$this->remote = $this->getArgument('remote', 'origin');
|
|
||||||
$this->onto = $this->getArgument('onto', $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')) {
|
if ($this->getArgument('merge')) {
|
||||||
$this->useSquash = false;
|
$this->useSquash = false;
|
||||||
|
@ -191,9 +204,12 @@ EOTEXT
|
||||||
$this->useSquash = !$this->isHistoryImmutable();
|
$this->useSquash = !$this->isHistoryImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->ontoRemoteBranch = $this->remote.'/'.$this->onto;
|
$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() {
|
private function validate() {
|
||||||
|
@ -211,13 +227,44 @@ EOTEXT
|
||||||
throw new ArcanistUsageException($message);
|
throw new ArcanistUsageException($message);
|
||||||
}
|
}
|
||||||
|
|
||||||
list($err) = $repository_api->execManualLocal(
|
if ($this->isHg) {
|
||||||
'rev-parse --verify %s',
|
if ($this->useSquash) {
|
||||||
$this->branch);
|
list ($err) = $repository_api->execManualLocal("rebase --help");
|
||||||
|
if ($err) {
|
||||||
|
throw new ArcanistUsageException(
|
||||||
|
"You must enable the rebase extension to use ".
|
||||||
|
"the --squash strategy.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($err) {
|
if ($repository_api->isBookmark($this->branch) &&
|
||||||
throw new ArcanistUsageException(
|
!$repository_api->isBookmark($this->onto)) {
|
||||||
"Branch '{$this->branch}' does not exist.");
|
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();
|
$this->requireCleanWorkingCopy();
|
||||||
|
@ -290,12 +337,15 @@ EOTEXT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->message = $this->getConduit()->callMethodSynchronous(
|
$message = $this->getConduit()->callMethodSynchronous(
|
||||||
'differential.getcommitmessage',
|
'differential.getcommitmessage',
|
||||||
array(
|
array(
|
||||||
'revision_id' => $rev_id,
|
'revision_id' => $rev_id,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
$this->messageFile = new TempFile();
|
||||||
|
Filesystem::writeFile($this->messageFile, $message);
|
||||||
|
|
||||||
echo "Landing revision 'D{$rev_id}: ".
|
echo "Landing revision 'D{$rev_id}: ".
|
||||||
"{$rev_title}'...\n";
|
"{$rev_title}'...\n";
|
||||||
}
|
}
|
||||||
|
@ -308,14 +358,64 @@ EOTEXT
|
||||||
"Switched to branch **%s**. Updating branch...\n",
|
"Switched to branch **%s**. Updating branch...\n",
|
||||||
$this->onto);
|
$this->onto);
|
||||||
|
|
||||||
$repository_api->execxLocal('pull --ff-only');
|
$local_ahead_of_remote = false;
|
||||||
|
if ($this->isGit) {
|
||||||
|
$repository_api->execxLocal('pull --ff-only');
|
||||||
|
|
||||||
list($out) = $repository_api->execxLocal(
|
list($out) = $repository_api->execxLocal(
|
||||||
'log %s/%s..%s',
|
'log %s/%s..%s',
|
||||||
$this->remote,
|
$this->remote,
|
||||||
$this->onto,
|
$this->onto,
|
||||||
$this->onto);
|
$this->onto);
|
||||||
if (strlen(trim($out))) {
|
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(
|
throw new ArcanistUsageException(
|
||||||
"Local branch '{$this->onto}' is ahead of remote branch ".
|
"Local branch '{$this->onto}' is ahead of remote branch ".
|
||||||
"'{$this->ontoRemoteBranch}', so landing a feature branch ".
|
"'{$this->ontoRemoteBranch}', so landing a feature branch ".
|
||||||
|
@ -328,25 +428,66 @@ EOTEXT
|
||||||
$repository_api = $this->getRepositoryAPI();
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
chdir($repository_api->getPath());
|
chdir($repository_api->getPath());
|
||||||
$err = phutil_passthru('git rebase %s', $this->onto);
|
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) {
|
if ($err) {
|
||||||
|
$command = $repository_api->getSourceControlSystemName();
|
||||||
throw new ArcanistUsageException(
|
throw new ArcanistUsageException(
|
||||||
"'git rebase {$this->onto}' failed. ".
|
"'{$command} rebase {$this->onto}' failed. ".
|
||||||
"You can abort with 'git rebase --abort', ".
|
"You can abort with '{$command} rebase --abort', ".
|
||||||
"or resolve conflicts and use 'git rebase ".
|
"or resolve conflicts and use '{$command} rebase ".
|
||||||
"--continue' to continue forward. After resolving the rebase, ".
|
"--continue' to continue forward. After resolving the rebase, ".
|
||||||
"run 'arc land' again.");
|
"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() {
|
private function squash() {
|
||||||
$repository_api = $this->getRepositoryAPI();
|
$repository_api = $this->getRepositoryAPI();
|
||||||
$repository_api->execxLocal('checkout %s', $this->onto);
|
$repository_api->execxLocal('checkout %s', $this->onto);
|
||||||
|
|
||||||
$repository_api->execxLocal(
|
if ($this->isGit) {
|
||||||
'merge --squash --ff-only %s',
|
$repository_api->execxLocal(
|
||||||
$this->branch);
|
'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() {
|
private function merge() {
|
||||||
|
@ -357,27 +498,47 @@ EOTEXT
|
||||||
$repository_api->execxLocal('checkout %s', $this->onto);
|
$repository_api->execxLocal('checkout %s', $this->onto);
|
||||||
|
|
||||||
chdir($repository_api->getPath());
|
chdir($repository_api->getPath());
|
||||||
$err = phutil_passthru(
|
if ($this->isGit) {
|
||||||
'git merge --no-ff --no-commit %s',
|
$err = phutil_passthru(
|
||||||
$this->branch);
|
'git merge --no-ff --no-commit %s',
|
||||||
|
$this->branch);
|
||||||
|
|
||||||
if ($err) {
|
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(
|
throw new ArcanistUsageException(
|
||||||
"'git merge' failed. Your working copy has been left in a partially ".
|
"--merge is not currently supported for hg repos.");
|
||||||
"merged state. You can: abort with 'git merge --abort'; or follow ".
|
|
||||||
"the instructions to complete the merge.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function push() {
|
private function push() {
|
||||||
$repository_api = $this->getRepositoryAPI();
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
$tmp_file = new TempFile();
|
if ($this->isGit) {
|
||||||
Filesystem::writeFile($tmp_file, $this->message);
|
$repository_api->execxLocal(
|
||||||
|
'commit -F %s',
|
||||||
$repository_api->execxLocal(
|
$this->messageFile);
|
||||||
'commit -F %s',
|
}
|
||||||
$tmp_file);
|
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')) {
|
if ($this->getArgument('hold')) {
|
||||||
echo phutil_console_format(
|
echo phutil_console_format(
|
||||||
|
@ -388,10 +549,18 @@ EOTEXT
|
||||||
|
|
||||||
chdir($repository_api->getPath());
|
chdir($repository_api->getPath());
|
||||||
|
|
||||||
$err = phutil_passthru(
|
if ($this->isGit) {
|
||||||
'git push %s %s',
|
$err = phutil_passthru(
|
||||||
$this->remote,
|
'git push %s %s',
|
||||||
$this->onto);
|
$this->remote,
|
||||||
|
$this->onto);
|
||||||
|
}
|
||||||
|
else if ($this->isHg) {
|
||||||
|
$err = $repository_api->execPassthru(
|
||||||
|
'push --new-branch -r %s %s',
|
||||||
|
$this->onto,
|
||||||
|
$this->remote);
|
||||||
|
}
|
||||||
|
|
||||||
if ($err) {
|
if ($err) {
|
||||||
$repo_command = $repository_api->getSourceControlSystemName();
|
$repo_command = $repository_api->getSourceControlSystemName();
|
||||||
|
@ -415,45 +584,82 @@ EOTEXT
|
||||||
$repository_api = $this->getRepositoryAPI();
|
$repository_api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
echo "Cleaning up feature branch...\n";
|
echo "Cleaning up feature branch...\n";
|
||||||
list($ref) = $repository_api->execxLocal(
|
if ($this->isGit) {
|
||||||
'rev-parse --verify %s',
|
list($ref) = $repository_api->execxLocal(
|
||||||
$this->branch);
|
'rev-parse --verify %s',
|
||||||
$ref = trim($ref);
|
$this->branch);
|
||||||
$recovery_command = csprintf(
|
$ref = trim($ref);
|
||||||
'git checkout -b %s %s',
|
$recovery_command = csprintf(
|
||||||
$this->branch,
|
'git checkout -b %s %s',
|
||||||
$ref);
|
$this->branch,
|
||||||
echo "(Use `{$recovery_command}` if you want it back.)\n";
|
$ref);
|
||||||
$repository_api->execxLocal(
|
echo "(Use `{$recovery_command}` if you want it back.)\n";
|
||||||
'branch -D %s',
|
$repository_api->execxLocal(
|
||||||
$this->branch);
|
'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->getArgument('delete-remote')) {
|
||||||
list($err, $ref) = $repository_api->execManualLocal(
|
if ($this->isGit) {
|
||||||
'rev-parse --verify %s/%s',
|
list($err, $ref) = $repository_api->execManualLocal(
|
||||||
$this->remote,
|
'rev-parse --verify %s/%s',
|
||||||
$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->remote,
|
||||||
$this->branch);
|
$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() {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue