Changeset View
Changeset View
Standalone View
Standalone View
src/land/engine/ArcanistMercurialLandEngine.php
<?php | <?php | ||||
final class ArcanistMercurialLandEngine | final class ArcanistMercurialLandEngine | ||||
extends ArcanistLandEngine { | extends ArcanistLandEngine { | ||||
private $ontoBranchMarker; | private $ontoBranchMarker; | ||||
private $ontoMarkers; | private $ontoMarkers; | ||||
private $rebasedActiveCommit; | // During merge this populates the original commits being landed mapped to | ||||
// the new rebased/squashed commit. | |||||
private $rebasedCommitMap = array(); | |||||
protected function getDefaultSymbols() { | protected function getDefaultSymbols() { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
// TODO: In Mercurial, you normally can not create a branch and a bookmark | // TODO: In Mercurial, you normally can not create a branch and a bookmark | ||||
// with the same name. However, you can fetch a branch or bookmark from | // with the same name. However, you can fetch a branch or bookmark from | ||||
// a remote that has the same name as a local branch or bookmark of the | // a remote that has the same name as a local branch or bookmark of the | ||||
▲ Show 20 Lines • Show All 748 Lines • ▼ Show 20 Lines | foreach ($symbols as $symbol) { | ||||
} | } | ||||
} | } | ||||
return $this->confirmCommits($into_commit, $symbols, $commit_map); | return $this->confirmCommits($into_commit, $symbols, $commit_map); | ||||
} | } | ||||
protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { | protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | |||||
if ($this->getStrategy() !== 'squash') { | if ($this->getStrategy() !== 'squash') { | ||||
$log->writeError( | |||||
pht('TODO'), | |||||
pht('Merge strategy is not yet supported on Mercurial repositories.')); | |||||
throw new Exception(pht('TODO: Support merge strategies')); | throw new Exception(pht('TODO: Support merge strategies')); | ||||
} | } | ||||
// See PHI1808. When we "hg rebase ..." below, Mercurial will move | // See PHI1808. When we "hg rebase ..." below, Mercurial will move | ||||
// bookmarks which point at the old commit range to point at the rebased | // bookmarks which point at the old commit range to point at the rebased | ||||
// commit. This is somewhat surprising and we don't want this to happen: | // commit. This is somewhat surprising and we don't want this to happen: | ||||
// save the old bookmark state so we can put the bookmarks back before | // save the old bookmark state so we can put the bookmarks back before | ||||
// we continue. | // we continue. | ||||
$bookmark_refs = $api->newMarkerRefQuery() | $bookmark_refs = $api->newMarkerRefQuery() | ||||
->withMarkerTypes( | ->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | ||||
array( | |||||
ArcanistMarkerRef::TYPE_BOOKMARK, | |||||
)) | |||||
->execute(); | ->execute(); | ||||
// TODO: Add a Mercurial version check requiring 2.1.1 or newer. | // TODO: Add a Mercurial version check requiring 2.1.1 or newer. | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'update --rev %s', | 'update --rev %s', | ||||
hgsprintf('%s', $into_commit)); | hgsprintf('%s', $into_commit)); | ||||
Show All 39 Lines | try { | ||||
$future = $api->execFutureLocalWithExtension( | $future = $api->execFutureLocalWithExtension( | ||||
'rebase', | 'rebase', | ||||
'rebase %Ls', | 'rebase %Ls', | ||||
$argv); | $argv); | ||||
$future->write($commit_message); | $future->write($commit_message); | ||||
$future->resolvex(); | $future->resolvex(); | ||||
} catch (CommandException $ex) { | } catch (CommandException $ex) { | ||||
$log->writeError( | |||||
pht('REBASE CONFLICT'), | |||||
pht( | |||||
'Commits for %s do not rebase cleanly onto %s. Manually rebase and '. | |||||
'resolve conflicts before landing again.', | |||||
$revision_ref->getRefDisplayName(), | |||||
$api->getDisplayHash($into_commit))); | |||||
cspeckmim: I saw this in `ArcanistGitLandEngine` and felt a little jealous
{F9714969} | |||||
// Aborting the rebase should restore the same state prior to running the | // Aborting the rebase should restore the same state prior to running the | ||||
// rebase command. | // rebase command. | ||||
$api->execManualLocalWithExtension( | $api->execManualLocalWithExtension( | ||||
'rebase', | 'rebase', | ||||
'rebase --abort'); | 'rebase --abort'); | ||||
throw $ex; | throw $ex; | ||||
} | } | ||||
// Find all the bookmarks which pointed at commits we just rebased, and | list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}'); | ||||
// put them back the way they were before rebasing moved them. We aren't | $new_cursor = trim($stdout); | ||||
// deleting the old commits yet and don't want to move the bookmarks. | |||||
$obsolete_map = array(); | |||||
foreach ($set->getCommits() as $commit) { | foreach ($set->getCommits() as $commit) { | ||||
$obsolete_map[$commit->getHash()] = true; | $this->rebasedCommitMap[$commit->getHash()] = $new_cursor; | ||||
} | } | ||||
// Find all the bookmarks which pointed at commits we just rebased, and | |||||
// put them back the way they were before rebasing moved them. We aren't | |||||
// deleting the old commits yet and don't want to move the bookmarks. | |||||
foreach ($bookmark_refs as $bookmark_ref) { | foreach ($bookmark_refs as $bookmark_ref) { | ||||
$bookmark_hash = $bookmark_ref->getCommitHash(); | $bookmark_hash = $bookmark_ref->getCommitHash(); | ||||
if (!isset($obsolete_map[$bookmark_hash])) { | if (!isset($this->rebasedCommitMap[$bookmark_hash])) { | ||||
continue; | continue; | ||||
} | } | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'bookmark --force --rev %s -- %s', | 'bookmark --force --rev %s -- %s', | ||||
$bookmark_hash, | $bookmark_hash, | ||||
$bookmark_ref->getName()); | $bookmark_ref->getName()); | ||||
} | } | ||||
list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}'); | |||||
$new_cursor = trim($stdout); | |||||
// If any of the commits that were rebased was the active commit before the | |||||
// workflow started, track the new commit so it can be used as the working | |||||
// directory after the land has succeeded. | |||||
if (isset($obsolete_map[$this->getLocalState()->getLocalCommit()])) { | |||||
$this->rebasedActiveCommit = $new_cursor; | |||||
} | |||||
return $new_cursor; | return $new_cursor; | ||||
Done Inline ActionsNow that the entire commit translation is being tracked I think I can remove $obsolete_map here along with $this->rebasedActiveCommit. I'll look into this real quick. cspeckmim: Now that the entire commit translation is being tracked I think I can remove `$obsolete_map`… | |||||
} | } | ||||
protected function pushChange($into_commit) { | protected function pushChange($into_commit) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
list($head, $body, $tail_pass, $tail_fail) = $this->newPushCommands( | list($head, $body, $tail_pass, $tail_fail) = $this->newPushCommands( | ||||
$into_commit); | $into_commit); | ||||
▲ Show 20 Lines • Show All 154 Lines • ▼ Show 20 Lines | protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { | ||||
// This has no effect when we're executing a merge strategy. | // This has no effect when we're executing a merge strategy. | ||||
if (!$this->isSquashStrategy()) { | if (!$this->isSquashStrategy()) { | ||||
return; | return; | ||||
} | } | ||||
$old_commit = last($set->getCommits())->getHash(); | $old_commit = last($set->getCommits())->getHash(); | ||||
$new_commit = $into_commit; | $new_commit = $into_commit; | ||||
// Rebase the child commits onto the commit which $old_commit was rebased | |||||
// into. The $into_commit will be the last set's final rebased commit but | |||||
// this set may be an intermediate set that was landed. | |||||
if (isset($this->rebasedCommitMap[$old_commit])) { | |||||
$new_commit = $this->rebasedCommitMap[$old_commit]; | |||||
} | |||||
list($output) = $api->execxLocal( | list($output) = $api->execxLocal( | ||||
'log --rev %s --template %s', | 'log --rev %s --template %s', | ||||
hgsprintf('children(%s)', $old_commit), | hgsprintf('children(%s)', $old_commit), | ||||
'{node}\n'); | '{node}\n'); | ||||
$child_hashes = phutil_split_lines($output, false); | $child_hashes = phutil_split_lines($output, false); | ||||
foreach ($child_hashes as $child_hash) { | foreach ($child_hashes as $child_hash) { | ||||
if (!strlen($child_hash)) { | if (!strlen($child_hash)) { | ||||
continue; | continue; | ||||
} | } | ||||
// TODO: If the only heads which are descendants of this child will | // If this child was rebased as part of landing then it shouldn't be | ||||
// be deleted, we can skip this rebase? | // rebased now. Doing so will result in rebase conflicts. | ||||
if (isset($this->rebasedCommitMap[$child_hash])) { | |||||
continue; | |||||
} | |||||
try { | try { | ||||
$api->execxLocalWithExtension( | $api->execxLocalWithExtension( | ||||
'rebase', | 'rebase', | ||||
'rebase --source %s --dest %s --keep --keepbranches', | 'rebase --source %s --dest %s --keep --keepbranches', | ||||
$child_hash, | $child_hash, | ||||
$new_commit); | $new_commit); | ||||
} catch (CommandException $ex) { | } catch (CommandException $ex) { | ||||
$log->writeError( | |||||
pht('REBASE CONFLICT'), | |||||
pht( | |||||
'Failed to rebase %s cleanly onto %s. Manually rebase and '. | |||||
'resolve conflicts.', | |||||
$api->getDisplayHash($child_hash), | |||||
$api->getDisplayHash($new_commit))); | |||||
// Aborting the rebase should restore the same state prior to running | // Aborting the rebase should restore the same state prior to running | ||||
// the rebase command. | // the rebase command. | ||||
$api->execManualLocalWithExtension( | $api->execManualLocalWithExtension( | ||||
'rebase', | 'rebase', | ||||
'rebase --abort'); | 'rebase --abort'); | ||||
throw $ex; | throw $ex; | ||||
} | } | ||||
} | } | ||||
▲ Show 20 Lines • Show All 90 Lines • ▼ Show 20 Lines | |||||
} | } | ||||
protected function reconcileLocalState( | protected function reconcileLocalState( | ||||
$into_commit, | $into_commit, | ||||
ArcanistRepositoryLocalState $state) { | ArcanistRepositoryLocalState $state) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
// If the starting working state was not part of land process just update | // If the previoius working directory was not part of land process just | ||||
// to that original working state. | // update to that original working directory. | ||||
if ($this->rebasedActiveCommit === null) { | $prev_local_commit = $this->getLocalState()->getLocalCommit(); | ||||
$update_marker = $this->getLocalState()->getLocalCommit(); | if (!isset($this->rebasedCommitMap[$prev_local_commit])) { | ||||
$update_marker = $prev_local_commit; | |||||
if ($this->getLocalState()->getLocalBookmark() !== null) { | if ($this->getLocalState()->getLocalBookmark() !== null) { | ||||
$update_marker = $this->getLocalState()->getLocalBookmark(); | $update_marker = $this->getLocalState()->getLocalBookmark(); | ||||
} | } | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'update -- %s', | 'update -- %s', | ||||
$update_marker); | $update_marker); | ||||
$state->discardLocalState(); | $state->discardLocalState(); | ||||
return; | return; | ||||
} | } | ||||
// If the working state was landed into multiple destinations then the | // If the working state was landed into multiple destinations then the | ||||
// resulting working state is ambiguous. | // resulting working state is ambiguous. | ||||
if (count($this->ontoMarkers) != 1) { | if (count($this->ontoMarkers) != 1) { | ||||
$state->discardLocalState(); | $state->discardLocalState(); | ||||
return; | return; | ||||
} | } | ||||
// Get the current state of bookmarks | // Get the current state of bookmarks | ||||
$bookmark_refs = $api->newMarkerRefQuery() | $bookmark_refs = $api->newMarkerRefQuery() | ||||
->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | ->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | ||||
->execute(); | ->execute(); | ||||
$update_marker = $this->rebasedActiveCommit; | // Resolve the previous working directory to the new rebased/squashed | ||||
// commit, to make that the new working directory. | |||||
$update_marker = $this->rebasedCommitMap[$prev_local_commit]; | |||||
// Find any bookmarks which exist on the commit which is the result of the | // Find any bookmarks which exist on the commit which is the result of the | ||||
// starting working directory's rebase. If any of those bookmarks are also | // starting working directory's rebase. If any of those bookmarks are also | ||||
// the destination marker then we use that bookmark as the update in order | // the destination marker then we use that bookmark as the update in order | ||||
// for it to become active. | // for it to become active. | ||||
$onto_marker = $this->ontoMarkers[0]->getName(); | $onto_marker = $this->ontoMarkers[0]->getName(); | ||||
foreach ($bookmark_refs as $bookmark_ref) { | foreach ($bookmark_refs as $bookmark_ref) { | ||||
if ($bookmark_ref->getCommitHash() == $this->rebasedActiveCommit && | if ($bookmark_ref->getCommitHash() == $update_marker && | ||||
$bookmark_ref->getName() == $onto_marker) { | $bookmark_ref->getName() == $onto_marker) { | ||||
$update_marker = $onto_marker; | $update_marker = $onto_marker; | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'update -- %s', | 'update -- %s', | ||||
▲ Show 20 Lines • Show All 56 Lines • Show Last 20 Lines |
I saw this in ArcanistGitLandEngine and felt a little jealous