1<?php
2
3/**
4 * Interfaces with the Mercurial working copies.
5 */
6final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
7
8  private $branch;
9  private $localCommitInfo;
10  private $rawDiffCache = array();
11
12  private $featureResults = array();
13  private $featureFutures = array();
14
15  protected function buildLocalFuture(array $argv) {
16    $env = $this->getMercurialEnvironmentVariables();
17
18    $argv[0] = 'hg '.$argv[0];
19
20    $future = newv('ExecFuture', $argv)
21      ->setEnv($env)
22      ->setCWD($this->getPath());
23
24    return $future;
25  }
26
27  public function newPassthru($pattern /* , ... */) {
28    $args = func_get_args();
29
30    $env = $this->getMercurialEnvironmentVariables();
31
32    $args[0] = 'hg '.$args[0];
33
34    return newv('PhutilExecPassthru', $args)
35      ->setEnv($env)
36      ->setCWD($this->getPath());
37  }
38
39  public function getSourceControlSystemName() {
40    return 'hg';
41  }
42
43  public function getMetadataPath() {
44    return $this->getPath('.hg');
45  }
46
47  public function getSourceControlBaseRevision() {
48    return $this->getCanonicalRevisionName($this->getBaseCommit());
49  }
50
51  public function getCanonicalRevisionName($string) {
52    list($stdout) = $this->execxLocal(
53      'log -l 1 --template %s -r %s --',
54      '{node}',
55      $string);
56
57    return $stdout;
58  }
59
60  public function getSourceControlPath() {
61    return '/';
62  }
63
64  public function getBranchName() {
65    if (!$this->branch) {
66      list($stdout) = $this->execxLocal('branch');
67      $this->branch = trim($stdout);
68    }
69    return $this->branch;
70  }
71
72  protected function didReloadCommitRange() {
73    $this->localCommitInfo = null;
74  }
75
76  protected function buildBaseCommit($symbolic_commit) {
77    if ($symbolic_commit !== null) {
78      try {
79        $commit = $this->getCanonicalRevisionName(
80          hgsprintf('ancestor(%s,.)', $symbolic_commit));
81      } catch (Exception $ex) {
82        // Try it as a revset instead of a commit id
83        try {
84          $commit = $this->getCanonicalRevisionName(
85            hgsprintf('ancestor(%R,.)', $symbolic_commit));
86        } catch (Exception $ex) {
87          throw new ArcanistUsageException(
88            pht(
89              "Commit '%s' is not a valid Mercurial commit identifier.",
90              $symbolic_commit));
91        }
92      }
93
94      $this->setBaseCommitExplanation(
95        pht(
96          'it is the greatest common ancestor of the working directory '.
97          'and the commit you specified explicitly.'));
98      return $commit;
99    }
100
101    if ($this->getBaseCommitArgumentRules() ||
102        $this->getConfigurationManager()->getConfigFromAnySource('base')) {
103      $base = $this->resolveBaseCommit();
104      if (!$base) {
105        throw new ArcanistUsageException(
106          pht(
107            "None of the rules in your 'base' configuration matched a valid ".
108            "commit. Adjust rules or specify which commit you want to use ".
109            "explicitly."));
110      }
111      return $base;
112    }
113
114    list($err, $stdout) = $this->execManualLocal(
115      'log --branch %s -r %s --style default',
116      $this->getBranchName(),
117      'draft()');
118
119    if (!$err) {
120      $logs = ArcanistMercurialParser::parseMercurialLog($stdout);
121    } else {
122      // Mercurial (in some versions?) raises an error when there's nothing
123      // outgoing.
124      $logs = array();
125    }
126
127    if (!$logs) {
128      $this->setBaseCommitExplanation(
129        pht(
130          'you have no outgoing commits, so arc assumes you intend to submit '.
131          'uncommitted changes in the working copy.'));
132      return $this->getWorkingCopyRevision();
133    }
134
135    $outgoing_revs = ipull($logs, 'rev');
136
137    // This is essentially an implementation of a theoretical `hg merge-base`
138    // command.
139    $against = $this->getWorkingCopyRevision();
140    while (true) {
141      // NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
142      // new as of July 2011, so do this in a compatible way. Also, "hg log"
143      // and "hg outgoing" don't necessarily show parents (even if given an
144      // explicit template consisting of just the parents token) so we need
145      // to separately execute "hg parents".
146
147      list($stdout) = $this->execxLocal(
148        'parents --style default --rev %s',
149        $against);
150      $parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
151
152      list($p1, $p2) = array_merge($parents_logs, array(null, null));
153
154      if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
155        $against = $p1['rev'];
156        break;
157      } else if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
158        $against = $p2['rev'];
159        break;
160      } else if ($p1) {
161        $against = $p1['rev'];
162      } else {
163        // This is the case where you have a new repository and the entire
164        // thing is outgoing; Mercurial literally accepts "--rev null" as
165        // meaning "diff against the empty state".
166        $against = 'null';
167        break;
168      }
169    }
170
171    if ($against == 'null') {
172      $this->setBaseCommitExplanation(
173        pht('this is a new repository (all changes are outgoing).'));
174    } else {
175      $this->setBaseCommitExplanation(
176        pht(
177          'it is the first commit reachable from the working copy state '.
178          'which is not outgoing.'));
179    }
180
181    return $against;
182  }
183
184  public function getLocalCommitInformation() {
185    if ($this->localCommitInfo === null) {
186      $base_commit = $this->getBaseCommit();
187      list($info) = $this->execxLocal(
188        'log --template %s --rev %s --branch %s --',
189        "{node}\1{rev}\1{author}\1".
190          "{date|rfc822date}\1{branch}\1{tag}\1{parents}\1{desc}\2",
191        hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
192        $this->getBranchName());
193      $logs = array_filter(explode("\2", $info));
194
195      $last_node = null;
196
197      $futures = array();
198
199      $commits = array();
200      foreach ($logs as $log) {
201        list($node, $rev, $full_author, $date, $branch, $tag,
202          $parents, $desc) = explode("\1", $log, 9);
203
204        list($author, $author_email) = $this->parseFullAuthor($full_author);
205
206        // NOTE: If a commit has only one parent, {parents} returns empty.
207        // If it has two parents, {parents} returns revs and short hashes, not
208        // full hashes. Try to avoid making calls to "hg parents" because it's
209        // relatively expensive.
210        $commit_parents = null;
211        if (!$parents) {
212          if ($last_node) {
213            $commit_parents = array($last_node);
214          }
215        }
216
217        if (!$commit_parents) {
218          // We didn't get a cheap hit on previous commit, so do the full-cost
219          // "hg parents" call. We can run these in parallel, at least.
220          $futures[$node] = $this->execFutureLocal(
221            'parents --template %s --rev %s',
222            '{node}\n',
223            $node);
224        }
225
226        $commits[$node] = array(
227          'author'  => $author,
228          'time'    => strtotime($date),
229          'branch'  => $branch,
230          'tag'     => $tag,
231          'commit'  => $node,
232          'rev'     => $node, // TODO: Remove eventually.
233          'local'   => $rev,
234          'parents' => $commit_parents,
235          'summary' => head(explode("\n", $desc)),
236          'message' => $desc,
237          'authorEmail' => $author_email,
238        );
239
240        $last_node = $node;
241      }
242
243      $futures = id(new FutureIterator($futures))
244        ->limit(4);
245      foreach ($futures as $node => $future) {
246        list($parents) = $future->resolvex();
247        $parents = array_filter(explode("\n", $parents));
248        $commits[$node]['parents'] = $parents;
249      }
250
251      // Put commits in newest-first order, to be consistent with Git and the
252      // expected order of "hg log" and "git log" under normal circumstances.
253      // The order of ancestors() is oldest-first.
254      $commits = array_reverse($commits);
255
256      $this->localCommitInfo = $commits;
257    }
258
259    return $this->localCommitInfo;
260  }
261
262  public function getAllFiles() {
263    // TODO: Handle paths with newlines.
264    $future = $this->buildLocalFuture(array('manifest'));
265    return new LinesOfALargeExecFuture($future);
266  }
267
268  public function getChangedFiles($since_commit) {
269    list($stdout) = $this->execxLocal(
270      'status --rev %s',
271      $since_commit);
272    return ArcanistMercurialParser::parseMercurialStatus($stdout);
273  }
274
275  public function getBlame($path) {
276    list($stdout) = $this->execxLocal(
277      'annotate -u -v -c --rev %s -- %s',
278      $this->getBaseCommit(),
279      $path);
280
281    $lines = phutil_split_lines($stdout, $retain_line_endings = true);
282
283    $blame = array();
284    foreach ($lines as $line) {
285      if (!strlen($line)) {
286        continue;
287      }
288
289      $matches = null;
290      $ok = preg_match('/^\s*([^:]+?) ([a-f0-9]{12}):/', $line, $matches);
291
292      if (!$ok) {
293        throw new Exception(
294          pht(
295            'Unable to parse Mercurial blame line: %s',
296            $line));
297      }
298
299      $revision = $matches[2];
300      $author = trim($matches[1]);
301      $blame[] = array($author, $revision);
302    }
303
304    return $blame;
305  }
306
307  protected function buildUncommittedStatus() {
308    list($stdout) = $this->execxLocal('status');
309
310    $results = new PhutilArrayWithDefaultValue();
311
312    $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
313    foreach ($working_status as $path => $mask) {
314      if (!($mask & parent::FLAG_UNTRACKED)) {
315        // Mark tracked files as uncommitted.
316        $mask |= self::FLAG_UNCOMMITTED;
317      }
318
319      $results[$path] |= $mask;
320    }
321
322    return $results->toArray();
323  }
324
325  protected function buildCommitRangeStatus() {
326    list($stdout) = $this->execxLocal(
327      'status --rev %s --rev tip',
328      $this->getBaseCommit());
329
330    $results = new PhutilArrayWithDefaultValue();
331
332    $working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
333    foreach ($working_status as $path => $mask) {
334      $results[$path] |= $mask;
335    }
336
337    return $results->toArray();
338  }
339
340  protected function didReloadWorkingCopy() {
341    // Diffs are against ".", so we need to drop the cache if we change the
342    // working copy.
343    $this->rawDiffCache = array();
344    $this->branch = null;
345  }
346
347  private function getDiffOptions() {
348    $options = array(
349      '--git',
350      '-U'.$this->getDiffLinesOfContext(),
351    );
352    return implode(' ', $options);
353  }
354
355  public function getRawDiffText($path) {
356    $options = $this->getDiffOptions();
357
358    $range = $this->getBaseCommit();
359
360    $raw_diff_cache_key = $options.' '.$range.' '.$path;
361    if (idx($this->rawDiffCache, $raw_diff_cache_key)) {
362      return idx($this->rawDiffCache, $raw_diff_cache_key);
363    }
364
365    list($stdout) = $this->execxLocal(
366      'diff %C --rev %s -- %s',
367      $options,
368      $range,
369      $path);
370
371    $this->rawDiffCache[$raw_diff_cache_key] = $stdout;
372
373    return $stdout;
374  }
375
376  public function getFullMercurialDiff() {
377    return $this->getRawDiffText('');
378  }
379
380  public function getOriginalFileData($path) {
381    return $this->getFileDataAtRevision($path, $this->getBaseCommit());
382  }
383
384  public function getCurrentFileData($path) {
385    return $this->getFileDataAtRevision(
386      $path,
387      $this->getWorkingCopyRevision());
388  }
389
390  public function getBulkOriginalFileData($paths) {
391    return $this->getBulkFileDataAtRevision($paths, $this->getBaseCommit());
392  }
393
394  public function getBulkCurrentFileData($paths) {
395    return $this->getBulkFileDataAtRevision(
396      $paths,
397      $this->getWorkingCopyRevision());
398  }
399
400  private function getBulkFileDataAtRevision($paths, $revision) {
401    // Calling 'hg cat' on each file individually is slow (1 second per file
402    // on a large repo) because mercurial has to decompress and parse the
403    // entire manifest every time. Do it in one large batch instead.
404
405    // hg cat will write the file data to files in a temp directory
406    $tmpdir = Filesystem::createTemporaryDirectory();
407
408    // Mercurial doesn't create the directories for us :(
409    foreach ($paths as $path) {
410      $tmppath = $tmpdir.'/'.$path;
411      Filesystem::createDirectory(dirname($tmppath), 0755, true);
412    }
413
414    // NOTE: The "%s%%p" construction passes a literal "%p" to Mercurial,
415    // which is a formatting directive for a repo-relative filepath. The
416    // particulars of the construction avoid Windows escaping issues. See
417    // PHI904.
418
419    list($err, $stdout) = $this->execManualLocal(
420      'cat --rev %s --output %s%%p -- %Ls',
421      $revision,
422      $tmpdir.DIRECTORY_SEPARATOR,
423      $paths);
424
425    $filedata = array();
426    foreach ($paths as $path) {
427      $tmppath = $tmpdir.'/'.$path;
428      if (Filesystem::pathExists($tmppath)) {
429        $filedata[$path] = Filesystem::readFile($tmppath);
430      }
431    }
432
433    Filesystem::remove($tmpdir);
434
435    return $filedata;
436  }
437
438  private function getFileDataAtRevision($path, $revision) {
439    list($err, $stdout) = $this->execManualLocal(
440      'cat --rev %s -- %s',
441      $revision,
442      $path);
443    if ($err) {
444      // Assume this is "no file at revision", i.e. a deleted or added file.
445      return null;
446    } else {
447      return $stdout;
448    }
449  }
450
451  public function getWorkingCopyRevision() {
452    return '.';
453  }
454
455  public function isHistoryDefaultImmutable() {
456    return true;
457  }
458
459  public function supportsAmend() {
460    list($err, $stdout) = $this->execManualLocal('help commit');
461    if ($err) {
462      return false;
463    } else {
464      return (strpos($stdout, 'amend') !== false);
465    }
466  }
467
468  public function supportsCommitRanges() {
469    return true;
470  }
471
472  public function supportsLocalCommits() {
473    return true;
474  }
475
476  public function getBaseCommitRef() {
477    $base_commit = $this->getBaseCommit();
478
479    if ($base_commit === 'null') {
480      return null;
481    }
482
483    $base_message = $this->getCommitMessage($base_commit);
484
485    return $this->newCommitRef()
486      ->setCommitHash($base_commit)
487      ->attachMessage($base_message);
488  }
489
490  public function hasLocalCommit($commit) {
491    try {
492      $this->getCanonicalRevisionName($commit);
493      return true;
494    } catch (Exception $ex) {
495      return false;
496    }
497  }
498
499  public function getCommitMessage($commit) {
500    list($message) = $this->execxLocal(
501      'log --template={desc} --rev %s',
502      $commit);
503    return $message;
504  }
505
506  public function getAllLocalChanges() {
507    $diff = $this->getFullMercurialDiff();
508    if (!strlen(trim($diff))) {
509      return array();
510    }
511    $parser = new ArcanistDiffParser();
512    return $parser->parseDiff($diff);
513  }
514
515  public function getFinalizedRevisionMessage() {
516    return pht(
517      "You may now push this commit upstream, as appropriate (e.g. with ".
518      "'%s' or by printing and faxing it).",
519      'hg push');
520  }
521
522  public function getCommitMessageLog() {
523    $base_commit = $this->getBaseCommit();
524    list($stdout) = $this->execxLocal(
525      'log --template %s --rev %s --branch %s --',
526      "{node}\1{desc}\2",
527      hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
528      $this->getBranchName());
529
530    $map = array();
531
532    $logs = explode("\2", trim($stdout));
533    foreach (array_filter($logs) as $log) {
534      list($node, $desc) = explode("\1", $log);
535      $map[$node] = $desc;
536    }
537
538    return array_reverse($map);
539  }
540
541  public function loadWorkingCopyDifferentialRevisions(
542    ConduitClient $conduit,
543    array $query) {
544
545    $messages = $this->getCommitMessageLog();
546    $parser = new ArcanistDiffParser();
547
548    // First, try to find revisions by explicit revision IDs in commit messages.
549    $reason_map = array();
550    $revision_ids = array();
551    foreach ($messages as $node_id => $message) {
552      $object = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
553
554      if ($object->getRevisionID()) {
555        $revision_ids[] = $object->getRevisionID();
556        $reason_map[$object->getRevisionID()] = $node_id;
557      }
558    }
559
560    if ($revision_ids) {
561      $results = $conduit->callMethodSynchronous(
562        'differential.query',
563        $query + array(
564          'ids' => $revision_ids,
565        ));
566
567      foreach ($results as $key => $result) {
568        $hash = substr($reason_map[$result['id']], 0, 16);
569        $results[$key]['why'] =
570          pht(
571            "Commit message for '%s' has explicit 'Differential Revision'.",
572            $hash);
573      }
574
575      return $results;
576    }
577
578    // Try to find revisions by hash.
579    $hashes = array();
580    foreach ($this->getLocalCommitInformation() as $commit) {
581      $hashes[] = array('hgcm', $commit['commit']);
582    }
583
584    if ($hashes) {
585
586      // NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working
587      // copy with dirty changes, there may be no local commits.
588
589      $results = $conduit->callMethodSynchronous(
590        'differential.query',
591        $query + array(
592          'commitHashes' => $hashes,
593        ));
594
595      foreach ($results as $key => $hash) {
596        $results[$key]['why'] = pht(
597          'A mercurial commit hash in the commit range is already attached '.
598          'to the Differential revision.');
599      }
600
601      return $results;
602    }
603
604    return array();
605  }
606
607  public function updateWorkingCopy() {
608    $this->execxLocal('up');
609    $this->reloadWorkingCopy();
610  }
611
612  private function getMercurialConfig($key, $default = null) {
613    list($stdout) = $this->execxLocal('showconfig %s', $key);
614    if ($stdout == '') {
615      return $default;
616    }
617    return rtrim($stdout);
618  }
619
620  public function getAuthor() {
621    $full_author = $this->getMercurialConfig('ui.username');
622    list($author, $author_email) = $this->parseFullAuthor($full_author);
623    return $author;
624  }
625
626  /**
627   * Parse the Mercurial author field.
628   *
629   * Not everyone enters their email address as a part of the username
630   * field. Try to make it work when it's obvious.
631   *
632   * @param string $full_author
633   * @return array
634   */
635  protected function parseFullAuthor($full_author) {
636    if (strpos($full_author, '@') === false) {
637      $author = $full_author;
638      $author_email = null;
639    } else {
640      $email = new PhutilEmailAddress($full_author);
641      $author = $email->getDisplayName();
642      $author_email = $email->getAddress();
643    }
644
645    return array($author, $author_email);
646  }
647
648  public function addToCommit(array $paths) {
649    $this->execxLocal(
650      'addremove -- %Ls',
651      $paths);
652    $this->reloadWorkingCopy();
653  }
654
655  public function doCommit($message) {
656    $tmp_file = new TempFile();
657    Filesystem::writeFile($tmp_file, $message);
658    $this->execxLocal('commit -l %s', $tmp_file);
659    $this->reloadWorkingCopy();
660  }
661
662  public function amendCommit($message = null) {
663    if ($message === null) {
664      $message = $this->getCommitMessage('.');
665    }
666
667    $tmp_file = new TempFile();
668    Filesystem::writeFile($tmp_file, $message);
669
670    try {
671      $this->execxLocal(
672        'commit --amend -l %s',
673        $tmp_file);
674    } catch (CommandException $ex) {
675      if (preg_match('/nothing changed/', $ex->getStdout())) {
676        // NOTE: Mercurial considers it an error to make a no-op amend. Although
677        // we generally defer to the underlying VCS to dictate behavior, this
678        // one seems a little goofy, and we use amend as part of various
679        // workflows under the assumption that no-op amends are fine. If this
680        // amend failed because it's a no-op, just continue.
681      } else {
682        throw $ex;
683      }
684    }
685
686    $this->reloadWorkingCopy();
687  }
688
689  public function getCommitSummary($commit) {
690    if ($commit == 'null') {
691      return pht('(The Empty Void)');
692    }
693
694    list($summary) = $this->execxLocal(
695      'log --template {desc} --limit 1 --rev %s',
696      $commit);
697
698    $summary = head(explode("\n", $summary));
699
700    return trim($summary);
701  }
702
703  public function resolveBaseCommitRule($rule, $source) {
704    list($type, $name) = explode(':', $rule, 2);
705
706    // NOTE: This function MUST return node hashes or symbolic commits (like
707    // branch names or the word "tip"), not revsets. This includes ".^" and
708    // similar, which a revset, not a symbolic commit identifier. If you return
709    // a revset it will be escaped later and looked up literally.
710
711    switch ($type) {
712      case 'hg':
713        $matches = null;
714        if (preg_match('/^gca\((.+)\)$/', $name, $matches)) {
715          list($err, $merge_base) = $this->execManualLocal(
716            'log --template={node} --rev %s',
717            sprintf('ancestor(., %s)', $matches[1]));
718          if (!$err) {
719            $this->setBaseCommitExplanation(
720              pht(
721                "it is the greatest common ancestor of '%s' and %s, as ".
722                "specified by '%s' in your %s 'base' configuration.",
723                $matches[1],
724                '.',
725                $rule,
726                $source));
727            return trim($merge_base);
728          }
729        } else {
730          list($err, $commit) = $this->execManualLocal(
731            'log --template {node} --rev %s',
732            hgsprintf('%s', $name));
733
734          if ($err) {
735            list($err, $commit) = $this->execManualLocal(
736              'log --template {node} --rev %s',
737              $name);
738          }
739          if (!$err) {
740            $this->setBaseCommitExplanation(
741              pht(
742                "it is specified by '%s' in your %s 'base' configuration.",
743                $rule,
744                $source));
745            return trim($commit);
746          }
747        }
748        break;
749      case 'arc':
750        switch ($name) {
751          case 'empty':
752            $this->setBaseCommitExplanation(
753              pht(
754                "you specified '%s' in your %s 'base' configuration.",
755                $rule,
756                $source));
757            return 'null';
758          case 'outgoing':
759            list($err, $outgoing_base) = $this->execManualLocal(
760              'log --template={node} --rev %s',
761              'limit(reverse(ancestors(.) - outgoing()), 1)');
762            if (!$err) {
763              $this->setBaseCommitExplanation(
764                pht(
765                  "it is the first ancestor of the working copy that is not ".
766                  "outgoing, and it matched the rule %s in your %s ".
767                  "'base' configuration.",
768                  $rule,
769                  $source));
770              return trim($outgoing_base);
771            }
772          case 'amended':
773            $text = $this->getCommitMessage('.');
774            $message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
775              $text);
776            if ($message->getRevisionID()) {
777              $this->setBaseCommitExplanation(
778                pht(
779                  "'%s' has been amended with 'Differential Revision:', ".
780                  "as specified by '%s' in your %s 'base' configuration.",
781                  '.',
782                  $rule,
783                  $source));
784              // NOTE: This should be safe because Mercurial doesn't support
785              // amend until 2.2.
786              return $this->getCanonicalRevisionName('.^');
787            }
788            break;
789          case 'bookmark':
790            $revset =
791              'limit('.
792              '  sort('.
793              '    (ancestors(.) and bookmark() - .) or'.
794              '    (ancestors(.) - outgoing()), '.
795              '  -rev),'.
796              '1)';
797            list($err, $bookmark_base) = $this->execManualLocal(
798              'log --template={node} --rev %s',
799              $revset);
800            if (!$err) {
801              $this->setBaseCommitExplanation(
802                pht(
803                  "it is the first ancestor of %s that either has a bookmark, ".
804                  "or is already in the remote and it matched the rule %s in ".
805                  "your %s 'base' configuration",
806                  '.',
807                  $rule,
808                  $source));
809              return trim($bookmark_base);
810            }
811            break;
812          case 'this':
813            $this->setBaseCommitExplanation(
814              pht(
815                "you specified '%s' in your %s 'base' configuration.",
816                $rule,
817                $source));
818            return $this->getCanonicalRevisionName('.^');
819          default:
820            if (preg_match('/^nodiff\((.+)\)$/', $name, $matches)) {
821              list($results) = $this->execxLocal(
822                'log --template %s --rev %s',
823                "{node}\1{desc}\2",
824                sprintf('ancestor(.,%s)::.^', $matches[1]));
825              $results = array_reverse(explode("\2", trim($results)));
826
827              foreach ($results as $result) {
828                if (empty($result)) {
829                  continue;
830                }
831
832                list($node, $desc) = explode("\1", $result, 2);
833
834                $message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
835                  $desc);
836                if ($message->getRevisionID()) {
837                  $this->setBaseCommitExplanation(
838                    pht(
839                      "it is the first ancestor of %s that has a diff and is ".
840                      "the gca or a descendant of the gca with '%s', ".
841                      "specified by '%s' in your %s 'base' configuration.",
842                      '.',
843                      $matches[1],
844                      $rule,
845                      $source));
846                  return $node;
847                }
848              }
849            }
850            break;
851          }
852        break;
853      default:
854        return null;
855    }
856
857    return null;
858
859  }
860
861  public function getSubversionInfo() {
862    $info = array();
863    $base_path = null;
864    $revision = null;
865    list($err, $raw_info) = $this->execManualLocal('svn info');
866    if (!$err) {
867      foreach (explode("\n", trim($raw_info)) as $line) {
868        list($key, $value) = explode(': ', $line, 2);
869        switch ($key) {
870          case 'URL':
871            $info['base_path'] = $value;
872            $base_path = $value;
873            break;
874          case 'Repository UUID':
875            $info['uuid'] = $value;
876            break;
877          case 'Revision':
878            $revision = $value;
879            break;
880          default:
881            break;
882        }
883      }
884      if ($base_path && $revision) {
885        $info['base_revision'] = $base_path.'@'.$revision;
886      }
887    }
888    return $info;
889  }
890
891  public function getActiveBookmark() {
892    $bookmark = $this->newMarkerRefQuery()
893      ->withMarkerTypes(
894        array(
895          ArcanistMarkerRef::TYPE_BOOKMARK,
896        ))
897      ->withIsActive(true)
898      ->executeOne();
899
900    if (!$bookmark) {
901      return null;
902    }
903
904    return $bookmark->getName();
905  }
906
907  public function getRemoteURI() {
908    // TODO: Remove this method in favor of RemoteRefQuery.
909
910    list($stdout) = $this->execxLocal('paths default');
911
912    $stdout = trim($stdout);
913    if (strlen($stdout)) {
914      return $stdout;
915    }
916
917    return null;
918  }
919
920  private function getMercurialEnvironmentVariables() {
921    $env = array();
922
923    // Mercurial has a "defaults" feature which basically breaks automation by
924    // allowing the user to add random flags to any command. This feature is
925    // "deprecated" and "a bad idea" that you should "forget ... existed"
926    // according to project lead Matt Mackall:
927    //
928    //  http://markmail.org/message/hl3d6eprubmkkqh5
929    //
930    // There is an HGPLAIN environmental variable which enables "plain mode"
931    // and hopefully disables this stuff.
932
933    $env['HGPLAIN'] = 1;
934
935    return $env;
936  }
937
938  protected function newLandEngine() {
939    return new ArcanistMercurialLandEngine();
940  }
941
942  protected function newWorkEngine() {
943    return new ArcanistMercurialWorkEngine();
944  }
945
946  public function newLocalState() {
947    return id(new ArcanistMercurialLocalState())
948      ->setRepositoryAPI($this);
949  }
950
951  public function willTestMercurialFeature($feature) {
952    $this->executeMercurialFeatureTest($feature, false);
953    return $this;
954  }
955
956  public function getMercurialFeature($feature) {
957    return $this->executeMercurialFeatureTest($feature, true);
958  }
959
960  private function executeMercurialFeatureTest($feature, $resolve) {
961    if (array_key_exists($feature, $this->featureResults)) {
962      return $this->featureResults[$feature];
963    }
964
965    if (!array_key_exists($feature, $this->featureFutures)) {
966      $future = $this->newMercurialFeatureFuture($feature);
967      $future->start();
968      $this->featureFutures[$feature] = $future;
969    }
970
971    if (!$resolve) {
972      return;
973    }
974
975    $future = $this->featureFutures[$feature];
976    $result = $this->resolveMercurialFeatureFuture($feature, $future);
977    $this->featureResults[$feature] = $result;
978
979    return $result;
980  }
981
982  private function newMercurialFeatureFuture($feature) {
983    switch ($feature) {
984      case 'shelve':
985        return $this->execFutureLocal(
986          '--config extensions.shelve= shelve --help --');
987      case 'evolve':
988        return $this->execFutureLocal('prune --help --');
989      default:
990        throw new Exception(
991          pht(
992            'Unknown Mercurial feature "%s".',
993            $feature));
994    }
995  }
996
997  private function resolveMercurialFeatureFuture($feature, $future) {
998    // By default, assume the feature is a simple capability test and the
999    // capability is present if the feature resolves without an error.
1000
1001    list($err) = $future->resolve();
1002    return !$err;
1003  }
1004
1005  protected function newSupportedMarkerTypes() {
1006    return array(
1007      ArcanistMarkerRef::TYPE_BRANCH,
1008      ArcanistMarkerRef::TYPE_BOOKMARK,
1009    );
1010  }
1011
1012  protected function newMarkerRefQueryTemplate() {
1013    return new ArcanistMercurialRepositoryMarkerQuery();
1014  }
1015
1016  protected function newRemoteRefQueryTemplate() {
1017    return new ArcanistMercurialRepositoryRemoteQuery();
1018  }
1019
1020  public function getMercurialExtensionArguments() {
1021    $path = phutil_get_library_root('arcanist');
1022    $path = dirname($path);
1023    $path = $path.'/support/hg/arc-hg.py';
1024
1025    return array(
1026      '--config',
1027      'extensions.arc-hg='.$path,
1028    );
1029  }
1030
1031  protected function newNormalizedURI($uri) {
1032    return new ArcanistRepositoryURINormalizer(
1033      ArcanistRepositoryURINormalizer::TYPE_MERCURIAL,
1034      $uri);
1035  }
1036
1037  protected function newCommitGraphQueryTemplate() {
1038    return new ArcanistMercurialCommitGraphQuery();
1039  }
1040
1041  protected function newPublishedCommitHashes() {
1042    $future = $this->newFuture(
1043      'log --rev %s --template %s',
1044      hgsprintf('parents(draft()) - draft()'),
1045      '{node}\n');
1046    list($lines) = $future->resolve();
1047
1048    $lines = phutil_split_lines($lines, false);
1049
1050    $hashes = array();
1051    foreach ($lines as $line) {
1052      if (!strlen(trim($line))) {
1053        continue;
1054      }
1055      $hashes[] = $line;
1056    }
1057
1058    return $hashes;
1059  }
1060
1061}
1062