4
* Interfaces with the Mercurial working copies.
6
final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
9
private $localCommitInfo;
10
private $rawDiffCache = array();
12
private $supportsRebase;
13
private $supportsPhases;
15
protected function buildLocalFuture(array $argv) {
16
// Mercurial has a "defaults" feature which basically breaks automation by
17
// allowing the user to add random flags to any command. This feature is
18
// "deprecated" and "a bad idea" that you should "forget ... existed"
19
// according to project lead Matt Mackall:
21
// http://markmail.org/message/hl3d6eprubmkkqh5
23
// There is an HGPLAIN environmental variable which enables "plain mode"
24
// and hopefully disables this stuff.
26
if (phutil_is_windows()) {
27
$argv[0] = 'set HGPLAIN=1 & hg '.$argv[0];
29
$argv[0] = 'HGPLAIN=1 hg '.$argv[0];
32
$future = newv('ExecFuture', $argv);
33
$future->setCWD($this->getPath());
37
public function execPassthru($pattern /* , ... */) {
38
$args = func_get_args();
39
if (phutil_is_windows()) {
40
$args[0] = 'hg '.$args[0];
42
$args[0] = 'HGPLAIN=1 hg '.$args[0];
45
return call_user_func_array('phutil_passthru', $args);
48
public function getSourceControlSystemName() {
52
public function getMetadataPath() {
53
return $this->getPath('.hg');
56
public function getSourceControlBaseRevision() {
57
return $this->getCanonicalRevisionName($this->getBaseCommit());
60
public function getCanonicalRevisionName($string) {
62
if ($this->isHgSubversionRepo() &&
63
preg_match('/@([0-9]+)$/', $string, $match)) {
64
$string = hgsprintf('svnrev(%s)', $match[1]);
67
list($stdout) = $this->execxLocal(
68
'log -l 1 --template %s -r %s --',
74
public function getHashFromFromSVNRevisionNumber($revision_id) {
76
$string = hgsprintf('svnrev(%s)', $revision_id);
77
list($stdout) = $this->execxLocal(
78
'log -l 1 --template %s -r %s --',
82
throw new ArcanistUsageException(
83
"Cannot find the HG equivalent of {$revision_id} given.");
89
public function getSVNRevisionNumberFromHash($hash) {
91
list($stdout) = $this->execxLocal(
92
'log -r %s --template {svnrev}', $hash);
94
throw new ArcanistUsageException(
95
"Cannot find the SVN equivalent of {$hash} given.");
100
public function getSourceControlPath() {
104
public function getBranchName() {
105
if (!$this->branch) {
106
list($stdout) = $this->execxLocal('branch');
107
$this->branch = trim($stdout);
109
return $this->branch;
112
public function didReloadCommitRange() {
113
$this->localCommitInfo = null;
116
protected function buildBaseCommit($symbolic_commit) {
117
if ($symbolic_commit !== null) {
119
$commit = $this->getCanonicalRevisionName(
120
hgsprintf('ancestor(%s,.)', $symbolic_commit));
121
} catch (Exception $ex) {
122
// Try it as a revset instead of a commit id
124
$commit = $this->getCanonicalRevisionName(
125
hgsprintf('ancestor(%R,.)', $symbolic_commit));
126
} catch (Exception $ex) {
127
throw new ArcanistUsageException(
128
"Commit '{$symbolic_commit}' is not a valid Mercurial commit ".
133
$this->setBaseCommitExplanation(
134
'it is the greatest common ancestor of the working directory '.
135
'and the commit you specified explicitly.');
139
if ($this->getBaseCommitArgumentRules() ||
140
$this->getConfigurationManager()->getConfigFromAnySource('base')) {
141
$base = $this->resolveBaseCommit();
143
throw new ArcanistUsageException(
144
"None of the rules in your 'base' configuration matched a valid ".
145
"commit. Adjust rules or specify which commit you want to use ".
151
// Mercurial 2.1 and up have phases which indicate if something is
152
// published or not. To find which revs are outgoing, it's much
153
// faster to check the phase instead of actually checking the server.
154
if ($this->supportsPhases()) {
155
list($err, $stdout) = $this->execManualLocal(
156
'log --branch %s -r %s --style default',
157
$this->getBranchName(),
160
list($err, $stdout) = $this->execManualLocal(
161
'outgoing --branch %s --style default',
162
$this->getBranchName());
166
$logs = ArcanistMercurialParser::parseMercurialLog($stdout);
168
// Mercurial (in some versions?) raises an error when there's nothing
174
$this->setBaseCommitExplanation(
175
'you have no outgoing commits, so arc assumes you intend to submit '.
176
'uncommitted changes in the working copy.');
177
return $this->getWorkingCopyRevision();
180
$outgoing_revs = ipull($logs, 'rev');
182
// This is essentially an implementation of a theoretical `hg merge-base`
184
$against = $this->getWorkingCopyRevision();
186
// NOTE: The "^" and "~" syntaxes were only added in hg 1.9, which is
187
// new as of July 2011, so do this in a compatible way. Also, "hg log"
188
// and "hg outgoing" don't necessarily show parents (even if given an
189
// explicit template consisting of just the parents token) so we need
190
// to separately execute "hg parents".
192
list($stdout) = $this->execxLocal(
193
'parents --style default --rev %s',
195
$parents_logs = ArcanistMercurialParser::parseMercurialLog($stdout);
197
list($p1, $p2) = array_merge($parents_logs, array(null, null));
199
if ($p1 && !in_array($p1['rev'], $outgoing_revs)) {
200
$against = $p1['rev'];
202
} else if ($p2 && !in_array($p2['rev'], $outgoing_revs)) {
203
$against = $p2['rev'];
206
$against = $p1['rev'];
208
// This is the case where you have a new repository and the entire
209
// thing is outgoing; Mercurial literally accepts "--rev null" as
210
// meaning "diff against the empty state".
216
if ($against == 'null') {
217
$this->setBaseCommitExplanation(
218
'this is a new repository (all changes are outgoing).');
220
$this->setBaseCommitExplanation(
221
'it is the first commit reachable from the working copy state '.
222
'which is not outgoing.');
228
public function getLocalCommitInformation() {
229
if ($this->localCommitInfo === null) {
230
$base_commit = $this->getBaseCommit();
231
list($info) = $this->execxLocal(
232
'log --template %s --rev %s --branch %s --',
233
"{node}\1{rev}\1{author}\1".
234
"{date|rfc822date}\1{branch}\1{tag}\1{parents}\1{desc}\2",
235
hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
236
$this->getBranchName());
237
$logs = array_filter(explode("\2", $info));
244
foreach ($logs as $log) {
245
list($node, $rev, $full_author, $date, $branch, $tag,
246
$parents, $desc) = explode("\1", $log, 9);
248
list ($author, $author_email) = $this->parseFullAuthor($full_author);
250
// NOTE: If a commit has only one parent, {parents} returns empty.
251
// If it has two parents, {parents} returns revs and short hashes, not
252
// full hashes. Try to avoid making calls to "hg parents" because it's
253
// relatively expensive.
254
$commit_parents = null;
257
$commit_parents = array($last_node);
261
if (!$commit_parents) {
262
// We didn't get a cheap hit on previous commit, so do the full-cost
263
// "hg parents" call. We can run these in parallel, at least.
264
$futures[$node] = $this->execFutureLocal(
265
'parents --template %s --rev %s',
270
$commits[$node] = array(
272
'time' => strtotime($date),
276
'rev' => $node, // TODO: Remove eventually.
278
'parents' => $commit_parents,
279
'summary' => head(explode("\n", $desc)),
281
'authorEmail' => $author_email,
287
foreach (Futures($futures)->limit(4) as $node => $future) {
288
list($parents) = $future->resolvex();
289
$parents = array_filter(explode("\n", $parents));
290
$commits[$node]['parents'] = $parents;
293
// Put commits in newest-first order, to be consistent with Git and the
294
// expected order of "hg log" and "git log" under normal circumstances.
295
// The order of ancestors() is oldest-first.
296
$commits = array_reverse($commits);
298
$this->localCommitInfo = $commits;
301
return $this->localCommitInfo;
304
public function getAllFiles() {
305
// TODO: Handle paths with newlines.
306
$future = $this->buildLocalFuture(array('manifest'));
307
return new LinesOfALargeExecFuture($future);
310
public function getChangedFiles($since_commit) {
311
list($stdout) = $this->execxLocal(
314
return ArcanistMercurialParser::parseMercurialStatus($stdout);
317
public function getBlame($path) {
318
list($stdout) = $this->execxLocal(
319
'annotate -u -v -c --rev %s -- %s',
320
$this->getBaseCommit(),
323
$lines = phutil_split_lines($stdout, $retain_line_endings = true);
326
foreach ($lines as $line) {
327
if (!strlen($line)) {
332
$ok = preg_match('/^\s*([^:]+?) ([a-f0-9]{12}):/', $line, $matches);
335
throw new Exception("Unable to parse Mercurial blame line: {$line}");
338
$revision = $matches[2];
339
$author = trim($matches[1]);
340
$blame[] = array($author, $revision);
346
protected function buildUncommittedStatus() {
347
list($stdout) = $this->execxLocal('status');
349
$results = new PhutilArrayWithDefaultValue();
351
$working_status = ArcanistMercurialParser::parseMercurialStatus($stdout);
352
foreach ($working_status as $path => $mask) {
353
if (!($mask & ArcanistRepositoryAPI::FLAG_UNTRACKED)) {
354
// Mark tracked files as uncommitted.
355
$mask |= self::FLAG_UNCOMMITTED;
358
$results[$path] |= $mask;
361
return $results->toArray();
364
protected function buildCommitRangeStatus() {
365
// TODO: Possibly we should use "hg status --rev X --rev ." for this
366
// instead, but we must run "hg diff" later anyway in most cases, so
367
// building and caching it shouldn't hurt us.
369
$diff = $this->getFullMercurialDiff();
374
$parser = new ArcanistDiffParser();
375
$changes = $parser->parseDiff($diff);
377
$status_map = array();
378
foreach ($changes as $change) {
380
switch ($change->getType()) {
381
case ArcanistDiffChangeType::TYPE_ADD:
382
case ArcanistDiffChangeType::TYPE_MOVE_HERE:
383
case ArcanistDiffChangeType::TYPE_COPY_HERE:
384
$flags |= self::FLAG_ADDED;
386
case ArcanistDiffChangeType::TYPE_CHANGE:
387
case ArcanistDiffChangeType::TYPE_COPY_AWAY: // Check for changes?
388
$flags |= self::FLAG_MODIFIED;
390
case ArcanistDiffChangeType::TYPE_DELETE:
391
case ArcanistDiffChangeType::TYPE_MOVE_AWAY:
392
case ArcanistDiffChangeType::TYPE_MULTICOPY:
393
$flags |= self::FLAG_DELETED;
396
$status_map[$change->getCurrentPath()] = $flags;
402
protected function didReloadWorkingCopy() {
403
// Diffs are against ".", so we need to drop the cache if we change the
405
$this->rawDiffCache = array();
406
$this->branch = null;
409
private function getDiffOptions() {
412
'-U'.$this->getDiffLinesOfContext(),
414
return implode(' ', $options);
417
public function getRawDiffText($path) {
418
$options = $this->getDiffOptions();
420
$range = $this->getBaseCommit();
422
$raw_diff_cache_key = $options.' '.$range.' '.$path;
423
if (idx($this->rawDiffCache, $raw_diff_cache_key)) {
424
return idx($this->rawDiffCache, $raw_diff_cache_key);
427
list($stdout) = $this->execxLocal(
428
'diff %C --rev %s -- %s',
433
$this->rawDiffCache[$raw_diff_cache_key] = $stdout;
438
public function getFullMercurialDiff() {
439
return $this->getRawDiffText('');
442
public function getOriginalFileData($path) {
443
return $this->getFileDataAtRevision($path, $this->getBaseCommit());
446
public function getCurrentFileData($path) {
447
return $this->getFileDataAtRevision(
449
$this->getWorkingCopyRevision());
452
public function getBulkOriginalFileData($paths) {
453
return $this->getBulkFileDataAtRevision($paths, $this->getBaseCommit());
456
public function getBulkCurrentFileData($paths) {
457
return $this->getBulkFileDataAtRevision(
459
$this->getWorkingCopyRevision());
462
private function getBulkFileDataAtRevision($paths, $revision) {
463
// Calling 'hg cat' on each file individually is slow (1 second per file
464
// on a large repo) because mercurial has to decompress and parse the
465
// entire manifest every time. Do it in one large batch instead.
467
// hg cat will write the file data to files in a temp directory
468
$tmpdir = Filesystem::createTemporaryDirectory();
470
// Mercurial doesn't create the directories for us :(
471
foreach ($paths as $path) {
472
$tmppath = $tmpdir.'/'.$path;
473
Filesystem::createDirectory(dirname($tmppath), 0755, true);
476
list($err, $stdout) = $this->execManualLocal(
477
'cat --rev %s --output %s -- %C',
479
// %p is the formatter for the repo-relative filepath
481
implode(' ', $paths));
484
foreach ($paths as $path) {
485
$tmppath = $tmpdir.'/'.$path;
486
if (Filesystem::pathExists($tmppath)) {
487
$filedata[$path] = Filesystem::readFile($tmppath);
491
Filesystem::remove($tmpdir);
496
private function getFileDataAtRevision($path, $revision) {
497
list($err, $stdout) = $this->execManualLocal(
498
'cat --rev %s -- %s',
502
// Assume this is "no file at revision", i.e. a deleted or added file.
509
public function getWorkingCopyRevision() {
513
public function isHistoryDefaultImmutable() {
517
public function supportsAmend() {
518
list($err, $stdout) = $this->execManualLocal('help commit');
522
return (strpos($stdout, 'amend') !== false);
526
public function supportsRebase() {
527
if ($this->supportsRebase === null) {
528
list ($err) = $this->execManualLocal('help rebase');
529
$this->supportsRebase = $err === 0;
532
return $this->supportsRebase;
535
public function supportsPhases() {
536
if ($this->supportsPhases === null) {
537
list ($err) = $this->execManualLocal('help phase');
538
$this->supportsPhases = $err === 0;
541
return $this->supportsPhases;
544
public function supportsCommitRanges() {
548
public function supportsLocalCommits() {
552
public function getAllBranches() {
553
list($branch_info) = $this->execxLocal('bookmarks');
554
if (trim($branch_info) == 'no bookmarks set') {
560
'/^\s*(\*?)\s*(.+)\s(\S+)$/m',
566
foreach ($matches as $match) {
567
list(, $current, $name) = $match;
569
'current' => (bool)$current,
570
'name' => rtrim($name),
576
public function hasLocalCommit($commit) {
578
$this->getCanonicalRevisionName($commit);
580
} catch (Exception $ex) {
585
public function getCommitMessage($commit) {
586
list($message) = $this->execxLocal(
587
'log --template={desc} --rev %s',
592
public function getAllLocalChanges() {
593
$diff = $this->getFullMercurialDiff();
594
if (!strlen(trim($diff))) {
597
$parser = new ArcanistDiffParser();
598
return $parser->parseDiff($diff);
601
public function supportsLocalBranchMerge() {
605
public function performLocalBranchMerge($branch, $message) {
607
$err = phutil_passthru(
608
'(cd %s && HGPLAIN=1 hg merge --rev %s && hg commit -m %s)',
613
$err = phutil_passthru(
614
'(cd %s && HGPLAIN=1 hg merge && hg commit -m %s)',
620
throw new ArcanistUsageException('Merge failed!');
624
public function getFinalizedRevisionMessage() {
625
return "You may now push this commit upstream, as appropriate (e.g. with ".
626
"'hg push' or by printing and faxing it).";
629
public function getCommitMessageLog() {
630
$base_commit = $this->getBaseCommit();
631
list($stdout) = $this->execxLocal(
632
'log --template %s --rev %s --branch %s --',
634
hgsprintf('(%s::. - %s)', $base_commit, $base_commit),
635
$this->getBranchName());
639
$logs = explode("\2", trim($stdout));
640
foreach (array_filter($logs) as $log) {
641
list($node, $desc) = explode("\1", $log);
645
return array_reverse($map);
648
public function loadWorkingCopyDifferentialRevisions(
649
ConduitClient $conduit,
652
$messages = $this->getCommitMessageLog();
653
$parser = new ArcanistDiffParser();
655
// First, try to find revisions by explicit revision IDs in commit messages.
656
$reason_map = array();
657
$revision_ids = array();
658
foreach ($messages as $node_id => $message) {
659
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
661
if ($object->getRevisionID()) {
662
$revision_ids[] = $object->getRevisionID();
663
$reason_map[$object->getRevisionID()] = $node_id;
668
$results = $conduit->callMethodSynchronous(
669
'differential.query',
671
'ids' => $revision_ids,
674
foreach ($results as $key => $result) {
675
$hash = substr($reason_map[$result['id']], 0, 16);
676
$results[$key]['why'] =
677
"Commit message for '{$hash}' has explicit 'Differential Revision'.";
683
// Try to find revisions by hash.
685
foreach ($this->getLocalCommitInformation() as $commit) {
686
$hashes[] = array('hgcm', $commit['commit']);
691
// NOTE: In the case of "arc diff . --uncommitted" in a Mercurial working
692
// copy with dirty changes, there may be no local commits.
694
$results = $conduit->callMethodSynchronous(
695
'differential.query',
697
'commitHashes' => $hashes,
700
foreach ($results as $key => $hash) {
701
$results[$key]['why'] =
702
'A mercurial commit hash in the commit range is already attached '.
703
'to the Differential revision.';
712
public function updateWorkingCopy() {
713
$this->execxLocal('up');
714
$this->reloadWorkingCopy();
717
private function getMercurialConfig($key, $default = null) {
718
list($stdout) = $this->execxLocal('showconfig %s', $key);
722
return rtrim($stdout);
725
public function getAuthor() {
726
$full_author = $this->getMercurialConfig('ui.username');
727
list($author, $author_email) = $this->parseFullAuthor($full_author);
732
* Parse the Mercurial author field.
734
* Not everyone enters their email address as a part of the username
735
* field. Try to make it work when it's obvious.
737
* @param string $full_author
740
protected function parseFullAuthor($full_author) {
741
if (strpos($full_author, '@') === false) {
742
$author = $full_author;
743
$author_email = null;
745
$email = new PhutilEmailAddress($full_author);
746
$author = $email->getDisplayName();
747
$author_email = $email->getAddress();
750
return array($author, $author_email);
753
public function addToCommit(array $paths) {
757
$this->reloadWorkingCopy();
760
public function doCommit($message) {
761
$tmp_file = new TempFile();
762
Filesystem::writeFile($tmp_file, $message);
763
$this->execxLocal('commit -l %s', $tmp_file);
764
$this->reloadWorkingCopy();
767
public function amendCommit($message = null) {
768
if ($message === null) {
769
$message = $this->getCommitMessage('.');
772
$tmp_file = new TempFile();
773
Filesystem::writeFile($tmp_file, $message);
777
'commit --amend -l %s',
779
} catch (CommandException $ex) {
780
if (preg_match('/nothing changed/', $ex->getStdOut())) {
781
// NOTE: Mercurial considers it an error to make a no-op amend. Although
782
// we generally defer to the underlying VCS to dictate behavior, this
783
// one seems a little goofy, and we use amend as part of various
784
// workflows under the assumption that no-op amends are fine. If this
785
// amend failed because it's a no-op, just continue.
791
$this->reloadWorkingCopy();
794
public function getCommitSummary($commit) {
795
if ($commit == 'null') {
796
return '(The Empty Void)';
799
list($summary) = $this->execxLocal(
800
'log --template {desc} --limit 1 --rev %s',
803
$summary = head(explode("\n", $summary));
805
return trim($summary);
808
public function backoutCommit($commit_hash) {
810
'backout -r %s', $commit_hash);
811
$this->reloadWorkingCopy();
812
if (!$this->getUncommittedStatus()) {
813
throw new ArcanistUsageException(
814
"{$commit_hash} has already been reverted.");
818
public function getBackoutMessage($commit_hash) {
819
return 'Backed out changeset '.$commit_hash.'.';
822
public function resolveBaseCommitRule($rule, $source) {
823
list($type, $name) = explode(':', $rule, 2);
825
// NOTE: This function MUST return node hashes or symbolic commits (like
826
// branch names or the word "tip"), not revsets. This includes ".^" and
827
// similar, which a revset, not a symbolic commit identifier. If you return
828
// a revset it will be escaped later and looked up literally.
833
if (preg_match('/^gca\((.+)\)$/', $name, $matches)) {
834
list($err, $merge_base) = $this->execManualLocal(
835
'log --template={node} --rev %s',
836
sprintf('ancestor(., %s)', $matches[1]));
838
$this->setBaseCommitExplanation(
839
"it is the greatest common ancestor of '{$matches[1]}' and ., as".
840
" specified by '{$rule}' in your {$source} 'base' ".
842
return trim($merge_base);
845
list($err, $commit) = $this->execManualLocal(
846
'log --template {node} --rev %s',
847
hgsprintf('%s', $name));
850
list($err, $commit) = $this->execManualLocal(
851
'log --template {node} --rev %s',
855
$this->setBaseCommitExplanation(
856
"it is specified by '{$rule}' in your {$source} 'base' ".
858
return trim($commit);
865
$this->setBaseCommitExplanation(
866
"you specified '{$rule}' in your {$source} 'base' ".
870
list($err, $outgoing_base) = $this->execManualLocal(
871
'log --template={node} --rev %s',
872
'limit(reverse(ancestors(.) - outgoing()), 1)');
874
$this->setBaseCommitExplanation(
875
"it is the first ancestor of the working copy that is not ".
876
"outgoing, and it matched the rule {$rule} in your {$source} ".
877
"'base' configuration.");
878
return trim($outgoing_base);
881
$text = $this->getCommitMessage('.');
882
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
884
if ($message->getRevisionID()) {
885
$this->setBaseCommitExplanation(
886
"'.' has been amended with 'Differential Revision:', ".
887
"as specified by '{$rule}' in your {$source} 'base' ".
889
// NOTE: This should be safe because Mercurial doesn't support
891
return $this->getCanonicalRevisionName('.^');
898
' (ancestors(.) and bookmark() - .) or'.
899
' (ancestors(.) - outgoing()), '.
902
list($err, $bookmark_base) = $this->execManualLocal(
903
'log --template={node} --rev %s',
906
$this->setBaseCommitExplanation(
907
"it is the first ancestor of . that either has a bookmark, or ".
908
"is already in the remote and it matched the rule {$rule} in ".
909
"your {$source} 'base' configuration");
910
return trim($bookmark_base);
914
$this->setBaseCommitExplanation(
915
"you specified '{$rule}' in your {$source} 'base' ".
917
return $this->getCanonicalRevisionName('.^');
919
if (preg_match('/^nodiff\((.+)\)$/', $name, $matches)) {
920
list($results) = $this->execxLocal(
921
'log --template %s --rev %s',
923
sprintf('ancestor(.,%s)::.^', $matches[1]));
924
$results = array_reverse(explode("\2", trim($results)));
926
foreach ($results as $result) {
927
if (empty($result)) {
931
list($node, $desc) = explode("\1", $result, 2);
933
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
935
if ($message->getRevisionID()) {
936
$this->setBaseCommitExplanation(
937
"it is the first ancestor of . that has a diff ".
938
"and is the gca or a descendant of the gca with ".
939
"'{$matches[1]}', specified by '{$rule}' in your ".
940
"{$source} 'base' configuration.");
956
public function isHgSubversionRepo() {
957
return file_exists($this->getPath('.hg/svn/rev_map'));
960
public function getSubversionInfo() {
964
list($err, $raw_info) = $this->execManualLocal('svn info');
966
foreach (explode("\n", trim($raw_info)) as $line) {
967
list($key, $value) = explode(': ', $line, 2);
970
$info['base_path'] = $value;
973
case 'Repository UUID':
974
$info['uuid'] = $value;
983
if ($base_path && $revision) {
984
$info['base_revision'] = $base_path.'@'.$revision;
990
public function getActiveBookmark() {
991
$bookmarks = $this->getBookmarks();
992
foreach ($bookmarks as $bookmark) {
993
if ($bookmark['is_active']) {
994
return $bookmark['name'];
1001
public function isBookmark($name) {
1002
$bookmarks = $this->getBookmarks();
1003
foreach ($bookmarks as $bookmark) {
1004
if ($bookmark['name'] === $name) {
1012
public function isBranch($name) {
1013
$branches = $this->getBranches();
1014
foreach ($branches as $branch) {
1015
if ($branch['name'] === $name) {
1023
public function getBranches() {
1024
list($stdout) = $this->execxLocal('--debug branches');
1025
$lines = ArcanistMercurialParser::parseMercurialBranches($stdout);
1027
$branches = array();
1028
foreach ($lines as $name => $spec) {
1029
$branches[] = array(
1031
'revision' => $spec['rev'],
1038
public function getBookmarks() {
1039
$bookmarks = array();
1041
list($raw_output) = $this->execxLocal('bookmarks');
1042
$raw_output = trim($raw_output);
1043
if ($raw_output !== 'no bookmarks set') {
1044
foreach (explode("\n", $raw_output) as $line) {
1045
// example line: * mybook 2:6b274d49be97
1046
list($name, $revision) = $this->splitBranchOrBookmarkLine($line);
1049
if ('*' === $name[0]) {
1051
$name = substr($name, 2);
1054
$bookmarks[] = array(
1055
'is_active' => $is_active,
1057
'revision' => $revision,
1065
private function splitBranchOrBookmarkLine($line) {
1066
// branches and bookmarks are printed in the format:
1067
// default 0:a5ead76cdf85 (inactive)
1068
// * mybook 2:6b274d49be97
1069
// this code divides the name half from the revision half
1070
// it does not parse the * and (inactive) bits
1071
$colon_index = strrpos($line, ':');
1072
$before_colon = substr($line, 0, $colon_index);
1073
$start_rev_index = strrpos($before_colon, ' ');
1074
$name = substr($line, 0, $start_rev_index);
1075
$rev = substr($line, $start_rev_index);
1077
return array(trim($name), trim($rev));
1080
public function getRemoteURI() {
1081
list($stdout) = $this->execxLocal('paths default');
1083
$stdout = trim($stdout);
1084
if (strlen($stdout)) {