1<?php
2
3final class ArcanistAmendWorkflow
4  extends ArcanistArcWorkflow {
5
6  public function getWorkflowName() {
7    return 'amend';
8  }
9
10  public function getWorkflowInformation() {
11    $help = pht(<<<EOTEXT
12Amend the working copy, synchronizing the local commit message from
13Differential.
14
15Supported in Mercurial 2.2 and newer.
16EOTEXT
17      );
18
19    return $this->newWorkflowInformation()
20      ->setSynopsis(
21        pht('Amend the working copy, synchronizing the local commit message.'))
22      ->addExample('**amend** [options] -- ')
23      ->setHelp($help);
24  }
25
26  public function getWorkflowArguments() {
27    return array(
28      $this->newWorkflowArgument('show')
29        ->setHelp(
30          pht(
31            'Show the amended commit message, without modifying the '.
32            'working copy.')),
33      $this->newWorkflowArgument('revision')
34        ->setParameter('id')
35        ->setHelp(
36          pht(
37            'Use the message from a specific revision. If you do not specify '.
38            'a revision, arc will guess which revision is in the working '.
39            'copy.')),
40    );
41  }
42
43  protected function newPrompts() {
44    return array(
45      $this->newPrompt('arc.amend.unrelated')
46        ->setDescription(
47          pht(
48            'Confirms use of a revision that does not appear to be '.
49            'present in the working copy.')),
50      $this->newPrompt('arc.amend.author')
51        ->setDescription(
52          pht(
53            'Confirms use of a revision that you are not the author '.
54            'of.')),
55      $this->newPrompt('arc.amend.immutable')
56        ->setDescription(
57          pht(
58            'Confirms history mutation in a working copy marked as '.
59            'immutable.')),
60    );
61  }
62
63  public function runWorkflow() {
64    $symbols = $this->getSymbolEngine();
65
66    $is_show = $this->getArgument('show');
67
68    $repository_api = $this->getRepositoryAPI();
69    if (!$is_show) {
70      $this->requireAmendSupport($repository_api);
71    }
72
73    $revision_symbol = $this->getArgument('revision');
74
75    // We only care about the local working copy state if we need it to
76    // figure out which revision we're operating on, or we're planning to
77    // mutate it. If the caller is running "arc amend --show --revision X",
78    // the local state does not matter.
79
80    $need_state =
81      ($revision_symbol === null) ||
82      (!$is_show);
83
84    if ($need_state) {
85      $state_ref = $repository_api->getCurrentWorkingCopyStateRef();
86
87      $this->loadHardpoints(
88        $state_ref,
89        ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS);
90
91      $revision_refs = $state_ref->getRevisionRefs();
92    }
93
94    if ($revision_symbol === null) {
95      $revision_ref = $this->selectRevisionRef($revision_refs);
96    } else {
97      $revision_ref = $symbols->loadRevisionForSymbol($revision_symbol);
98      if (!$revision_ref) {
99        throw new PhutilArgumentUsageException(
100          pht(
101            'Revision "%s" does not exist, or you do not have permission '.
102            'to see it.',
103            $revision_symbol));
104      }
105    }
106
107    if (!$is_show) {
108      echo tsprintf(
109        "%s\n\n%s\n",
110        pht('Amending commit message to reflect revision:'),
111        $revision_ref->newRefView());
112
113      $this->confirmAmendAuthor($revision_ref);
114      $this->confirmAmendNotFound($revision_ref, $state_ref);
115    }
116
117    $this->loadHardpoints(
118      $revision_ref,
119      ArcanistRevisionRef::HARDPOINT_COMMITMESSAGE);
120
121    $message = $revision_ref->getCommitMessage();
122
123    if ($is_show) {
124      echo tsprintf(
125        "%B\n",
126        $message);
127    } else {
128      $repository_api->amendCommit($message);
129    }
130
131    return 0;
132  }
133
134  private function requireAmendSupport(ArcanistRepositoryAPI $api) {
135    if (!$api->supportsAmend()) {
136      if ($api instanceof ArcanistMercurialAPI) {
137        throw new PhutilArgumentUsageException(
138          pht(
139            '"arc amend" is only supported under Mercurial 2.2 or newer. '.
140            'Older versions of Mercurial do not support the "--amend" flag '.
141            'to "hg commit ...", which this workflow requires.'));
142      }
143
144      throw new PhutilArgumentUsageException(
145        pht(
146          '"arc amend" must be run from inside a working copy of a '.
147          'repository using a version control system that supports '.
148          'amending commits, like Git or Mercurial.'));
149    }
150
151    if ($this->isHistoryImmutable()) {
152      echo tsprintf(
153        "%!\n\n%W\n",
154        pht('IMMUTABLE WORKING COPY'),
155        pht(
156          'This working copy is configured to have an immutable local '.
157          'history, using the "history.immutable" configuration option. '.
158          'Amending the working copy will mutate local history.'));
159
160      $prompt = pht('Are you sure you want to mutate history?');
161
162      $this->getPrompt('arc.amend.immutable')
163        ->setQuery($prompt)
164        ->execute();
165    }
166
167    return;
168
169    if ($api->getUncommittedChanges()) {
170      // TODO: Make this class of error show the uncommitted changes.
171
172      // TODO: This only needs to check for staged-but-uncommitted changes.
173      // We can safely amend with untracked and unstaged changes.
174
175      throw new PhutilArgumentUsageException(
176        pht(
177          'You have uncommitted changes in this working copy. Commit or '.
178          'revert them before proceeding.'));
179    }
180  }
181
182  private function selectRevisionRef(array $revisions) {
183    if (!$revisions) {
184      throw new PhutilArgumentUsageException(
185        pht(
186          'No revision specified with "--revision", and no revisions found '.
187          'that match the current working copy state. Use "--revision <id>" '.
188          'to specify which revision you want to amend.'));
189    }
190
191     if (count($revisions) > 1) {
192       echo tsprintf(
193         "%!\n%W\n\n%B\n",
194         pht('MULTIPLE REVISIONS IN WORKING COPY'),
195         pht('More than one revision was found in the working copy:'),
196         mpull($revisions, 'newRefView'));
197
198      throw new PhutilArgumentUsageException(
199        pht(
200          'Use "--revision <id>" to specify which revision you want '.
201          'to amend.'));
202    }
203
204    return head($revisions);
205  }
206
207  private function confirmAmendAuthor(ArcanistRevisionRef $revision_ref) {
208    $viewer = $this->getViewer();
209    $viewer_phid = $viewer->getPHID();
210
211    $author_phid = $revision_ref->getAuthorPHID();
212
213    if ($viewer_phid === $author_phid) {
214      return;
215    }
216
217    $symbols = $this->getSymbolEngine();
218    $author_ref = $symbols->loadUserForSymbol($author_phid);
219    if (!$author_ref) {
220      // If we don't have any luck loading the author, skip this warning.
221      return;
222    }
223
224    echo tsprintf(
225      "%!\n\n%W\n\n%s",
226      pht('NOT REVISION AUTHOR'),
227      array(
228        pht(
229          'You are amending the working copy using information from '.
230          'a revision you are not the author of.'),
231        "\n\n",
232        pht(
233          'The author of this revision (%s) is:',
234          $revision_ref->getMonogram()),
235      ),
236      $author_ref->newRefView());
237
238    $prompt = pht(
239      'Amend working copy using revision owned by %s?',
240      $author_ref->getMonogram());
241
242    $this->getPrompt('arc.amend.author')
243      ->setQuery($prompt)
244      ->execute();
245  }
246
247  private function confirmAmendNotFound(
248    ArcanistRevisionRef $revision_ref,
249    ArcanistWorkingCopyStateRef $state_ref) {
250
251    $local_refs = $state_ref->getRevisionRefs();
252    $local_refs = mpull($local_refs, null, 'getPHID');
253
254    $revision_phid = $revision_ref->getPHID();
255    $is_local = isset($local_refs[$revision_phid]);
256
257    if ($is_local) {
258      return;
259    }
260
261    echo tsprintf(
262      "%!\n\n%W\n",
263      pht('UNRELATED REVISION'),
264      pht(
265        'You are amending the working copy using information from '.
266        'a revision that does not appear to be associated with the '.
267        'current state of the working copy.'));
268
269    $prompt = pht(
270      'Amend working copy using unrelated revision %s?',
271      $revision_ref->getMonogram());
272
273    $this->getPrompt('arc.amend.unrelated')
274      ->setQuery($prompt)
275      ->execute();
276  }
277
278}
279