1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-12-23 14:00:55 +01:00

Make rules for guessing onto/remote more powerful and more explicit in arc land

Summary:
Fixes T9543. Fixes T9658. Ref T3855.

Major functional change is that you can have a sequence of branches like:

  origin/master -> notmaster -> feature1

...where they track each other, but you named your local master something else. Currently, we resolve only one level of upstreams, so we try to land onto "notmaster" in this case, which is wrong.

Instead, keep resolving upstreams until we either hit a cycle, don't have another upstream to look at, or find someting in a remote. In this case we'll eventually find "origin/master" and select "origin" as the remote and "master" as the target.

Other minor changes:

  - Make this selection process explicit.
  - Make the help 3000x longer.

Also fix a bug where we could incorrectly try to tell Differential to update awith `--preview`.

Test Plan:
  - Landed from a tag.
  - Landed from a tracking branch.
  - Landed from an nth-degree tracking branch.
  - Tried to land from a local branch with a cycle in upstreams.
  - Landed with --remote and --onto.
  - Read `arc help land`.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T9658, T3855, T9543

Differential Revision: https://secure.phabricator.com/D14357
This commit is contained in:
epriestley 2015-10-28 08:28:46 -07:00
parent a03c6079bb
commit aa5c023fe8
3 changed files with 278 additions and 16 deletions

View file

@ -167,6 +167,8 @@ final class ArcanistGitLandEngine
$original_date,
$this->getCommitMessageFile());
$this->getWorkflow()->didCommitMerge();
list($stdout) = $api->execxLocal(
'rev-parse --verify %s',
'HEAD');

View file

@ -1333,4 +1333,71 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
$this->resolvedHeadCommit = null;
}
/**
* Follow the chain of tracking branches upstream until we reach a remote
* or cycle locally.
*
* @param string Ref to start from.
* @return list<wild> Path to an upstream.
*/
public function getPathToUpstream($start) {
$cursor = $start;
$path = array();
while (true) {
list($err, $upstream) = $this->execManualLocal(
'rev-parse --symbolic-full-name %s@{upstream}',
$cursor);
if ($err) {
// We ended up somewhere with no tracking branch, so we're done.
break;
}
$upstream = trim($upstream);
if (preg_match('(^refs/heads/)', $upstream)) {
$upstream = preg_replace('(^refs/heads/)', '', $upstream);
$is_cycle = isset($path[$upstream]);
$path[$cursor] = array(
'type' => 'local',
'name' => $upstream,
'cycle' => $is_cycle,
);
if ($is_cycle) {
// We ran into a local cycle, so we're done.
break;
}
// We found another local branch, so follow that one upriver.
$cursor = $upstream;
continue;
}
if (preg_match('(^refs/remotes/)', $upstream)) {
$upstream = preg_replace('(^refs/remotes/)', '', $upstream);
list($remote, $branch) = explode('/', $upstream, 2);
$path[$cursor] = array(
'type' => 'remote',
'name' => $branch,
'remote' => $remote,
);
// We found a remote, so we're done.
break;
}
throw new Exception(
pht(
'Got unrecognized upstream format ("%s") from Git, expected '.
'"refs/heads/..." or "refs/remotes/...".',
$upstream));
}
return $path;
}
}

View file

