1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-12-23 05:50:54 +01:00

Disambiguate various types of Mercurial remote markers with "hg arc-ls-remote"

Summary: Ref T13546. Ref T9948. It seems challenging to examine a remote in vanilla Mercurial. Provide an "hg arc-ls-remote" command which functions like "git ls-remote" so we can figure out if "--into X" is a bookmark, branch, both, neither, or a branch with multiple heads without mutating the working copy as a side effect.

Test Plan: Ran various "arc land --into ..." commands in a Mercurial working copy, saw apparently-sensible resolution of remote marker names.

Maniphest Tasks: T13546, T9948

Differential Revision: https://secure.phabricator.com/D21343
This commit is contained in:
epriestley 2020-06-09 10:23:25 -07:00
parent 1bb054ef47
commit b1f807f7ca
4 changed files with 258 additions and 14 deletions

3
.gitignore vendored
View file

@ -33,3 +33,6 @@
# Generated shell completion rulesets.
/support/shell/rules/
# Python extension compiled files.
/support/hg/arc-hg.pyc

View file

@ -7,6 +7,16 @@ final class ArcanistMercurialLandEngine
$api = $this->getRepositoryAPI();
$log = $this->getLogEngine();
// 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
// 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
// name. We should detect this and treat it as an error.
// TODO: In Mercurial, you can create local bookmarks named
// "default@default" and similar which do not surive a round trip through
// a remote. Possibly, we should disallow interacting with these bookmarks.
$markers = $api->newMarkerRefQuery()
->withIsActive(true)
->execute();
@ -436,27 +446,156 @@ final class ArcanistMercurialLandEngine
$api = $this->getRepositoryAPI();
$log = $this->getLogEngine();
// TODO: Support bookmarks.
// TODO: Deal with bookmark save/restore behavior.
// TODO: Raise a good error message when the ref does not exist.
// See T9948. If the user specified "--into X", we don't know if it's a
// 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.
// TODO: This is fairly silly/confusing to show to users in the common
// case where it does not require credentials, particularly because the
// actual command line is full of nonsense.
$tmpfile = new TempFile();
Filesystem::remove($tmpfile);
$err = $this->newPassthru(
'pull -b %s -- %s',
$target->getRef(),
'%Ls arc-ls-remote --output %s -- %s',
$api->getMercurialExtensionArguments(),
phutil_string_cast($tmpfile),
$target->getRemote());
if ($err) {
throw new Exception(
pht(
'Call to "hg arc-ls-remote" failed with error "%s".',
$err));
}
// TODO: Deal with errors.
// TODO: Deal with multiple branch heads.
$raw_data = Filesystem::readFile($tmpfile);
unset($tmpfile);
list($stdout) = $api->execxLocal(
'log --rev %s --template %s --',
hgsprintf(
'last(ancestors(%s) and !outgoing(%s))',
$markers = phutil_json_decode($raw_data);
$target_name = $target->getRef();
$bookmarks = array();
$branches = array();
foreach ($markers as $marker) {
if ($marker['name'] !== $target_name) {
continue;
}
if ($marker['type'] === 'bookmark') {
$bookmarks[] = $marker;
} else {
$branches[] = $marker;
}
}
if (!$bookmarks && !$branches) {
throw new PhutilArgumentUsageException(
pht(
'Remote "%s" has no bookmark or branch named "%s".',
$target->getRemote(),
$target->getRef()));
}
if ($bookmarks && $branches) {
echo tsprintf(
"\n%!\n%W\n\n",
pht('AMBIGUOUS MARKER'),
pht(
'In remote "%s", the name "%s" identifies one or more branch '.
'heads and one or more bookmarks. Close, rename, or delete all '.
'but one of these markers, or pull the state you want to merge '.
'into and use "--into-local --into <hash>" to disambiguate the '.
'desired merge target.',
$target->getRemote(),
$target->getRef()));
throw new PhutilArgumentUsageException(
pht('Merge target is ambiguous.'));
}
$is_bookmark = false;
$is_branch = false;
if ($bookmarks) {
if (count($bookmarks) > 1) {
throw new Exception(
pht(
'Remote "%s" has multiple bookmarks with name "%s". This '.
'is unexpected.',
$target->getRemote(),
$target->getRef()));
}
$bookmark = head($bookmarks);
$target_hash = $bookmark['node'];
$is_bookmark = true;
}
if ($branches) {
if (count($branches) > 1) {
echo tsprintf(
"\n%!\n%W\n\n",
pht('MULTIPLE BRANCH HEADS'),
pht(
'Remote "%s" has multiple branch heads named "%s". Close all '.
'but one, or pull the head you want and use "--into-local '.
'--into <hash>" to specify an explicit merge target.',
$target->getRemote(),
$target->getRef()));
throw new PhutilArgumentUsageException(
pht(
'Remote branch has multiple heads.'));
}
$branch = head($branches);
$target_hash = $branch['node'];
$is_branch = true;
}
if ($is_branch) {
$err = $this->newPassthru(
'pull -b %s -- %s',
$target->getRef(),
$target->getRemote()),
'{node}');
$target->getRemote());
} else {
return trim($stdout);
// NOTE: This may have side effects:
//
// - It can create a "bookmark@remote" bookmark if there is a local
// bookmark with the same name that is not an ancestor.
// - It can create an arbitrary number of other bookmarks.
//
// Since these seem to generally be intentional behaviors in Mercurial,
// and should theoretically be familiar to Mercurial users, just accept
// them as the cost of doing business.
$err = $this->newPassthru(
'pull -B %s -- %s',
$target->getRef(),
$target->getRemote());
}
// NOTE: It's possible that between the time we ran "ls-remote" and the
// time we ran "pull" that the remote changed.
// 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,
// assume this didn't happen: it's so unlikely that it's probably not
// worth spending 100ms to check.
// TODO: If the Mercurial command server is revived, this check becomes
// more reasonable if it's cheap.
return $target_hash;
}
protected function selectCommits($into_commit, array $symbols) {

View file

@ -1012,4 +1012,16 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return new ArcanistMercurialRepositoryRemoteQuery();
}
public function getMercurialExtensionArguments() {
$path = phutil_get_library_root('arcanist');
$path = dirname($path);
$path = $path.'/support/hg/arc-hg.py';
return array(
'--config',
'extensions.arc-hg='.$path,
);
}
}

