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 $ontoMarkers; | |||||
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 | ||||
// other type, and end up with a local branch and bookmark with the same | // other type, and end up with a local branch and bookmark with the same | ||||
▲ Show 20 Lines • Show All 223 Lines • ▼ Show 20 Lines | $log->writeStatus( | ||||
pht( | pht( | ||||
'Landing onto target "%s", the default target under Mercurial.', | 'Landing onto target "%s", the default target under Mercurial.', | ||||
$default_onto)); | $default_onto)); | ||||
return array($default_onto); | return array($default_onto); | ||||
} | } | ||||
protected function confirmOntoRefs(array $onto_refs) { | protected function confirmOntoRefs(array $onto_refs) { | ||||
$api = $this->getRepositoryAPI(); | |||||
foreach ($onto_refs as $onto_ref) { | foreach ($onto_refs as $onto_ref) { | ||||
if (!strlen($onto_ref)) { | if (!strlen($onto_ref)) { | ||||
throw new PhutilArgumentUsageException( | throw new PhutilArgumentUsageException( | ||||
pht( | pht( | ||||
'Selected "onto" ref "%s" is invalid: the empty string is not '. | 'Selected "onto" ref "%s" is invalid: the empty string is not '. | ||||
'a valid ref.', | 'a valid ref.', | ||||
$onto_ref)); | $onto_ref)); | ||||
} | } | ||||
} | } | ||||
$remote_ref = id(new ArcanistRemoteRef()) | |||||
->setRemoteName($this->getOntoRemote()); | |||||
$markers = $api->newMarkerRefQuery() | |||||
->withRemotes(array($remote_ref)) | |||||
->execute(); | |||||
$onto_markers = array(); | |||||
$new_markers = array(); | |||||
foreach ($onto_refs as $onto_ref) { | |||||
$matches = array(); | |||||
foreach ($markers as $marker) { | |||||
if ($marker->getName() === $onto_ref) { | |||||
$matches[] = $marker; | |||||
} | |||||
} | |||||
$match_count = count($matches); | |||||
if ($match_count > 1) { | |||||
throw new PhutilArgumentUsageException( | |||||
pht( | |||||
'TODO: Ambiguous ref.')); | |||||
} else if (!$match_count) { | |||||
$new_bookmark = id(new ArcanistMarkerRef()) | |||||
->setMarkerType(ArcanistMarkerRef::TYPE_BOOKMARK) | |||||
->setName($onto_ref) | |||||
->attachRemoteRef($remote_ref); | |||||
$onto_markers[] = $new_bookmark; | |||||
$new_markers[] = $new_bookmark; | |||||
} else { | |||||
$onto_markers[] = $marker; | |||||
} | |||||
} | |||||
$branches = array(); | |||||
foreach ($onto_markers as $onto_marker) { | |||||
if ($onto_marker->isBranch()) { | |||||
$branches[] = $onto_marker; | |||||
} | |||||
$branch_count = count($branches); | |||||
if ($branch_count > 1) { | |||||
throw new PhutilArgumentUsageException( | |||||
pht( | |||||
'TODO: You can not push onto multiple branches in Mercurial.')); | |||||
} else if ($branch_count) { | |||||
$this->ontoBranchMarker = head($branches); | |||||
} | |||||
} | |||||
if ($new_markers) { | |||||
// TODO: If we're creating bookmarks, ask the user to confirm. | |||||
} | |||||
$this->ontoMarkers = $onto_markers; | |||||
} | } | ||||
protected function selectIntoRemote() { | protected function selectIntoRemote() { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
if ($this->getIntoEmptyArgument()) { | if ($this->getIntoEmptyArgument()) { | ||||
$this->setIntoEmpty(true); | $this->setIntoEmpty(true); | ||||
▲ Show 20 Lines • Show All 99 Lines • ▼ Show 20 Lines | if (count($ontos) > 1) { | ||||
pht( | pht( | ||||
'Will merge into target "%s" by default, because this is the "onto" '. | 'Will merge into target "%s" by default, because this is the "onto" '. | ||||
'target.', | 'target.', | ||||
$onto)); | $onto)); | ||||
} | } | ||||
} | } | ||||
protected function selectIntoCommit() { | protected function selectIntoCommit() { | ||||
// Make sure that our "into" target is valid. | |||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
if ($this->getIntoEmpty()) { | if ($this->getIntoEmpty()) { | ||||
// If we're running under "--into-empty", we don't have to do anything. | // If we're running under "--into-empty", we don't have to do anything. | ||||
$log->writeStatus( | $log->writeStatus( | ||||
pht('INTO COMMIT'), | pht('INTO COMMIT'), | ||||
pht('Preparing merge into the empty state.')); | pht('Preparing merge into the empty state.')); | ||||
return null; | return null; | ||||
} | } | ||||
if ($this->getIntoLocal()) { | if ($this->getIntoLocal()) { | ||||
// If we're running under "--into-local", just make sure that the | // If we're running under "--into-local", just make sure that the | ||||
// target identifies some actual commit. | // target identifies some actual commit. | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$local_ref = $this->getIntoRef(); | $local_ref = $this->getIntoRef(); | ||||
// TODO: This error handling could probably be cleaner. | // TODO: This error handling could probably be cleaner, it will just | ||||
// raise an exception without any context. | |||||
$into_commit = $api->getCanonicalRevisionName($local_ref); | $into_commit = $api->getCanonicalRevisionName($local_ref); | ||||
$log->writeStatus( | $log->writeStatus( | ||||
pht('INTO COMMIT'), | pht('INTO COMMIT'), | ||||
pht( | pht( | ||||
'Preparing merge into local target "%s", at commit "%s".', | 'Preparing merge into local target "%s", at commit "%s".', | ||||
$local_ref, | $local_ref, | ||||
▲ Show 20 Lines • Show All 44 Lines • ▼ Show 20 Lines | protected function selectIntoCommit() { | ||||
return null; | return null; | ||||
} | } | ||||
private function fetchTarget(ArcanistLandTarget $target) { | private function fetchTarget(ArcanistLandTarget $target) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
// See T9948. If the user specified "--into X", we don't know if it's a | $target_name = $target->getRef(); | ||||
// branch, a bookmark, or a symbol which doesn't exist yet. | |||||
// In native Mercurial it is difficult to figure this out, so we use | |||||
// an extension to provide a command which works like "git ls-remote". | |||||
// NOTE: We're using passthru on this because it's a remote command and | |||||
// may prompt the user for credentials. | |||||
$tmpfile = new TempFile(); | |||||
Filesystem::remove($tmpfile); | |||||
$command = $this->newPassthruCommand( | |||||
'%Ls arc-ls-remote --output %s -- %s', | |||||
$api->getMercurialExtensionArguments(), | |||||
phutil_string_cast($tmpfile), | |||||
$target->getRemote()); | |||||
$command->setDisplayCommand( | |||||
'hg ls-remote -- %s', | |||||
$target->getRemote()); | |||||
$err = $command->execute(); | |||||
if ($err) { | |||||
throw new Exception( | |||||
pht( | |||||
'Call to "hg arc-ls-remote" failed with error "%s".', | |||||
$err)); | |||||
} | |||||
$raw_data = Filesystem::readFile($tmpfile); | |||||
unset($tmpfile); | |||||
$markers = phutil_json_decode($raw_data); | $remote_ref = id(new ArcanistRemoteRef()) | ||||
->setRemoteName($target->getRemote()); | |||||
$target_name = $target->getRef(); | $markers = $api->newMarkerRefQuery() | ||||
->withRemotes(array($remote_ref)) | |||||
->withNames(array($target_name)) | |||||
->execute(); | |||||
$bookmarks = array(); | $bookmarks = array(); | ||||
$branches = array(); | $branches = array(); | ||||
foreach ($markers as $marker) { | foreach ($markers as $marker) { | ||||
if ($marker['name'] !== $target_name) { | if ($marker->isBookmark()) { | ||||
continue; | |||||
} | |||||
if ($marker['type'] === 'bookmark') { | |||||
$bookmarks[] = $marker; | $bookmarks[] = $marker; | ||||
} else { | } else { | ||||
$branches[] = $marker; | $branches[] = $marker; | ||||
} | } | ||||
} | } | ||||
if (!$bookmarks && !$branches) { | if (!$bookmarks && !$branches) { | ||||
throw new PhutilArgumentUsageException( | throw new PhutilArgumentUsageException( | ||||
Show All 29 Lines | if ($bookmarks) { | ||||
pht( | pht( | ||||
'Remote "%s" has multiple bookmarks with name "%s". This '. | 'Remote "%s" has multiple bookmarks with name "%s". This '. | ||||
'is unexpected.', | 'is unexpected.', | ||||
$target->getRemote(), | $target->getRemote(), | ||||
$target->getRef())); | $target->getRef())); | ||||
} | } | ||||
$bookmark = head($bookmarks); | $bookmark = head($bookmarks); | ||||
$target_hash = $bookmark['node']; | $target_marker = $bookmark; | ||||
$is_bookmark = true; | |||||
} | } | ||||
if ($branches) { | if ($branches) { | ||||
if (count($branches) > 1) { | if (count($branches) > 1) { | ||||
echo tsprintf( | echo tsprintf( | ||||
"\n%!\n%W\n\n", | "\n%!\n%W\n\n", | ||||
pht('MULTIPLE BRANCH HEADS'), | pht('MULTIPLE BRANCH HEADS'), | ||||
pht( | pht( | ||||
'Remote "%s" has multiple branch heads named "%s". Close all '. | 'Remote "%s" has multiple branch heads named "%s". Close all '. | ||||
'but one, or pull the head you want and use "--into-local '. | 'but one, or pull the head you want and use "--into-local '. | ||||
'--into <hash>" to specify an explicit merge target.', | '--into <hash>" to specify an explicit merge target.', | ||||
$target->getRemote(), | $target->getRemote(), | ||||
$target->getRef())); | $target->getRef())); | ||||
throw new PhutilArgumentUsageException( | throw new PhutilArgumentUsageException( | ||||
pht( | pht( | ||||
'Remote branch has multiple heads.')); | 'Remote branch has multiple heads.')); | ||||
} | } | ||||
$branch = head($branches); | $branch = head($branches); | ||||
$target_hash = $branch['node']; | $target_marker = $branch; | ||||
$is_branch = true; | |||||
} | } | ||||
if ($is_branch) { | if ($is_branch) { | ||||
$err = $this->newPassthru( | $err = $this->newPassthru( | ||||
'pull -b %s -- %s', | 'pull --branch %s -- %s', | ||||
$target->getRef(), | $target->getRef(), | ||||
$target->getRemote()); | $target->getRemote()); | ||||
} else { | } else { | ||||
// NOTE: This may have side effects: | // NOTE: This may have side effects: | ||||
// | // | ||||
// - It can create a "bookmark@remote" bookmark if there is a local | // - It can create a "bookmark@remote" bookmark if there is a local | ||||
// bookmark with the same name that is not an ancestor. | // bookmark with the same name that is not an ancestor. | ||||
// - It can create an arbitrary number of other bookmarks. | // - It can create an arbitrary number of other bookmarks. | ||||
// | // | ||||
// Since these seem to generally be intentional behaviors in Mercurial, | // Since these seem to generally be intentional behaviors in Mercurial, | ||||
// and should theoretically be familiar to Mercurial users, just accept | // and should theoretically be familiar to Mercurial users, just accept | ||||
// them as the cost of doing business. | // them as the cost of doing business. | ||||
$err = $this->newPassthru( | $err = $this->newPassthru( | ||||
'pull -B %s -- %s', | 'pull --bookmark %s -- %s', | ||||
$target->getRef(), | $target->getRef(), | ||||
$target->getRemote()); | $target->getRemote()); | ||||
} | } | ||||
// NOTE: It's possible that between the time we ran "ls-remote" and the | // NOTE: It's possible that between the time we ran "ls-markers" and the | ||||
// time we ran "pull" that the remote changed. | // time we ran "pull" that the remote changed. | ||||
// It may even have been rewound or rewritten, in which case we did not | // It may even have been rewound or rewritten, in which case we did not | ||||
// actually fetch the ref we are about to return as a target. For now, | // actually fetch the ref we are about to return as a target. For now, | ||||
// assume this didn't happen: it's so unlikely that it's probably not | // assume this didn't happen: it's so unlikely that it's probably not | ||||
// worth spending 100ms to check. | // worth spending 100ms to check. | ||||
// TODO: If the Mercurial command server is revived, this check becomes | // TODO: If the Mercurial command server is revived, this check becomes | ||||
// more reasonable if it's cheap. | // more reasonable if it's cheap. | ||||
return $target_hash; | return $target_marker->getCommitHash(); | ||||
} | } | ||||
protected function selectCommits($into_commit, array $symbols) { | protected function selectCommits($into_commit, array $symbols) { | ||||
assert_instances_of($symbols, 'ArcanistLandSymbol'); | assert_instances_of($symbols, 'ArcanistLandSymbol'); | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$commit_map = array(); | $commit_map = array(); | ||||
foreach ($symbols as $symbol) { | foreach ($symbols as $symbol) { | ||||
▲ Show 20 Lines • Show All 77 Lines • ▼ Show 20 Lines | protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { | ||||
$commits = $set->getCommits(); | $commits = $set->getCommits(); | ||||
$min_commit = last($commits)->getHash(); | $min_commit = last($commits)->getHash(); | ||||
$max_commit = head($commits)->getHash(); | $max_commit = head($commits)->getHash(); | ||||
$revision_ref = $set->getRevisionRef(); | $revision_ref = $set->getRevisionRef(); | ||||
$commit_message = $revision_ref->getCommitMessage(); | $commit_message = $revision_ref->getCommitMessage(); | ||||
// If we're landing "--onto" a branch, set that as the branch marker | |||||
// before creating the new commit. | |||||
// TODO: We could skip this if we know that the "$into_commit" already | |||||
// has the right branch, which it will if we created it. | |||||
$branch_marker = $this->ontoBranchMarker; | |||||
if ($branch_marker) { | |||||
$api->execxLocal('branch -- %s', $branch_marker); | |||||
} | |||||
try { | try { | ||||
$argv = array(); | $argv = array(); | ||||
$argv[] = '--dest'; | $argv[] = '--dest'; | ||||
$argv[] = hgsprintf('%s', $into_commit); | $argv[] = hgsprintf('%s', $into_commit); | ||||
$argv[] = '--rev'; | $argv[] = '--rev'; | ||||
$argv[] = hgsprintf('%s..%s', $min_commit, $max_commit); | $argv[] = hgsprintf('%s..%s', $min_commit, $max_commit); | ||||
Show All 17 Lines | protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { | ||||
$new_cursor = trim($stdout); | $new_cursor = trim($stdout); | ||||
return $new_cursor; | return $new_cursor; | ||||
} | } | ||||
protected function pushChange($into_commit) { | protected function pushChange($into_commit) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
// TODO: This does not respect "--into" or "--onto" properly. | $bookmarks = array(); | ||||
foreach ($this->ontoMarkers as $onto_marker) { | |||||
if (!$onto_marker->isBookmark()) { | |||||
continue; | |||||
} | |||||
$bookmarks[] = $onto_marker; | |||||
} | |||||
// If we're pushing to bookmarks, move all the bookmarks we want to push | |||||
// to the merge commit. (There doesn't seem to be any way to specify | |||||
// "push commit X as bookmark Y" in Mercurial.) | |||||
$restore = array(); | |||||
if ($bookmarks) { | |||||
$markers = $api->newMarkerRefQuery() | |||||
->withNames(array(mpull($bookmarks, 'getName'))) | |||||
->withTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | |||||
->execute(); | |||||
$markers = mpull($markers, 'getCommitHash', 'getName'); | |||||
foreach ($bookmarks as $bookmark) { | |||||
$bookmark_name = $bookmark->getName(); | |||||
$old_position = idx($markers, $bookmark_name); | |||||
$new_position = $into_commit; | |||||
if ($old_position === $new_position) { | |||||
continue; | |||||
} | |||||
$api->execxLocal( | |||||
'bookmark --force --rev %s -- %s', | |||||
hgsprintf('%s', $new_position), | |||||
$bookmark_name); | |||||
$restore[$bookmark_name] = $old_position; | |||||
} | |||||
} | |||||
// Now, do the actual push. | |||||
$argv = array(); | |||||
$argv[] = 'push'; | |||||
if ($bookmarks) { | |||||
// If we're pushing at least one bookmark, we can just specify the list | |||||
// of bookmarks as things we want to push. | |||||
foreach ($bookmarks as $bookmark) { | |||||
$argv[] = '--bookmark'; | |||||
$argv[] = $bookmark->getName(); | |||||
} | |||||
} else { | |||||
// Otherwise, specify the commit itself. | |||||
$argv[] = '--rev'; | |||||
$argv[] = hgsprintf('%s', $into_commit); | |||||
} | |||||
$argv[] = '--'; | |||||
$argv[] = $this->getOntoRemote(); | |||||
try { | |||||
$this->newPassthru('%Ls', $argv); | |||||
} finally { | |||||
foreach ($restore as $bookmark_name => $old_position) { | |||||
if ($old_position === null) { | |||||
$api->execxLocal( | |||||
'bookmark --delete -- %s', | |||||
$bookmark_name); | |||||
} else { | |||||
$api->execxLocal( | |||||
'bookmark --force --rev %s -- %s', | |||||
hgsprintf('%s', $old_position), | |||||
$bookmark_name); | |||||
} | |||||
} | |||||
} | |||||
$this->newPassthru( | |||||
'push --rev %s -- %s', | |||||
hgsprintf('%s', $into_commit), | |||||
$this->getOntoRemote()); | |||||
} | } | ||||
protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { | protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
// 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()) { | ||||
▲ Show 20 Lines • Show All 125 Lines • ▼ Show 20 Lines | protected function didHoldChanges($into_commit) { | ||||
} | } | ||||
echo tsprintf( | echo tsprintf( | ||||
"%s\n", | "%s\n", | ||||
pht( | pht( | ||||
'Local branches and bookmarks have not been changed, and are still '. | 'Local branches and bookmarks have not been changed, and are still '. | ||||
'in the same state as before.')); | 'in the same state as before.')); | ||||
} | } | ||||
private function newRemoteMarkers($remote) { | |||||
// See T9948. If the user specified "--into X" or "--onto X", we don't know | |||||
// if it's a branch, a bookmark, or a symbol which doesn't exist yet. | |||||
} | |||||
} | } |