@ -38,7 +38,7 @@ final class ArcanistLandWorkflow extends ArcanistWorkflow {
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**land** [__options__] [__branch__] [--onto __master__]
**land** [__options__] [__ref__]
EOTEXT
);
}
@ -47,18 +47,71 @@ EOTEXT
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.
Publish an accepted revision after review. This command is the last
step in the standard Differential pre-publish code review workflow.
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).
This command merges and pushes changes associated with an accepted
revision that are currently sitting in __ref__, which is usually the
name of a local branch. Without __ref__, the current working copy
state will be used.
Under Git: branches, tags, and arbitrary commits (detached HEADs)
may be landed.
Under Mercurial: branches and bookmarks may be landed, but only
onto a target of the same type. See T3855.
The workflow selects a target branch to land onto and a remote where
the change will be pushed to.
A target branch is selected by examining these sources in order:
- the **--onto** flag;
- the upstream of the current branch, recursively (Git only);
- the __arc.land.onto.default__ configuration setting;
- or by falling back to a standard default:
- "master" in Git;
- "default" in Mercurial.
A remote is selected by examining these sources in order:
- the **--remote** flag;
- the upstream of the current branch, recursively (Git only);
- or by falling back to a standard default:
- "origin" in Git;
- the default remote in Mercurial.
After selecting a target branch and a remote, the commits which will
be landed are printed.
With **--preview**, execution stops here, before the change is
merged.
The change is merged into the target branch, following these rules:
In mutable repositories or with **--squash**, this will perform a
squash merge (the entire branch will be represented as one commit on
the target branch).
In immutable repositories or with **--merge**, this will perform a
strict merge (a merge commit will always be created, and local
commits will be preserved).
The resulting commit will be given an up-to-date commit message
describing the final state of the revision in Differential.
With **--hold**, execution stops here, before the change is pushed.
The change is pushed into the remote.
Consulting mystical sources of power, the workflow makes a guess
about what state you wanted to end up in after the process finishes
and the working copy is put into that state.
The branch which was landed is deleted, unless the **--keep-branch**
flag was passed or the landing branch is the same as the target
branch.
Under hg, bookmarks can be landed the same way as branches.
EOTEXT
);
}
@ -203,6 +256,8 @@ EOTEXT
}
if ($engine) {
$this->readEngineArguments();
$obsolete = array(
'delete-remote',
'update-with-merge',
@ -236,7 +291,7 @@ EOTEXT
$engine->execute();
if (!$should_hold) {
if (!$should_hold && !$this->preview) {
$this->didPush();
}
@ -303,6 +358,137 @@ EOTEXT
return null;
}
private function readEngineArguments() {
// NOTE: This is hard-coded for Git right now.
// TODO: Clean this up and move it into LandEngines.
$onto = $this->getEngineOnto();
$remote = $this->getEngineRemote();
// This just overwrites work we did earlier, but it has to be up in this
// class for now because other parts of the workflow still depend on it.
$this->onto = $onto;
$this->remote = $remote;
$this->ontoRemoteBranch = $this->remote.'/'.$onto;
}
private function getEngineOnto() {
$onto = $this->getArgument('onto');
if ($onto !== null) {
$this->writeInfo(
pht('TARGET'),
pht(
'Landing onto "%s", selected by the --onto flag.',
$onto));
return $onto;
}
$api = $this->getRepositoryAPI();
$path = $api->getPathToUpstream($this->branch);
if ($path) {
$last = last($path);
if (isset($last['cycle'])) {
$this->writeWarn(
pht('LOCAL CYCLE'),
pht(
'Local branch tracks an upstream, but following it leads to a '.
'local cycle; ignoring branch upstream.'));
echo tsprintf(
"\n %s\n\n",
$this->formatUpstreamPathCycle($path));
} else {
if ($last['type'] == 'remote') {
$onto = $last['name'];
$this->writeInfo(
pht('TARGET'),
pht(
'Landing onto "%s", selected by following tracking branches '.
'upstream to the closest remote.',
$onto));
return $onto;
} else {
$this->writeInfo(
pht('NO PATH TO UPSTREAM'),
pht(
'Local branch tracks an upstream, but there is no path '.
'to a remote; ignoring branch upstream.'));
}
}
}
$config_key = 'arc.land.onto.default';
$onto = $this->getConfigFromAnySource($config_key);
if ($onto !== null) {
$this->writeInfo(
pht('TARGET'),
pht(
'Landing onto "%s", selected by "%s" configuration.',
$onto,
$config_key));
return $onto;
}
$onto = 'master';
$this->writeInfo(
pht('TARGET'),
pht(
'Landing onto "%s", the default target under git.',
$onto));
return $onto;
}
private function getEngineRemote() {
$remote = $this->getArgument('remote');
if ($remote !== null) {
$this->writeInfo(
pht('REMOTE'),
pht(
'Using remote "%s", selected by the --remote flag.',
$remote));
return $remote;
}
$api = $this->getRepositoryAPI();
$path = $api->getPathToUpstream($this->branch);
if ($path) {
$last = last($path);
if ($last['type'] == 'remote') {
$remote = $last['remote'];
$this->writeInfo(
pht('REMOTE'),
pht(
'Using remote "%s", selected by following tracking branches '.
'upstream to the closest remote.',
$remote));
return $remote;
}
}
$remote = 'origin';
$this->writeInfo(
pht('REMOTE'),
pht(
'Using remote "%s", the default remote under git.',
$remote));
return $remote;
}
private function formatUpstreamPathCycle(array $cycle) {
$parts = array();
foreach ($cycle as $key => $value) {
$parts[] = $key;
}
$parts[] = idx(last($cycle), 'name');
$parts[] = pht('...');
return implode(' -> ', $parts);
}
private function readArguments() {
$repository_api = $this->getRepositoryAPI();
$this->isGit = $repository_api instanceof ArcanistGitAPI;
@ -320,9 +506,12 @@ EOTEXT
$branch = $this->getArgument('branch');
if (empty($branch)) {
$branch = $this->getBranchOrBookmark();
if ($branch) {
$this->branchType = $this->getBranchType($branch);
// TODO: This message is misleading when landing a detached head or
// a tag in Git.
echo pht("Landing current %s '%s'.", $this->branchType, $branch), "\n";
$branch = array($branch);
}
@ -1079,9 +1268,7 @@ EOTEXT
// We dispatch this event so we can run checks on the merged revision,
// right before it gets pushed out. It's easier to do this in arc land
// than to try to hook into git/hg.
$this->dispatchEvent(
ArcanistEventType::TYPE_LAND_WILLPUSHREVISION,
array());
$this->didCommitMerge();
} catch (Exception $ex) {
$this->executeCleanupAfterFailedPush();
throw $ex;
@ -1366,6 +1553,12 @@ EOTEXT
$engine->setCommitMessageFile($this->messageFile);
}
public function didCommitMerge() {
$this->dispatchEvent(
ArcanistEventType::TYPE_LAND_WILLPUSHREVISION,
array());
}
public function didPush() {
$this->askForRepositoryUpdate();