90
support/hg/arc-hg.py Normal file
View file

@ -0,0 +1,90 @@
from __future__ import absolute_import
import os
import json
from mercurial import (
cmdutil,
bookmarks,
bundlerepo,
error,
hg,
i18n,
node,
registrar,
)
_ = i18n._
cmdtable = {}
command = registrar.command(cmdtable)
@command(
"arc-ls-remote",
[('', 'output', '',
_('file to output refs to'), _('FILE')),
] + cmdutil.remoteopts,
_('[--output FILENAME] [SOURCE]'))
def lsremote(ui, repo, source="default", **opts):
"""list markers in a remote
Show the current branch heads and bookmarks in a specified path/URL or the
default pull location.
Markers are printed to stdout in JSON.
(This is an Arcanist extension to Mercurial.)
Returns 0 if listing the markers succeeds, 1 otherwise.
"""
# Disable status output from fetching a remote.
ui.quiet = True
source, branches = hg.parseurl(ui.expandpath(source))
remote = hg.peer(repo, opts, source)
markers = []
bundle, remotebranches, cleanup = bundlerepo.getremotechanges(
ui,
repo,
remote)
try:
for n in remotebranches:
ctx = bundle[n]
markers.append({
'type': 'branch',
'name': ctx.branch(),
'node': node.hex(ctx.node()),
})
finally:
cleanup()
with remote.commandexecutor() as e:
remotemarks = bookmarks.unhexlifybookmarks(e.callcommand('listkeys', {
'namespace': 'bookmarks',
}).result())
for mark in remotemarks:
markers.append({
'type': 'bookmark',
'name': mark,
'node': node.hex(remotemarks[mark]),
})
json_opts = {
'indent': 2,
'sort_keys': True,
}
output_file = opts.get('output')
if output_file:
if os.path.exists(output_file):
raise error.Abort(_('File "%s" already exists.' % output_file))
with open(output_file, 'w+') as f:
json.dump(markers, f, **json_opts)
else:
print json.dumps(markers, output_file, **json_opts)
return 0