Page MenuHomePhorge

No OneTemporary

diff --git a/src/land/ArcanistGitLandEngine.php b/src/land/ArcanistGitLandEngine.php
index 945a5f39..f2b5d632 100644
--- a/src/land/ArcanistGitLandEngine.php
+++ b/src/land/ArcanistGitLandEngine.php
@@ -1,393 +1,529 @@
<?php
final class ArcanistGitLandEngine
extends ArcanistLandEngine {
private $localRef;
private $localCommit;
private $sourceCommit;
private $mergedRef;
private $restoreWhenDestroyed;
public function execute() {
$this->verifySourceAndTargetExist();
$this->fetchTarget();
$this->printLandingCommits();
if ($this->getShouldPreview()) {
$this->writeInfo(
pht('PREVIEW'),
pht('Completed preview of operation.'));
return;
}
$this->saveLocalState();
try {
$this->identifyRevision();
$this->updateWorkingCopy();
if ($this->getShouldHold()) {
$this->writeInfo(
pht('HOLD'),
pht('Holding change locally, it has not been pushed.'));
} else {
$this->pushChange();
$this->reconcileLocalState();
$api = $this->getRepositoryAPI();
$api->execxLocal('submodule update --init --recursive');
if ($this->getShouldKeep()) {
echo tsprintf(
"%s\n",
pht('Keeping local branch.'));
} else {
$this->destroyLocalBranch();
}
$this->writeOkay(
pht('DONE'),
pht('Landed changes.'));
}
$this->restoreWhenDestroyed = false;
} catch (Exception $ex) {
$this->restoreLocalState();
throw $ex;
}
}
public function __destruct() {
if ($this->restoreWhenDestroyed) {
$this->writeWARN(
pht('INTERRUPTED!'),
pht('Restoring working copy to its original state.'));
$this->restoreLocalState();
}
}
protected function getLandingCommits() {
$api = $this->getRepositoryAPI();
list($out) = $api->execxLocal(
'log --oneline %s..%s --',
$this->getTargetFullRef(),
$this->sourceCommit);
$out = trim($out);
if (!strlen($out)) {
return array();
} else {
return phutil_split_lines($out, false);
}
}
private function identifyRevision() {
$api = $this->getRepositoryAPI();
$api->execxLocal('checkout %s --', $this->getSourceRef());
call_user_func($this->getBuildMessageCallback(), $this);
}
private function verifySourceAndTargetExist() {
$api = $this->getRepositoryAPI();
list($err) = $api->execManualLocal(
'rev-parse --verify %s',
$this->getTargetFullRef());
if ($err) {
throw new Exception(
pht(
'Branch "%s" does not exist in remote "%s".',
$this->getTargetOnto(),
$this->getTargetRemote()));
}
list($err, $stdout) = $api->execManualLocal(
'rev-parse --verify %s',
$this->getSourceRef());
if ($err) {
throw new Exception(
pht(
'Branch "%s" does not exist in the local working copy.',
$this->getSourceRef()));
}
$this->sourceCommit = trim($stdout);
}
private function fetchTarget() {
$api = $this->getRepositoryAPI();
$ref = $this->getTargetFullRef();
$this->writeInfo(
pht('FETCH'),
pht('Fetching %s...', $ref));
$api->execxLocal(
'fetch -- %s %s',
$this->getTargetRemote(),
$this->getTargetOnto());
}
private function updateWorkingCopy() {
$api = $this->getRepositoryAPI();
$source = $this->sourceCommit;
$api->execxLocal(
'checkout %s --',
$this->getTargetFullRef());
list($original_author, $original_date) = $this->getAuthorAndDate($source);
try {
if ($this->getShouldSquash()) {
$api->execxLocal(
'merge --no-stat --no-commit --squash -- %s',
$source);
} else {
$api->execxLocal(
'merge --no-stat --no-commit --no-ff -- %s',
$source);
}
} catch (Exception $ex) {
$api->execManualLocal('merge --abort');
+ $api->execManualLocal('reset --hard HEAD --');
- // TODO: Maybe throw a better or more helpful exception here?
+ throw new Exception(
+ pht(
+ 'Local "%s" does not merge cleanly into "%s". Merge or rebase '.
+ 'local changes so they can merge cleanly.',
+ $this->getSourceRef(),
+ $this->getTargetFullRef()));
+ }
- throw $ex;
+ list($changes) = $api->execxLocal('diff HEAD --');
+ $changes = trim($changes);
+ if (!strlen($changes)) {
+ throw new Exception(
+ pht(
+ 'Merging local "%s" into "%s" produces an empty diff. '.
+ 'This usually means these changes have already landed.',
+ $this->getSourceRef(),
+ $this->getTargetFullRef()));
}
$api->execxLocal(
'commit --author %s --date %s -F %s --',
$original_author,
$original_date,
$this->getCommitMessageFile());
$this->getWorkflow()->didCommitMerge();
list($stdout) = $api->execxLocal(
'rev-parse --verify %s',
'HEAD');
$this->mergedRef = trim($stdout);
}
private function pushChange() {
$api = $this->getRepositoryAPI();
$this->writeInfo(
pht('PUSHING'),
pht('Pushing changes to "%s".', $this->getTargetFullRef()));
list($err) = $api->execPassthru(
'push -- %s %s:%s',
$this->getTargetRemote(),
$this->mergedRef,
$this->getTargetOnto());
if ($err) {
throw new ArcanistUsageException(
pht(
'Push failed! Fix the error and run "%s" again.',
'arc land'));
}
}
private function reconcileLocalState() {
$api = $this->getRepositoryAPI();
// Try to put the user into the best final state we can. This is very
// complicated because users are incredibly creative and their local
// branches may have the same names as branches in the remote but no
// relationship to them.
if ($this->localRef != $this->getSourceRef()) {
// The user ran `arc land X` but was on a different branch, so just put
// them back wherever they were before.
- echo tsprintf(
- "%s\n",
+ $this->writeInfo(
+ pht('RESTORE'),
pht('Switching back to "%s".', $this->localRef));
$this->restoreLocalState();
return;
}
- list($err) = $api->execManualLocal(
- 'rev-parse --verify %s',
- $this->getTargetOnto());
- if ($err) {
- echo tsprintf(
- "%s\n",
+ // We're going to try to find a path to the upstream target branch. We
+ // try in two different ways:
+ //
+ // - follow the source branch directly along tracking branches until
+ // we reach the upstream; or
+ // - follow a local branch with the same name as the target branch until
+ // we reach the upstream.
+
+ // First, get the path from whatever we landed to wherever it goes.
+ $local_branch = $this->getSourceRef();
+
+ $path = $api->getPathToUpstream($local_branch);
+ if ($path->getLength()) {
+ // We may want to discard the thing we landed from the path, if we're
+ // going to delete it. In this case, we don't want to update it or worry
+ // if it's dirty.
+ if ($this->getSourceRef() == $this->getTargetOnto()) {
+ // In this case, we've done something like land "master" onto itself,
+ // so we do want to update the actual branch. We're going to use the
+ // entire path.
+ } else {
+ // Otherwise, we're going to delete the branch at the end of the
+ // workflow, so throw it away the most-local branch that isn't long
+ // for this world.
+ $path->removeUpstream($local_branch);
+
+ if (!$path->getLength()) {
+ $this->writeInfo(
+ pht('UPDATE'),
+ pht(
+ 'Local branch "%s" directly tracks remote, staying on '.
+ 'detached HEAD.',
+ $local_branch));
+ return;
+ }
+
+ $local_branch = head($path->getLocalBranches());
+ }
+ } else {
+ // The source branch has no upstream, so look for a local branch with
+ // the same name as the target branch. This corresponds to the common
+ // case where you have "master" and checkout local branches from it
+ // with "git checkout -b feature", then land onto "master".
+
+ $local_branch = $this->getTargetOnto();
+
+ list($err) = $api->execManualLocal(
+ 'rev-parse --verify %s',
+ $local_branch);
+ if ($err) {
+ $this->writeInfo(
+ pht('UPDATE'),
+ pht(
+ 'Local branch "%s" does not exist, staying on detached HEAD.',
+ $local_branch));
+ return;
+ }
+
+ $path = $api->getPathToUpstream($local_branch);
+ }
+
+ if ($path->getCycle()) {
+ $this->writeWarn(
+ pht('LOCAL CYCLE'),
pht(
- 'Local branch "%s" does not exist, staying on detached HEAD.',
- $this->getTargetOnto()));
+ 'Local branch "%s" tracks an upstream but following it leads to '.
+ 'a local cycle, staying on detached HEAD.',
+ $local_branch));
return;
}
- list($err, $upstream) = $api->execManualLocal(
- 'rev-parse --verify --symbolic-full-name %s',
- $this->getTargetOnto().'@{upstream}');
- if ($err) {
- echo tsprintf(
- "%s\n",
+ if (!$path->isConnectedToRemote()) {
+ $this->writeInfo(
+ pht('UPDATE'),
pht(
- 'Local branch "%s" has no upstream, staying on detached HEAD.',
- $this->getTargetOnto()));
+ 'Local branch "%s" is not connected to a remote, staying on '.
+ 'detached HEAD.',
+ $local_branch));
return;
}
- $upstream = trim($upstream);
- $expect_upstream = 'refs/remotes/'.$this->getTargetFullRef();
- if ($upstream != $expect_upstream) {
- echo tsprintf(
- "%s\n",
+ $remote_remote = $path->getRemoteRemoteName();
+ $remote_branch = $path->getRemoteBranchName();
+
+ $remote_actual = $remote_remote.'/'.$remote_branch;
+ $remote_expect = $this->getTargetFullRef();
+ if ($remote_actual != $remote_expect) {
+ $this->writeInfo(
+ pht('UPDATE'),
pht(
- 'Local branch "%s" tracks remote "%s" (not target remote "%s"), '.
- 'staying on detached HEAD.',
- $this->getTargetOnto(),
- $upstream,
- $expect_upstream));
+ 'Local branch "%s" is connected to a remote ("%s") other than '.
+ 'the target remote ("%s"), staying on detached HEAD.',
+ $local_branch,
+ $remote_actual,
+ $remote_expect));
return;
}
- list($stdout) = $api->execxLocal(
- 'log %s..%s --',
- $this->mergedRef,
- $this->getTargetOnto());
- $stdout = trim($stdout);
+ // If we get this far, we have a sequence of branches which ultimately
+ // connect to the remote. We're going to try to update them all in reverse
+ // order, from most-upstream to most-local.
+
+ $cascade_branches = $path->getLocalBranches();
+ $cascade_branches = array_reverse($cascade_branches);
+
+ // First, check if any of them are ahead of the remote.
- if (!strlen($stdout)) {
- echo tsprintf(
- "%s\n",
+ $ahead_of_remote = array();
+ foreach ($cascade_branches as $cascade_branch) {
+ list($stdout) = $api->execxLocal(
+ 'log %s..%s --',
+ $this->mergedRef,
+ $cascade_branch);
+ $stdout = trim($stdout);
+
+ if (strlen($stdout)) {
+ $ahead_of_remote[$cascade_branch] = $cascade_branch;
+ }
+ }
+
+ // We're going to handle the last branch (the thing we ultimately intend
+ // to check out) differently. It's OK if it's ahead of the remote, as long
+ // as we just landed it.
+
+ $local_ahead = isset($ahead_of_remote[$local_branch]);
+ unset($ahead_of_remote[$local_branch]);
+ $land_self = ($this->getTargetOnto() === $this->getSourceRef());
+
+ // We aren't going to pull anything if anything upstream from us is ahead
+ // of the remote, or the local is ahead of the remote and we didn't land
+ // it onto itself.
+ $skip_pull = ($ahead_of_remote || ($local_ahead && !$land_self));
+
+ if ($skip_pull) {
+ $this->writeInfo(
+ pht('UPDATE'),
pht(
- 'Local "%s" tracks target remote "%s", checking out and '.
- 'pulling changes.',
- $this->getTargetOnto(),
- $this->getTargetFullRef()));
+ 'Local "%s" is ahead of remote "%s". Checking out "%s" but '.
+ 'not pulling changes.',
+ nonempty(head($ahead_of_remote), $local_branch),
+ $this->getTargetFullRef(),
+ $local_branch));
- $api->execxLocal('checkout %s --', $this->getTargetOnto());
- $api->execxLocal('pull --');
+ $this->writeInfo(
+ pht('CHECKOUT'),
+ pht(
+ 'Checking out "%s".',
+ $local_branch));
+
+ $api->execxLocal('checkout %s --', $local_branch);
return;
}
- if ($this->getTargetOnto() !== $this->getSourceRef()) {
- echo tsprintf(
- "%s\n",
+ // If nothing upstream from our nearest branch is ahead of the remote,
+ // pull it all.
+
+ $cascade_targets = array();
+ if (!$ahead_of_remote) {
+ foreach ($cascade_branches as $cascade_branch) {
+ if ($local_ahead && ($local_branch == $cascade_branch)) {
+ continue;
+ }
+ $cascade_targets[] = $cascade_branch;
+ }
+ }
+
+ if ($cascade_targets) {
+ $this->writeInfo(
+ pht('UPDATE'),
pht(
- 'Local "%s" is ahead of remote "%s". Checking out but '.
- 'not pulling changes.',
- $this->getTargetOnto(),
+ 'Local "%s" tracks target remote "%s", checking out and '.
+ 'pulling changes.',
+ $local_branch,
$this->getTargetFullRef()));
- $api->execxLocal('checkout %s --', $this->getTargetOnto());
+ foreach ($cascade_targets as $cascade_branch) {
+ $this->writeInfo(
+ pht('PULL'),
+ pht(
+ 'Checking out and pulling "%s".',
+ $cascade_branch));
- return;
+ $api->execxLocal('checkout %s --', $cascade_branch);
+ $api->execxLocal('pull --');
+ }
+
+ if (!$local_ahead) {
+ return;
+ }
}
// In this case, the user did something like land a branch onto itself,
// and the branch is tracking the correct remote. We're going to discard
// the local state and reset it to the state we just pushed.
- echo tsprintf(
- "%s\n",
+ $this->writeInfo(
+ pht('RESET'),
pht(
'Local "%s" landed into remote "%s", resetting local branch to '.
'remote state.',
$this->getTargetOnto(),
$this->getTargetFullRef()));
- $api->execxLocal('checkout %s --', $this->getTargetOnto());
+ $api->execxLocal('checkout %s --', $local_branch);
$api->execxLocal('reset --hard %s --', $this->getTargetFullRef());
+
+ return;
}
private function destroyLocalBranch() {
$api = $this->getRepositoryAPI();
if ($this->getSourceRef() == $this->getTargetOnto()) {
// If we landed a branch into a branch with the same name, so don't
// destroy it. This prevents us from cleaning up "master" if you're
// landing master into itself.
return;
}
// TODO: Maybe this should also recover the proper upstream?
$recovery_command = csprintf(
'git checkout -b %R %R',
$this->getSourceRef(),
$this->sourceCommit);
echo tsprintf(
"%s\n",
pht('Cleaning up branch "%s"...', $this->getSourceRef()));
echo tsprintf(
"%s\n",
pht('(Use `%s` if you want it back.)', $recovery_command));
$api->execxLocal('branch -D -- %s', $this->getSourceRef());
}
/**
* Save the local working copy state so we can restore it later.
*/
private function saveLocalState() {
$api = $this->getRepositoryAPI();
$this->localCommit = $api->getWorkingCopyRevision();
list($ref) = $api->execxLocal('rev-parse --abbrev-ref HEAD');
$ref = trim($ref);
if ($ref === 'HEAD') {
$ref = $this->localCommit;
}
$this->localRef = $ref;
$this->restoreWhenDestroyed = true;
}
/**
* Restore the working copy to the state it was in before we started
* performing writes.
*/
private function restoreLocalState() {
$api = $this->getRepositoryAPI();
$api->execxLocal('checkout %s --', $this->localRef);
$api->execxLocal('reset --hard %s --', $this->localCommit);
$api->execxLocal('submodule update --init --recursive');
$this->restoreWhenDestroyed = false;
}
private function getTargetFullRef() {
return $this->getTargetRemote().'/'.$this->getTargetOnto();
}
private function getAuthorAndDate($commit) {
$api = $this->getRepositoryAPI();
// TODO: This is working around Windows escaping problems, see T8298.
list($info) = $api->execxLocal(
'log -n1 --format=%C %s --',
'%aD%n%an%n%ae',
$commit);
$info = trim($info);
list($date, $author, $email) = explode("\n", $info, 3);
return array(
"$author <{$email}>",
$date,
);
}
}
diff --git a/src/repository/api/ArcanistGitUpstreamPath.php b/src/repository/api/ArcanistGitUpstreamPath.php
index 1d3feed0..ad487106 100644
--- a/src/repository/api/ArcanistGitUpstreamPath.php
+++ b/src/repository/api/ArcanistGitUpstreamPath.php
@@ -1,82 +1,90 @@
<?php
final class ArcanistGitUpstreamPath extends Phobject {
private $path = array();
const TYPE_LOCAL = 'local';
const TYPE_REMOTE = 'remote';
public function addUpstream($key, array $spec) {
$this->path[$key] = $spec;
return $this;
}
+ public function removeUpstream($key) {
+ unset($this->path[$key]);
+ return $this;
+ }
+
public function getUpstream($key) {
return idx($this->path, $key);
}
public function getLength() {
return count($this->path);
}
/**
* Test if this path eventually connects to a remote.
*
* @return bool True if the path connects to a remote.
*/
public function isConnectedToRemote() {
$last = last($this->path);
if (!$last) {
return false;
}
return ($last['type'] == self::TYPE_REMOTE);
}
+ public function getLocalBranches() {
+ return array_keys($this->path);
+ }
public function getRemoteBranchName() {
if (!$this->isConnectedToRemote()) {
return null;
}
return idx(last($this->path), 'name');
}
public function getRemoteRemoteName() {
if (!$this->isConnectedToRemote()) {
return null;
}
return idx(last($this->path), 'remote');
}
/**
* If this path contains a cycle, return a description of it.
*
* @return list<string>|null Cycle, if the path contains one.
*/
public function getCycle() {
$last = last($this->path);
if (!$last) {
return null;
}
if (empty($last['cycle'])) {
return null;
}
$parts = array();
foreach ($this->path as $key => $item) {
$parts[] = $key;
}
$parts[] = $item['name'];
$parts[] = pht('...');
return $parts;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 15:40 (3 w, 8 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126087
Default Alt Text
(19 KB)

Event Timeline