1<?php 2 3final class ArcanistGitLandEngine 4 extends ArcanistLandEngine { 5 6 private $isGitPerforce; 7 private $landTargetCommitMap = array(); 8 private $deletedBranches = array(); 9 10 private function setIsGitPerforce($is_git_perforce) { 11 $this->isGitPerforce = $is_git_perforce; 12 return $this; 13 } 14 15 private function getIsGitPerforce() { 16 return $this->isGitPerforce; 17 } 18 19 protected function pruneBranches(array $sets) { 20 $api = $this->getRepositoryAPI(); 21 $log = $this->getLogEngine(); 22 23 $old_commits = array(); 24 foreach ($sets as $set) { 25 $hash = last($set->getCommits())->getHash(); 26 $old_commits[] = $hash; 27 } 28 29 $branch_map = $this->getBranchesForCommits( 30 $old_commits, 31 $is_contains = false); 32 33 foreach ($branch_map as $branch_name => $branch_hash) { 34 $recovery_command = csprintf( 35 'git checkout -b %s %s', 36 $branch_name, 37 $api->getDisplayHash($branch_hash)); 38 39 $log->writeStatus( 40 pht('CLEANUP'), 41 pht('Cleaning up branch "%s". To recover, run:', $branch_name)); 42 43 echo tsprintf( 44 "\n **$** %s\n\n", 45 $recovery_command); 46 47 $api->execxLocal('branch -D -- %s', $branch_name); 48 $this->deletedBranches[$branch_name] = true; 49 } 50 } 51 52 private function getBranchesForCommits(array $hashes, $is_contains) { 53 $api = $this->getRepositoryAPI(); 54 55 $format = '%(refname) %(objectname)'; 56 57 $result = array(); 58 foreach ($hashes as $hash) { 59 if ($is_contains) { 60 $command = csprintf( 61 'for-each-ref --contains %s --format %s --', 62 $hash, 63 $format); 64 } else { 65 $command = csprintf( 66 'for-each-ref --points-at %s --format %s --', 67 $hash, 68 $format); 69 } 70 71 list($foreach_lines) = $api->execxLocal('%C', $command); 72 $foreach_lines = phutil_split_lines($foreach_lines, false); 73 74 foreach ($foreach_lines as $line) { 75 if (!strlen($line)) { 76 continue; 77 } 78 79 $expect_parts = 2; 80 $parts = explode(' ', $line, $expect_parts); 81 if (count($parts) !== $expect_parts) { 82 throw new Exception( 83 pht( 84 'Failed to explode line "%s".', 85 $line)); 86 } 87 88 $ref_name = $parts[0]; 89 $ref_hash = $parts[1]; 90 91 $matches = null; 92 $ok = preg_match('(^refs/heads/(.*)\z)', $ref_name, $matches); 93 if ($ok === false) { 94 throw new Exception( 95 pht( 96 'Failed to match against branch pattern "%s".', 97 $line)); 98 } 99 100 if (!$ok) { 101 continue; 102 } 103 104 $result[$matches[1]] = $ref_hash; 105 } 106 } 107 108 // Sort the result so that branches are processed in natural order. 109 $names = array_keys($result); 110 natcasesort($names); 111 $result = array_select_keys($result, $names); 112 113 return $result; 114 } 115 116 protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { 117 $api = $this->getRepositoryAPI(); 118 $log = $this->getLogEngine(); 119 120 // This has no effect when we're executing a merge strategy. 121 if (!$this->isSquashStrategy()) { 122 return; 123 } 124 125 $min_commit = head($set->getCommits())->getHash(); 126 $old_commit = last($set->getCommits())->getHash(); 127 $new_commit = $into_commit; 128 129 $branch_map = $this->getBranchesForCommits( 130 array($old_commit), 131 $is_contains = true); 132 133 $log = $this->getLogEngine(); 134 foreach ($branch_map as $branch_name => $branch_head) { 135 // If this branch just points at the old state, don't bother rebasing 136 // it. We'll update or delete it later. 137 if ($branch_head === $old_commit) { 138 continue; 139 } 140 141 $log->writeStatus( 142 pht('CASCADE'), 143 pht( 144 'Rebasing "%s" onto landed state...', 145 $branch_name)); 146 147 // If we used "--pick" to select this commit, we want to rebase branches 148 // that descend from it onto its ancestor, not onto the landed change. 149 150 // For example, if the change sequence was "W", "X", "Y", "Z" and we 151 // landed "Y" onto "master" using "--pick", we want to rebase "Z" onto 152 // "X" (so "W" and "X", which it will often depend on, are still 153 // its ancestors), not onto the new "master". 154 155 if ($set->getIsPick()) { 156 $rebase_target = $min_commit.'^'; 157 } else { 158 $rebase_target = $new_commit; 159 } 160 161 try { 162 $api->execxLocal( 163 'rebase --onto %s -- %s %s', 164 $rebase_target, 165 $old_commit, 166 $branch_name); 167 } catch (CommandException $ex) { 168 $api->execManualLocal('rebase --abort'); 169 $api->execManualLocal('reset --hard HEAD --'); 170 171 $log->writeWarning( 172 pht('REBASE CONFLICT'), 173 pht( 174 'Branch "%s" does not rebase cleanly from "%s" onto '. 175 '"%s", skipping.', 176 $branch_name, 177 $api->getDisplayHash($old_commit), 178 $api->getDisplayHash($rebase_target))); 179 } 180 } 181 } 182 183 private function fetchTarget(ArcanistLandTarget $target) { 184 $api = $this->getRepositoryAPI(); 185 $log = $this->getLogEngine(); 186 187 // NOTE: Although this output isn't hugely useful, we need to passthru 188 // instead of using a subprocess here because `git fetch` may prompt the 189 // user to enter a password if they're fetching over HTTP with basic 190 // authentication. See T10314. 191 192 if ($this->getIsGitPerforce()) { 193 $log->writeStatus( 194 pht('P4 SYNC'), 195 pht( 196 'Synchronizing "%s" from Perforce...', 197 $target->getRef())); 198 199 $err = $this->newPassthru( 200 'p4 sync --silent --branch %s --', 201 $target->getRemote().'/'.$target->getRef()); 202 if ($err) { 203 throw new ArcanistUsageException( 204 pht( 205 'Perforce sync failed! Fix the error and run "arc land" again.')); 206 } 207 208 return $this->getLandTargetLocalCommit($target); 209 } 210 211 $exists = $this->getLandTargetLocalExists($target); 212 if (!$exists) { 213 $log->writeWarning( 214 pht('TARGET'), 215 pht( 216 'No local copy of ref "%s" in remote "%s" exists, attempting '. 217 'fetch...', 218 $target->getRef(), 219 $target->getRemote())); 220 221 $this->fetchLandTarget($target, $ignore_failure = true); 222 223 $exists = $this->getLandTargetLocalExists($target); 224 if (!$exists) { 225 return null; 226 } 227 228 $log->writeStatus( 229 pht('FETCHED'), 230 pht( 231 'Fetched ref "%s" from remote "%s".', 232 $target->getRef(), 233 $target->getRemote())); 234 235 return $this->getLandTargetLocalCommit($target); 236 } 237 238 $log->writeStatus( 239 pht('FETCH'), 240 pht( 241 'Fetching "%s" from remote "%s"...', 242 $target->getRef(), 243 $target->getRemote())); 244 245 $this->fetchLandTarget($target, $ignore_failure = false); 246 247 return $this->getLandTargetLocalCommit($target); 248 } 249 250 protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { 251 $api = $this->getRepositoryAPI(); 252 $log = $this->getLogEngine(); 253 254 $this->confirmLegacyStrategyConfiguration(); 255 256 $is_empty = ($into_commit === null); 257 258 if ($is_empty) { 259 $empty_commit = ArcanistGitRawCommit::newEmptyCommit(); 260 $into_commit = $api->writeRawCommit($empty_commit); 261 } 262 263 $commits = $set->getCommits(); 264 265 $min_commit = head($commits); 266 $min_hash = $min_commit->getHash(); 267 268 $max_commit = last($commits); 269 $max_hash = $max_commit->getHash(); 270 271 // NOTE: See T11435 for some history. See PHI1727 for a case where a user 272 // modified their working copy while running "arc land". This attempts to 273 // resist incorrectly detecting simultaneous working copy modifications 274 // as changes. 275 276 list($changes) = $api->execxLocal( 277 'diff --no-ext-diff %s --', 278 gitsprintf( 279 '%s..%s', 280 $into_commit, 281 $max_hash)); 282 $changes = trim($changes); 283 if (!strlen($changes)) { 284 285 // TODO: We could make a more significant effort to identify the 286 // human-readable symbol which led us to try to land this ref. 287 288 throw new PhutilArgumentUsageException( 289 pht( 290 'Merging local "%s" into "%s" produces an empty diff. '. 291 'This usually means these changes have already landed.', 292 $api->getDisplayHash($max_hash), 293 $api->getDisplayHash($into_commit))); 294 } 295 296 $log->writeStatus( 297 pht('MERGING'), 298 pht( 299 '%s %s', 300 $api->getDisplayHash($max_hash), 301 $max_commit->getDisplaySummary())); 302 303 $argv = array(); 304 $argv[] = '--no-stat'; 305 $argv[] = '--no-commit'; 306 307 // When we're merging into the empty state, Git refuses to perform the 308 // merge until we tell it explicitly that we're doing something unusual. 309 if ($is_empty) { 310 $argv[] = '--allow-unrelated-histories'; 311 } 312 313 if ($this->isSquashStrategy()) { 314 // NOTE: We're explicitly specifying "--ff" to override the presence 315 // of "merge.ff" options in user configuration. 316 $argv[] = '--ff'; 317 $argv[] = '--squash'; 318 } else { 319 $argv[] = '--no-ff'; 320 } 321 322 $argv[] = '--'; 323 324 $is_rebasing = false; 325 $is_merging = false; 326 try { 327 if ($this->isSquashStrategy() && !$is_empty) { 328 // If we're performing a squash merge, we're going to rebase the 329 // commit range first. We only want to merge the specific commits 330 // in the range, and merging too much can create conflicts. 331 332 $api->execxLocal('checkout %s --', $max_hash); 333 334 $is_rebasing = true; 335 $api->execxLocal( 336 'rebase --onto %s -- %s', 337 $into_commit, 338 $min_hash.'^'); 339 $is_rebasing = false; 340 341 $merge_hash = $api->getCanonicalRevisionName('HEAD'); 342 } else { 343 $merge_hash = $max_hash; 344 } 345 346 $api->execxLocal('checkout %s --', $into_commit); 347 348 $argv[] = $merge_hash; 349 350 $is_merging = true; 351 $api->execxLocal('merge %Ls', $argv); 352 $is_merging = false; 353 } catch (CommandException $ex) { 354 $direct_symbols = $max_commit->getDirectSymbols(); 355 $indirect_symbols = $max_commit->getIndirectSymbols(); 356 if ($direct_symbols) { 357 $message = pht( 358 'Local commit "%s" (%s) does not merge cleanly into "%s". '. 359 'Merge or rebase local changes so they can merge cleanly.', 360 $api->getDisplayHash($max_hash), 361 $this->getDisplaySymbols($direct_symbols), 362 $api->getDisplayHash($into_commit)); 363 } else if ($indirect_symbols) { 364 $message = pht( 365 'Local commit "%s" (reachable from: %s) does not merge cleanly '. 366 'into "%s". Merge or rebase local changes so they can merge '. 367 'cleanly.', 368 $api->getDisplayHash($max_hash), 369 $this->getDisplaySymbols($indirect_symbols), 370 $api->getDisplayHash($into_commit)); 371 } else { 372 $message = pht( 373 'Local commit "%s" does not merge cleanly into "%s". Merge or '. 374 'rebase local changes so they can merge cleanly.', 375 $api->getDisplayHash($max_hash), 376 $api->getDisplayHash($into_commit)); 377 } 378 379 echo tsprintf( 380 "\n%!\n%W\n\n", 381 pht('MERGE CONFLICT'), 382 $message); 383 384 if ($this->getHasUnpushedChanges()) { 385 echo tsprintf( 386 "%?\n\n", 387 pht( 388 'Use "--incremental" to merge and push changes one by one.')); 389 } 390 391 if ($is_rebasing) { 392 $api->execManualLocal('rebase --abort'); 393 } 394 395 if ($is_merging) { 396 $api->execManualLocal('merge --abort'); 397 } 398 399 if ($is_merging || $is_rebasing) { 400 $api->execManualLocal('reset --hard HEAD --'); 401 } 402 403 throw new PhutilArgumentUsageException( 404 pht('Encountered a merge conflict.')); 405 } 406 407 list($original_author, $original_date) = $this->getAuthorAndDate( 408 $max_hash); 409 410 $revision_ref = $set->getRevisionRef(); 411 $commit_message = $revision_ref->getCommitMessage(); 412 413 $future = $api->execFutureLocal( 414 'commit --author %s --date %s -F - --', 415 $original_author, 416 $original_date); 417 $future->write($commit_message); 418 $future->resolvex(); 419 420 list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD'); 421 $new_cursor = trim($stdout); 422 423 if ($is_empty) { 424 // See T12876. If we're landing into the empty state, we just did a fake 425 // merge on top of an empty commit. We're now on a commit with all of the 426 // right details except that it has an extra empty commit as a parent. 427 428 // Create a new commit which is the same as the current HEAD, except that 429 // it doesn't have the extra parent. 430 431 $raw_commit = $api->readRawCommit($new_cursor); 432 if ($this->isSquashStrategy()) { 433 $raw_commit->setParents(array()); 434 } else { 435 $raw_commit->setParents(array($merge_hash)); 436 } 437 $new_cursor = $api->writeRawCommit($raw_commit); 438 439 $api->execxLocal('checkout %s --', $new_cursor); 440 } 441 442 return $new_cursor; 443 } 444 445 protected function pushChange($into_commit) { 446 $api = $this->getRepositoryAPI(); 447 $log = $this->getLogEngine(); 448 449 if ($this->getIsGitPerforce()) { 450 451 // TODO: Specifying "--onto" more than once is almost certainly an error 452 // in Perforce. 453 454 $log->writeStatus( 455 pht('SUBMITTING'), 456 pht( 457 'Submitting changes to "%s".', 458 $this->getOntoRemote())); 459 460 $config_argv = array(); 461 462 // Skip the "git p4 submit" interactive editor workflow. We expect 463 // the commit message that "arc land" has built to be satisfactory. 464 $config_argv[] = '-c'; 465 $config_argv[] = 'git-p4.skipSubmitEdit=true'; 466 467 // Skip the "git p4 submit" confirmation prompt if the user does not edit 468 // the submit message. 469 $config_argv[] = '-c'; 470 $config_argv[] = 'git-p4.skipSubmitEditCheck=true'; 471 472 $flags_argv = array(); 473 474 // Disable implicit "git p4 rebase" as part of submit. We're allowing 475 // the implicit "git p4 sync" to go through since this puts us in a 476 // state which is generally similar to the state after "git push", with 477 // updated remotes. 478 479 // We could do a manual "git p4 sync" with a more narrow "--branch" 480 // instead, but it's not clear that this is beneficial. 481 $flags_argv[] = '--disable-rebase'; 482 483 // Detect moves and submit them to Perforce as move operations. 484 $flags_argv[] = '-M'; 485 486 // If we run into a conflict, abort the operation. We expect users to 487 // fix conflicts and run "arc land" again. 488 $flags_argv[] = '--conflict=quit'; 489 490 $err = $this->newPassthru( 491 '%LR p4 submit %LR --commit %R --', 492 $config_argv, 493 $flags_argv, 494 $into_commit); 495 if ($err) { 496 throw new ArcanistLandPushFailureException( 497 pht( 498 'Submit failed! Fix the error and run "arc land" again.')); 499 } 500 501 return; 502 } 503 504 $log->writeStatus( 505 pht('PUSHING'), 506 pht('Pushing changes to "%s".', $this->getOntoRemote())); 507 508 $err = $this->newPassthru( 509 'push -- %s %Ls', 510 $this->getOntoRemote(), 511 $this->newOntoRefArguments($into_commit)); 512 513 if ($err) { 514 throw new ArcanistLandPushFailureException( 515 pht( 516 'Push failed! Fix the error and run "arc land" again.')); 517 } 518 } 519 520 protected function reconcileLocalState( 521 $into_commit, 522 ArcanistRepositoryLocalState $state) { 523 524 $api = $this->getRepositoryAPI(); 525 $log = $this->getWorkflow()->getLogEngine(); 526 527 // Try to put the user into the best final state we can. This is very 528 // complicated because users are incredibly creative and their local 529 // branches may, for example, have the same names as branches in the 530 // remote but no relationship to them. 531 532 // First, we're going to try to update these local branches: 533 // 534 // - the branch we started on originally; and 535 // - the local upstreams of the branch we started on originally; and 536 // - the local branch with the same name as the "into" ref; and 537 // - the local branch with the same name as the "onto" ref. 538 // 539 // These branches may not all exist and may not all be unique. 540 // 541 // To be updated, these branches must: 542 // 543 // - exist; 544 // - have not been deleted; and 545 // - be connected to the remote we pushed into. 546 547 $update_branches = array(); 548 549 $local_ref = $state->getLocalRef(); 550 if ($local_ref !== null) { 551 $update_branches[] = $local_ref; 552 } 553 554 $local_path = $state->getLocalPath(); 555 if ($local_path) { 556 foreach ($local_path->getLocalBranches() as $local_branch) { 557 $update_branches[] = $local_branch; 558 } 559 } 560 561 if (!$this->getIntoEmpty() && !$this->getIntoLocal()) { 562 $update_branches[] = $this->getIntoRef(); 563 } 564 565 foreach ($this->getOntoRefs() as $onto_ref) { 566 $update_branches[] = $onto_ref; 567 } 568 569 $update_branches = array_fuse($update_branches); 570 571 // Remove any branches we know we deleted. 572 foreach ($update_branches as $key => $update_branch) { 573 if (isset($this->deletedBranches[$update_branch])) { 574 unset($update_branches[$key]); 575 } 576 } 577 578 // Now, remove any branches which don't actually exist. 579 foreach ($update_branches as $key => $update_branch) { 580 list($err) = $api->execManualLocal( 581 'rev-parse --verify %s', 582 $update_branch); 583 if ($err) { 584 unset($update_branches[$key]); 585 } 586 } 587 588 $is_perforce = $this->getIsGitPerforce(); 589 if ($is_perforce) { 590 // If we're in Perforce mode, we don't expect to have a meaningful 591 // path to the remote: the "p4" remote is not a real remote, and 592 // "git p4" commands do not configure branch upstreams to provide 593 // a path. 594 595 // Additionally, we've already set the remote to the right state with an 596 // implicit "git p4 sync" during "git p4 submit", and "git pull" isn't a 597 // meaningful operation. 598 599 // We're going to skip everything here and just switch to the most 600 // desirable branch (if we can find one), then reset the state (if that 601 // operation is safe). 602 603 if (!$update_branches) { 604 $log->writeStatus( 605 pht('DETACHED HEAD'), 606 pht( 607 'Unable to find any local branches to update, staying on '. 608 'detached head.')); 609 $state->discardLocalState(); 610 return; 611 } 612 613 $dst_branch = head($update_branches); 614 if (!$this->isAncestorOf($dst_branch, $into_commit)) { 615 $log->writeStatus( 616 pht('CHECKOUT'), 617 pht( 618 'Local branch "%s" has unpublished changes, checking it out '. 619 'but leaving them in place.', 620 $dst_branch)); 621 $do_reset = false; 622 } else { 623 $log->writeStatus( 624 pht('UPDATE'), 625 pht( 626 'Switching to local branch "%s".', 627 $dst_branch)); 628 $do_reset = true; 629 } 630 631 $api->execxLocal('checkout %s --', $dst_branch); 632 633 if ($do_reset) { 634 $api->execxLocal('reset --hard %s --', $into_commit); 635 } 636 637 $state->discardLocalState(); 638 return; 639 } 640 641 $onto_refs = array_fuse($this->getOntoRefs()); 642 643 $pull_branches = array(); 644 foreach ($update_branches as $update_branch) { 645 $update_path = $api->getPathToUpstream($update_branch); 646 647 // Remove any branches which contain upstream cycles. 648 if ($update_path->getCycle()) { 649 $log->writeWarning( 650 pht('LOCAL CYCLE'), 651 pht( 652 'Local branch "%s" tracks an upstream but following it leads to '. 653 'a local cycle, ignoring branch.', 654 $update_branch)); 655 continue; 656 } 657 658 // Remove any branches not connected to a remote. 659 if (!$update_path->isConnectedToRemote()) { 660 continue; 661 } 662 663 // Remove any branches connected to a remote other than the remote 664 // we actually pushed to. 665 $remote_name = $update_path->getRemoteRemoteName(); 666 if ($remote_name !== $this->getOntoRemote()) { 667 continue; 668 } 669 670 // Remove any branches not connected to a branch we pushed to. 671 $remote_branch = $update_path->getRemoteBranchName(); 672 if (!isset($onto_refs[$remote_branch])) { 673 continue; 674 } 675 676 // This is the most-desirable path between some local branch and 677 // an impacted upstream. Select it and continue. 678 $pull_branches = $update_path->getLocalBranches(); 679 break; 680 } 681 682 // When we update these branches later, we want to start with the branch 683 // closest to the upstream and work our way down. 684 $pull_branches = array_reverse($pull_branches); 685 $pull_branches = array_fuse($pull_branches); 686 687 // If we started on a branch and it still exists but is not impacted 688 // by the changes we made to the remote (i.e., we aren't actually going 689 // to pull or update it if we continue), just switch back to it now. It's 690 // okay if this branch is completely unrelated to the changes we just 691 // landed. 692 693 if ($local_ref !== null) { 694 if (isset($update_branches[$local_ref])) { 695 if (!isset($pull_branches[$local_ref])) { 696 697 $log->writeStatus( 698 pht('RETURN'), 699 pht( 700 'Returning to original branch "%s" in original state.', 701 $local_ref)); 702 703 $state->restoreLocalState(); 704 return; 705 } 706 } 707 } 708 709 // Otherwise, if we don't have any path from the upstream to any local 710 // branch, we don't want to switch to some unrelated branch which happens 711 // to have the same name as a branch we interacted with. Just stay where 712 // we ended up. 713 714 $dst_branch = null; 715 if ($pull_branches) { 716 $dst_branch = null; 717 foreach ($pull_branches as $pull_branch) { 718 if (!$this->isAncestorOf($pull_branch, $into_commit)) { 719 720 $log->writeStatus( 721 pht('LOCAL CHANGES'), 722 pht( 723 'Local branch "%s" has unpublished changes, ending updates.', 724 $pull_branch)); 725 726 break; 727 } 728 729 $log->writeStatus( 730 pht('UPDATE'), 731 pht( 732 'Updating local branch "%s"...', 733 $pull_branch)); 734 735 $api->execxLocal( 736 'branch -f %s %s --', 737 $pull_branch, 738 $into_commit); 739 740 $dst_branch = $pull_branch; 741 } 742 } 743 744 if ($dst_branch) { 745 $log->writeStatus( 746 pht('CHECKOUT'), 747 pht( 748 'Checking out "%s".', 749 $dst_branch)); 750 751 $api->execxLocal('checkout %s --', $dst_branch); 752 } else { 753 $log->writeStatus( 754 pht('DETACHED HEAD'), 755 pht( 756 'Unable to find any local branches to update, staying on '. 757 'detached head.')); 758 } 759 760 $state->discardLocalState(); 761 } 762 763 private function isAncestorOf($branch, $commit) { 764 $api = $this->getRepositoryAPI(); 765 766 list($stdout) = $api->execxLocal( 767 'merge-base -- %s %s', 768 $branch, 769 $commit); 770 $merge_base = trim($stdout); 771 772 list($stdout) = $api->execxLocal( 773 'rev-parse --verify %s', 774 $branch); 775 $branch_hash = trim($stdout); 776 777 return ($merge_base === $branch_hash); 778 } 779 780 private function getAuthorAndDate($commit) { 781 $api = $this->getRepositoryAPI(); 782 783 list($info) = $api->execxLocal( 784 'log -n1 --format=%s %s --', 785 '%aD%n%an%n%ae', 786 gitsprintf('%s', $commit)); 787 788 $info = trim($info); 789 list($date, $author, $email) = explode("\n", $info, 3); 790 791 return array( 792 "$author <{$email}>", 793 $date, 794 ); 795 } 796 797 protected function didHoldChanges($into_commit) { 798 $log = $this->getLogEngine(); 799 $local_state = $this->getLocalState(); 800 801 if ($this->getIsGitPerforce()) { 802 $message = pht( 803 'Holding changes locally, they have not been submitted.'); 804 805 $push_command = csprintf( 806 'git p4 submit -M --commit %s --', 807 $into_commit); 808 } else { 809 $message = pht( 810 'Holding changes locally, they have not been pushed.'); 811 812 $push_command = csprintf( 813 'git push -- %s %Ls', 814 $this->getOntoRemote(), 815 $this->newOntoRefArguments($into_commit)); 816 } 817 818 echo tsprintf( 819 "\n%!\n%s\n\n", 820 pht('HOLD CHANGES'), 821 $message); 822 823 echo tsprintf( 824 "%s\n\n%>\n", 825 pht('To push changes manually, run this command:'), 826 $push_command); 827 828 $restore_commands = $local_state->getRestoreCommandsForDisplay(); 829 if ($restore_commands) { 830 echo tsprintf( 831 "%s\n\n", 832 pht( 833 'To go back to how things were before you ran "arc land", run '. 834 'these %s command(s):', 835 phutil_count($restore_commands))); 836 837 foreach ($restore_commands as $restore_command) { 838 echo tsprintf('%>', $restore_command); 839 } 840 841 echo tsprintf("\n"); 842 } 843 844 echo tsprintf( 845 "%s\n", 846 pht( 847 'Local branches have not been changed, and are still in the '. 848 'same state as before.')); 849 } 850 851 protected function resolveSymbols(array $symbols) { 852 assert_instances_of($symbols, 'ArcanistLandSymbol'); 853 $api = $this->getRepositoryAPI(); 854 855 foreach ($symbols as $symbol) { 856 $raw_symbol = $symbol->getSymbol(); 857 858 list($err, $stdout) = $api->execManualLocal( 859 'rev-parse --verify %s', 860 $raw_symbol); 861 862 if ($err) { 863 throw new PhutilArgumentUsageException( 864 pht( 865 'Branch "%s" does not exist in the local working copy.', 866 $raw_symbol)); 867 } 868 869 $commit = trim($stdout); 870 $symbol->setCommit($commit); 871 } 872 } 873 874 protected function confirmOntoRefs(array $onto_refs) { 875 $api = $this->getRepositoryAPI(); 876 877 foreach ($onto_refs as $onto_ref) { 878 if (!strlen($onto_ref)) { 879 throw new PhutilArgumentUsageException( 880 pht( 881 'Selected "onto" ref "%s" is invalid: the empty string is not '. 882 'a valid ref.', 883 $onto_ref)); 884 } 885 } 886 887 $markers = $api->newMarkerRefQuery() 888 ->withRemotes(array($this->getOntoRemoteRef())) 889 ->withNames($onto_refs) 890 ->execute(); 891 892 $markers = mgroup($markers, 'getName'); 893 894 $new_markers = array(); 895 foreach ($onto_refs as $onto_ref) { 896 if (isset($markers[$onto_ref])) { 897 // Remote already has a branch with this name, so we're fine: we 898 // aren't creatinga new branch. 899 continue; 900 } 901 902 $new_markers[] = id(new ArcanistMarkerRef()) 903 ->setMarkerType(ArcanistMarkerRef::TYPE_BRANCH) 904 ->setName($onto_ref); 905 } 906 907 if ($new_markers) { 908 echo tsprintf( 909 "\n%!\n%W\n\n", 910 pht('CREATE %s BRANCHE(S)', phutil_count($new_markers)), 911 pht( 912 'These %s symbol(s) do not exist in the remote. They will be '. 913 'created as new branches:', 914 phutil_count($new_markers))); 915 916 foreach ($new_markers as $new_marker) { 917 echo tsprintf('%s', $new_marker->newRefView()); 918 } 919 920 echo tsprintf("\n"); 921 922 $is_hold = $this->getShouldHold(); 923 if ($is_hold) { 924 echo tsprintf( 925 "%?\n", 926 pht( 927 'You are using "--hold", so execution will stop before the '. 928 '%s branche(s) are actually created. You will be given '. 929 'instructions to create the branches.', 930 phutil_count($new_markers))); 931 } 932 933 $query = pht( 934 'Create %s new branche(s) in the remote?', 935 phutil_count($new_markers)); 936 937 $this->getWorkflow() 938 ->getPrompt('arc.land.create') 939 ->setQuery($query) 940 ->execute(); 941 } 942 } 943 944 protected function selectOntoRefs(array $symbols) { 945 assert_instances_of($symbols, 'ArcanistLandSymbol'); 946 $log = $this->getLogEngine(); 947 948 $onto = $this->getOntoArguments(); 949 if ($onto) { 950 951 $log->writeStatus( 952 pht('ONTO TARGET'), 953 pht( 954 'Refs were selected with the "--onto" flag: %s.', 955 implode(', ', $onto))); 956 957 return $onto; 958 } 959 960 $onto = $this->getOntoFromConfiguration(); 961 if ($onto) { 962 $onto_key = $this->getOntoConfigurationKey(); 963 964 $log->writeStatus( 965 pht('ONTO TARGET'), 966 pht( 967 'Refs were selected by reading "%s" configuration: %s.', 968 $onto_key, 969 implode(', ', $onto))); 970 971 return $onto; 972 } 973 974 $api = $this->getRepositoryAPI(); 975 976 $remote_onto = array(); 977 foreach ($symbols as $symbol) { 978 $raw_symbol = $symbol->getSymbol(); 979 $path = $api->getPathToUpstream($raw_symbol); 980 981 if (!$path->getLength()) { 982 continue; 983 } 984 985 $cycle = $path->getCycle(); 986 if ($cycle) { 987 $log->writeWarning( 988 pht('LOCAL CYCLE'), 989 pht( 990 'Local branch "%s" tracks an upstream, but following it leads '. 991 'to a local cycle; ignoring branch upstream.', 992 $raw_symbol)); 993 994 $log->writeWarning( 995 pht('LOCAL CYCLE'), 996 implode(' -> ', $cycle)); 997 998 continue; 999 } 1000 1001 if (!$path->isConnectedToRemote()) { 1002 $log->writeWarning( 1003 pht('NO PATH TO REMOTE'), 1004 pht( 1005 'Local branch "%s" tracks an upstream, but there is no path '. 1006 'to a remote; ignoring branch upstream.', 1007 $raw_symbol)); 1008 1009 continue; 1010 } 1011 1012 $onto = $path->getRemoteBranchName(); 1013 1014 $remote_onto[$onto] = $onto; 1015 } 1016 1017 if (count($remote_onto) > 1) { 1018 throw new PhutilArgumentUsageException( 1019 pht( 1020 'The branches you are landing are connected to multiple different '. 1021 'remote branches via Git branch upstreams. Use "--onto" to select '. 1022 'the refs you want to push to.')); 1023 } 1024 1025 if ($remote_onto) { 1026 $remote_onto = array_values($remote_onto); 1027 1028 $log->writeStatus( 1029 pht('ONTO TARGET'), 1030 pht( 1031 'Landing onto target "%s", selected by following tracking branches '. 1032 'upstream to the closest remote branch.', 1033 head($remote_onto))); 1034 1035 return $remote_onto; 1036 } 1037 1038 $default_onto = 'master'; 1039 1040 $log->writeStatus( 1041 pht('ONTO TARGET'), 1042 pht( 1043 'Landing onto target "%s", the default target under Git.', 1044 $default_onto)); 1045 1046 return array($default_onto); 1047 } 1048 1049 protected function selectOntoRemote(array $symbols) { 1050 assert_instances_of($symbols, 'ArcanistLandSymbol'); 1051 $remote = $this->newOntoRemote($symbols); 1052 1053 $api = $this->getRepositoryAPI(); 1054 $log = $this->getLogEngine(); 1055 $is_pushable = $api->isPushableRemote($remote); 1056 $is_perforce = $api->isPerforceRemote($remote); 1057 1058 if (!$is_pushable && !$is_perforce) { 1059 throw new PhutilArgumentUsageException( 1060 pht( 1061 'No pushable remote "%s" exists. Use the "--onto-remote" flag to '. 1062 'choose a valid, pushable remote to land changes onto.', 1063 $remote)); 1064 } 1065 1066 if ($is_perforce) { 1067 $this->setIsGitPerforce(true); 1068 1069 $log->writeWarning( 1070 pht('P4 MODE'), 1071 pht( 1072 'Operating in Git/Perforce mode after selecting a Perforce '. 1073 'remote.')); 1074 1075 if (!$this->isSquashStrategy()) { 1076 throw new PhutilArgumentUsageException( 1077 pht( 1078 'Perforce mode does not support the "merge" land strategy. '. 1079 'Use the "squash" land strategy when landing to a Perforce '. 1080 'remote (you can use "--squash" to select this strategy).')); 1081 } 1082 } 1083 1084 return $remote; 1085 } 1086 1087 private function newOntoRemote(array $onto_symbols) { 1088 assert_instances_of($onto_symbols, 'ArcanistLandSymbol'); 1089 $log = $this->getLogEngine(); 1090 1091 $remote = $this->getOntoRemoteArgument(); 1092 if ($remote !== null) { 1093 1094 $log->writeStatus( 1095 pht('ONTO REMOTE'), 1096 pht( 1097 'Remote "%s" was selected with the "--onto-remote" flag.', 1098 $remote)); 1099 1100 return $remote; 1101 } 1102 1103 $remote = $this->getOntoRemoteFromConfiguration(); 1104 if ($remote !== null) { 1105 $remote_key = $this->getOntoRemoteConfigurationKey(); 1106 1107 $log->writeStatus( 1108 pht('ONTO REMOTE'), 1109 pht( 1110 'Remote "%s" was selected by reading "%s" configuration.', 1111 $remote, 1112 $remote_key)); 1113 1114 return $remote; 1115 } 1116 1117 $api = $this->getRepositoryAPI(); 1118 1119 $upstream_remotes = array(); 1120 foreach ($onto_symbols as $onto_symbol) { 1121 $path = $api->getPathToUpstream($onto_symbol->getSymbol()); 1122 1123 $remote = $path->getRemoteRemoteName(); 1124 if ($remote !== null) { 1125 $upstream_remotes[$remote][] = $onto_symbol; 1126 } 1127 } 1128 1129 if (count($upstream_remotes) > 1) { 1130 throw new PhutilArgumentUsageException( 1131 pht( 1132 'The "onto" refs you have selected are connected to multiple '. 1133 'different remotes via Git branch upstreams. Use "--onto-remote" '. 1134 'to select a single remote.')); 1135 } 1136 1137 if ($upstream_remotes) { 1138 $upstream_remote = head_key($upstream_remotes); 1139 1140 $log->writeStatus( 1141 pht('ONTO REMOTE'), 1142 pht( 1143 'Remote "%s" was selected by following tracking branches '. 1144 'upstream to the closest remote.', 1145 $remote)); 1146 1147 return $upstream_remote; 1148 } 1149 1150 $perforce_remote = 'p4'; 1151 if ($api->isPerforceRemote($remote)) { 1152 1153 $log->writeStatus( 1154 pht('ONTO REMOTE'), 1155 pht( 1156 'Peforce remote "%s" was selected because the existence of '. 1157 'this remote implies this working copy was synchronized '. 1158 'from a Perforce repository.', 1159 $remote)); 1160 1161 return $remote; 1162 } 1163 1164 $default_remote = 'origin'; 1165 1166 $log->writeStatus( 1167 pht('ONTO REMOTE'), 1168 pht( 1169 'Landing onto remote "%s", the default remote under Git.', 1170 $default_remote)); 1171 1172 return $default_remote; 1173 } 1174 1175 protected function selectIntoRemote() { 1176 $api = $this->getRepositoryAPI(); 1177 $log = $this->getLogEngine(); 1178 1179 if ($this->getIntoEmptyArgument()) { 1180 $this->setIntoEmpty(true); 1181 1182 $log->writeStatus( 1183 pht('INTO REMOTE'), 1184 pht( 1185 'Will merge into empty state, selected with the "--into-empty" '. 1186 'flag.')); 1187 1188 return; 1189 } 1190 1191 if ($this->getIntoLocalArgument()) { 1192 $this->setIntoLocal(true); 1193 1194 $log->writeStatus( 1195 pht('INTO REMOTE'), 1196 pht( 1197 'Will merge into local state, selected with the "--into-local" '. 1198 'flag.')); 1199 1200 return; 1201 } 1202 1203 $into = $this->getIntoRemoteArgument(); 1204 if ($into !== null) { 1205 1206 // TODO: We could allow users to pass a URI argument instead, but 1207 // this also requires some updates to the fetch logic elsewhere. 1208 1209 if (!$api->isFetchableRemote($into)) { 1210 throw new PhutilArgumentUsageException( 1211 pht( 1212 'Remote "%s", specified with "--into", is not a valid fetchable '. 1213 'remote.', 1214 $into)); 1215 } 1216 1217 $this->setIntoRemote($into); 1218 1219 $log->writeStatus( 1220 pht('INTO REMOTE'), 1221 pht( 1222 'Will merge into remote "%s", selected with the "--into" flag.', 1223 $into)); 1224 1225 return; 1226 } 1227 1228 $onto = $this->getOntoRemote(); 1229 $this->setIntoRemote($onto); 1230 1231 $log->writeStatus( 1232 pht('INTO REMOTE'), 1233 pht( 1234 'Will merge into remote "%s" by default, because this is the remote '. 1235 'the change is landing onto.', 1236 $onto)); 1237 } 1238 1239 protected function selectIntoRef() { 1240 $log = $this->getLogEngine(); 1241 1242 if ($this->getIntoEmptyArgument()) { 1243 $log->writeStatus( 1244 pht('INTO TARGET'), 1245 pht( 1246 'Will merge into empty state, selected with the "--into-empty" '. 1247 'flag.')); 1248 1249 return; 1250 } 1251 1252 $into = $this->getIntoArgument(); 1253 if ($into !== null) { 1254 $this->setIntoRef($into); 1255 1256 $log->writeStatus( 1257 pht('INTO TARGET'), 1258 pht( 1259 'Will merge into target "%s", selected with the "--into" flag.', 1260 $into)); 1261 1262 return; 1263 } 1264 1265 $ontos = $this->getOntoRefs(); 1266 $onto = head($ontos); 1267 1268 $this->setIntoRef($onto); 1269 if (count($ontos) > 1) { 1270 $log->writeStatus( 1271 pht('INTO TARGET'), 1272 pht( 1273 'Will merge into target "%s" by default, because this is the first '. 1274 '"onto" target.', 1275 $onto)); 1276 } else { 1277 $log->writeStatus( 1278 pht('INTO TARGET'), 1279 pht( 1280 'Will merge into target "%s" by default, because this is the "onto" '. 1281 'target.', 1282 $onto)); 1283 } 1284 } 1285 1286 protected function selectIntoCommit() { 1287 $api = $this->getRepositoryAPI(); 1288 // Make sure that our "into" target is valid. 1289 $log = $this->getLogEngine(); 1290 $api = $this->getRepositoryAPI(); 1291 1292 if ($this->getIntoEmpty()) { 1293 // If we're running under "--into-empty", we don't have to do anything. 1294 1295 $log->writeStatus( 1296 pht('INTO COMMIT'), 1297 pht('Preparing merge into the empty state.')); 1298 1299 return null; 1300 } 1301 1302 if ($this->getIntoLocal()) { 1303 // If we're running under "--into-local", just make sure that the 1304 // target identifies some actual commit. 1305 $local_ref = $this->getIntoRef(); 1306 1307 list($err, $stdout) = $api->execManualLocal( 1308 'rev-parse --verify %s', 1309 $local_ref); 1310 1311 if ($err) { 1312 throw new PhutilArgumentUsageException( 1313 pht( 1314 'Local ref "%s" does not exist.', 1315 $local_ref)); 1316 } 1317 1318 $into_commit = trim($stdout); 1319 1320 $log->writeStatus( 1321 pht('INTO COMMIT'), 1322 pht( 1323 'Preparing merge into local target "%s", at commit "%s".', 1324 $local_ref, 1325 $api->getDisplayHash($into_commit))); 1326 1327 return $into_commit; 1328 } 1329 1330 $target = id(new ArcanistLandTarget()) 1331 ->setRemote($this->getIntoRemote()) 1332 ->setRef($this->getIntoRef()); 1333 1334 $commit = $this->fetchTarget($target); 1335 if ($commit !== null) { 1336 $log->writeStatus( 1337 pht('INTO COMMIT'), 1338 pht( 1339 'Preparing merge into "%s" from remote "%s", at commit "%s".', 1340 $target->getRef(), 1341 $target->getRemote(), 1342 $api->getDisplayHash($commit))); 1343 return $commit; 1344 } 1345 1346 // If we have no valid target and the user passed "--into" explicitly, 1347 // treat this as an error. For example, "arc land --into Q --onto Q", 1348 // where "Q" does not exist, is an error. 1349 if ($this->getIntoArgument()) { 1350 throw new PhutilArgumentUsageException( 1351 pht( 1352 'Ref "%s" does not exist in remote "%s".', 1353 $target->getRef(), 1354 $target->getRemote())); 1355 } 1356 1357 // Otherwise, treat this as implying "--into-empty". For example, 1358 // "arc land --onto Q", where "Q" does not exist, is equivalent to 1359 // "arc land --into-empty --onto Q". 1360 $this->setIntoEmpty(true); 1361 1362 $log->writeStatus( 1363 pht('INTO COMMIT'), 1364 pht( 1365 'Preparing merge into the empty state to create target "%s" '. 1366 'in remote "%s".', 1367 $target->getRef(), 1368 $target->getRemote())); 1369 1370 return null; 1371 } 1372 1373 private function getLandTargetLocalCommit(ArcanistLandTarget $target) { 1374 $commit = $this->resolveLandTargetLocalCommit($target); 1375 1376 if ($commit === null) { 1377 throw new Exception( 1378 pht( 1379 'No ref "%s" exists in remote "%s".', 1380 $target->getRef(), 1381 $target->getRemote())); 1382 } 1383 1384 return $commit; 1385 } 1386 1387 private function getLandTargetLocalExists(ArcanistLandTarget $target) { 1388 $commit = $this->resolveLandTargetLocalCommit($target); 1389 return ($commit !== null); 1390 } 1391 1392 private function resolveLandTargetLocalCommit(ArcanistLandTarget $target) { 1393 $target_key = $target->getLandTargetKey(); 1394 1395 if (!array_key_exists($target_key, $this->landTargetCommitMap)) { 1396 $full_ref = sprintf( 1397 'refs/remotes/%s/%s', 1398 $target->getRemote(), 1399 $target->getRef()); 1400 1401 $api = $this->getRepositoryAPI(); 1402 1403 list($err, $stdout) = $api->execManualLocal( 1404 'rev-parse --verify %s', 1405 $full_ref); 1406 1407 if ($err) { 1408 $result = null; 1409 } else { 1410 $result = trim($stdout); 1411 } 1412 1413 $this->landTargetCommitMap[$target_key] = $result; 1414 } 1415 1416 return $this->landTargetCommitMap[$target_key]; 1417 } 1418 1419 private function fetchLandTarget( 1420 ArcanistLandTarget $target, 1421 $ignore_failure = false) { 1422 $api = $this->getRepositoryAPI(); 1423 1424 $err = $this->newPassthru( 1425 'fetch --no-tags --quiet -- %s %s', 1426 $target->getRemote(), 1427 $target->getRef()); 1428 if ($err && !$ignore_failure) { 1429 throw new ArcanistUsageException( 1430 pht( 1431 'Fetch of "%s" from remote "%s" failed! Fix the error and '. 1432 'run "arc land" again.', 1433 $target->getRef(), 1434 $target->getRemote())); 1435 } 1436 1437 // TODO: If the remote is a bare URI, we could read ".git/FETCH_HEAD" 1438 // here and write the commit into the map. For now, settle for clearing 1439 // the cache. 1440 1441 // We could also fetch into some named "refs/arc-land-temporary" named 1442 // ref, then read that. 1443 1444 if (!$err) { 1445 $target_key = $target->getLandTargetKey(); 1446 unset($this->landTargetCommitMap[$target_key]); 1447 } 1448 } 1449 1450 protected function selectCommits($into_commit, array $symbols) { 1451 assert_instances_of($symbols, 'ArcanistLandSymbol'); 1452 $api = $this->getRepositoryAPI(); 1453 1454 $commit_map = array(); 1455 foreach ($symbols as $symbol) { 1456 $symbol_commit = $symbol->getCommit(); 1457 $format = '--format=%H%x00%P%x00%s%x00'; 1458 1459 if ($into_commit === null) { 1460 list($commits) = $api->execxLocal( 1461 'log %s %s --', 1462 $format, 1463 gitsprintf('%s', $symbol_commit)); 1464 } else { 1465 list($commits) = $api->execxLocal( 1466 'log %s %s --not %s --', 1467 $format, 1468 gitsprintf('%s', $symbol_commit), 1469 gitsprintf('%s', $into_commit)); 1470 } 1471 1472 $commits = phutil_split_lines($commits, false); 1473 $is_first = true; 1474 foreach ($commits as $line) { 1475 if (!strlen($line)) { 1476 continue; 1477 } 1478 1479 $parts = explode("\0", $line, 4); 1480 if (count($parts) < 3) { 1481 throw new Exception( 1482 pht( 1483 'Unexpected output from "git log ...": %s', 1484 $line)); 1485 } 1486 1487 $hash = $parts[0]; 1488 if (!isset($commit_map[$hash])) { 1489 $parents = $parts[1]; 1490 $parents = trim($parents); 1491 if (strlen($parents)) { 1492 $parents = explode(' ', $parents); 1493 } else { 1494 $parents = array(); 1495 } 1496 1497 $summary = $parts[2]; 1498 1499 $commit_map[$hash] = id(new ArcanistLandCommit()) 1500 ->setHash($hash) 1501 ->setParents($parents) 1502 ->setSummary($summary); 1503 } 1504 1505 $commit = $commit_map[$hash]; 1506 if ($is_first) { 1507 $commit->addDirectSymbol($symbol); 1508 $is_first = false; 1509 } 1510 1511 $commit->addIndirectSymbol($symbol); 1512 } 1513 } 1514 1515 return $this->confirmCommits($into_commit, $symbols, $commit_map); 1516 } 1517 1518 protected function getDefaultSymbols() { 1519 $api = $this->getRepositoryAPI(); 1520 $log = $this->getLogEngine(); 1521 1522 $branch = $api->getBranchName(); 1523 if ($branch !== null) { 1524 $log->writeStatus( 1525 pht('SOURCE'), 1526 pht( 1527 'Landing the current branch, "%s".', 1528 $branch)); 1529 1530 return array($branch); 1531 } 1532 1533 $commit = $api->getCurrentCommitRef(); 1534 1535 $log->writeStatus( 1536 pht('SOURCE'), 1537 pht( 1538 'Landing the current HEAD, "%s".', 1539 $commit->getCommitHash())); 1540 1541 return array($commit->getCommitHash()); 1542 } 1543 1544 private function newOntoRefArguments($into_commit) { 1545 $api = $this->getRepositoryAPI(); 1546 $refspecs = array(); 1547 1548 foreach ($this->getOntoRefs() as $onto_ref) { 1549 $refspecs[] = sprintf( 1550 '%s:refs/heads/%s', 1551 $api->getDisplayHash($into_commit), 1552 $onto_ref); 1553 } 1554 1555 return $refspecs; 1556 } 1557 1558 private function confirmLegacyStrategyConfiguration() { 1559 // TODO: See T13547. Remove this check in the future. This prevents users 1560 // from accidentally executing a "squash" workflow under a configuration 1561 // which would previously have executed a "merge" workflow. 1562 1563 // We're fine if we have an explicit "--strategy". 1564 if ($this->getStrategyArgument() !== null) { 1565 return; 1566 } 1567 1568 // We're fine if we have an explicit "arc.land.strategy". 1569 if ($this->getStrategyFromConfiguration() !== null) { 1570 return; 1571 } 1572 1573 // We're fine if "history.immutable" is not set to "true". 1574 $source_list = $this->getWorkflow()->getConfigurationSourceList(); 1575 $config_list = $source_list->getStorageValueList('history.immutable'); 1576 if (!$config_list) { 1577 return; 1578 } 1579 1580 $config_value = (bool)last($config_list)->getValue(); 1581 if (!$config_value) { 1582 return; 1583 } 1584 1585 // We're in trouble: we would previously have selected "merge" and will 1586 // now select "squash". Make sure the user knows what they're in for. 1587 1588 echo tsprintf( 1589 "\n%!\n%W\n\n", 1590 pht('MERGE STRATEGY IS AMBIGUOUS'), 1591 pht( 1592 'See <%s>. The default merge strategy under Git with '. 1593 '"history.immutable" has changed from "merge" to "squash". Your '. 1594 'configuration is ambiguous under this behavioral change. '. 1595 '(Use "--strategy" or configure "arc.land.strategy" to bypass '. 1596 'this check.)', 1597 'https://secure.phabricator.com/T13547')); 1598 1599 throw new PhutilArgumentUsageException( 1600 pht( 1601 'Desired merge strategy is ambiguous, choose an explicit strategy.')); 1602 } 1603 1604} 1605