Page MenuHomePhabricator

D21343.diff
No OneTemporary

D21343.diff

diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,6 @@
# Generated shell completion rulesets.
/support/shell/rules/
+
+# Python extension compiled files.
+/support/hg/arc-hg.pyc
diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php
--- a/src/land/engine/ArcanistMercurialLandEngine.php
+++ b/src/land/engine/ArcanistMercurialLandEngine.php
@@ -7,6 +7,16 @@
$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 @@
$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));
+ }
+
+ $raw_data = Filesystem::readFile($tmpfile);
+ unset($tmpfile);
+
+ $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()));
- // TODO: Deal with errors.
- // TODO: Deal with multiple branch heads.
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Remote branch has multiple heads.'));
+ }
+
+ $branch = head($branches);
- list($stdout) = $api->execxLocal(
- 'log --rev %s --template %s --',
- hgsprintf(
- 'last(ancestors(%s) and !outgoing(%s))',
+ $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 {
+
+ // 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 trim($stdout);
+ return $target_hash;
}
protected function selectCommits($into_commit, array $symbols) {
diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php
--- a/src/repository/api/ArcanistMercurialAPI.php
+++ b/src/repository/api/ArcanistMercurialAPI.php
@@ -1012,4 +1012,16 @@
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,
+ );
+ }
+
}
diff --git a/support/hg/arc-hg.py b/support/hg/arc-hg.py
new file mode 100644
--- /dev/null
+++ b/support/hg/arc-hg.py
@@ -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

File Metadata

Mime Type
text/plain
Expires
Sun, Dec 22, 12:32 PM (18 h, 43 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6918261
Default Alt Text
D21343.diff (9 KB)

Event Timeline