mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-08 16:02:39 +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:
parent
1bb054ef47
commit
b1f807f7ca
4 changed files with 258 additions and 14 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -33,3 +33,6 @@
|
||||||
|
|
||||||
# Generated shell completion rulesets.
|
# Generated shell completion rulesets.
|
||||||
/support/shell/rules/
|
/support/shell/rules/
|
||||||
|
|
||||||
|
# Python extension compiled files.
|
||||||
|
/support/hg/arc-hg.pyc
|
||||||
|
|
|
@ -7,6 +7,16 @@ final class ArcanistMercurialLandEngine
|
||||||
$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
|
||||||
|
// 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()
|
$markers = $api->newMarkerRefQuery()
|
||||||
->withIsActive(true)
|
->withIsActive(true)
|
||||||
->execute();
|
->execute();
|
||||||
|
@ -436,27 +446,156 @@ final class ArcanistMercurialLandEngine
|
||||||
$api = $this->getRepositoryAPI();
|
$api = $this->getRepositoryAPI();
|
||||||
$log = $this->getLogEngine();
|
$log = $this->getLogEngine();
|
||||||
|
|
||||||
// TODO: Support bookmarks.
|
// See T9948. If the user specified "--into X", we don't know if it's a
|
||||||
// TODO: Deal with bookmark save/restore behavior.
|
// branch, a bookmark, or a symbol which doesn't exist yet.
|
||||||
// TODO: Raise a good error message when the ref does not exist.
|
|
||||||
|
// 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(
|
$err = $this->newPassthru(
|
||||||
'pull -b %s -- %s',
|
'%Ls arc-ls-remote --output %s -- %s',
|
||||||
$target->getRef(),
|
$api->getMercurialExtensionArguments(),
|
||||||
|
phutil_string_cast($tmpfile),
|
||||||
$target->getRemote());
|
$target->getRemote());
|
||||||
|
if ($err) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Call to "hg arc-ls-remote" failed with error "%s".',
|
||||||
|
$err));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Deal with errors.
|
$raw_data = Filesystem::readFile($tmpfile);
|
||||||
// TODO: Deal with multiple branch heads.
|
unset($tmpfile);
|
||||||
|
|
||||||
list($stdout) = $api->execxLocal(
|
$markers = phutil_json_decode($raw_data);
|
||||||
'log --rev %s --template %s --',
|
|
||||||
hgsprintf(
|
$target_name = $target->getRef();
|
||||||
'last(ancestors(%s) and !outgoing(%s))',
|
|
||||||
|
$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->getRef(),
|
||||||
$target->getRemote()),
|
$target->getRemote());
|
||||||
'{node}');
|
} 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) {
|
protected function selectCommits($into_commit, array $symbols) {
|
||||||
|
|
|
@ -1012,4 +1012,16 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
||||||
return new ArcanistMercurialRepositoryRemoteQuery();
|
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
90
support/hg/arc-hg.py
Normal 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
|
Loading…
Reference in a new issue