1<?php
2
3/**
4 * Lands a branch by rebasing, merging and amending it.
5 */
6final class ArcanistLandWorkflow
7  extends ArcanistArcWorkflow {
8
9  public function getWorkflowName() {
10    return 'land';
11  }
12
13  public function getWorkflowInformation() {
14    $help = pht(<<<EOTEXT
15Supports: git, git/p4, git/svn, hg
16
17Publish accepted revisions after review. This command is the last step in the
18standard Differential code review workflow.
19
20To publish changes in local branch or bookmark "feature1", you will usually
21run this command:
22
23  **$ arc land feature1**
24
25This workflow merges and pushes changes associated with revisions that are
26ancestors of __ref__. Without __ref__, the current working copy state will be
27used. You can specify multiple __ref__ arguments to publish multiple changes at
28once.
29
30A __ref__ can be any symbol which identifies a commit: a branch name, a tag
31name, a bookmark name, a topic name, a raw commit hash, a symbolic reference,
32etc.
33
34When you provide a __ref__, all unpublished changes which are present in
35ancestors of that __ref__ will be selected for publishing. (With the
36**--pick** flag, only the unpublished changes you directly reference will be
37selected.)
38
39For example, if you provide local branch "feature3" as a __ref__ argument, that
40may also select the changes in "feature1" and "feature2" (if they are ancestors
41of "feature3"). If you stack changes in a single local branch, all commits in
42the stack may be selected.
43
44The workflow merges unpublished changes reachable from __ref__ "into" some
45intermediate branch, then pushes the combined state "onto" some destination
46branch (or list of branches).
47
48(In Mercurial, the "into" and "onto" branches may be bookmarks instead.)
49
50In the most common case, there is only one "onto" branch (often "master" or
51"default" or some similar branch) and the "into" branch is the same branch. For
52example, it is common to merge local feature branch "feature1" into
53"origin/master", then push it onto "origin/master".
54
55The list of "onto" branches is selected by examining these sources in order:
56
57  - the **--onto** flags;
58  - the __arc.land.onto__ configuration setting;
59  - (in Git) the upstream of the branch targeted by the land operation,
60    recursively;
61  - or by falling back to a standard default:
62    - (in Git) "master";
63    - (in Mercurial) "default".
64
65The remote to push "onto" is selected by examining these sources in order:
66
67  - the **--onto-remote** flag;
68  - the __arc.land.onto-remote__ configuration setting;
69  - (in Git) the upstream of the current branch, recursively;
70  - (in Git) the special "p4" remote which indicates a repository has
71    been synchronized with Perforce;
72  - or by falling back to a standard default:
73    - (in Git) "origin";
74    - (in Mercurial) "default".
75
76The branch to merge "into" is selected by examining these sources in order:
77
78  - the **--into** flag;
79  - the **--into-empty** flag;
80  - or by falling back to the first "onto" branch.
81
82The remote to merge "into" is selected by examining these sources in order:
83
84  - the **--into-remote** flag;
85  - the **--into-local** flag (which disables fetching before merging);
86  - or by falling back to the "onto" remote.
87
88After selecting remotes and branches, the commits which will land are printed.
89
90With **--preview**, execution stops here, before the change is merged.
91
92The "into" branch is fetched from the "into" remote (unless **--into-local** or
93**--into-empty** are specified) and the changes are merged into the state in
94the "into" branch according to the selected merge strategy.
95
96The default merge strategy is "squash", which produces a single commit from
97all local commits for each change. A different strategy can be selected with
98the **--strategy** flag.
99
100The resulting merged change will be given an up-to-date commit message
101describing the final state of the revision in Differential.
102
103With **--hold**, execution stops here, before the change is pushed.
104
105The change is pushed onto all of the "onto" branches in the "onto" remote.
106
107If you are landing multiple changes, they are normally all merged locally and
108then all pushed in a single operation. Instead, you can merge and push them one
109at a time with **--incremental**.
110
111Under merge strategies which mutate history (including the default "squash"
112strategy), local refs which descend from commits that were published are
113now updated. For example, if you land "feature4", local branches "feature5" and
114"feature6" may now be rebased on the published version of the change.
115
116Once everything has been pushed, cleanup occurs. Consulting mystical sources of
117power, the workflow makes a guess about what state you wanted to end up in
118after the process finishes. The working copy is put into that state.
119
120Any obsolete refs that point at commits which were published are deleted,
121unless the **--keep-branches** flag is passed.
122EOTEXT
123      );
124
125    return $this->newWorkflowInformation()
126      ->setSynopsis(pht('Publish reviewed changes.'))
127      ->addExample(pht('**land** [__options__] -- [__ref__ ...]'))
128      ->setHelp($help);
129  }
130
131  public function getWorkflowArguments() {
132    return array(
133      $this->newWorkflowArgument('hold')
134        ->setHelp(
135          pht(
136            'Prepare the changes to be pushed, but do not actually push '.
137            'them.')),
138      $this->newWorkflowArgument('keep-branches')
139        ->setHelp(
140          pht(
141            'Keep local branches around after changes are pushed. By '.
142            'default, local branches are deleted after the changes they '.
143            'contain are published.')),
144      $this->newWorkflowArgument('onto-remote')
145        ->setParameter('remote-name')
146        ->setHelp(pht('Push to a remote other than the default.'))
147        ->addRelatedConfig('arc.land.onto-remote'),
148      $this->newWorkflowArgument('onto')
149        ->setParameter('branch-name')
150        ->setRepeatable(true)
151        ->addRelatedConfig('arc.land.onto')
152        ->setHelp(
153          array(
154            pht(
155              'After merging, push changes onto a specified branch.'),
156            pht(
157              'Specifying this flag multiple times will push to multiple '.
158              'branches.'),
159          )),
160      $this->newWorkflowArgument('strategy')
161        ->setParameter('strategy-name')
162        ->addRelatedConfig('arc.land.strategy')
163        ->setHelp(
164          array(
165            pht(
166              'Merge using a particular strategy. Supported strategies are '.
167              '"squash" and "merge".'),
168            pht(
169              'The "squash" strategy collapses multiple local commits into '.
170              'a single commit when publishing. It produces a linear '.
171              'published history (but discards local checkpoint commits). '.
172              'This is the default strategy.'),
173            pht(
174              'The "merge" strategy generates a merge commit when publishing '.
175              'that retains local checkpoint commits (but produces a '.
176              'nonlinear published history). Select this strategy if you do '.
177              'not want "arc land" to discard checkpoint commits.'),
178          )),
179      $this->newWorkflowArgument('revision')
180        ->setParameter('revision-identifier')
181        ->setHelp(
182          pht(
183            'Land a specific revision, rather than determining revisions '.
184            'automatically from the commits that are landing.')),
185      $this->newWorkflowArgument('preview')
186        ->setHelp(
187          pht(
188            'Show the changes that will land. Does not modify the working '.
189            'copy or the remote.')),
190      $this->newWorkflowArgument('into')
191        ->setParameter('commit-ref')
192        ->setHelp(
193          pht(
194            'Specify the state to merge into. By default, this is the same '.
195            'as the "onto" ref.')),
196      $this->newWorkflowArgument('into-remote')
197        ->setParameter('remote-name')
198        ->setHelp(
199          pht(
200            'Specifies the remote to fetch the "into" ref from. By '.
201            'default, this is the same as the "onto" remote.')),
202      $this->newWorkflowArgument('into-local')
203        ->setHelp(
204          pht(
205            'Use the local "into" ref state instead of fetching it from '.
206            'a remote.')),
207      $this->newWorkflowArgument('into-empty')
208        ->setHelp(
209          pht(
210            'Merge into the empty state instead of an existing state. This '.
211            'mode is primarily useful when creating a new repository, and '.
212            'selected automatically if the "onto" ref does not exist and the '.
213            '"into" state is not specified.')),
214      $this->newWorkflowArgument('incremental')
215        ->setHelp(
216          array(
217            pht(
218              'When landing multiple revisions at once, push and rebase '.
219              'after each merge completes instead of waiting until all '.
220              'merges are completed to push.'),
221            pht(
222              'This is slower than the default behavior and not atomic, '.
223              'but may make it easier to resolve conflicts and land '.
224              'complicated changes by allowing you to make progress one '.
225              'step at a time.'),
226          )),
227      $this->newWorkflowArgument('pick')
228        ->setHelp(
229          pht(
230            'Land only the changes directly named by arguments, instead '.
231            'of all reachable ancestors.')),
232      $this->newWorkflowArgument('ref')
233        ->setWildcard(true),
234    );
235  }
236
237  protected function newPrompts() {
238    return array(
239      $this->newPrompt('arc.land.large-working-set')
240        ->setDescription(
241          pht(
242            'Confirms landing more than %s commit(s) in a single operation.',
243            new PhutilNumber($this->getLargeWorkingSetLimit()))),
244      $this->newPrompt('arc.land.confirm')
245        ->setDescription(
246          pht(
247            'Confirms that the correct changes have been selected to '.
248            'land.')),
249      $this->newPrompt('arc.land.implicit')
250        ->setDescription(
251          pht(
252            'Confirms that local commits which are not associated with '.
253            'a revision have been associated correctly and should land.')),
254      $this->newPrompt('arc.land.unauthored')
255        ->setDescription(
256          pht(
257            'Confirms that revisions you did not author should land.')),
258      $this->newPrompt('arc.land.changes-planned')
259        ->setDescription(
260          pht(
261            'Confirms that revisions with changes planned should land.')),
262      $this->newPrompt('arc.land.published')
263        ->setDescription(
264          pht(
265            'Confirms that revisions that are already published should land.')),
266      $this->newPrompt('arc.land.not-accepted')
267        ->setDescription(
268          pht(
269            'Confirms that revisions that are not accepted should land.')),
270      $this->newPrompt('arc.land.open-parents')
271        ->setDescription(
272          pht(
273            'Confirms that revisions with open parent revisions should '.
274            'land.')),
275      $this->newPrompt('arc.land.failed-builds')
276        ->setDescription(
277          pht(
278            'Confirms that revisions with failed builds should land.')),
279      $this->newPrompt('arc.land.ongoing-builds')
280        ->setDescription(
281          pht(
282            'Confirms that revisions with ongoing builds should land.')),
283      $this->newPrompt('arc.land.create')
284        ->setDescription(
285          pht(
286            'Confirms that new branches or bookmarks should be created '.
287            'in the remote.')),
288    );
289  }
290
291  public function getLargeWorkingSetLimit() {
292    return 50;
293  }
294
295  public function runWorkflow() {
296    $working_copy = $this->getWorkingCopy();
297    $repository_api = $working_copy->getRepositoryAPI();
298
299    $land_engine = $repository_api->getLandEngine();
300    if (!$land_engine) {
301      throw new PhutilArgumentUsageException(
302        pht(
303          '"arc land" must be run in a Git or Mercurial working copy.'));
304    }
305
306    $is_incremental = $this->getArgument('incremental');
307    $source_refs = $this->getArgument('ref');
308
309    $onto_remote_arg = $this->getArgument('onto-remote');
310    $onto_args = $this->getArgument('onto');
311
312    $into_remote = $this->getArgument('into-remote');
313    $into_empty = $this->getArgument('into-empty');
314    $into_local = $this->getArgument('into-local');
315    $into = $this->getArgument('into');
316
317    $is_preview = $this->getArgument('preview');
318    $should_hold = $this->getArgument('hold');
319    $should_keep = $this->getArgument('keep-branches');
320
321    $revision = $this->getArgument('revision');
322    $strategy = $this->getArgument('strategy');
323    $pick = $this->getArgument('pick');
324
325    $land_engine
326      ->setViewer($this->getViewer())
327      ->setWorkflow($this)
328      ->setLogEngine($this->getLogEngine())
329      ->setSourceRefs($source_refs)
330      ->setShouldHold($should_hold)
331      ->setShouldKeep($should_keep)
332      ->setStrategyArgument($strategy)
333      ->setShouldPreview($is_preview)
334      ->setOntoRemoteArgument($onto_remote_arg)
335      ->setOntoArguments($onto_args)
336      ->setIntoRemoteArgument($into_remote)
337      ->setIntoEmptyArgument($into_empty)
338      ->setIntoLocalArgument($into_local)
339      ->setIntoArgument($into)
340      ->setPickArgument($pick)
341      ->setIsIncremental($is_incremental)
342      ->setRevisionSymbol($revision);
343
344    $land_engine->execute();
345  }
346
347}
348