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