1<?php 2 3final class ArcanistMercurialLandEngine 4 extends ArcanistLandEngine { 5 6 private $ontoBranchMarker; 7 private $ontoMarkers; 8 9 protected function getDefaultSymbols() { 10 $api = $this->getRepositoryAPI(); 11 $log = $this->getLogEngine(); 12 13 // TODO: In Mercurial, you normally can not create a branch and a bookmark 14 // with the same name. However, you can fetch a branch or bookmark from 15 // a remote that has the same name as a local branch or bookmark of the 16 // other type, and end up with a local branch and bookmark with the same 17 // name. We should detect this and treat it as an error. 18 19 // TODO: In Mercurial, you can create local bookmarks named 20 // "default@default" and similar which do not surive a round trip through 21 // a remote. Possibly, we should disallow interacting with these bookmarks. 22 23 $markers = $api->newMarkerRefQuery() 24 ->withIsActive(true) 25 ->execute(); 26 27 $bookmark = null; 28 foreach ($markers as $marker) { 29 if ($marker->isBookmark()) { 30 $bookmark = $marker->getName(); 31 break; 32 } 33 } 34 35 if ($bookmark !== null) { 36 $log->writeStatus( 37 pht('SOURCE'), 38 pht( 39 'Landing the active bookmark, "%s".', 40 $bookmark)); 41 42 return array($bookmark); 43 } 44 45 $branch = null; 46 foreach ($markers as $marker) { 47 if ($marker->isBranch()) { 48 $branch = $marker->getName(); 49 break; 50 } 51 } 52 53 if ($branch !== null) { 54 $log->writeStatus( 55 pht('SOURCE'), 56 pht( 57 'Landing the active branch, "%s".', 58 $branch)); 59 60 return array($branch); 61 } 62 63 $commit = $api->getCanonicalRevisionName('.'); 64 $commit = $api->getDisplayHash($commit); 65 66 $log->writeStatus( 67 pht('SOURCE'), 68 pht( 69 'Landing the active commit, "%s".', 70 $api->getDisplayHash($commit))); 71 72 return array($commit); 73 } 74 75 protected function resolveSymbols(array $symbols) { 76 assert_instances_of($symbols, 'ArcanistLandSymbol'); 77 $api = $this->getRepositoryAPI(); 78 79 $marker_types = array( 80 ArcanistMarkerRef::TYPE_BOOKMARK, 81 ArcanistMarkerRef::TYPE_BRANCH, 82 ); 83 84 $unresolved = $symbols; 85 foreach ($marker_types as $marker_type) { 86 $markers = $api->newMarkerRefQuery() 87 ->withMarkerTypes(array($marker_type)) 88 ->execute(); 89 90 $markers = mgroup($markers, 'getName'); 91 92 foreach ($unresolved as $key => $symbol) { 93 $raw_symbol = $symbol->getSymbol(); 94 95 $named_markers = idx($markers, $raw_symbol); 96 if (!$named_markers) { 97 continue; 98 } 99 100 if (count($named_markers) > 1) { 101 echo tsprintf( 102 "\n%!\n%W\n\n", 103 pht('AMBIGUOUS SYMBOL'), 104 pht( 105 'Symbol "%s" is ambiguous: it matches multiple markers '. 106 '(of type "%s"). Use an unambiguous identifier.', 107 $raw_symbol, 108 $marker_type)); 109 110 foreach ($named_markers as $named_marker) { 111 echo tsprintf('%s', $named_marker->newRefView()); 112 } 113 114 echo tsprintf("\n"); 115 116 throw new PhutilArgumentUsageException( 117 pht( 118 'Symbol "%s" is ambiguous.', 119 $raw_symbol)); 120 } 121 122 $marker = head($named_markers); 123 124 $symbol->setCommit($marker->getCommitHash()); 125 126 unset($unresolved[$key]); 127 } 128 } 129 130 foreach ($unresolved as $symbol) { 131 $raw_symbol = $symbol->getSymbol(); 132 133 // TODO: This doesn't have accurate error behavior if the user provides 134 // a revset like "x::y". 135 try { 136 $commit = $api->getCanonicalRevisionName($raw_symbol); 137 } catch (CommandException $ex) { 138 $commit = null; 139 } 140 141 if ($commit === null) { 142 throw new PhutilArgumentUsageException( 143 pht( 144 'Symbol "%s" does not identify a bookmark, branch, or commit.', 145 $raw_symbol)); 146 } 147 148 $symbol->setCommit($commit); 149 } 150 } 151 152 protected function selectOntoRemote(array $symbols) { 153 assert_instances_of($symbols, 'ArcanistLandSymbol'); 154 $api = $this->getRepositoryAPI(); 155 156 $remote = $this->newOntoRemote($symbols); 157 158 $remote_ref = $api->newRemoteRefQuery() 159 ->withNames(array($remote)) 160 ->executeOne(); 161 if (!$remote_ref) { 162 throw new PhutilArgumentUsageException( 163 pht( 164 'No remote "%s" exists in this repository.', 165 $remote)); 166 } 167 168 // TODO: Allow selection of a bare URI. 169 170 return $remote; 171 } 172 173 private function newOntoRemote(array $symbols) { 174 assert_instances_of($symbols, 'ArcanistLandSymbol'); 175 $api = $this->getRepositoryAPI(); 176 $log = $this->getLogEngine(); 177 178 $remote = $this->getOntoRemoteArgument(); 179 if ($remote !== null) { 180 181 $log->writeStatus( 182 pht('ONTO REMOTE'), 183 pht( 184 'Remote "%s" was selected with the "--onto-remote" flag.', 185 $remote)); 186 187 return $remote; 188 } 189 190 $remote = $this->getOntoRemoteFromConfiguration(); 191 if ($remote !== null) { 192 $remote_key = $this->getOntoRemoteConfigurationKey(); 193 194 $log->writeStatus( 195 pht('ONTO REMOTE'), 196 pht( 197 'Remote "%s" was selected by reading "%s" configuration.', 198 $remote, 199 $remote_key)); 200 201 return $remote; 202 } 203 204 $api = $this->getRepositoryAPI(); 205 206 $default_remote = 'default'; 207 208 $log->writeStatus( 209 pht('ONTO REMOTE'), 210 pht( 211 'Landing onto remote "%s", the default remote under Mercurial.', 212 $default_remote)); 213 214 return $default_remote; 215 } 216 217 protected function selectOntoRefs(array $symbols) { 218 assert_instances_of($symbols, 'ArcanistLandSymbol'); 219 $log = $this->getLogEngine(); 220 221 $onto = $this->getOntoArguments(); 222 if ($onto) { 223 224 $log->writeStatus( 225 pht('ONTO TARGET'), 226 pht( 227 'Refs were selected with the "--onto" flag: %s.', 228 implode(', ', $onto))); 229 230 return $onto; 231 } 232 233 $onto = $this->getOntoFromConfiguration(); 234 if ($onto) { 235 $onto_key = $this->getOntoConfigurationKey(); 236 237 $log->writeStatus( 238 pht('ONTO TARGET'), 239 pht( 240 'Refs were selected by reading "%s" configuration: %s.', 241 $onto_key, 242 implode(', ', $onto))); 243 244 return $onto; 245 } 246 247 $api = $this->getRepositoryAPI(); 248 249 $default_onto = 'default'; 250 251 $log->writeStatus( 252 pht('ONTO TARGET'), 253 pht( 254 'Landing onto target "%s", the default target under Mercurial.', 255 $default_onto)); 256 257 return array($default_onto); 258 } 259 260 protected function confirmOntoRefs(array $onto_refs) { 261 $api = $this->getRepositoryAPI(); 262 263 foreach ($onto_refs as $onto_ref) { 264 if (!strlen($onto_ref)) { 265 throw new PhutilArgumentUsageException( 266 pht( 267 'Selected "onto" ref "%s" is invalid: the empty string is not '. 268 'a valid ref.', 269 $onto_ref)); 270 } 271 } 272 273 $remote_ref = $this->getOntoRemoteRef(); 274 275 $markers = $api->newMarkerRefQuery() 276 ->withRemotes(array($remote_ref)) 277 ->execute(); 278 279 $onto_markers = array(); 280 $new_markers = array(); 281 foreach ($onto_refs as $onto_ref) { 282 $matches = array(); 283 foreach ($markers as $marker) { 284 if ($marker->getName() === $onto_ref) { 285 $matches[] = $marker; 286 } 287 } 288 289 $match_count = count($matches); 290 if ($match_count > 1) { 291 throw new PhutilArgumentUsageException( 292 pht( 293 'TODO: Ambiguous ref.')); 294 } else if (!$match_count) { 295 $new_bookmark = id(new ArcanistMarkerRef()) 296 ->setMarkerType(ArcanistMarkerRef::TYPE_BOOKMARK) 297 ->setName($onto_ref) 298 ->attachRemoteRef($remote_ref); 299 300 $onto_markers[] = $new_bookmark; 301 $new_markers[] = $new_bookmark; 302 } else { 303 $onto_markers[] = head($matches); 304 } 305 } 306 307 $branches = array(); 308 foreach ($onto_markers as $onto_marker) { 309 if ($onto_marker->isBranch()) { 310 $branches[] = $onto_marker; 311 } 312 313 $branch_count = count($branches); 314 if ($branch_count > 1) { 315 echo tsprintf( 316 "\n%!\n%W\n\n%W\n\n%W\n\n", 317 pht('MULTIPLE "ONTO" BRANCHES'), 318 pht( 319 'You have selected multiple branches to push changes onto. '. 320 'Pushing to multiple branches is not supported by "arc land" '. 321 'in Mercurial: Mercurial commits may only belong to one '. 322 'branch, so this operation can not be executed atomically.'), 323 pht( 324 'You may land one branches and any number of bookmarks in a '. 325 'single operation.'), 326 pht('These branches were selected:')); 327 328 foreach ($branches as $branch) { 329 echo tsprintf('%s', $branch->newRefView()); 330 } 331 332 echo tsprintf("\n"); 333 334 throw new PhutilArgumentUsageException( 335 pht( 336 'Landing onto multiple branches at once is not supported in '. 337 'Mercurial.')); 338 } else if ($branch_count) { 339 $this->ontoBranchMarker = head($branches); 340 } 341 } 342 343 if ($new_markers) { 344 echo tsprintf( 345 "\n%!\n%W\n\n", 346 pht('CREATE %s BOOKMARK(S)', phutil_count($new_markers)), 347 pht( 348 'These %s symbol(s) do not exist in the remote. They will be '. 349 'created as new bookmarks:', 350 phutil_count($new_markers))); 351 352 353 foreach ($new_markers as $new_marker) { 354 echo tsprintf('%s', $new_marker->newRefView()); 355 } 356 357 echo tsprintf("\n"); 358 359 $is_hold = $this->getShouldHold(); 360 if ($is_hold) { 361 echo tsprintf( 362 "%?\n", 363 pht( 364 'You are using "--hold", so execution will stop before the '. 365 '%s bookmark(s) are actually created. You will be given '. 366 'instructions to create the bookmarks.', 367 phutil_count($new_markers))); 368 } 369 370 $query = pht( 371 'Create %s new remote bookmark(s)?', 372 phutil_count($new_markers)); 373 374 $this->getWorkflow() 375 ->getPrompt('arc.land.create') 376 ->setQuery($query) 377 ->execute(); 378 } 379 380 $this->ontoMarkers = $onto_markers; 381 } 382 383 protected function selectIntoRemote() { 384 $api = $this->getRepositoryAPI(); 385 $log = $this->getLogEngine(); 386 387 if ($this->getIntoEmptyArgument()) { 388 $this->setIntoEmpty(true); 389 390 $log->writeStatus( 391 pht('INTO REMOTE'), 392 pht( 393 'Will merge into empty state, selected with the "--into-empty" '. 394 'flag.')); 395 396 return; 397 } 398 399 if ($this->getIntoLocalArgument()) { 400 $this->setIntoLocal(true); 401 402 $log->writeStatus( 403 pht('INTO REMOTE'), 404 pht( 405 'Will merge into local state, selected with the "--into-local" '. 406 'flag.')); 407 408 return; 409 } 410 411 $into = $this->getIntoRemoteArgument(); 412 if ($into !== null) { 413 414 $remote_ref = $api->newRemoteRefQuery() 415 ->withNames(array($into)) 416 ->executeOne(); 417 if (!$remote_ref) { 418 throw new PhutilArgumentUsageException( 419 pht( 420 'No remote "%s" exists in this repository.', 421 $into)); 422 } 423 424 // TODO: Allow a raw URI. 425 426 $this->setIntoRemote($into); 427 428 $log->writeStatus( 429 pht('INTO REMOTE'), 430 pht( 431 'Will merge into remote "%s", selected with the "--into" flag.', 432 $into)); 433 434 return; 435 } 436 437 $onto = $this->getOntoRemote(); 438 $this->setIntoRemote($onto); 439 440 $log->writeStatus( 441 pht('INTO REMOTE'), 442 pht( 443 'Will merge into remote "%s" by default, because this is the remote '. 444 'the change is landing onto.', 445 $onto)); 446 } 447 448 protected function selectIntoRef() { 449 $log = $this->getLogEngine(); 450 451 if ($this->getIntoEmptyArgument()) { 452 $log->writeStatus( 453 pht('INTO TARGET'), 454 pht( 455 'Will merge into empty state, selected with the "--into-empty" '. 456 'flag.')); 457 458 return; 459 } 460 461 $into = $this->getIntoArgument(); 462 if ($into !== null) { 463 $this->setIntoRef($into); 464 465 $log->writeStatus( 466 pht('INTO TARGET'), 467 pht( 468 'Will merge into target "%s", selected with the "--into" flag.', 469 $into)); 470 471 return; 472 } 473 474 $ontos = $this->getOntoRefs(); 475 $onto = head($ontos); 476 477 $this->setIntoRef($onto); 478 if (count($ontos) > 1) { 479 $log->writeStatus( 480 pht('INTO TARGET'), 481 pht( 482 'Will merge into target "%s" by default, because this is the first '. 483 '"onto" target.', 484 $onto)); 485 } else { 486 $log->writeStatus( 487 pht('INTO TARGET'), 488 pht( 489 'Will merge into target "%s" by default, because this is the "onto" '. 490 'target.', 491 $onto)); 492 } 493 } 494 495 protected function selectIntoCommit() { 496 $api = $this->getRepositoryAPI(); 497 $log = $this->getLogEngine(); 498 499 if ($this->getIntoEmpty()) { 500 // If we're running under "--into-empty", we don't have to do anything. 501 502 $log->writeStatus( 503 pht('INTO COMMIT'), 504 pht('Preparing merge into the empty state.')); 505 506 return null; 507 } 508 509 if ($this->getIntoLocal()) { 510 // If we're running under "--into-local", just make sure that the 511 // target identifies some actual commit. 512 $local_ref = $this->getIntoRef(); 513 514 // TODO: This error handling could probably be cleaner, it will just 515 // raise an exception without any context. 516 517 $into_commit = $api->getCanonicalRevisionName($local_ref); 518 519 $log->writeStatus( 520 pht('INTO COMMIT'), 521 pht( 522 'Preparing merge into local target "%s", at commit "%s".', 523 $local_ref, 524 $api->getDisplayHash($into_commit))); 525 526 return $into_commit; 527 } 528 529 $target = id(new ArcanistLandTarget()) 530 ->setRemote($this->getIntoRemote()) 531 ->setRef($this->getIntoRef()); 532 533 $commit = $this->fetchTarget($target); 534 if ($commit !== null) { 535 $log->writeStatus( 536 pht('INTO COMMIT'), 537 pht( 538 'Preparing merge into "%s" from remote "%s", at commit "%s".', 539 $target->getRef(), 540 $target->getRemote(), 541 $api->getDisplayHash($commit))); 542 return $commit; 543 } 544 545 // If we have no valid target and the user passed "--into" explicitly, 546 // treat this as an error. For example, "arc land --into Q --onto Q", 547 // where "Q" does not exist, is an error. 548 if ($this->getIntoArgument()) { 549 throw new PhutilArgumentUsageException( 550 pht( 551 'Ref "%s" does not exist in remote "%s".', 552 $target->getRef(), 553 $target->getRemote())); 554 } 555 556 // Otherwise, treat this as implying "--into-empty". For example, 557 // "arc land --onto Q", where "Q" does not exist, is equivalent to 558 // "arc land --into-empty --onto Q". 559 $this->setIntoEmpty(true); 560 561 $log->writeStatus( 562 pht('INTO COMMIT'), 563 pht( 564 'Preparing merge into the empty state to create target "%s" '. 565 'in remote "%s".', 566 $target->getRef(), 567 $target->getRemote())); 568 569 return null; 570 } 571 572 private function fetchTarget(ArcanistLandTarget $target) { 573 $api = $this->getRepositoryAPI(); 574 $log = $this->getLogEngine(); 575 576 $target_name = $target->getRef(); 577 578 $remote_ref = id(new ArcanistRemoteRef()) 579 ->setRemoteName($target->getRemote()); 580 581 $markers = $api->newMarkerRefQuery() 582 ->withRemotes(array($remote_ref)) 583 ->withNames(array($target_name)) 584 ->execute(); 585 586 $bookmarks = array(); 587 $branches = array(); 588 foreach ($markers as $marker) { 589 if ($marker->isBookmark()) { 590 $bookmarks[] = $marker; 591 } else { 592 $branches[] = $marker; 593 } 594 } 595 596 if (!$bookmarks && !$branches) { 597 throw new PhutilArgumentUsageException( 598 pht( 599 'Remote "%s" has no bookmark or branch named "%s".', 600 $target->getRemote(), 601 $target->getRef())); 602 } 603 604 if ($bookmarks && $branches) { 605 echo tsprintf( 606 "\n%!\n%W\n\n", 607 pht('AMBIGUOUS MARKER'), 608 pht( 609 'In remote "%s", the name "%s" identifies one or more branch '. 610 'heads and one or more bookmarks. Close, rename, or delete all '. 611 'but one of these markers, or pull the state you want to merge '. 612 'into and use "--into-local --into <hash>" to disambiguate the '. 613 'desired merge target.', 614 $target->getRemote(), 615 $target->getRef())); 616 617 throw new PhutilArgumentUsageException( 618 pht('Merge target is ambiguous.')); 619 } 620 621 if ($bookmarks) { 622 if (count($bookmarks) > 1) { 623 throw new Exception( 624 pht( 625 'Remote "%s" has multiple bookmarks with name "%s". This '. 626 'is unexpected.', 627 $target->getRemote(), 628 $target->getRef())); 629 } 630 $bookmark = head($bookmarks); 631 632 $target_marker = $bookmark; 633 } 634 635 if ($branches) { 636 if (count($branches) > 1) { 637 echo tsprintf( 638 "\n%!\n%W\n\n", 639 pht('MULTIPLE BRANCH HEADS'), 640 pht( 641 'Remote "%s" has multiple branch heads named "%s". Close all '. 642 'but one, or pull the head you want and use "--into-local '. 643 '--into <hash>" to specify an explicit merge target.', 644 $target->getRemote(), 645 $target->getRef())); 646 647 throw new PhutilArgumentUsageException( 648 pht( 649 'Remote branch has multiple heads.')); 650 } 651 652 $branch = head($branches); 653 654 $target_marker = $branch; 655 } 656 657 if ($target_marker->isBranch()) { 658 $err = $this->newPassthru( 659 'pull --branch %s -- %s', 660 $target->getRef(), 661 $target->getRemote()); 662 } else { 663 664 // NOTE: This may have side effects: 665 // 666 // - It can create a "bookmark@remote" bookmark if there is a local 667 // bookmark with the same name that is not an ancestor. 668 // - It can create an arbitrary number of other bookmarks. 669 // 670 // Since these seem to generally be intentional behaviors in Mercurial, 671 // and should theoretically be familiar to Mercurial users, just accept 672 // them as the cost of doing business. 673 674 $err = $this->newPassthru( 675 'pull --bookmark %s -- %s', 676 $target->getRef(), 677 $target->getRemote()); 678 } 679 680 // NOTE: It's possible that between the time we ran "ls-markers" and the 681 // time we ran "pull" that the remote changed. 682 683 // It may even have been rewound or rewritten, in which case we did not 684 // actually fetch the ref we are about to return as a target. For now, 685 // assume this didn't happen: it's so unlikely that it's probably not 686 // worth spending 100ms to check. 687 688 // TODO: If the Mercurial command server is revived, this check becomes 689 // more reasonable if it's cheap. 690 691 return $target_marker->getCommitHash(); 692 } 693 694 protected function selectCommits($into_commit, array $symbols) { 695 assert_instances_of($symbols, 'ArcanistLandSymbol'); 696 $api = $this->getRepositoryAPI(); 697 698 $commit_map = array(); 699 foreach ($symbols as $symbol) { 700 $symbol_commit = $symbol->getCommit(); 701 $template = '{node}-{parents}-'; 702 703 if ($into_commit === null) { 704 list($commits) = $api->execxLocal( 705 'log --rev %s --template %s --', 706 hgsprintf('reverse(ancestors(%s))', $into_commit), 707 $template); 708 } else { 709 list($commits) = $api->execxLocal( 710 'log --rev %s --template %s --', 711 hgsprintf( 712 'reverse(ancestors(%s) - ancestors(%s))', 713 $symbol_commit, 714 $into_commit), 715 $template); 716 } 717 718 $commits = phutil_split_lines($commits, false); 719 $is_first = true; 720 foreach ($commits as $line) { 721 if (!strlen($line)) { 722 continue; 723 } 724 725 $parts = explode('-', $line, 3); 726 if (count($parts) < 3) { 727 throw new Exception( 728 pht( 729 'Unexpected output from "hg log ...": %s', 730 $line)); 731 } 732 733 $hash = $parts[0]; 734 if (!isset($commit_map[$hash])) { 735 $parents = $parts[1]; 736 $parents = trim($parents); 737 if (strlen($parents)) { 738 $parents = explode(' ', $parents); 739 } else { 740 $parents = array(); 741 } 742 743 $summary = $parts[2]; 744 745 $commit_map[$hash] = id(new ArcanistLandCommit()) 746 ->setHash($hash) 747 ->setParents($parents) 748 ->setSummary($summary); 749 } 750 751 $commit = $commit_map[$hash]; 752 if ($is_first) { 753 $commit->addDirectSymbol($symbol); 754 $is_first = false; 755 } 756 757 $commit->addIndirectSymbol($symbol); 758 } 759 } 760 761 return $this->confirmCommits($into_commit, $symbols, $commit_map); 762 } 763 764 protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { 765 $api = $this->getRepositoryAPI(); 766 767 if ($this->getStrategy() !== 'squash') { 768 throw new Exception(pht('TODO: Support merge strategies')); 769 } 770 771 // See PHI1808. When we "hg rebase ..." below, Mercurial will move 772 // bookmarks which point at the old commit range to point at the rebased 773 // commit. This is somewhat surprising and we don't want this to happen: 774 // save the old bookmark state so we can put the bookmarks back before 775 // we continue. 776 777 $bookmark_refs = $api->newMarkerRefQuery() 778 ->withMarkerTypes( 779 array( 780 ArcanistMarkerRef::TYPE_BOOKMARK, 781 )) 782 ->execute(); 783 784 // TODO: Add a Mercurial version check requiring 2.1.1 or newer. 785 786 $api->execxLocal( 787 'update --rev %s', 788 hgsprintf('%s', $into_commit)); 789 790 $commits = $set->getCommits(); 791 792 $min_commit = last($commits)->getHash(); 793 $max_commit = head($commits)->getHash(); 794 795 $revision_ref = $set->getRevisionRef(); 796 $commit_message = $revision_ref->getCommitMessage(); 797 798 // If we're landing "--onto" a branch, set that as the branch marker 799 // before creating the new commit. 800 801 // TODO: We could skip this if we know that the "$into_commit" already 802 // has the right branch, which it will if we created it. 803 804 $branch_marker = $this->ontoBranchMarker; 805 if ($branch_marker) { 806 $api->execxLocal('branch -- %s', $branch_marker->getName()); 807 } 808 809 try { 810 $argv = array(); 811 $argv[] = '--dest'; 812 $argv[] = hgsprintf('%s', $into_commit); 813 814 $argv[] = '--rev'; 815 $argv[] = hgsprintf('%s..%s', $min_commit, $max_commit); 816 817 $argv[] = '--logfile'; 818 $argv[] = '-'; 819 820 $argv[] = '--keep'; 821 $argv[] = '--collapse'; 822 823 $future = $api->execFutureLocal('rebase %Ls', $argv); 824 $future->write($commit_message); 825 $future->resolvex(); 826 827 } catch (CommandException $ex) { 828 // TODO 829 // $api->execManualLocal('rebase --abort'); 830 throw $ex; 831 } 832 833 // Find all the bookmarks which pointed at commits we just rebased, and 834 // put them back the way they were before rebasing moved them. We aren't 835 // deleting the old commits yet and don't want to move the bookmarks. 836 837 $obsolete_map = array(); 838 foreach ($set->getCommits() as $commit) { 839 $obsolete_map[$commit->getHash()] = true; 840 } 841 842 foreach ($bookmark_refs as $bookmark_ref) { 843 $bookmark_hash = $bookmark_ref->getCommitHash(); 844 845 if (!isset($obsolete_map[$bookmark_hash])) { 846 continue; 847 } 848 849 $api->execxLocal( 850 'bookmark --force --rev %s -- %s', 851 $bookmark_hash, 852 $bookmark_ref->getName()); 853 } 854 855 list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}'); 856 $new_cursor = trim($stdout); 857 858 return $new_cursor; 859 } 860 861 protected function pushChange($into_commit) { 862 $api = $this->getRepositoryAPI(); 863 864 list($head, $body, $tail) = $this->newPushCommands($into_commit); 865 866 foreach ($head as $command) { 867 $api->execxLocal('%Ls', $command); 868 } 869 870 try { 871 foreach ($body as $command) { 872 $err = $this->newPassthru('%Ls', $command); 873 if ($err) { 874 throw new ArcanistLandPushFailureException( 875 pht( 876 'Push failed! Fix the error and run "arc land" again.')); 877 } 878 } 879 } finally { 880 foreach ($tail as $command) { 881 $api->execxLocal('%Ls', $command); 882 } 883 } 884 } 885 886 private function newPushCommands($into_commit) { 887 $api = $this->getRepositoryAPI(); 888 889 $head_commands = array(); 890 $body_commands = array(); 891 $tail_commands = array(); 892 893 $bookmarks = array(); 894 foreach ($this->ontoMarkers as $onto_marker) { 895 if (!$onto_marker->isBookmark()) { 896 continue; 897 } 898 $bookmarks[] = $onto_marker; 899 } 900 901 // If we're pushing to bookmarks, move all the bookmarks we want to push 902 // to the merge commit. (There doesn't seem to be any way to specify 903 // "push commit X as bookmark Y" in Mercurial.) 904 905 $restore = array(); 906 if ($bookmarks) { 907 $markers = $api->newMarkerRefQuery() 908 ->withNames(mpull($bookmarks, 'getName')) 909 ->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) 910 ->execute(); 911 $markers = mpull($markers, 'getCommitHash', 'getName'); 912 913 foreach ($bookmarks as $bookmark) { 914 $bookmark_name = $bookmark->getName(); 915 916 $old_position = idx($markers, $bookmark_name); 917 $new_position = $into_commit; 918 919 if ($old_position === $new_position) { 920 continue; 921 } 922 923 $head_commands[] = array( 924 'bookmark', 925 '--force', 926 '--rev', 927 hgsprintf('%s', $api->getDisplayHash($new_position)), 928 '--', 929 $bookmark_name, 930 ); 931 932 $api->execxLocal( 933 'bookmark --force --rev %s -- %s', 934 hgsprintf('%s', $new_position), 935 $bookmark_name); 936 937 $restore[$bookmark_name] = $old_position; 938 } 939 } 940 941 // Now, prepare the actual push. 942 943 $argv = array(); 944 $argv[] = 'push'; 945 946 if ($bookmarks) { 947 // If we're pushing at least one bookmark, we can just specify the list 948 // of bookmarks as things we want to push. 949 foreach ($bookmarks as $bookmark) { 950 $argv[] = '--bookmark'; 951 $argv[] = $bookmark->getName(); 952 } 953 } else { 954 // Otherwise, specify the commit itself. 955 $argv[] = '--rev'; 956 $argv[] = hgsprintf('%s', $into_commit); 957 } 958 959 $argv[] = '--'; 960 $argv[] = $this->getOntoRemote(); 961 962 $body_commands[] = $argv; 963 964 // Finally, restore the bookmarks. 965 966 foreach ($restore as $bookmark_name => $old_position) { 967 $tail = array(); 968 $tail[] = 'bookmark'; 969 970 if ($old_position === null) { 971 $tail[] = '--delete'; 972 } else { 973 $tail[] = '--force'; 974 $tail[] = '--rev'; 975 $tail[] = hgsprintf('%s', $api->getDisplayHash($old_position)); 976 } 977 978 $tail[] = '--'; 979 $tail[] = $bookmark_name; 980 981 $tail_commands[] = $tail; 982 } 983 984 return array( 985 $head_commands, 986 $body_commands, 987 $tail_commands, 988 ); 989 } 990 991 protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { 992 $api = $this->getRepositoryAPI(); 993 $log = $this->getLogEngine(); 994 995 // This has no effect when we're executing a merge strategy. 996 if (!$this->isSquashStrategy()) { 997 return; 998 } 999 1000 $old_commit = last($set->getCommits())->getHash(); 1001 $new_commit = $into_commit; 1002 1003 list($output) = $api->execxLocal( 1004 'log --rev %s --template %s', 1005 hgsprintf('children(%s)', $old_commit), 1006 '{node}\n'); 1007 $child_hashes = phutil_split_lines($output, false); 1008 1009 foreach ($child_hashes as $child_hash) { 1010 if (!strlen($child_hash)) { 1011 continue; 1012 } 1013 1014 // TODO: If the only heads which are descendants of this child will 1015 // be deleted, we can skip this rebase? 1016 1017 try { 1018 $api->execxLocal( 1019 'rebase --source %s --dest %s --keep --keepbranches', 1020 $child_hash, 1021 $new_commit); 1022 } catch (CommandException $ex) { 1023 // TODO: Recover state. 1024 throw $ex; 1025 } 1026 } 1027 } 1028 1029 1030 protected function pruneBranches(array $sets) { 1031 assert_instances_of($sets, 'ArcanistLandCommitSet'); 1032 $api = $this->getRepositoryAPI(); 1033 $log = $this->getLogEngine(); 1034 1035 // This has no effect when we're executing a merge strategy. 1036 if (!$this->isSquashStrategy()) { 1037 return; 1038 } 1039 1040 $revs = array(); 1041 $obsolete_map = array(); 1042 1043 // We've rebased all descendants already, so we can safely delete all 1044 // of these commits. 1045 1046 $sets = array_reverse($sets); 1047 foreach ($sets as $set) { 1048 $commits = $set->getCommits(); 1049 1050 $min_commit = head($commits)->getHash(); 1051 $max_commit = last($commits)->getHash(); 1052 1053 $revs[] = hgsprintf('%s::%s', $min_commit, $max_commit); 1054 1055 foreach ($commits as $commit) { 1056 $obsolete_map[$commit->getHash()] = true; 1057 } 1058 } 1059 1060 $rev_set = '('.implode(') or (', $revs).')'; 1061 1062 // See PHI45. If we have "hg evolve", get rid of old commits using 1063 // "hg prune" instead of "hg strip". 1064 1065 // If we "hg strip" a commit which has an obsolete predecessor, it 1066 // removes the obsolescence marker and revives the predecessor. This is 1067 // not desirable: we want to destroy all predecessors of these commits. 1068 1069 // See PHI1808. Both "hg strip" and "hg prune" move bookmarks backwards in 1070 // history rather than destroying them. Instead, we want to destroy any 1071 // bookmarks which point at these now-obsoleted commits. 1072 1073 $bookmark_refs = $api->newMarkerRefQuery() 1074 ->withMarkerTypes( 1075 array( 1076 ArcanistMarkerRef::TYPE_BOOKMARK, 1077 )) 1078 ->execute(); 1079 foreach ($bookmark_refs as $bookmark_ref) { 1080 $bookmark_hash = $bookmark_ref->getCommitHash(); 1081 $bookmark_name = $bookmark_ref->getName(); 1082 1083 if (!isset($obsolete_map[$bookmark_hash])) { 1084 continue; 1085 } 1086 1087 $log->writeStatus( 1088 pht('CLEANUP'), 1089 pht('Deleting bookmark "%s".', $bookmark_name)); 1090 1091 $api->execxLocal( 1092 'bookmark --delete -- %s', 1093 $bookmark_name); 1094 } 1095 1096 if ($api->getMercurialFeature('evolve')) { 1097 $api->execxLocal( 1098 'prune --rev %s', 1099 $rev_set); 1100 } else { 1101 $api->execxLocal( 1102 '--config extensions.strip= strip --rev %s', 1103 $rev_set); 1104 } 1105 } 1106 1107 protected function reconcileLocalState( 1108 $into_commit, 1109 ArcanistRepositoryLocalState $state) { 1110 1111 // TODO: For now, just leave users wherever they ended up. 1112 1113 $state->discardLocalState(); 1114 } 1115 1116 protected function didHoldChanges($into_commit) { 1117 $log = $this->getLogEngine(); 1118 $local_state = $this->getLocalState(); 1119 1120 $message = pht( 1121 'Holding changes locally, they have not been pushed.'); 1122 1123 list($head, $body, $tail) = $this->newPushCommands($into_commit); 1124 $commands = array_merge($head, $body, $tail); 1125 1126 echo tsprintf( 1127 "\n%!\n%s\n\n", 1128 pht('HOLD CHANGES'), 1129 $message); 1130 1131 echo tsprintf( 1132 "%s\n\n", 1133 pht('To push changes manually, run these %s command(s):', 1134 phutil_count($commands))); 1135 1136 foreach ($commands as $command) { 1137 echo tsprintf('%>', csprintf('hg %Ls', $command)); 1138 } 1139 1140 echo tsprintf("\n"); 1141 1142 $restore_commands = $local_state->getRestoreCommandsForDisplay(); 1143 if ($restore_commands) { 1144 echo tsprintf( 1145 "%s\n\n", 1146 pht( 1147 'To go back to how things were before you ran "arc land", run '. 1148 'these %s command(s):', 1149 phutil_count($restore_commands))); 1150 1151 foreach ($restore_commands as $restore_command) { 1152 echo tsprintf('%>', $restore_command); 1153 } 1154 1155 echo tsprintf("\n"); 1156 } 1157 1158 echo tsprintf( 1159 "%s\n", 1160 pht( 1161 'Local branches and bookmarks have not been changed, and are still '. 1162 'in the same state as before.')); 1163 } 1164 1165} 1166