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. | |||||
Lint: TODO Comment: This comment has a TODO. | |||||
| } | |||||
| $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 | ||||
Lint: TODO Comment This comment has a TODO. Lint: TODO Comment: This comment has a TODO. | |||||
| // 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 | |||||
Lint: TODO Comment This comment has a TODO. Lint: TODO Comment: This comment has a TODO. | |||||
| // 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. | |||||
| } | |||||
| } | } | ||||
This comment has a TODO.