Changeset View
Changeset View
Standalone View
Standalone View
src/land/ArcanistGitLandEngine.php
- This file was added.
<?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(); | |||||
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'); | |||||
// TODO: Maybe throw a better or more helpful exception here? | |||||
throw $ex; | |||||
} | |||||
$api->execxLocal( | |||||
'commit --author %s --date %s -F %s --', | |||||
$original_author, | |||||
$original_date, | |||||
$this->getCommitMessageFile()); | |||||
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", | |||||
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", | |||||
pht( | |||||
'Local branch "%s" does not exist, staying on detached HEAD.', | |||||
$this->getTargetOnto())); | |||||
return; | |||||
} | |||||
list($err, $upstream) = $api->execManualLocal( | |||||
'rev-parse --verify --symbolic-full-name %s', | |||||
$this->getTargetOnto().'@{upstream}'); | |||||
if ($err) { | |||||
echo tsprintf( | |||||
"%s\n", | |||||
pht( | |||||
'Local branch "%s" has no upstream, staying on detached HEAD.', | |||||
$this->getTargetOnto())); | |||||
return; | |||||
} | |||||
$upstream = trim($upstream); | |||||
$expect_upstream = 'refs/remotes/'.$this->getTargetFullRef(); | |||||
if ($upstream != $expect_upstream) { | |||||
echo tsprintf( | |||||
"%s\n", | |||||
pht( | |||||
'Local branch "%s" tracks remote "%s" (not target remote "%s"), '. | |||||
'staying on detached HEAD.', | |||||
$this->getTargetOnto(), | |||||
$upstream, | |||||
$expect_upstream)); | |||||
return; | |||||
} | |||||
list($stdout) = $api->execxLocal( | |||||
'log %s..%s --', | |||||
$this->mergedRef, | |||||
$this->getTargetOnto()); | |||||
$stdout = trim($stdout); | |||||
if (!strlen($stdout)) { | |||||
echo tsprintf( | |||||
"%s\n", | |||||
pht( | |||||
'Local "%s" tracks target remote "%s", checking out and '. | |||||
'pulling changes.', | |||||
$this->getTargetOnto(), | |||||
$this->getTargetFullRef())); | |||||
$api->execxLocal('checkout %s --', $this->getTargetOnto()); | |||||
$api->execxLocal('pull --'); | |||||
$api->execxLocal('submodule update --init --recursive'); | |||||
return; | |||||
} | |||||
if ($this->getTargetOnto() !== $this->getSourceRef()) { | |||||
echo tsprintf( | |||||
"%s\n", | |||||
pht( | |||||
'Local "%s" is ahead of remote "%s". Checking out but '. | |||||
'not pulling changes.', | |||||
$this->getTargetOnto(), | |||||
$this->getTargetFullRef())); | |||||
$api->execxLocal('checkout %s --', $this->getTargetOnto()); | |||||
$api->execxLocal('submodule update --init --recursive'); | |||||
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", | |||||
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('reset --hard %s --', $this->getTargetFullRef()); | |||||
$api->execxLocal('submodule update --init --recursive'); | |||||
} | |||||
private function destroyLocalBranch() { | |||||
$api = $this->getRepositoryAPI(); | |||||
if ($this->localRef == $this->getSourceRef()) { | |||||
// If we landed a branch onto itself, don't destroy it. | |||||
return; | |||||
} | |||||
$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, | |||||
); | |||||
} | |||||
} |