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