diff --git a/.gitignore b/.gitignore
index 096c30cb..d40646a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,35 +1,38 @@
 # NOTE: Thinking about adding files created by your operating system, IDE,
 # or text editor here? Don't! Add them to your per-user .gitignore instead.
 
 # Diviner
 /docs/
 /.divinercache/
 
 # libphutil
 /src/.phutil_module_cache
 
 # User extensions
 /externals/includes/*
 /src/extensions/*
 
 # XHPAST
 /support/xhpast/*.a
 /support/xhpast/*.o
 /support/xhpast/parser.yacc.output
 /support/xhpast/node_names.hpp
 /support/xhpast/xhpast
 /support/xhpast/xhpast.exe
 /src/parser/xhpast/bin/xhpast
 
 ## NOTE: Don't .gitignore these files! Even though they're build artifacts, we
 ## want to check them in so users can build xhpast without flex/bison.
 # /support/xhpast/parser.yacc.cpp
 # /support/xhpast/parser.yacc.hpp
 # /support/xhpast/scanner.lex.cpp
 # /support/xhpast/scanner.lex.hpp
 
 # This is an OS X build artifact.
 /support/xhpast/xhpast.dSYM
 
 # 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
index 241015e7..68dd20f7 100644
--- a/src/land/engine/ArcanistMercurialLandEngine.php
+++ b/src/land/engine/ArcanistMercurialLandEngine.php
@@ -1,733 +1,872 @@
 <?php
 
 final class ArcanistMercurialLandEngine
   extends ArcanistLandEngine {
 
   protected function getDefaultSymbols() {
     $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();
 
     $bookmark = null;
     foreach ($markers as $marker) {
       if ($marker->isBookmark()) {
         $bookmark = $marker->getName();
         break;
       }
     }
 
     if ($bookmark !== null) {
       $log->writeStatus(
         pht('SOURCE'),
         pht(
           'Landing the active bookmark, "%s".',
           $bookmark));
 
       return array($bookmark);
     }
 
     $branch = null;
     foreach ($markers as $marker) {
       if ($marker->isBranch()) {
         $branch = $marker->getName();
         break;
       }
     }
 
     if ($branch !== null) {
       $log->writeStatus(
         pht('SOURCE'),
         pht(
           'Landing the active branch, "%s".',
           $branch));
 
       return array($branch);
     }
 
     $commit = $api->getCanonicalRevisionName('.');
     $commit = $this->getDisplayHash($commit);
 
     $log->writeStatus(
       pht('SOURCE'),
       pht(
         'Landing the active commit, "%s".',
         $this->getDisplayHash($commit)));
 
     return array($commit);
   }
 
   protected function resolveSymbols(array $symbols) {
     assert_instances_of($symbols, 'ArcanistLandSymbol');
     $api = $this->getRepositoryAPI();
 
     $marker_types = array(
       ArcanistMarkerRef::TYPE_BOOKMARK,
       ArcanistMarkerRef::TYPE_BRANCH,
     );
 
     $unresolved = $symbols;
     foreach ($marker_types as $marker_type) {
       $markers = $api->newMarkerRefQuery()
         ->withMarkerTypes(array($marker_type))
         ->execute();
 
       $markers = mgroup($markers, 'getName');
 
       foreach ($unresolved as $key =>  $symbol) {
         $raw_symbol = $symbol->getSymbol();
 
         $named_markers = idx($markers, $raw_symbol);
         if (!$named_markers) {
           continue;
         }
 
         if (count($named_markers) > 1) {
           throw new PhutilArgumentUsageException(
             pht(
               'Symbol "%s" is ambiguous: it matches multiple markers '.
               '(of type "%s"). Use an unambiguous identifier.',
               $raw_symbol,
               $marker_type));
         }
 
         $marker = head($named_markers);
 
         $symbol->setCommit($marker->getCommitHash());
 
         unset($unresolved[$key]);
       }
     }
 
     foreach ($unresolved as $symbol) {
       $raw_symbol = $symbol->getSymbol();
 
       // TODO: This doesn't have accurate error behavior if the user provides
       // a revset like "x::y".
       try {
         $commit = $api->getCanonicalRevisionName($raw_symbol);
       } catch (CommandException $ex) {
         $commit = null;
       }
 
       if ($commit === null) {
         throw new PhutilArgumentUsageException(
           pht(
             'Symbol "%s" does not identify a bookmark, branch, or commit.',
             $raw_symbol));
       }
 
       $symbol->setCommit($commit);
     }
   }
 
   protected function selectOntoRemote(array $symbols) {
     assert_instances_of($symbols, 'ArcanistLandSymbol');
     $api = $this->getRepositoryAPI();
 
     $remote = $this->newOntoRemote($symbols);
 
     $remote_ref = $api->newRemoteRefQuery()
       ->withNames(array($remote))
       ->executeOne();
     if (!$remote_ref) {
       throw new PhutilArgumentUsageException(
         pht(
           'No remote "%s" exists in this repository.',
           $remote));
     }
 
     // TODO: Allow selection of a bare URI.
 
     return $remote;
   }
 
   private function newOntoRemote(array $symbols) {
     assert_instances_of($symbols, 'ArcanistLandSymbol');
     $api = $this->getRepositoryAPI();
     $log = $this->getLogEngine();
 
     $remote = $this->getOntoRemoteArgument();
     if ($remote !== null) {
 
       $log->writeStatus(
         pht('ONTO REMOTE'),
         pht(
           'Remote "%s" was selected with the "--onto-remote" flag.',
           $remote));
 
       return $remote;
     }
 
     $remote = $this->getOntoRemoteFromConfiguration();
     if ($remote !== null) {
       $remote_key = $this->getOntoRemoteConfigurationKey();
 
       $log->writeStatus(
         pht('ONTO REMOTE'),
         pht(
           'Remote "%s" was selected by reading "%s" configuration.',
           $remote,
           $remote_key));
 
       return $remote;
     }
 
     $api = $this->getRepositoryAPI();
 
     $default_remote = 'default';
 
     $log->writeStatus(
       pht('ONTO REMOTE'),
       pht(
         'Landing onto remote "%s", the default remote under Mercurial.',
         $default_remote));
 
     return $default_remote;
   }
 
   protected function selectOntoRefs(array $symbols) {
     assert_instances_of($symbols, 'ArcanistLandSymbol');
     $log = $this->getLogEngine();
 
     $onto = $this->getOntoArguments();
     if ($onto) {
 
       $log->writeStatus(
         pht('ONTO TARGET'),
         pht(
           'Refs were selected with the "--onto" flag: %s.',
           implode(', ', $onto)));
 
       return $onto;
     }
 
     $onto = $this->getOntoFromConfiguration();
     if ($onto) {
       $onto_key = $this->getOntoConfigurationKey();
 
       $log->writeStatus(
         pht('ONTO TARGET'),
         pht(
           'Refs were selected by reading "%s" configuration: %s.',
           $onto_key,
           implode(', ', $onto)));
 
       return $onto;
     }
 
     $api = $this->getRepositoryAPI();
 
     $default_onto = 'default';
 
     $log->writeStatus(
       pht('ONTO TARGET'),
       pht(
         'Landing onto target "%s", the default target under Mercurial.',
         $default_onto));
 
     return array($default_onto);
   }
 
   protected function confirmOntoRefs(array $onto_refs) {
     foreach ($onto_refs as $onto_ref) {
       if (!strlen($onto_ref)) {
         throw new PhutilArgumentUsageException(
           pht(
             'Selected "onto" ref "%s" is invalid: the empty string is not '.
             'a valid ref.',
             $onto_ref));
       }
     }
   }
 
   protected function selectIntoRemote() {
     $api = $this->getRepositoryAPI();
     $log = $this->getLogEngine();
 
     if ($this->getIntoEmptyArgument()) {
       $this->setIntoEmpty(true);
 
       $log->writeStatus(
         pht('INTO REMOTE'),
         pht(
           'Will merge into empty state, selected with the "--into-empty" '.
           'flag.'));
 
       return;
     }
 
     if ($this->getIntoLocalArgument()) {
       $this->setIntoLocal(true);
 
       $log->writeStatus(
         pht('INTO REMOTE'),
         pht(
           'Will merge into local state, selected with the "--into-local" '.
           'flag.'));
 
       return;
     }
 
     $into = $this->getIntoRemoteArgument();
     if ($into !== null) {
 
       $remote_ref = $api->newRemoteRefQuery()
         ->withNames(array($into))
         ->executeOne();
       if (!$remote_ref) {
         throw new PhutilArgumentUsageException(
           pht(
             'No remote "%s" exists in this repository.',
             $into));
       }
 
       // TODO: Allow a raw URI.
 
       $this->setIntoRemote($into);
 
       $log->writeStatus(
         pht('INTO REMOTE'),
         pht(
           'Will merge into remote "%s", selected with the "--into" flag.',
           $into));
 
       return;
     }
 
     $onto = $this->getOntoRemote();
     $this->setIntoRemote($onto);
 
     $log->writeStatus(
       pht('INTO REMOTE'),
       pht(
         'Will merge into remote "%s" by default, because this is the remote '.
         'the change is landing onto.',
         $onto));
   }
 
   protected function selectIntoRef() {
     $log = $this->getLogEngine();
 
     if ($this->getIntoEmptyArgument()) {
       $log->writeStatus(
         pht('INTO TARGET'),
         pht(
           'Will merge into empty state, selected with the "--into-empty" '.
           'flag.'));
 
       return;
     }
 
     $into = $this->getIntoArgument();
     if ($into !== null) {
       $this->setIntoRef($into);
 
       $log->writeStatus(
         pht('INTO TARGET'),
         pht(
           'Will merge into target "%s", selected with the "--into" flag.',
           $into));
 
       return;
     }
 
     $ontos = $this->getOntoRefs();
     $onto = head($ontos);
 
     $this->setIntoRef($onto);
     if (count($ontos) > 1) {
       $log->writeStatus(
         pht('INTO TARGET'),
         pht(
           'Will merge into target "%s" by default, because this is the first '.
           '"onto" target.',
           $onto));
     } else {
       $log->writeStatus(
         pht('INTO TARGET'),
         pht(
           'Will merge into target "%s" by default, because this is the "onto" '.
           'target.',
           $onto));
     }
   }
 
   protected function selectIntoCommit() {
     // Make sure that our "into" target is valid.
     $log = $this->getLogEngine();
 
     if ($this->getIntoEmpty()) {
       // If we're running under "--into-empty", we don't have to do anything.
 
       $log->writeStatus(
         pht('INTO COMMIT'),
         pht('Preparing merge into the empty state.'));
 
       return null;
     }
 
     if ($this->getIntoLocal()) {
       // If we're running under "--into-local", just make sure that the
       // target identifies some actual commit.
       $api = $this->getRepositoryAPI();
       $local_ref = $this->getIntoRef();
 
       // TODO: This error handling could probably be cleaner.
 
       $into_commit = $api->getCanonicalRevisionName($local_ref);
 
       $log->writeStatus(
         pht('INTO COMMIT'),
         pht(
           'Preparing merge into local target "%s", at commit "%s".',
           $local_ref,
           $this->getDisplayHash($into_commit)));
 
       return $into_commit;
     }
 
     $target = id(new ArcanistLandTarget())
       ->setRemote($this->getIntoRemote())
       ->setRef($this->getIntoRef());
 
     $commit = $this->fetchTarget($target);
     if ($commit !== null) {
       $log->writeStatus(
         pht('INTO COMMIT'),
         pht(
           'Preparing merge into "%s" from remote "%s", at commit "%s".',
           $target->getRef(),
           $target->getRemote(),
           $this->getDisplayHash($commit)));
       return $commit;
     }
 
     // If we have no valid target and the user passed "--into" explicitly,
     // treat this as an error. For example, "arc land --into Q --onto Q",
     // where "Q" does not exist, is an error.
     if ($this->getIntoArgument()) {
       throw new PhutilArgumentUsageException(
         pht(
           'Ref "%s" does not exist in remote "%s".',
           $target->getRef(),
           $target->getRemote()));
     }
 
     // Otherwise, treat this as implying "--into-empty". For example,
     // "arc land --onto Q", where "Q" does not exist, is equivalent to
     // "arc land --into-empty --onto Q".
     $this->setIntoEmpty(true);
 
     $log->writeStatus(
       pht('INTO COMMIT'),
       pht(
         'Preparing merge into the empty state to create target "%s" '.
         'in remote "%s".',
         $target->getRef(),
         $target->getRemote()));
 
     return null;
   }
 
   private function fetchTarget(ArcanistLandTarget $target) {
     $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) {
     assert_instances_of($symbols, 'ArcanistLandSymbol');
     $api = $this->getRepositoryAPI();
 
     $commit_map = array();
     foreach ($symbols as $symbol) {
       $symbol_commit = $symbol->getCommit();
       $template = '{node}-{parents}-';
 
       if ($into_commit === null) {
         list($commits) = $api->execxLocal(
           'log --rev %s --template %s --',
           hgsprintf('reverse(ancestors(%s))', $into_commit),
           $template);
       } else {
         list($commits) = $api->execxLocal(
           'log --rev %s --template %s --',
           hgsprintf(
             'reverse(ancestors(%s) - ancestors(%s))',
             $symbol_commit,
             $into_commit),
           $template);
       }
 
       $commits = phutil_split_lines($commits, false);
       $is_first = true;
       foreach ($commits as $line) {
         if (!strlen($line)) {
           continue;
         }
 
         $parts = explode('-', $line, 3);
         if (count($parts) < 3) {
           throw new Exception(
             pht(
               'Unexpected output from "hg log ...": %s',
               $line));
         }
 
         $hash = $parts[0];
         if (!isset($commit_map[$hash])) {
           $parents = $parts[1];
           $parents = trim($parents);
           if (strlen($parents)) {
             $parents = explode(' ', $parents);
           } else {
             $parents = array();
           }
 
           $summary = $parts[2];
 
           $commit_map[$hash] = id(new ArcanistLandCommit())
             ->setHash($hash)
             ->setParents($parents)
             ->setSummary($summary);
         }
 
         $commit = $commit_map[$hash];
         if ($is_first) {
           $commit->addDirectSymbol($symbol);
           $is_first = false;
         }
 
         $commit->addIndirectSymbol($symbol);
       }
     }
 
     return $this->confirmCommits($into_commit, $symbols, $commit_map);
   }
 
   protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) {
     $api = $this->getRepositoryAPI();
 
     if ($this->getStrategy() !== 'squash') {
       throw new Exception(pht('TODO: Support merge strategies'));
     }
 
     // TODO: Add a Mercurial version check requiring 2.1.1 or newer.
 
     $api->execxLocal(
       'update --rev %s',
       hgsprintf('%s', $into_commit));
 
     $commits = $set->getCommits();
 
     $min_commit = last($commits)->getHash();
     $max_commit = head($commits)->getHash();
 
     $revision_ref = $set->getRevisionRef();
     $commit_message = $revision_ref->getCommitMessage();
 
     try {
       $argv = array();
       $argv[] = '--dest';
       $argv[] = hgsprintf('%s', $into_commit);
 
       $argv[] = '--rev';
       $argv[] = hgsprintf('%s..%s', $min_commit, $max_commit);
 
       $argv[] = '--logfile';
       $argv[] = '-';
 
       $argv[] = '--keep';
       $argv[] = '--collapse';
 
       $future = $api->execFutureLocal('rebase %Ls', $argv);
       $future->write($commit_message);
       $future->resolvex();
 
     } catch (CommandException $ex) {
       // TODO
       // $api->execManualLocal('rebase --abort');
       throw $ex;
     }
 
     list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}');
     $new_cursor = trim($stdout);
 
     return $new_cursor;
   }
 
   protected function pushChange($into_commit) {
     $api = $this->getRepositoryAPI();
 
     // TODO: This does not respect "--into" or "--onto" properly.
 
     $this->newPassthru(
       'push --rev %s -- %s',
       hgsprintf('%s', $into_commit),
       $this->getOntoRemote());
   }
 
   protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) {
     $api = $this->getRepositoryAPI();
     $log = $this->getLogEngine();
 
     // This has no effect when we're executing a merge strategy.
     if (!$this->isSquashStrategy()) {
       return;
     }
 
     $old_commit = last($set->getCommits())->getHash();
     $new_commit = $into_commit;
 
     list($output) = $api->execxLocal(
       'log --rev %s --template %s',
       hgsprintf('children(%s)', $old_commit),
       '{node}\n');
     $child_hashes = phutil_split_lines($output, false);
 
     foreach ($child_hashes as $child_hash) {
       if (!strlen($child_hash)) {
         continue;
       }
 
       // TODO: If the only heads which are descendants of this child will
       // be deleted, we can skip this rebase?
 
       try {
         $api->execxLocal(
           'rebase --source %s --dest %s --keep --keepbranches',
           $child_hash,
           $new_commit);
       } catch (CommandException $ex) {
         // TODO: Recover state.
         throw $ex;
       }
     }
   }
 
 
   protected function pruneBranches(array $sets) {
     assert_instances_of($sets, 'ArcanistLandCommitSet');
     $api = $this->getRepositoryAPI();
     $log = $this->getLogEngine();
 
     // This has no effect when we're executing a merge strategy.
     if (!$this->isSquashStrategy()) {
       return;
     }
 
     $strip = array();
 
     // We've rebased all descendants already, so we can safely delete all
     // of these commits.
 
     $sets = array_reverse($sets);
     foreach ($sets as $set) {
       $commits = $set->getCommits();
 
       $min_commit = head($commits)->getHash();
       $max_commit = last($commits)->getHash();
 
       $strip[] = hgsprintf('%s::%s', $min_commit, $max_commit);
     }
 
     $rev_set = '('.implode(') or (', $strip).')';
 
     // See PHI45. If we have "hg evolve", get rid of old commits using
     // "hg prune" instead of "hg strip".
 
     // If we "hg strip" a commit which has an obsolete predecessor, it
     // removes the obsolescence marker and revives the predecessor. This is
     // not desirable: we want to destroy all predecessors of these commits.
 
     try {
       $api->execxLocal(
         '--config extensions.evolve= prune --rev %s',
         $rev_set);
     } catch (CommandException $ex) {
       $api->execxLocal(
         '--config extensions.strip= strip --rev %s',
         $rev_set);
     }
   }
 
   protected function reconcileLocalState(
     $into_commit,
     ArcanistRepositoryLocalState $state) {
 
     // TODO: For now, just leave users wherever they ended up.
 
     $state->discardLocalState();
   }
 
   protected function didHoldChanges($into_commit) {
     $log = $this->getLogEngine();
     $local_state = $this->getLocalState();
 
     $message = pht(
       'Holding changes locally, they have not been pushed.');
 
     // TODO: This is only vaguely correct.
 
     $push_command = csprintf(
       '$ hg push --rev %s -- %s',
       hgsprintf('%s', $this->getDisplayHash($into_commit)),
       $this->getOntoRemote());
 
     echo tsprintf(
       "\n%!\n%s\n\n",
       pht('HOLD CHANGES'),
       $message);
 
     echo tsprintf(
       "%s\n\n    **%s**\n\n",
       pht('To push changes manually, run this command:'),
       $push_command);
 
     $restore_commands = $local_state->getRestoreCommandsForDisplay();
     if ($restore_commands) {
       echo tsprintf(
         "%s\n\n",
         pht(
           'To go back to how things were before you ran "arc land", run '.
           'these %s command(s):',
           phutil_count($restore_commands)));
 
       foreach ($restore_commands as $restore_command) {
         echo tsprintf("    **%s**\n", $restore_command);
       }
 
       echo tsprintf("\n");
     }
 
     echo tsprintf(
       "%s\n",
       pht(
         'Local branches and bookmarks have not been changed, and are still '.
         'in the same state as before.'));
   }
 }
diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php
index 12b78bf4..4756d068 100644
--- a/src/repository/api/ArcanistMercurialAPI.php
+++ b/src/repository/api/ArcanistMercurialAPI.php
@@ -1,1015 +1,1027 @@
 <?php
 
 /**
  * Interfaces with the Mercurial working copies.
  */
 final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
 
   private $branch;
   private $localCommitInfo;
   private $rawDiffCache = array();
 
   private $featureResults = array();
   private $featureFutures = array();
 
   protected function buildLocalFuture(array $argv) {
     $env = $this->getMercurialEnvironmentVariables();
 
     $argv[0] = 'hg '.$argv[0];
 
     $future = newv('ExecFuture', $argv)
       ->setEnv($env)
       ->setCWD($this->getPath());
 
     return $future;
   }
 
   public function newPassthru($pattern /* , ... */) {
     $args = func_get_args();
 
     $env = $this->getMercurialEnvironmentVariables();
 
     $args[0] = 'hg '.$args[0];
 
     return newv('PhutilExecPassthru', $args)
       ->setEnv($env)
       ->setCWD($this->getPath());
   }
 
   public function getSourceControlSystemName() {
     return 'hg';
   }
 
   public function getMetadataPath() {
     return $this->getPath('.hg');
   }
 
   public function getSourceControlBaseRevision() {
     return $this->getCanonicalRevisionName($this->getBaseCommit());
   }
 
   public function getCanonicalRevisionName($string) {
     list($stdout) = $this->execxLocal(
       'log -l 1 --template %s -r %s --',
       '{node}',
       $string);
 
     return $stdout;
   }
 
   public function getSourceControlPath() {
     return '/';
   }
 
   public function getBranchName() {
     if (!$this->branch) {
       list($stdout) = $this->execxLocal('branch');
       $this->branch = trim($stdout);
     }
     return $this->branch;
   }
 
   protected function didReloadCommitRange() {
     $this->localCommitInfo = null;
   }
 
   protected function buildBaseCommit($symbolic_commit) {
     if ($symbolic_commit !== null) {
       try {
         $commit = $this->getCanonicalRevisionName(
           hgsprintf('ancestor(%s,.)', $symbolic_commit));
       } catch (Exception $ex) {
         // Try it as a revset instead of a commit id
         try {
           $commit = $this->getCanonicalRevisionName(
             hgsprintf('ancestor(%R,.)', $symbolic_commit));
         } catch (Exception $ex) {
           throw new ArcanistUsageException(
             pht(
               "Commit '%s' is not a valid Mercurial commit identifier.",
               $symbolic_commit));
         }
       }
 
       $this->setBaseCommitExplanation(
         pht(
           'it is the greatest common ancestor of the working directory '.
           'and the commit you specified explicitly.'));
       return $commit;
     }
 
     if ($this->getBaseCommitArgumentRules() ||
         $this->getConfigurationManager()->getConfigFromAnySource('base')) {
       $base = $this->resolveBaseCommit();
       if (!$base) {
         throw new ArcanistUsageException(
           pht(
             "None of the rules in your 'base' configuration matched a valid ".
             "commit. Adjust rules or specify which commit you want to use ".
             "explicitly."));
       }
       return $base;
     }
 
     list($err, $stdout) = $this->execManualLocal(
       'log --branch %s -r %s --style default',
       $this->getBranchName(),
       'draft()');
 
     if (!$err) {
       $logs = ArcanistMercurialParser::parseMercurialLog($stdout);
     } else {
       // Mercurial (in some versions?) raises an error when there's nothing
       // outgoing.
       $logs = array();
     }
 
     if (!$logs) {
       $this->setBaseCommitExplanation(
         pht(
           'you have no outgoing commits, so arc assumes you intend to submit '.
           'uncommitted changes in the working copy.'));
       return $this->getWorkingCopyRevision();
     }
 
     $outgoing_revs = ipull($logs, 'rev');
 
     // This is essentially an implementation of a theoretical `hg merge-base`
     // command.
     $against = $this->getWorkingCopyRevision();
     while (true) {
       // NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
       // new as of July 2011, so do this in a compatible way. Also, "hg log"
       // and "hg outgoing" don't necessarily show parents (even if given an
       // explicit template consisting of just the parents token) so we need
       // to separately execute "hg parents".
 
       list($stdout) = $this->execxLocal(
         'parents --style default --rev %s',
         $against);
       $parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
 
       list($p1, $p2) = array_merge($parents_logs, array(null, null));
 
       if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
         $against = $p1['rev'];
         break;
       } else if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
         $against = $p2['rev'];
         break;
       } else if ($p1) {
         $against = $p1['rev'];
       } else {
         // This is the case where you have a new repository and the entire
         // thing is outgoing; Mercurial literally accepts "--rev null" as
         // meaning "diff against the empty state".
         $against = 'null';
         break;
       }
     }
 
     if ($against == 'null') {
       $this->setBaseCommitExplanation(
         pht('this is a new repository (all changes are outgoing).'));
     } else {
       $this->setBaseCommitExplanation(
         pht(
           'it is the first commit reachable from the working copy state '.
           'which is not outgoing.'));
     }
 
     return $against;
   }
 
   public function getLocalCommitInformation() {
     if ($this->localCommitInfo === null) {
       $base_commit = $this->getBaseCommit();
       list($info) = $this->execxLocal(
         'log --template %s --rev %s --branch %s --',
         "{node}\1{rev}\1{author}\1".
           "{date|rfc822date}\1{branch}\1{tag}\1{parents}\1{desc}\2",
         hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
         $this->getBranchName());
       $logs = array_filter(explode("\2", $info));
 
       $last_node = null;
 
       $futures = array();
 
       $commits = array();
       foreach ($logs as $log) {
         list($node, $rev, $full_author, $date, $branch, $tag,
           $parents, $desc) = explode("\1", $log, 9);
 
         list($author, $author_email) = $this->parseFullAuthor($full_author);
 
         // NOTE: If a commit has only one parent, {parents} returns empty.
         // If it has two parents, {parents} returns revs and short hashes, not
         // full hashes. Try to avoid making calls to "hg parents" because it's
         // relatively expensive.
         $commit_parents = null;
         if (!$parents) {
           if ($last_node) {
             $commit_parents = array($last_node);
           }
         }
 
         if (!$commit_parents) {
           // We didn't get a cheap hit on previous commit, so do the full-cost
           // "hg parents" call. We can run these in parallel, at least.
           $futures[$node] = $this->execFutureLocal(
             'parents --template %s --rev %s',
             '{node}\n',
             $node);
         }
 
         $commits[$node] = array(
           'author'  => $author,
           'time'    => strtotime($date),
           'branch'  => $branch,
           'tag'     => $tag,
           'commit'  => $node,
           'rev'     => $node, // TODO: Remove eventually.
           'local'   => $rev,
           'parents' => $commit_parents,
           'summary' => head(explode("\n", $desc)),
           'message' => $desc,
           'authorEmail' => $author_email,
         );
 
         $last_node = $node;
       }
 
       $futures = id(new FutureIterator($futures))
         ->limit(4);
       foreach ($futures as $node => $future) {
         list($parents) = $future->resolvex();
         $parents = array_filter(explode("\n", $parents));
         $commits[$node]['parents'] = $parents;
       }
 
       // Put commits in newest-first order, to be consistent with Git and the
       // expected order of "hg log" and "git log" under normal circumstances.
       // The order of ancestors() is oldest-first.
       $commits = array_reverse($commits);
 
       $this->localCommitInfo = $commits;
     }
 
     return $this->localCommitInfo;
   }
 
   public function getAllFiles() {
     // TODO: Handle paths with newlines.
     $future = $this->buildLocalFuture(array('manifest'));
     return new LinesOfALargeExecFuture($future);
   }
 
   public function getChangedFiles($since_commit) {
     list($stdout) = $this->execxLocal(
       'status --rev %s',
       $since_commit);
     return ArcanistMercurialParser::parseMercurialStatus($stdout);
   }
 
   public function getBlame($path) {
     list($stdout) = $this->execxLocal(
       'annotate -u -v -c --rev %s -- %s',
       $this->getBaseCommit(),
       $path);
 
     $lines = phutil_split_lines($stdout, $retain_line_endings = true);
 
     $blame = array();
     foreach ($lines as $line) {
       if (!strlen($line)) {
         continue;
       }
 
       $matches = null;
       $ok = preg_match('/^\s*([^:]+?) ([a-f0-9]{12}):/', $line, $matches);
 
       if (!$ok) {
         throw new Exception(
           pht(
             'Unable to parse Mercurial blame line: %s',
             $line));
       }
 
       $revision = $matches[2];
       $author = trim($matches[1]);
       $blame[] = array($author, $revision);
     }
 
     return $blame;
   }
 
   protected function buildUncommittedStatus() {
     list($stdout) = $this->execxLocal('status');
 
     $results = new PhutilArrayWithDefaultValue();
 
     $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
     foreach ($working_status as $path => $mask) {
       if (!($mask & parent::FLAG_UNTRACKED)) {
         // Mark tracked files as uncommitted.
         $mask |= self::FLAG_UNCOMMITTED;
       }
 
       $results[$path] |= $mask;
     }
 
     return $results->toArray();
   }
 
   protected function buildCommitRangeStatus() {
     list($stdout) = $this->execxLocal(
       'status --rev %s --rev tip',
       $this->getBaseCommit());
 
     $results = new PhutilArrayWithDefaultValue();
 
     $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
     foreach ($working_status as $path => $mask) {
       $results[$path] |= $mask;
     }
 
     return $results->toArray();
   }
 
   protected function didReloadWorkingCopy() {
     // Diffs are against ".", so we need to drop the cache if we change the
     // working copy.
     $this->rawDiffCache = array();
     $this->branch = null;
   }
 
   private function getDiffOptions() {
     $options = array(
       '--git',
       '-U'.$this->getDiffLinesOfContext(),
     );
     return implode(' ', $options);
   }
 
   public function getRawDiffText($path) {
     $options = $this->getDiffOptions();
 
     $range = $this->getBaseCommit();
 
     $raw_diff_cache_key = $options.' '.$range.' '.$path;
     if (idx($this->rawDiffCache, $raw_diff_cache_key)) {
       return idx($this->rawDiffCache, $raw_diff_cache_key);
     }
 
     list($stdout) = $this->execxLocal(
       'diff %C --rev %s -- %s',
       $options,
       $range,
       $path);
 
     $this->rawDiffCache[$raw_diff_cache_key] = $stdout;
 
     return $stdout;
   }
 
   public function getFullMercurialDiff() {
     return $this->getRawDiffText('');
   }
 
   public function getOriginalFileData($path) {
     return $this->getFileDataAtRevision($path, $this->getBaseCommit());
   }
 
   public function getCurrentFileData($path) {
     return $this->getFileDataAtRevision(
       $path,
       $this->getWorkingCopyRevision());
   }
 
   public function getBulkOriginalFileData($paths) {
     return $this->getBulkFileDataAtRevision($paths, $this->getBaseCommit());
   }
 
   public function getBulkCurrentFileData($paths) {
     return $this->getBulkFileDataAtRevision(
       $paths,
       $this->getWorkingCopyRevision());
   }
 
   private function getBulkFileDataAtRevision($paths, $revision) {
     // Calling 'hg cat' on each file individually is slow (1 second per file
     // on a large repo) because mercurial has to decompress and parse the
     // entire manifest every time. Do it in one large batch instead.
 
     // hg cat will write the file data to files in a temp directory
     $tmpdir = Filesystem::createTemporaryDirectory();
 
     // Mercurial doesn't create the directories for us :(
     foreach ($paths as $path) {
       $tmppath = $tmpdir.'/'.$path;
       Filesystem::createDirectory(dirname($tmppath), 0755, true);
     }
 
     // NOTE: The "%s%%p" construction passes a literal "%p" to Mercurial,
     // which is a formatting directive for a repo-relative filepath. The
     // particulars of the construction avoid Windows escaping issues. See
     // PHI904.
 
     list($err, $stdout) = $this->execManualLocal(
       'cat --rev %s --output %s%%p -- %Ls',
       $revision,
       $tmpdir.DIRECTORY_SEPARATOR,
       $paths);
 
     $filedata = array();
     foreach ($paths as $path) {
       $tmppath = $tmpdir.'/'.$path;
       if (Filesystem::pathExists($tmppath)) {
         $filedata[$path] = Filesystem::readFile($tmppath);
       }
     }
 
     Filesystem::remove($tmpdir);
 
     return $filedata;
   }
 
   private function getFileDataAtRevision($path, $revision) {
     list($err, $stdout) = $this->execManualLocal(
       'cat --rev %s -- %s',
       $revision,
       $path);
     if ($err) {
       // Assume this is "no file at revision", i.e. a deleted or added file.
       return null;
     } else {
       return $stdout;
     }
   }
 
   public function getWorkingCopyRevision() {
     return '.';
   }
 
   public function isHistoryDefaultImmutable() {
     return true;
   }
 
   public function supportsAmend() {
     list($err, $stdout) = $this->execManualLocal('help commit');
     if ($err) {
       return false;
     } else {
       return (strpos($stdout, 'amend') !== false);
     }
   }
 
   public function supportsCommitRanges() {
     return true;
   }
 
   public function supportsLocalCommits() {
     return true;
   }
 
   public function getBaseCommitRef() {
     $base_commit = $this->getBaseCommit();
 
     if ($base_commit === 'null') {
       return null;
     }
 
     $base_message = $this->getCommitMessage($base_commit);
 
     return $this->newCommitRef()
       ->setCommitHash($base_commit)
       ->attachMessage($base_message);
   }
 
   public function hasLocalCommit($commit) {
     try {
       $this->getCanonicalRevisionName($commit);
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
   public function getCommitMessage($commit) {
     list($message) = $this->execxLocal(
       'log --template={desc} --rev %s',
       $commit);
     return $message;
   }
 
   public function getAllLocalChanges() {
     $diff = $this->getFullMercurialDiff();
     if (!strlen(trim($diff))) {
       return array();
     }
     $parser = new ArcanistDiffParser();
     return $parser->parseDiff($diff);
   }
 
   public function getFinalizedRevisionMessage() {
     return pht(
       "You may now push this commit upstream, as appropriate (e.g. with ".
       "'%s' or by printing and faxing it).",
       'hg push');
   }
 
   public function getCommitMessageLog() {
     $base_commit = $this->getBaseCommit();
     list($stdout) = $this->execxLocal(
       'log --template %s --rev %s --branch %s --',
       "{node}\1{desc}\2",
       hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
       $this->getBranchName());
 
     $map = array();
 
     $logs = explode("\2", trim($stdout));
     foreach (array_filter($logs) as $log) {
       list($node, $desc) = explode("\1", $log);
       $map[$node] = $desc;
     }
 
     return array_reverse($map);
   }
 
   public function loadWorkingCopyDifferentialRevisions(
     ConduitClient $conduit,
     array $query) {
 
     $messages = $this->getCommitMessageLog();
     $parser = new ArcanistDiffParser();
 
     // First, try to find revisions by explicit revision IDs in commit messages.
     $reason_map = array();
     $revision_ids = array();
     foreach ($messages as $node_id => $message) {
       $object = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
 
       if ($object->getRevisionID()) {
         $revision_ids[] = $object->getRevisionID();
         $reason_map[$object->getRevisionID()] = $node_id;
       }
     }
 
     if ($revision_ids) {
       $results = $conduit->callMethodSynchronous(
         'differential.query',
         $query + array(
           'ids' => $revision_ids,
         ));
 
       foreach ($results as $key => $result) {
         $hash = substr($reason_map[$result['id']], 0, 16);
         $results[$key]['why'] =
           pht(
             "Commit message for '%s' has explicit 'Differential Revision'.",
             $hash);
       }
 
       return $results;
     }
 
     // Try to find revisions by hash.
     $hashes = array();
     foreach ($this->getLocalCommitInformation() as $commit) {
       $hashes[] = array('hgcm', $commit['commit']);
     }
 
     if ($hashes) {
 
       // NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working
       // copy with dirty changes, there may be no local commits.
 
       $results = $conduit->callMethodSynchronous(
         'differential.query',
         $query + array(
           'commitHashes' => $hashes,
         ));
 
       foreach ($results as $key => $hash) {
         $results[$key]['why'] = pht(
           'A mercurial commit hash in the commit range is already attached '.
           'to the Differential revision.');
       }
 
       return $results;
     }
 
     return array();
   }
 
   public function updateWorkingCopy() {
     $this->execxLocal('up');
     $this->reloadWorkingCopy();
   }
 
   private function getMercurialConfig($key, $default = null) {
     list($stdout) = $this->execxLocal('showconfig %s', $key);
     if ($stdout == '') {
       return $default;
     }
     return rtrim($stdout);
   }
 
   public function getAuthor() {
     $full_author = $this->getMercurialConfig('ui.username');
     list($author, $author_email) = $this->parseFullAuthor($full_author);
     return $author;
   }
 
   /**
    * Parse the Mercurial author field.
    *
    * Not everyone enters their email address as a part of the username
    * field. Try to make it work when it's obvious.
    *
    * @param string $full_author
    * @return array
    */
   protected function parseFullAuthor($full_author) {
     if (strpos($full_author, '@') === false) {
       $author = $full_author;
       $author_email = null;
     } else {
       $email = new PhutilEmailAddress($full_author);
       $author = $email->getDisplayName();
       $author_email = $email->getAddress();
     }
 
     return array($author, $author_email);
   }
 
   public function addToCommit(array $paths) {
     $this->execxLocal(
       'addremove -- %Ls',
       $paths);
     $this->reloadWorkingCopy();
   }
 
   public function doCommit($message) {
     $tmp_file = new TempFile();
     Filesystem::writeFile($tmp_file, $message);
     $this->execxLocal('commit -l %s', $tmp_file);
     $this->reloadWorkingCopy();
   }
 
   public function amendCommit($message = null) {
     if ($message === null) {
       $message = $this->getCommitMessage('.');
     }
 
     $tmp_file = new TempFile();
     Filesystem::writeFile($tmp_file, $message);
 
     try {
       $this->execxLocal(
         'commit --amend -l %s',
         $tmp_file);
     } catch (CommandException $ex) {
       if (preg_match('/nothing changed/', $ex->getStdout())) {
         // NOTE: Mercurial considers it an error to make a no-op amend. Although
         // we generally defer to the underlying VCS to dictate behavior, this
         // one seems a little goofy, and we use amend as part of various
         // workflows under the assumption that no-op amends are fine. If this
         // amend failed because it's a no-op, just continue.
       } else {
         throw $ex;
       }
     }
 
     $this->reloadWorkingCopy();
   }
 
   public function getCommitSummary($commit) {
     if ($commit == 'null') {
       return pht('(The Empty Void)');
     }
 
     list($summary) = $this->execxLocal(
       'log --template {desc} --limit 1 --rev %s',
       $commit);
 
     $summary = head(explode("\n", $summary));
 
     return trim($summary);
   }
 
   public function resolveBaseCommitRule($rule, $source) {
     list($type, $name) = explode(':', $rule, 2);
 
     // NOTE: This function MUST return node hashes or symbolic commits (like
     // branch names or the word "tip"), not revsets. This includes ".^" and
     // similar, which a revset, not a symbolic commit identifier. If you return
     // a revset it will be escaped later and looked up literally.
 
     switch ($type) {
       case 'hg':
         $matches = null;
         if (preg_match('/^gca\((.+)\)$/', $name, $matches)) {
           list($err, $merge_base) = $this->execManualLocal(
             'log --template={node} --rev %s',
             sprintf('ancestor(., %s)', $matches[1]));
           if (!$err) {
             $this->setBaseCommitExplanation(
               pht(
                 "it is the greatest common ancestor of '%s' and %s, as ".
                 "specified by '%s' in your %s 'base' configuration.",
                 $matches[1],
                 '.',
                 $rule,
                 $source));
             return trim($merge_base);
           }
         } else {
           list($err, $commit) = $this->execManualLocal(
             'log --template {node} --rev %s',
             hgsprintf('%s', $name));
 
           if ($err) {
             list($err, $commit) = $this->execManualLocal(
               'log --template {node} --rev %s',
               $name);
           }
           if (!$err) {
             $this->setBaseCommitExplanation(
               pht(
                 "it is specified by '%s' in your %s 'base' configuration.",
                 $rule,
                 $source));
             return trim($commit);
           }
         }
         break;
       case 'arc':
         switch ($name) {
           case 'empty':
             $this->setBaseCommitExplanation(
               pht(
                 "you specified '%s' in your %s 'base' configuration.",
                 $rule,
                 $source));
             return 'null';
           case 'outgoing':
             list($err, $outgoing_base) = $this->execManualLocal(
               'log --template={node} --rev %s',
               'limit(reverse(ancestors(.) - outgoing()), 1)');
             if (!$err) {
               $this->setBaseCommitExplanation(
                 pht(
                   "it is the first ancestor of the working copy that is not ".
                   "outgoing, and it matched the rule %s in your %s ".
                   "'base' configuration.",
                   $rule,
                   $source));
               return trim($outgoing_base);
             }
           case 'amended':
             $text = $this->getCommitMessage('.');
             $message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
               $text);
             if ($message->getRevisionID()) {
               $this->setBaseCommitExplanation(
                 pht(
                   "'%s' has been amended with 'Differential Revision:', ".
                   "as specified by '%s' in your %s 'base' configuration.",
                   '.'.
                   $rule,
                   $source));
               // NOTE: This should be safe because Mercurial doesn't support
               // amend until 2.2.
               return $this->getCanonicalRevisionName('.^');
             }
             break;
           case 'bookmark':
             $revset =
               'limit('.
               '  sort('.
               '    (ancestors(.) and bookmark() - .) or'.
               '    (ancestors(.) - outgoing()), '.
               '  -rev),'.
               '1)';
             list($err, $bookmark_base) = $this->execManualLocal(
               'log --template={node} --rev %s',
               $revset);
             if (!$err) {
               $this->setBaseCommitExplanation(
                 pht(
                   "it is the first ancestor of %s that either has a bookmark, ".
                   "or is already in the remote and it matched the rule %s in ".
                   "your %s 'base' configuration",
                   '.',
                   $rule,
                   $source));
               return trim($bookmark_base);
             }
             break;
           case 'this':
             $this->setBaseCommitExplanation(
               pht(
                 "you specified '%s' in your %s 'base' configuration.",
                 $rule,
                 $source));
             return $this->getCanonicalRevisionName('.^');
           default:
             if (preg_match('/^nodiff\((.+)\)$/', $name, $matches)) {
               list($results) = $this->execxLocal(
                 'log --template %s --rev %s',
                 "{node}\1{desc}\2",
                 sprintf('ancestor(.,%s)::.^', $matches[1]));
               $results = array_reverse(explode("\2", trim($results)));
 
               foreach ($results as $result) {
                 if (empty($result)) {
                   continue;
                 }
 
                 list($node, $desc) = explode("\1", $result, 2);
 
                 $message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
                   $desc);
                 if ($message->getRevisionID()) {
                   $this->setBaseCommitExplanation(
                     pht(
                       "it is the first ancestor of %s that has a diff and is ".
                       "the gca or a descendant of the gca with '%s', ".
                       "specified by '%s' in your %s 'base' configuration.",
                       '.',
                       $matches[1],
                       $rule,
                       $source));
                   return $node;
                 }
               }
             }
             break;
           }
         break;
       default:
         return null;
     }
 
     return null;
 
   }
 
   public function getSubversionInfo() {
     $info = array();
     $base_path = null;
     $revision = null;
     list($err, $raw_info) = $this->execManualLocal('svn info');
     if (!$err) {
       foreach (explode("\n", trim($raw_info)) as $line) {
         list($key, $value) = explode(': ', $line, 2);
         switch ($key) {
           case 'URL':
             $info['base_path'] = $value;
             $base_path = $value;
             break;
           case 'Repository UUID':
             $info['uuid'] = $value;
             break;
           case 'Revision':
             $revision = $value;
             break;
           default:
             break;
         }
       }
       if ($base_path && $revision) {
         $info['base_revision'] = $base_path.'@'.$revision;
       }
     }
     return $info;
   }
 
   public function getActiveBookmark() {
     $bookmark = $this->newMarkerRefQuery()
       ->withMarkerTypes(ArcanistMarkerRef::TYPE_BOOKMARK)
       ->withIsActive(true)
       ->executeOne();
 
     if (!$bookmark) {
       return null;
     }
 
     return $bookmark->getName();
   }
 
   public function getRemoteURI() {
     // TODO: Remove this method in favor of RemoteRefQuery.
 
     list($stdout) = $this->execxLocal('paths default');
 
     $stdout = trim($stdout);
     if (strlen($stdout)) {
       return $stdout;
     }
 
     return null;
   }
 
   private function getMercurialEnvironmentVariables() {
     $env = array();
 
     // Mercurial has a "defaults" feature which basically breaks automation by
     // allowing the user to add random flags to any command. This feature is
     // "deprecated" and "a bad idea" that you should "forget ... existed"
     // according to project lead Matt Mackall:
     //
     //  http://markmail.org/message/hl3d6eprubmkkqh5
     //
     // There is an HGPLAIN environmental variable which enables "plain mode"
     // and hopefully disables this stuff.
 
     $env['HGPLAIN'] = 1;
 
     return $env;
   }
 
   protected function newLandEngine() {
     return new ArcanistMercurialLandEngine();
   }
 
   protected function newWorkEngine() {
     return new ArcanistMercurialWorkEngine();
   }
 
   public function newLocalState() {
     return id(new ArcanistMercurialLocalState())
       ->setRepositoryAPI($this);
   }
 
   public function willTestMercurialFeature($feature) {
     $this->executeMercurialFeatureTest($feature, false);
     return $this;
   }
 
   public function getMercurialFeature($feature) {
     return $this->executeMercurialFeatureTest($feature, true);
   }
 
   private function executeMercurialFeatureTest($feature, $resolve) {
     if (array_key_exists($feature, $this->featureResults)) {
       return $this->featureResults[$feature];
     }
 
     if (!array_key_exists($feature, $this->featureFutures)) {
       $future = $this->newMercurialFeatureFuture($feature);
       $future->start();
       $this->featureFutures[$feature] = $future;
     }
 
     if (!$resolve) {
       return;
     }
 
     $future = $this->featureFutures[$feature];
     $result = $this->resolveMercurialFeatureFuture($feature, $future);
     $this->featureResults[$feature] = $result;
 
     return $result;
   }
 
   private function newMercurialFeatureFuture($feature) {
     switch ($feature) {
       case 'shelve':
         return $this->execFutureLocal(
           '--config extensions.shelve= shelve --help');
       default:
         throw new Exception(
           pht(
             'Unknown Mercurial feature "%s".',
             $feature));
     }
   }
 
   private function resolveMercurialFeatureFuture($feature, $future) {
     // By default, assume the feature is a simple capability test and the
     // capability is present if the feature resolves without an error.
 
     list($err) = $future->resolve();
     return !$err;
   }
 
   protected function newSupportedMarkerTypes() {
     return array(
       ArcanistMarkerRef::TYPE_BRANCH,
       ArcanistMarkerRef::TYPE_BOOKMARK,
     );
   }
 
   protected function newMarkerRefQueryTemplate() {
     return new ArcanistMercurialRepositoryMarkerQuery();
   }
 
   protected function newRemoteRefQueryTemplate() {
     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
index 00000000..5a2e52f9
--- /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