1<?php
2
3/**
4 * Sends changes from your working copy to Differential for code review.
5 *
6 * @task lintunit   Lint and Unit Tests
7 * @task message    Commit and Update Messages
8 * @task diffspec   Diff Specification
9 * @task diffprop   Diff Properties
10 */
11final class ArcanistDiffWorkflow extends ArcanistWorkflow {
12
13  private $console;
14  private $hasWarnedExternals = false;
15  private $unresolvedLint;
16  private $testResults;
17  private $diffID;
18  private $revisionID;
19  private $diffPropertyFutures = array();
20  private $commitMessageFromRevision;
21  private $hitAutotargets;
22  private $revisionTransactions;
23  private $revisionIsDraft;
24
25  const STAGING_PUSHED = 'pushed';
26  const STAGING_USER_SKIP = 'user.skip';
27  const STAGING_DIFF_RAW = 'diff.raw';
28  const STAGING_REPOSITORY_UNKNOWN = 'repository.unknown';
29  const STAGING_REPOSITORY_UNAVAILABLE = 'repository.unavailable';
30  const STAGING_REPOSITORY_UNSUPPORTED = 'repository.unsupported';
31  const STAGING_REPOSITORY_UNCONFIGURED = 'repository.unconfigured';
32  const STAGING_CLIENT_UNSUPPORTED = 'client.unsupported';
33
34  public function getWorkflowName() {
35    return 'diff';
36  }
37
38  public function getCommandSynopses() {
39    return phutil_console_format(<<<EOTEXT
40      **diff** [__paths__] (svn)
41      **diff** [__commit__] (git, hg)
42EOTEXT
43      );
44  }
45
46  public function getCommandHelp() {
47    return phutil_console_format(<<<EOTEXT
48          Supports: git, svn, hg
49          Generate a Differential diff or revision from local changes.
50
51          Under git and mercurial, you can specify a commit (like __HEAD^^^__
52          or __master__) and Differential will generate a diff against the
53          merge base of that commit and your current working directory parent.
54
55          Under svn, you can choose to include only some of the modified files
56          in the working copy in the diff by specifying their paths. If you
57          omit paths, all changes are included in the diff.
58EOTEXT
59      );
60  }
61
62  public function requiresWorkingCopy() {
63    return !$this->isRawDiffSource();
64  }
65
66  public function requiresConduit() {
67    return true;
68  }
69
70  public function requiresAuthentication() {
71    return true;
72  }
73
74  public function requiresRepositoryAPI() {
75    if (!$this->isRawDiffSource()) {
76      return true;
77    }
78
79    return false;
80  }
81
82  public function getDiffID() {
83    return $this->diffID;
84  }
85
86  public function getArguments() {
87    $arguments = array(
88      'message' => array(
89        'short'       => 'm',
90        'param'       => 'message',
91        'help' => pht(
92          'When updating a revision, use the specified message instead of '.
93          'prompting.'),
94      ),
95      'message-file' => array(
96        'short' => 'F',
97        'param' => 'file',
98        'paramtype' => 'file',
99        'help' => pht(
100          'When creating a revision, read revision information '.
101          'from this file.'),
102      ),
103      'edit' => array(
104        'supports'    => array(
105          'git',
106          'hg',
107        ),
108        'nosupport'   => array(
109          'svn' => pht('Edit revisions via the web interface when using SVN.'),
110        ),
111        'help' => pht(
112          'When updating a revision under git, edit revision information '.
113          'before updating.'),
114      ),
115      'raw' => array(
116        'help' => pht(
117          'Read diff from stdin, not from the working copy. This disables '.
118          'many Arcanist/Phabricator features which depend on having access '.
119          'to the working copy.'),
120        'conflicts' => array(
121          'apply-patches'       => pht('%s disables lint.', '--raw'),
122          'never-apply-patches' => pht('%s disables lint.', '--raw'),
123
124          'create'              => pht(
125            '%s and %s both need stdin. Use %s.',
126            '--raw',
127            '--create',
128            '--raw-command'),
129          'edit'                => pht(
130            '%s and %s both need stdin. Use %s.',
131            '--raw',
132            '--edit',
133            '--raw-command'),
134          'raw-command'         => null,
135        ),
136      ),
137      'raw-command' => array(
138        'param' => 'command',
139        'help' => pht(
140          'Generate diff by executing a specified command, not from the '.
141          'working copy. This disables many Arcanist/Phabricator features '.
142          'which depend on having access to the working copy.'),
143        'conflicts' => array(
144          'apply-patches'       => pht('%s disables lint.', '--raw-command'),
145          'never-apply-patches' => pht('%s disables lint.', '--raw-command'),
146        ),
147      ),
148      'create' => array(
149        'help' => pht('Always create a new revision.'),
150        'conflicts' => array(
151          'edit'    => pht(
152            '%s can not be used with %s.',
153            '--create',
154            '--edit'),
155          'only' => pht(
156            '%s can not be used with %s.',
157            '--create',
158            '--only'),
159          'update'  => pht(
160            '%s can not be used with %s.',
161            '--create',
162            '--update'),
163        ),
164      ),
165      'update' => array(
166        'param' => 'revision_id',
167        'help'  => pht('Always update a specific revision.'),
168      ),
169      'draft' => array(
170        'help' => pht(
171          'Create a draft revision so you can look over your changes before '.
172          'involving anyone else. Other users will not be notified about the '.
173          'revision until you later use "Request Review" to publish it. You '.
174          'can still share the draft by giving someone the link.'),
175        'conflicts' => array(
176          'edit' => null,
177          'only' => null,
178          'update' => null,
179        ),
180      ),
181      'nounit' => array(
182        'help' => pht('Do not run unit tests.'),
183      ),
184      'nolint' => array(
185        'help' => pht('Do not run lint.'),
186        'conflicts' => array(
187          'apply-patches' => pht('%s suppresses lint.', '--nolint'),
188          'never-apply-patches' => pht('%s suppresses lint.', '--nolint'),
189        ),
190      ),
191      'only' => array(
192        'help' => pht(
193          'Instead of creating or updating a revision, only create a diff, '.
194          'which you may later attach to a revision.'),
195        'conflicts' => array(
196          'edit'      => pht('%s does affect revisions.', '--only'),
197          'message'   => pht('%s does not update any revision.', '--only'),
198        ),
199      ),
200      'allow-untracked' => array(
201        'help' => pht('Skip checks for untracked files in the working copy.'),
202      ),
203      'apply-patches' => array(
204        'help' => pht(
205          'Apply patches suggested by lint to the working copy without '.
206          'prompting.'),
207        'conflicts' => array(
208          'never-apply-patches' => true,
209        ),
210        'passthru' => array(
211          'lint' => true,
212        ),
213      ),
214      'never-apply-patches' => array(
215        'help' => pht('Never apply patches suggested by lint.'),
216        'conflicts' => array(
217          'apply-patches' => true,
218        ),
219        'passthru' => array(
220          'lint' => true,
221        ),
222      ),
223      'amend-all' => array(
224        'help' => pht(
225          'When linting git repositories, amend HEAD with all patches '.
226          'suggested by lint without prompting.'),
227        'passthru' => array(
228          'lint' => true,
229        ),
230      ),
231      'amend-autofixes' => array(
232        'help' => pht(
233          'When linting git repositories, amend HEAD with autofix '.
234          'patches suggested by lint without prompting.'),
235        'passthru' => array(
236          'lint' => true,
237        ),
238      ),
239      'add-all' => array(
240        'short' => 'a',
241        'help' => pht(
242          'Automatically add all unstaged and uncommitted '.
243          'files to the commit.'),
244      ),
245      'json' => array(
246        'help' => pht(
247          'Emit machine-readable JSON. EXPERIMENTAL! Probably does not work!'),
248      ),
249      'no-amend' => array(
250        'help' => pht(
251          'Never amend commits in the working copy with lint patches.'),
252      ),
253      'uncommitted' => array(
254        'help' => pht('Suppress warning about uncommitted changes.'),
255        'supports' => array(
256          'hg',
257        ),
258      ),
259      'verbatim' => array(
260        'help' => pht(
261          'When creating a revision, try to use the working copy commit '.
262          'message verbatim, without prompting to edit it. When updating a '.
263          'revision, update some fields from the local commit message.'),
264        'supports' => array(
265          'hg',
266          'git',
267        ),
268        'conflicts' => array(
269          'update'              => true,
270          'only' => true,
271          'raw'                 => true,
272          'raw-command'         => true,
273          'message-file'        => true,
274        ),
275      ),
276      'reviewers' => array(
277        'param' => 'usernames',
278        'help' => pht('When creating a revision, add reviewers.'),
279        'conflicts' => array(
280          'only' => true,
281          'update'  => true,
282        ),
283      ),
284      'cc' => array(
285        'param' => 'usernames',
286        'help' => pht('When creating a revision, add CCs.'),
287        'conflicts' => array(
288          'only' => true,
289          'update'  => true,
290        ),
291      ),
292      'skip-binaries' => array(
293        'help'  => pht('Do not upload binaries (like images).'),
294      ),
295      'skip-staging' => array(
296        'help' => pht('Do not copy changes to the staging area.'),
297      ),
298      'base' => array(
299        'param' => 'rules',
300        'help'  => pht('Additional rules for determining base revision.'),
301        'nosupport' => array(
302          'svn' => pht('Subversion does not use base commits.'),
303        ),
304        'supports' => array('git', 'hg'),
305      ),
306      'coverage' => array(
307        'help' => pht('Always enable coverage information.'),
308        'conflicts' => array(
309          'no-coverage' => null,
310        ),
311        'passthru' => array(
312          'unit' => true,
313        ),
314      ),
315      'no-coverage' => array(
316        'help' => pht('Always disable coverage information.'),
317        'passthru' => array(
318          'unit' => true,
319        ),
320      ),
321      'browse' => array(
322        'help' => pht(
323          'After creating a diff or revision, open it in a web browser.'),
324      ),
325      '*' => 'paths',
326      'head' => array(
327        'param' => 'commit',
328        'help' => pht(
329          'Specify the end of the commit range. This disables many '.
330          'Arcanist/Phabricator features which depend on having access to '.
331          'the working copy.'),
332        'supports' => array('git'),
333        'nosupport' => array(
334          'svn' => pht('Subversion does not support commit ranges.'),
335          'hg' => pht('Mercurial does not support %s yet.', '--head'),
336        ),
337      ),
338    );
339
340    return $arguments;
341  }
342
343  public function isRawDiffSource() {
344    return $this->getArgument('raw') || $this->getArgument('raw-command');
345  }
346
347  public function run() {
348    $this->console = PhutilConsole::getConsole();
349
350    $this->runRepositoryAPISetup();
351    $this->runDiffSetupBasics();
352
353    $commit_message = $this->buildCommitMessage();
354
355    $this->dispatchEvent(
356      ArcanistEventType::TYPE_DIFF_DIDBUILDMESSAGE,
357      array(
358        'message' => $commit_message,
359      ));
360
361    if (!$this->shouldOnlyCreateDiff()) {
362      $revision = $this->buildRevisionFromCommitMessage($commit_message);
363    }
364
365    $data = $this->runLintUnit();
366
367    $lint_result = $data['lintResult'];
368    $this->unresolvedLint = $data['unresolvedLint'];
369    $unit_result = $data['unitResult'];
370    $this->testResults = $data['testResults'];
371
372    $changes = $this->generateChanges();
373    if (!$changes) {
374      throw new ArcanistUsageException(
375        pht('There are no changes to generate a diff from!'));
376    }
377
378    $diff_spec = array(
379      'changes' => mpull($changes, 'toDictionary'),
380      'lintStatus' => $this->getLintStatus($lint_result),
381      'unitStatus' => $this->getUnitStatus($unit_result),
382    ) + $this->buildDiffSpecification();
383
384    $conduit = $this->getConduit();
385    $diff_info = $conduit->callMethodSynchronous(
386      'differential.creatediff',
387      $diff_spec);
388
389    $this->diffID = $diff_info['diffid'];
390
391    $event = $this->dispatchEvent(
392      ArcanistEventType::TYPE_DIFF_WASCREATED,
393      array(
394        'diffID' => $diff_info['diffid'],
395        'lintResult' => $lint_result,
396        'unitResult' => $unit_result,
397      ));
398
399    $this->submitChangesToStagingArea($this->diffID);
400
401    $phid = idx($diff_info, 'phid');
402    if ($phid) {
403      $this->hitAutotargets = $this->updateAutotargets(
404        $phid,
405        $unit_result);
406    }
407
408    $this->updateLintDiffProperty();
409    $this->updateUnitDiffProperty();
410    $this->updateLocalDiffProperty();
411    $this->updateOntoDiffProperty();
412    $this->resolveDiffPropertyUpdates();
413
414    $output_json = $this->getArgument('json');
415
416    if ($this->shouldOnlyCreateDiff()) {
417      if (!$output_json) {
418        echo phutil_console_format(
419          "%s\n        **%s** __%s__\n\n",
420          pht('Created a new Differential diff:'),
421          pht('Diff URI:'),
422          $diff_info['uri']);
423      } else {
424        $human = ob_get_clean();
425        echo json_encode(array(
426          'diffURI' => $diff_info['uri'],
427          'diffID'  => $this->getDiffID(),
428          'human'   => $human,
429        ))."\n";
430        ob_start();
431      }
432
433      if ($this->shouldOpenCreatedObjectsInBrowser()) {
434        $this->openURIsInBrowser(array($diff_info['uri']));
435      }
436    } else {
437      $is_draft = $this->getArgument('draft');
438      $revision['diffid'] = $this->getDiffID();
439
440      if ($commit_message->getRevisionID()) {
441        if ($is_draft) {
442          // TODO: In at least some cases, we could raise this earlier in the
443          // workflow to save users some time before the workflow aborts.
444          if ($this->revisionIsDraft) {
445            $this->writeWarn(
446              pht('ALREADY A DRAFT'),
447              pht(
448                'You are updating a revision ("%s") with the "--draft" flag, '.
449                'but this revision is already a draft. You only need to '.
450                'provide the "--draft" flag when creating a revision. Draft '.
451                'revisions are not published until you explicitly request '.
452                'review from the web UI.',
453                $commit_message->getRevisionMonogram()));
454          } else {
455            throw new ArcanistUsageException(
456              pht(
457                'You are updating a revision ("%s") with the "--draft" flag, '.
458                'but this revision has already been published for review. '.
459                'You can not turn a revision back into a draft once it has '.
460                'been published.',
461                $commit_message->getRevisionMonogram()));
462          }
463        }
464
465        $result = $conduit->callMethodSynchronous(
466          'differential.updaterevision',
467          $revision);
468
469        foreach (array('edit-messages.json', 'update-messages.json') as $file) {
470          $messages = $this->readScratchJSONFile($file);
471          unset($messages[$revision['id']]);
472          $this->writeScratchJSONFile($file, $messages);
473        }
474
475        $result_uri = $result['uri'];
476        $result_id = $result['revisionid'];
477
478        echo pht('Updated an existing Differential revision:')."\n";
479      } else {
480        // NOTE: We're either using "differential.revision.edit" (preferred)
481        // if we can, or falling back to "differential.createrevision"
482        // (the older way) if not.
483
484        $xactions = $this->revisionTransactions;
485        if ($xactions) {
486          $xactions[] = array(
487            'type' => 'update',
488            'value' => $diff_info['phid'],
489          );
490
491          if ($is_draft) {
492            $xactions[] = array(
493              'type' => 'draft',
494              'value' => true,
495            );
496          }
497
498          $result = $conduit->callMethodSynchronous(
499            'differential.revision.edit',
500            array(
501              'transactions' => $xactions,
502            ));
503
504          $result_id = idxv($result, array('object', 'id'));
505          if (!$result_id) {
506            throw new Exception(
507              pht(
508                'Expected a revision ID to be returned by '.
509                '"differential.revision.edit".'));
510          }
511
512          // TODO: This is hacky, but we don't currently receive a URI back
513          // from "differential.revision.edit".
514          $result_uri = id(new PhutilURI($this->getConduitURI()))
515            ->setPath('/D'.$result_id);
516        } else {
517          if ($is_draft) {
518            throw new ArcanistUsageException(
519              pht(
520                'You have specified "--draft", but the version of Phabricator '.
521                'on the server is too old to support draft revisions. Omit '.
522                'the flag or upgrade the server software.'));
523          }
524
525          $revision = $this->dispatchWillCreateRevisionEvent($revision);
526
527          $result = $conduit->callMethodSynchronous(
528            'differential.createrevision',
529            $revision);
530
531          $result_uri = $result['uri'];
532          $result_id = $result['revisionid'];
533        }
534
535        $revised_message = $conduit->callMethodSynchronous(
536          'differential.getcommitmessage',
537          array(
538            'revision_id' => $result_id,
539          ));
540
541        if ($this->shouldAmend()) {
542          $repository_api = $this->getRepositoryAPI();
543          if ($repository_api->supportsAmend()) {
544            echo pht('Updating commit message...')."\n";
545            $repository_api->amendCommit($revised_message);
546          } else {
547            echo pht(
548              'Commit message was not amended. Amending commit message is '.
549              'only supported in git and hg (version 2.2 or newer)');
550          }
551        }
552
553        echo pht('Created a new Differential revision:')."\n";
554      }
555
556      $uri = $result_uri;
557      echo phutil_console_format(
558        "        **%s** __%s__\n\n",
559        pht('Revision URI:'),
560        $uri);
561
562      if ($this->shouldOpenCreatedObjectsInBrowser()) {
563        $this->openURIsInBrowser(array($uri));
564      }
565    }
566
567    echo pht('Included changes:')."\n";
568    foreach ($changes as $change) {
569      echo '  '.$change->renderTextSummary()."\n";
570    }
571
572    if ($output_json) {
573      ob_get_clean();
574    }
575
576    $this->removeScratchFile('create-message');
577
578    return 0;
579  }
580
581  private function runRepositoryAPISetup() {
582    if (!$this->requiresRepositoryAPI()) {
583      return;
584    }
585
586    $repository_api = $this->getRepositoryAPI();
587
588    $repository_api->setBaseCommitArgumentRules(
589      $this->getArgument('base', ''));
590
591    if ($repository_api->supportsCommitRanges()) {
592      $this->parseBaseCommitArgument($this->getArgument('paths'));
593    }
594
595    $head_commit = $this->getArgument('head');
596    if ($head_commit !== null) {
597      $repository_api->setHeadCommit($head_commit);
598    }
599
600  }
601
602  private function runDiffSetupBasics() {
603    $output_json = $this->getArgument('json');
604    if ($output_json) {
605      // TODO: We should move this to a higher-level and put an indirection
606      // layer between echoing stuff and stdout.
607      ob_start();
608    }
609
610    if ($this->requiresWorkingCopy()) {
611      $repository_api = $this->getRepositoryAPI();
612      if ($this->getArgument('add-all')) {
613        $this->setCommitMode(self::COMMIT_ENABLE);
614      } else if ($this->getArgument('uncommitted')) {
615        $this->setCommitMode(self::COMMIT_DISABLE);
616      } else {
617        $this->setCommitMode(self::COMMIT_ALLOW);
618      }
619      if ($repository_api instanceof ArcanistSubversionAPI) {
620        $repository_api->limitStatusToPaths($this->getArgument('paths'));
621      }
622      if (!$this->getArgument('head')) {
623        $this->requireCleanWorkingCopy();
624      }
625    }
626
627    $this->dispatchEvent(
628      ArcanistEventType::TYPE_DIFF_DIDCOLLECTCHANGES,
629      array());
630  }
631
632  private function buildRevisionFromCommitMessage(
633    ArcanistDifferentialCommitMessage $message) {
634
635    $conduit = $this->getConduit();
636
637    $revision_id = $message->getRevisionID();
638    $revision = array(
639      'fields' => $message->getFields(),
640    );
641    $xactions = $message->getTransactions();
642
643    if ($revision_id) {
644
645      // With '--verbatim', pass the (possibly modified) local fields. This
646      // allows the user to edit some fields (like "title" and "summary")
647      // locally without '--edit' and have changes automatically synchronized.
648      // Without '--verbatim', we do not update the revision to reflect local
649      // commit message changes.
650      if ($this->getArgument('verbatim')) {
651        $use_fields = $message->getFields();
652      } else {
653        $use_fields = array();
654      }
655
656      $should_edit = $this->getArgument('edit');
657      $edit_messages = $this->readScratchJSONFile('edit-messages.json');
658      $remote_corpus = idx($edit_messages, $revision_id);
659
660      if (!$should_edit || !$remote_corpus || $use_fields) {
661        if ($this->commitMessageFromRevision) {
662          $remote_corpus = $this->commitMessageFromRevision;
663        } else {
664          $remote_corpus = $conduit->callMethodSynchronous(
665            'differential.getcommitmessage',
666            array(
667              'revision_id' => $revision_id,
668              'edit'        => 'edit',
669              'fields'      => $use_fields,
670            ));
671        }
672      }
673
674      if ($should_edit) {
675        $edited = $this->newInteractiveEditor($remote_corpus)
676          ->setName('differential-edit-revision-info')
677          ->editInteractively();
678        if ($edited != $remote_corpus) {
679          $remote_corpus = $edited;
680          $edit_messages[$revision_id] = $remote_corpus;
681          $this->writeScratchJSONFile('edit-messages.json', $edit_messages);
682        }
683      }
684
685      if ($this->commitMessageFromRevision == $remote_corpus) {
686        $new_message = $message;
687      } else {
688        $remote_corpus = ArcanistCommentRemover::removeComments(
689          $remote_corpus);
690        $new_message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
691          $remote_corpus);
692        $new_message->pullDataFromConduit($conduit);
693      }
694
695      $revision['fields'] = $new_message->getFields();
696      $xactions = $new_message->getTransactions();
697
698      $revision['id'] = $revision_id;
699      $this->revisionID = $revision_id;
700
701      $revision['message'] = $this->getArgument('message');
702      if (!strlen($revision['message'])) {
703        $update_messages = $this->readScratchJSONFile('update-messages.json');
704
705        $update_messages[$revision_id] = $this->getUpdateMessage(
706          $revision['fields'],
707          idx($update_messages, $revision_id));
708
709        $revision['message'] = ArcanistCommentRemover::removeComments(
710          $update_messages[$revision_id]);
711        if (!strlen(trim($revision['message']))) {
712          throw new ArcanistUserAbortException();
713        }
714
715        $this->writeScratchJSONFile('update-messages.json', $update_messages);
716      }
717    }
718
719    $this->revisionTransactions = $xactions;
720
721    return $revision;
722  }
723
724  protected function shouldOnlyCreateDiff() {
725    if ($this->getArgument('create')) {
726      return false;
727    }
728
729    if ($this->getArgument('update')) {
730      return false;
731    }
732
733    if ($this->isRawDiffSource()) {
734      return true;
735    }
736
737    return $this->getArgument('only');
738  }
739
740  private function generateAffectedPaths() {
741    if ($this->isRawDiffSource()) {
742      return array();
743    }
744
745    $repository_api = $this->getRepositoryAPI();
746    if ($repository_api instanceof ArcanistSubversionAPI) {
747      $file_list = new FileList($this->getArgument('paths', array()));
748      $paths = $repository_api->getSVNStatus($externals = true);
749      foreach ($paths as $path => $mask) {
750        if (!$file_list->contains($repository_api->getPath($path), true)) {
751          unset($paths[$path]);
752        }
753      }
754
755      $warn_externals = array();
756      foreach ($paths as $path => $mask) {
757        $any_mod = ($mask & ArcanistRepositoryAPI::FLAG_ADDED) ||
758                   ($mask & ArcanistRepositoryAPI::FLAG_MODIFIED) ||
759                   ($mask & ArcanistRepositoryAPI::FLAG_DELETED);
760        if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) {
761          unset($paths[$path]);
762          if ($any_mod) {
763            $warn_externals[] = $path;
764          }
765        }
766      }
767
768      if ($warn_externals && !$this->hasWarnedExternals) {
769        echo phutil_console_format(
770          "%s\n\n%s\n\n",
771          pht(
772            "The working copy includes changes to '%s' paths. These ".
773            "changes will not be included in the diff because SVN can not ".
774            "commit 'svn:externals' changes alongside normal changes.",
775            'svn:externals'),
776          pht(
777            "Modified '%s' files:",
778            'svn:externals'),
779          phutil_console_wrap(implode("\n", $warn_externals), 8));
780        $prompt = pht('Generate a diff (with just local changes) anyway?');
781        if (!phutil_console_confirm($prompt)) {
782          throw new ArcanistUserAbortException();
783        } else {
784          $this->hasWarnedExternals = true;
785        }
786      }
787
788    } else {
789      $paths = $repository_api->getWorkingCopyStatus();
790    }
791
792    foreach ($paths as $path => $mask) {
793      if ($mask & ArcanistRepositoryAPI::FLAG_UNTRACKED) {
794        unset($paths[$path]);
795      }
796    }
797
798    return $paths;
799  }
800
801
802  protected function generateChanges() {
803    $parser = $this->newDiffParser();
804
805    $is_raw = $this->isRawDiffSource();
806    if ($is_raw) {
807
808      if ($this->getArgument('raw')) {
809        fwrite(STDERR, pht('Reading diff from stdin...')."\n");
810        $raw_diff = file_get_contents('php://stdin');
811      } else if ($this->getArgument('raw-command')) {
812        list($raw_diff) = execx('%C', $this->getArgument('raw-command'));
813      } else {
814        throw new Exception(pht('Unknown raw diff source.'));
815      }
816
817      $changes = $parser->parseDiff($raw_diff);
818      foreach ($changes as $key => $change) {
819        // Remove "message" changes, e.g. from "git show".
820        if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
821          unset($changes[$key]);
822        }
823      }
824      return $changes;
825    }
826
827    $repository_api = $this->getRepositoryAPI();
828
829    if ($repository_api instanceof ArcanistSubversionAPI) {
830      $paths = $this->generateAffectedPaths();
831      $this->primeSubversionWorkingCopyData($paths);
832
833      // Check to make sure the user is diffing from a consistent base revision.
834      // This is mostly just an abuse sanity check because it's silly to do this
835      // and makes the code more difficult to effectively review, but it also
836      // affects patches and makes them nonportable.
837      $bases = $repository_api->getSVNBaseRevisions();
838
839      // Remove all files with baserev "0"; these files are new.
840      foreach ($bases as $path => $baserev) {
841        if ($bases[$path] <= 0) {
842          unset($bases[$path]);
843        }
844      }
845
846      if ($bases) {
847        $rev = reset($bases);
848
849        $revlist = array();
850        foreach ($bases as $path => $baserev) {
851          $revlist[] = '    '.pht('Revision %s, %s', $baserev, $path);
852        }
853        $revlist = implode("\n", $revlist);
854
855        foreach ($bases as $path => $baserev) {
856          if ($baserev !== $rev) {
857            throw new ArcanistUsageException(
858              pht(
859                "Base revisions of changed paths are mismatched. Update all ".
860                "paths to the same base revision before creating a diff: ".
861                "\n\n%s",
862                $revlist));
863          }
864        }
865
866        // If you have a change which affects several files, all of which are
867        // at a consistent base revision, treat that revision as the effective
868        // base revision. The use case here is that you made a change to some
869        // file, which updates it to HEAD, but want to be able to change it
870        // again without updating the entire working copy. This is a little
871        // sketchy but it arises in Facebook Ops workflows with config files and
872        // doesn't have any real material tradeoffs (e.g., these patches are
873        // perfectly applyable).
874        $repository_api->overrideSVNBaseRevisionNumber($rev);
875      }
876
877      $changes = $parser->parseSubversionDiff(
878        $repository_api,
879        $paths);
880    } else if ($repository_api instanceof ArcanistGitAPI) {
881      $diff = $repository_api->getFullGitDiff(
882        $repository_api->getBaseCommit(),
883        $repository_api->getHeadCommit());
884      if (!strlen($diff)) {
885        throw new ArcanistUsageException(
886          pht('No changes found. (Did you specify the wrong commit range?)'));
887      }
888      $changes = $parser->parseDiff($diff);
889    } else if ($repository_api instanceof ArcanistMercurialAPI) {
890      $diff = $repository_api->getFullMercurialDiff();
891      if (!strlen($diff)) {
892        throw new ArcanistUsageException(
893          pht('No changes found. (Did you specify the wrong commit range?)'));
894      }
895      $changes = $parser->parseDiff($diff);
896    } else {
897      throw new Exception(pht('Repository API is not supported.'));
898    }
899
900    $limit = 1024 * 1024 * 4;
901    foreach ($changes as $change) {
902      $size = 0;
903      foreach ($change->getHunks() as $hunk) {
904        $size += strlen($hunk->getCorpus());
905      }
906      if ($size > $limit) {
907        $byte_warning = pht(
908          "Diff for '%s' with context is %s bytes in length. ".
909          "Generally, source changes should not be this large.",
910          $change->getCurrentPath(),
911          new PhutilNumber($size));
912        if ($repository_api instanceof ArcanistSubversionAPI) {
913          throw new ArcanistUsageException(
914            $byte_warning.' '.
915            pht(
916              "If the file is not a text file, mark it as binary with:".
917              "\n\n  $ %s\n",
918              'svn propset svn:mime-type application/octet-stream <filename>'));
919        } else {
920          $confirm = $byte_warning.' '.pht(
921            "If the file is not a text file, you can mark it 'binary'. ".
922            "Mark this file as 'binary' and continue?");
923          if (phutil_console_confirm($confirm)) {
924            $change->convertToBinaryChange($repository_api);
925          } else {
926            throw new ArcanistUsageException(
927              pht('Aborted generation of gigantic diff.'));
928          }
929        }
930      }
931    }
932
933    $utf8_problems = array();
934    foreach ($changes as $change) {
935      foreach ($change->getHunks() as $hunk) {
936        $corpus = $hunk->getCorpus();
937        if (!phutil_is_utf8($corpus)) {
938
939          // If this corpus is heuristically binary, don't try to convert it.
940          // mb_check_encoding() and mb_convert_encoding() are both very very
941          // liberal about what they're willing to process.
942          $is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus);
943          if (!$is_binary) {
944
945            try {
946              $try_encoding = $this->getRepositoryEncoding();
947            } catch (ConduitClientException $e) {
948              if ($e->getErrorCode() == 'ERR-BAD-ARCANIST-PROJECT') {
949                echo phutil_console_wrap(
950                  pht('Lookup of encoding in arcanist project failed: %s',
951                      $e->getMessage())."\n");
952              } else {
953                throw $e;
954              }
955            }
956
957            if ($try_encoding) {
958              $corpus = phutil_utf8_convert($corpus, 'UTF-8', $try_encoding);
959              $name = $change->getCurrentPath();
960              if (phutil_is_utf8($corpus)) {
961                $this->writeStatusMessage(
962                  pht(
963                    "Converted a '%s' hunk from '%s' to UTF-8.\n",
964                    $name,
965                    $try_encoding));
966                $hunk->setCorpus($corpus);
967                continue;
968              }
969            }
970          }
971          $utf8_problems[] = $change;
972          break;
973        }
974      }
975    }
976
977    // If there are non-binary files which aren't valid UTF-8, warn the user
978    // and treat them as binary changes. See D327 for discussion of why Arcanist
979    // has this behavior.
980    if ($utf8_problems) {
981      $utf8_warning =
982        sprintf(
983          "%s\n\n%s\n\n    %s\n",
984          pht(
985            'This diff includes %s file(s) which are not valid UTF-8 (they '.
986            'contain invalid byte sequences). You can either stop this '.
987            'workflow and fix these files, or continue. If you continue, '.
988            'these files will be marked as binary.',
989            phutil_count($utf8_problems)),
990          pht(
991            "You can learn more about how Phabricator handles character ".
992            "encodings (and how to configure encoding settings and detect and ".
993            "correct encoding problems) by reading 'User Guide: UTF-8 and ".
994            "Character Encoding' in the Phabricator documentation."),
995          pht(
996            '%s AFFECTED FILE(S)',
997            phutil_count($utf8_problems)));
998      $confirm = pht(
999        'Do you want to mark these %s file(s) as binary and continue?',
1000        phutil_count($utf8_problems));
1001
1002      echo phutil_console_format(
1003        "**%s**\n",
1004        pht('Invalid Content Encoding (Non-UTF8)'));
1005      echo phutil_console_wrap($utf8_warning);
1006
1007      $file_list = mpull($utf8_problems, 'getCurrentPath');
1008      $file_list = '    '.implode("\n    ", $file_list);
1009      echo $file_list;
1010
1011      if (!phutil_console_confirm($confirm, $default_no = false)) {
1012        throw new ArcanistUsageException(pht('Aborted workflow to fix UTF-8.'));
1013      } else {
1014        foreach ($utf8_problems as $change) {
1015          $change->convertToBinaryChange($repository_api);
1016        }
1017      }
1018    }
1019
1020    $this->uploadFilesForChanges($changes);
1021
1022    return $changes;
1023  }
1024
1025  private function getGitParentLogInfo() {
1026    $info = array(
1027      'parent'        => null,
1028      'base_revision' => null,
1029      'base_path'     => null,
1030      'uuid'          => null,
1031    );
1032
1033    $repository_api = $this->getRepositoryAPI();
1034
1035    $parser = $this->newDiffParser();
1036    $history_messages = $repository_api->getGitHistoryLog();
1037    if (!$history_messages) {
1038      // This can occur on the initial commit.
1039      return $info;
1040    }
1041    $history_messages = $parser->parseDiff($history_messages);
1042
1043    foreach ($history_messages as $key => $change) {
1044      try {
1045        $message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
1046          $change->getMetadata('message'));
1047        if ($message->getRevisionID() && $info['parent'] === null) {
1048          $info['parent'] = $message->getRevisionID();
1049        }
1050        if ($message->getGitSVNBaseRevision() &&
1051            $info['base_revision'] === null) {
1052          $info['base_revision'] = $message->getGitSVNBaseRevision();
1053          $info['base_path']     = $message->getGitSVNBasePath();
1054        }
1055        if ($message->getGitSVNUUID()) {
1056          $info['uuid'] = $message->getGitSVNUUID();
1057        }
1058        if ($info['parent'] && $info['base_revision']) {
1059          break;
1060        }
1061      } catch (ArcanistDifferentialCommitMessageParserException $ex) {
1062        // Ignore.
1063      } catch (ArcanistUsageException $ex) {
1064        // Ignore an invalid Differential Revision field in the parent commit
1065      }
1066    }
1067
1068    return $info;
1069  }
1070
1071  protected function primeSubversionWorkingCopyData($paths) {
1072    $repository_api = $this->getRepositoryAPI();
1073
1074    $futures = array();
1075    $targets = array();
1076    foreach ($paths as $path => $mask) {
1077      $futures[] = $repository_api->buildDiffFuture($path);
1078      $targets[] = array('command' => 'diff', 'path' => $path);
1079      $futures[] = $repository_api->buildInfoFuture($path);
1080      $targets[] = array('command' => 'info', 'path' => $path);
1081    }
1082
1083    $futures = id(new FutureIterator($futures))
1084      ->limit(8);
1085    foreach ($futures as $key => $future) {
1086      $target = $targets[$key];
1087      if ($target['command'] == 'diff') {
1088        $repository_api->primeSVNDiffResult(
1089          $target['path'],
1090          $future->resolve());
1091      } else {
1092        $repository_api->primeSVNInfoResult(
1093          $target['path'],
1094          $future->resolve());
1095      }
1096    }
1097  }
1098
1099  private function shouldAmend() {
1100    if ($this->isRawDiffSource()) {
1101      return false;
1102    }
1103
1104    if ($this->getArgument('no-amend')) {
1105      return false;
1106    }
1107
1108    if ($this->getArgument('head') !== null) {
1109      return false;
1110    }
1111
1112    // Run this last: with --raw or --raw-command, we won't have a repository
1113    // API.
1114    if ($this->isHistoryImmutable()) {
1115      return false;
1116    }
1117
1118    return true;
1119  }
1120
1121
1122/* -(  Lint and Unit Tests  )------------------------------------------------ */
1123
1124
1125  /**
1126   * @task lintunit
1127   */
1128  private function runLintUnit() {
1129    $lint_result = $this->runLint();
1130    $unit_result = $this->runUnit();
1131    return array(
1132      'lintResult' => $lint_result,
1133      'unresolvedLint' => $this->unresolvedLint,
1134      'unitResult' => $unit_result,
1135      'testResults' => $this->testResults,
1136    );
1137  }
1138
1139
1140  /**
1141   * @task lintunit
1142   */
1143  private function runLint() {
1144    if ($this->getArgument('nolint') ||
1145        $this->isRawDiffSource() ||
1146        $this->getArgument('head')) {
1147      return ArcanistLintWorkflow::RESULT_SKIP;
1148    }
1149
1150    $repository_api = $this->getRepositoryAPI();
1151
1152    $this->console->writeOut("%s\n", pht('Linting...'));
1153    try {
1154      $argv = $this->getPassthruArgumentsAsArgv('lint');
1155      if ($repository_api->supportsCommitRanges()) {
1156        $argv[] = '--rev';
1157        $argv[] = $repository_api->getBaseCommit();
1158      }
1159
1160      $lint_workflow = $this->buildChildWorkflow('lint', $argv);
1161
1162      if ($this->shouldAmend()) {
1163        // TODO: We should offer to create a checkpoint commit.
1164        $lint_workflow->setShouldAmendChanges(true);
1165      }
1166
1167      $lint_result = $lint_workflow->run();
1168
1169      switch ($lint_result) {
1170        case ArcanistLintWorkflow::RESULT_OKAY:
1171          $this->console->writeOut(
1172            "<bg:green>** %s **</bg> %s\n",
1173            pht('LINT OKAY'),
1174            pht('No lint problems.'));
1175          break;
1176        case ArcanistLintWorkflow::RESULT_WARNINGS:
1177          $this->console->writeOut(
1178            "<bg:yellow>** %s **</bg> %s\n",
1179            pht('LINT MESSAGES'),
1180            pht('Lint issued unresolved warnings.'));
1181          break;
1182        case ArcanistLintWorkflow::RESULT_ERRORS:
1183          $this->console->writeOut(
1184            "<bg:red>** %s **</bg> %s\n",
1185            pht('LINT ERRORS'),
1186            pht('Lint raised errors!'));
1187          break;
1188      }
1189
1190      $this->unresolvedLint = array();
1191      foreach ($lint_workflow->getUnresolvedMessages() as $message) {
1192        $this->unresolvedLint[] = $message->toDictionary();
1193      }
1194
1195      return $lint_result;
1196    } catch (ArcanistNoEngineException $ex) {
1197      $this->console->writeOut(
1198        "%s\n",
1199        pht('No lint engine configured for this project.'));
1200    } catch (ArcanistNoEffectException $ex) {
1201      $this->console->writeOut("%s\n", $ex->getMessage());
1202    }
1203
1204    return null;
1205  }
1206
1207
1208  /**
1209   * @task lintunit
1210   */
1211  private function runUnit() {
1212    if ($this->getArgument('nounit') ||
1213        $this->isRawDiffSource() ||
1214        $this->getArgument('head')) {
1215      return ArcanistUnitWorkflow::RESULT_SKIP;
1216    }
1217
1218    $repository_api = $this->getRepositoryAPI();
1219
1220    $this->console->writeOut("%s\n", pht('Running unit tests...'));
1221    try {
1222      $argv = $this->getPassthruArgumentsAsArgv('unit');
1223      if ($repository_api->supportsCommitRanges()) {
1224        $argv[] = '--rev';
1225        $argv[] = $repository_api->getBaseCommit();
1226      }
1227      $unit_workflow = $this->buildChildWorkflow('unit', $argv);
1228      $unit_result = $unit_workflow->run();
1229
1230      switch ($unit_result) {
1231        case ArcanistUnitWorkflow::RESULT_OKAY:
1232          $this->console->writeOut(
1233            "<bg:green>** %s **</bg> %s\n",
1234            pht('UNIT OKAY'),
1235            pht('No unit test failures.'));
1236          break;
1237        case ArcanistUnitWorkflow::RESULT_UNSOUND:
1238          $continue = phutil_console_confirm(
1239            pht(
1240              'Unit test results included failures, but all failing tests '.
1241              'are known to be unsound. Ignore unsound test failures?'));
1242          if (!$continue) {
1243            throw new ArcanistUserAbortException();
1244          }
1245
1246          echo phutil_console_format(
1247            "<bg:yellow>** %s **</bg> %s\n",
1248            pht('UNIT UNSOUND'),
1249            pht(
1250              'Unit testing raised errors, but all '.
1251              'failing tests are unsound.'));
1252          break;
1253        case ArcanistUnitWorkflow::RESULT_FAIL:
1254          $this->console->writeOut(
1255            "<bg:red>** %s **</bg> %s\n",
1256            pht('UNIT ERRORS'),
1257            pht('Unit testing raised errors!'));
1258          break;
1259      }
1260
1261      $this->testResults = array();
1262      foreach ($unit_workflow->getTestResults() as $test) {
1263        $this->testResults[] = $test->toDictionary();
1264      }
1265
1266      return $unit_result;
1267    } catch (ArcanistNoEngineException $ex) {
1268      $this->console->writeOut(
1269        "%s\n",
1270        pht('No unit test engine is configured for this project.'));
1271    } catch (ArcanistNoEffectException $ex) {
1272      $this->console->writeOut("%s\n", $ex->getMessage());
1273    }
1274
1275    return null;
1276  }
1277
1278  public function getTestResults() {
1279    return $this->testResults;
1280  }
1281
1282
1283/* -(  Commit and Update Messages  )----------------------------------------- */
1284
1285
1286  /**
1287   * @task message
1288   */
1289  private function buildCommitMessage() {
1290    if ($this->getArgument('only')) {
1291      return null;
1292    }
1293
1294    $is_create = $this->getArgument('create');
1295    $is_update = $this->getArgument('update');
1296    $is_raw = $this->isRawDiffSource();
1297    $is_verbatim = $this->getArgument('verbatim');
1298
1299    if ($is_verbatim) {
1300      return $this->getCommitMessageFromUser();
1301    }
1302
1303
1304    if (!$is_raw && !$is_create && !$is_update) {
1305      $repository_api = $this->getRepositoryAPI();
1306      $revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
1307        $this->getConduit(),
1308        array(
1309          'authors' => array($this->getUserPHID()),
1310          'status'  => 'status-open',
1311        ));
1312      if (!$revisions) {
1313        $is_create = true;
1314      } else if (count($revisions) == 1) {
1315        $revision = head($revisions);
1316        $is_update = $revision['id'];
1317      } else {
1318        throw new ArcanistUsageException(
1319          pht(
1320            "There are several revisions which match the working copy:\n\n%s\n".
1321            "Use '%s' to choose one, or '%s' to create a new revision.",
1322            $this->renderRevisionList($revisions),
1323            '--update',
1324            '--create'));
1325      }
1326    }
1327
1328    $message = null;
1329    if ($is_create) {
1330      $message_file = $this->getArgument('message-file');
1331      if ($message_file) {
1332        return $this->getCommitMessageFromFile($message_file);
1333      } else {
1334        return $this->getCommitMessageFromUser();
1335      }
1336    } else if ($is_update) {
1337      $revision_id = $this->normalizeRevisionID($is_update);
1338      if (!is_numeric($revision_id)) {
1339        throw new ArcanistUsageException(
1340          pht(
1341            'Parameter to %s must be a Differential Revision number.',
1342            '--update'));
1343      }
1344      return $this->getCommitMessageFromRevision($revision_id);
1345    } else {
1346      // This is --raw without enough info to create a revision, so force just
1347      // a diff.
1348      return null;
1349    }
1350  }
1351
1352
1353  /**
1354   * @task message
1355   */
1356  private function getCommitMessageFromUser() {
1357    $conduit = $this->getConduit();
1358
1359    $template = null;
1360
1361    if (!$this->getArgument('verbatim')) {
1362      $saved = $this->readScratchFile('create-message');
1363      if ($saved) {
1364        $where = $this->getReadableScratchFilePath('create-message');
1365
1366        $preview = explode("\n", $saved);
1367        $preview = array_shift($preview);
1368        $preview = trim($preview);
1369        $preview = id(new PhutilUTF8StringTruncator())
1370          ->setMaximumGlyphs(64)
1371          ->truncateString($preview);
1372
1373        if ($preview) {
1374          $preview = pht('Message begins:')."\n\n       {$preview}\n\n";
1375        } else {
1376          $preview = null;
1377        }
1378
1379        echo pht(
1380          "You have a saved revision message in '%s'.\n%s".
1381          "You can use this message, or discard it.",
1382          $where,
1383          $preview);
1384
1385        $use = phutil_console_confirm(
1386          pht('Do you want to use this message?'),
1387          $default_no = false);
1388        if ($use) {
1389          $template = $saved;
1390        } else {
1391          $this->removeScratchFile('create-message');
1392        }
1393      }
1394    }
1395
1396    $template_is_default = false;
1397    $notes = array();
1398    $included = array();
1399
1400    list($fields, $notes, $included_commits) = $this->getDefaultCreateFields();
1401    if ($template) {
1402      $fields = array();
1403      $notes = array();
1404    } else {
1405      if (!$fields) {
1406        $template_is_default = true;
1407      }
1408
1409      if ($notes) {
1410        $commit = head($this->getRepositoryAPI()->getLocalCommitInformation());
1411        $template = $commit['message'];
1412      } else {
1413        $template = $conduit->callMethodSynchronous(
1414          'differential.getcommitmessage',
1415          array(
1416            'revision_id' => null,
1417            'edit'        => 'create',
1418            'fields'      => $fields,
1419          ));
1420      }
1421    }
1422
1423    $old_message = $template;
1424
1425    $included = array();
1426    if ($included_commits) {
1427      foreach ($included_commits as $commit) {
1428        $included[] = '        '.$commit;
1429      }
1430
1431      if (!$this->isRawDiffSource()) {
1432        $message = pht(
1433          'Included commits in branch %s:',
1434          $this->getRepositoryAPI()->getBranchName());
1435      } else {
1436        $message = pht('Included commits:');
1437      }
1438      $included = array_merge(
1439        array(
1440          '',
1441          $message,
1442          '',
1443        ),
1444        $included);
1445    }
1446
1447    $issues = array_merge(
1448      array(
1449        pht('NEW DIFFERENTIAL REVISION'),
1450        pht('Describe the changes in this new revision.'),
1451      ),
1452      $included,
1453      array(
1454        '',
1455        pht(
1456          'arc could not identify any existing revision in your working copy.'),
1457        pht('If you intended to update an existing revision, use:'),
1458        '',
1459        '  $ arc diff --update <revision>',
1460      ));
1461    if ($notes) {
1462      $issues = array_merge($issues, array(''), $notes);
1463    }
1464
1465    $done = false;
1466    $first = true;
1467    while (!$done) {
1468      $template = rtrim($template, "\r\n")."\n\n";
1469      foreach ($issues as $issue) {
1470        $template .= rtrim('# '.$issue)."\n";
1471      }
1472      $template .= "\n";
1473
1474      if ($first && $this->getArgument('verbatim') && !$template_is_default) {
1475        $new_template = $template;
1476      } else {
1477        $new_template = $this->newInteractiveEditor($template)
1478          ->setName('new-commit')
1479          ->editInteractively();
1480      }
1481      $first = false;
1482
1483      if ($template_is_default && ($new_template == $template)) {
1484        throw new ArcanistUsageException(pht('Template not edited.'));
1485      }
1486
1487      $template = ArcanistCommentRemover::removeComments($new_template);
1488
1489      // With --raw-command, we may not have a repository API.
1490      if ($this->hasRepositoryAPI()) {
1491        $repository_api = $this->getRepositoryAPI();
1492        // special check for whether to amend here. optimizes a common git
1493        // workflow. we can't do this for mercurial because the mq extension
1494        // is popular and incompatible with hg commit --amend ; see T2011.
1495        $should_amend = (count($included_commits) == 1 &&
1496                         $repository_api instanceof ArcanistGitAPI &&
1497                         $this->shouldAmend());
1498      } else {
1499        $should_amend = false;
1500      }
1501
1502      if ($should_amend) {
1503        $wrote = (rtrim($old_message) != rtrim($template));
1504        if ($wrote) {
1505          $repository_api->amendCommit($template);
1506          $where = pht('commit message');
1507        }
1508      } else {
1509        $wrote = $this->writeScratchFile('create-message', $template);
1510        $where = "'".$this->getReadableScratchFilePath('create-message')."'";
1511      }
1512
1513      try {
1514        $message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
1515          $template);
1516        $message->pullDataFromConduit($conduit);
1517        $this->validateCommitMessage($message);
1518        $done = true;
1519      } catch (ArcanistDifferentialCommitMessageParserException $ex) {
1520        echo pht('Commit message has errors:')."\n\n";
1521        $issues = array(pht('Resolve these errors:'));
1522        foreach ($ex->getParserErrors() as $error) {
1523          echo phutil_console_wrap("- ".$error."\n", 6);
1524          $issues[] = '  - '.$error;
1525        }
1526        echo "\n";
1527        echo pht('You must resolve these errors to continue.');
1528        $again = phutil_console_confirm(
1529          pht('Do you want to edit the message?'),
1530          $default_no = false);
1531        if ($again) {
1532          // Keep going.
1533        } else {
1534          $saved = null;
1535          if ($wrote) {
1536            $saved = pht('A copy was saved to %s.', $where);
1537          }
1538          throw new ArcanistUsageException(
1539            pht('Message has unresolved errors.')." {$saved}");
1540        }
1541      } catch (Exception $ex) {
1542        if ($wrote) {
1543          echo phutil_console_wrap(pht('(Message saved to %s.)', $where)."\n");
1544        }
1545        throw $ex;
1546      }
1547    }
1548
1549    return $message;
1550  }
1551
1552
1553  /**
1554   * @task message
1555   */
1556  private function getCommitMessageFromFile($file) {
1557    $conduit = $this->getConduit();
1558
1559    $data = Filesystem::readFile($file);
1560    $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($data);
1561    $message->pullDataFromConduit($conduit);
1562
1563    $this->validateCommitMessage($message);
1564
1565    return $message;
1566  }
1567
1568
1569  /**
1570   * @task message
1571   */
1572  private function getCommitMessageFromRevision($revision_id) {
1573    $id = $revision_id;
1574
1575    $revision = $this->getConduit()->callMethodSynchronous(
1576      'differential.query',
1577      array(
1578        'ids' => array($id),
1579      ));
1580    $revision = head($revision);
1581
1582    if (!$revision) {
1583      throw new ArcanistUsageException(
1584        pht(
1585          "Revision '%s' does not exist!",
1586          $revision_id));
1587    }
1588
1589    $this->checkRevisionOwnership($revision);
1590
1591    // TODO: Save this status to improve a prompt later. See PHI458. This is
1592    // extra awful until we move to "differential.revision.search" because
1593    // the "differential.query" method doesn't return a real draft status for
1594    // compatibility.
1595    $this->revisionIsDraft = (idx($revision, 'statusName') === 'Draft');
1596
1597    $message = $this->getConduit()->callMethodSynchronous(
1598      'differential.getcommitmessage',
1599      array(
1600        'revision_id' => $id,
1601        'edit'        => false,
1602      ));
1603    $this->commitMessageFromRevision = $message;
1604
1605    $obj = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
1606    $obj->pullDataFromConduit($this->getConduit());
1607
1608    return $obj;
1609  }
1610
1611
1612  /**
1613   * @task message
1614   */
1615  private function validateCommitMessage(
1616    ArcanistDifferentialCommitMessage $message) {
1617    $futures = array();
1618
1619    $revision_id = $message->getRevisionID();
1620    if ($revision_id) {
1621      $futures['revision'] = $this->getConduit()->callMethod(
1622        'differential.query',
1623        array(
1624          'ids' => array($revision_id),
1625        ));
1626    }
1627
1628    $reviewers = $message->getFieldValue('reviewerPHIDs');
1629    if ($reviewers) {
1630      $futures['reviewers'] = $this->getConduit()->callMethod(
1631        'user.query',
1632        array(
1633          'phids' => $reviewers,
1634        ));
1635    }
1636
1637    foreach (new FutureIterator($futures) as $key => $future) {
1638      $result = $future->resolve();
1639      switch ($key) {
1640        case 'revision':
1641          if (empty($result)) {
1642            throw new ArcanistUsageException(
1643              pht(
1644                'There is no revision %s.',
1645                "D{$revision_id}"));
1646          }
1647          $this->checkRevisionOwnership(head($result));
1648          break;
1649        case 'reviewers':
1650          $away = array();
1651          foreach ($result as $user) {
1652            if (idx($user, 'currentStatus') != 'away') {
1653              continue;
1654            }
1655
1656            $username = $user['userName'];
1657            $real_name = $user['realName'];
1658
1659            if (strlen($real_name)) {
1660              $name = pht('%s (%s)', $username, $real_name);
1661            } else {
1662              $name = pht('%s', $username);
1663            }
1664
1665            $away[] = array(
1666              'name' => $name,
1667              'until' => $user['currentStatusUntil'],
1668            );
1669          }
1670
1671          if ($away) {
1672            if (count($away) == count($reviewers)) {
1673              $earliest_return = min(ipull($away, 'until'));
1674
1675              $message = pht(
1676                'All reviewers are away until %s:',
1677                date('l, M j Y', $earliest_return));
1678            } else {
1679              $message = pht('Some reviewers are currently away:');
1680            }
1681
1682            echo tsprintf(
1683              "%s\n\n",
1684              $message);
1685
1686            $list = id(new PhutilConsoleList());
1687            foreach ($away as $spec) {
1688              $list->addItem(
1689                pht(
1690                  '%s (until %s)',
1691                  $spec['name'],
1692                  date('l, M j Y', $spec['until'])));
1693            }
1694
1695            echo tsprintf(
1696              '%B',
1697              $list->drawConsoleString());
1698
1699            $confirm = pht('Continue even though reviewers are unavailable?');
1700            if (!phutil_console_confirm($confirm)) {
1701              throw new ArcanistUsageException(
1702                pht('Specify available reviewers and retry.'));
1703            }
1704          }
1705          break;
1706      }
1707    }
1708
1709  }
1710
1711
1712  /**
1713   * @task message
1714   */
1715  private function getUpdateMessage(array $fields, $template = '') {
1716    if ($this->getArgument('raw')) {
1717      throw new ArcanistUsageException(
1718        pht(
1719          "When using '%s' to update a revision, specify an update message ".
1720          "with '%s'. (Normally, we'd launch an editor to ask you for a ".
1721          "message, but can not do that because stdin is the diff source.)",
1722          '--raw',
1723          '--message'));
1724    }
1725
1726    // When updating a revision using git without specifying '--message', try
1727    // to prefill with the message in HEAD if it isn't a template message. The
1728    // idea is that if you do:
1729    //
1730    //  $ git commit -a -m 'fix some junk'
1731    //  $ arc diff
1732    //
1733    // ...you shouldn't have to retype the update message. Similar things apply
1734    // to Mercurial.
1735
1736    if ($template == '') {
1737      $comments = $this->getDefaultUpdateMessage();
1738
1739      $template = sprintf(
1740        "%s\n\n# %s\n#\n# %s\n# %s\n#\n# %s\n#  $ %s\n\n",
1741        rtrim($comments),
1742        pht(
1743          'Updating %s: %s',
1744          "D{$fields['revisionID']}",
1745          $fields['title']),
1746        pht(
1747          'Enter a brief description of the changes included in this update.'),
1748        pht('The first line is used as subject, next lines as comment.'),
1749        pht('If you intended to create a new revision, use:'),
1750        'arc diff --create');
1751    }
1752
1753    $comments = $this->newInteractiveEditor($template)
1754      ->setName('differential-update-comments')
1755      ->editInteractively();
1756
1757    return $comments;
1758  }
1759
1760  private function getDefaultCreateFields() {
1761    $result = array(array(), array(), array());
1762
1763    if ($this->isRawDiffSource()) {
1764      return $result;
1765    }
1766
1767    $repository_api = $this->getRepositoryAPI();
1768    $local = $repository_api->getLocalCommitInformation();
1769    if ($local) {
1770      $result = $this->parseCommitMessagesIntoFields($local);
1771      if ($this->getArgument('create')) {
1772        unset($result[0]['revisionID']);
1773      }
1774    }
1775
1776    $result[0] = $this->dispatchWillBuildEvent($result[0]);
1777
1778    return $result;
1779  }
1780
1781  /**
1782   * Convert a list of commits from `getLocalCommitInformation()` into
1783   * a format usable by arc to create a new diff. Specifically, we emit:
1784   *
1785   *   - A dictionary of commit message fields.
1786   *   - A list of errors encountered while parsing the messages.
1787   *   - A human-readable list of the commits themselves.
1788   *
1789   * For example, if the user runs "arc diff HEAD^^^" and selects a diff range
1790   * which includes several diffs, we attempt to merge them somewhat
1791   * intelligently into a single message, because we can only send one
1792   * "Summary:", "Reviewers:", etc., field to Differential. We also return
1793   * errors (e.g., if the user typed a reviewer name incorrectly) and a
1794   * summary of the commits themselves.
1795   *
1796   * @param dict  Local commit information.
1797   * @return list Complex output, see summary.
1798   * @task message
1799   */
1800  private function parseCommitMessagesIntoFields(array $local) {
1801    $conduit = $this->getConduit();
1802    $local = ipull($local, null, 'commit');
1803
1804    // If the user provided "--reviewers" or "--ccs", add a faux message to
1805    // the list with the implied fields.
1806
1807    $faux_message = array();
1808    if ($this->getArgument('reviewers')) {
1809      $faux_message[] = pht('Reviewers: %s', $this->getArgument('reviewers'));
1810    }
1811    if ($this->getArgument('cc')) {
1812      $faux_message[] = pht('CC: %s', $this->getArgument('cc'));
1813    }
1814
1815    // NOTE: For now, this isn't a real field, so it just ends up as the first
1816    // part of the summary.
1817    $depends_ref = $this->getDependsOnRevisionRef();
1818    if ($depends_ref) {
1819      $faux_message[] = pht(
1820        'Depends on %s. ',
1821        $depends_ref->getMonogram());
1822    }
1823
1824    // See T12069. After T10312, the first line of a message is always parsed
1825    // as a title. Add a placeholder so "Reviewers" and "CC" are never the
1826    // first line.
1827    $placeholder_title = pht('<placeholder>');
1828
1829    if ($faux_message) {
1830      array_unshift($faux_message, $placeholder_title);
1831      $faux_message = implode("\n\n", $faux_message);
1832      $local = array(
1833        '(Flags)     ' => array(
1834          'message' => $faux_message,
1835          'summary' => pht('Command-Line Flags'),
1836        ),
1837      ) + $local;
1838    }
1839
1840    // Build a human-readable list of the commits, so we can show the user which
1841    // commits are included in the diff.
1842    $included = array();
1843    foreach ($local as $hash => $info) {
1844      $included[] = substr($hash, 0, 12).' '.$info['summary'];
1845    }
1846
1847    // Parse all of the messages into fields.
1848    $messages = array();
1849    foreach ($local as $hash => $info) {
1850      $text = $info['message'];
1851      $obj = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
1852      $messages[$hash] = $obj;
1853    }
1854
1855    $notes = array();
1856    $fields = array();
1857    foreach ($messages as $hash => $message) {
1858      try {
1859        $message->pullDataFromConduit($conduit, $partial = true);
1860        $fields[$hash] = $message->getFields();
1861      } catch (ArcanistDifferentialCommitMessageParserException $ex) {
1862        if ($this->getArgument('verbatim')) {
1863          // In verbatim mode, just bail when we hit an error. The user can
1864          // rerun without --verbatim if they want to fix it manually. Most
1865          // users will probably `git commit --amend` instead.
1866          throw $ex;
1867        }
1868        $fields[$hash] = $message->getFields();
1869
1870        $frev = substr($hash, 0, 12);
1871        $notes[] = pht(
1872          'NOTE: commit %s could not be completely parsed:',
1873          $frev);
1874        foreach ($ex->getParserErrors() as $error) {
1875          $notes[] = "  - {$error}";
1876        }
1877      }
1878    }
1879
1880    // Merge commit message fields. We do this somewhat-intelligently so that
1881    // multiple "Reviewers" or "CC" fields will merge into the concatenation
1882    // of all values.
1883
1884    // We have special parsing rules for 'title' because we can't merge
1885    // multiple titles, and one-line commit messages like "fix stuff" will
1886    // parse as titles. Instead, pick the first title we encounter. When we
1887    // encounter subsequent titles, treat them as part of the summary. Then
1888    // we merge all the summaries together below.
1889
1890    $result = array();
1891
1892    // Process fields in oldest-first order, so earlier commits get to set the
1893    // title of record and reviewers/ccs are listed in chronological order.
1894    $fields = array_reverse($fields);
1895
1896    foreach ($fields as $hash => $dict) {
1897      $title = idx($dict, 'title');
1898      if (!strlen($title)) {
1899        continue;
1900      }
1901
1902      if ($title === $placeholder_title) {
1903        continue;
1904      }
1905
1906      if (!isset($result['title'])) {
1907        // We don't have a title yet, so use this one.
1908        $result['title'] = $title;
1909      } else {
1910        // We already have a title, so merge this new title into the summary.
1911        $summary = idx($dict, 'summary');
1912        if ($summary) {
1913          $summary = $title."\n\n".$summary;
1914        } else {
1915          $summary = $title;
1916        }
1917        $fields[$hash]['summary'] = $summary;
1918      }
1919    }
1920
1921    // Now, merge all the other fields in a general sort of way.
1922
1923    foreach ($fields as $hash => $dict) {
1924      foreach ($dict as $key => $value) {
1925        if ($key == 'title') {
1926          // This has been handled above, and either assigned directly or
1927          // merged into the summary.
1928          continue;
1929        }
1930
1931        if (is_array($value)) {
1932          // For array values, merge the arrays, appending the new values.
1933          // Examples are "Reviewers" and "Cc", where this produces a list of
1934          // all users specified as reviewers.
1935          $cur = idx($result, $key, array());
1936          $new = array_merge($cur, $value);
1937          $result[$key] = $new;
1938          continue;
1939        } else {
1940          if (!strlen(trim($value))) {
1941            // Ignore empty fields.
1942            continue;
1943          }
1944
1945          // For string values, append the new field to the old field with
1946          // a blank line separating them. Examples are "Test Plan" and
1947          // "Summary".
1948          $cur = idx($result, $key, '');
1949          if (strlen($cur)) {
1950            $new = $cur."\n\n".$value;
1951          } else {
1952            $new = $value;
1953          }
1954          $result[$key] = $new;
1955        }
1956      }
1957    }
1958
1959    return array($result, $notes, $included);
1960  }
1961
1962  private function getDefaultUpdateMessage() {
1963    if ($this->isRawDiffSource()) {
1964      return null;
1965    }
1966
1967    $repository_api = $this->getRepositoryAPI();
1968    if ($repository_api instanceof ArcanistGitAPI) {
1969      return $this->getGitUpdateMessage();
1970    }
1971
1972    if ($repository_api instanceof ArcanistMercurialAPI) {
1973      return $this->getMercurialUpdateMessage();
1974    }
1975
1976    return null;
1977  }
1978
1979  /**
1980   * Retrieve the git messages between HEAD and the last update.
1981   *
1982   * @task message
1983   */
1984  private function getGitUpdateMessage() {
1985    $repository_api = $this->getRepositoryAPI();
1986
1987    $parser = $this->newDiffParser();
1988    $commit_messages = $repository_api->getGitCommitLog();
1989    $commit_messages = $parser->parseDiff($commit_messages);
1990
1991    if (count($commit_messages) == 1) {
1992      // If there's only one message, assume this is an amend-based workflow and
1993      // that using it to prefill doesn't make sense.
1994      return null;
1995    }
1996
1997    // We have more than one message, so figure out which ones are new. We
1998    // do this by pulling the current diff and comparing commit hashes in the
1999    // working copy with attached commit hashes. It's not super important that
2000    // we always get this 100% right, we're just trying to do something
2001    // reasonable.
2002
2003    $hashes = $this->loadActiveDiffLocalCommitHashes();
2004    $hashes = array_fuse($hashes);
2005
2006    $usable = array();
2007    foreach ($commit_messages as $message) {
2008      $text = $message->getMetadata('message');
2009
2010      $parsed = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
2011      if ($parsed->getRevisionID()) {
2012        // If this is an amended commit message with a revision ID, it's
2013        // certainly not new. Stop marking commits as usable and break out.
2014        break;
2015      }
2016
2017      if (isset($hashes[$message->getCommitHash()])) {
2018        // If this commit is currently part of the diff, stop using commit
2019        // messages, since anything older than this isn't new.
2020        break;
2021      }
2022
2023      // Otherwise, this looks new, so it's a usable commit message.
2024      $usable[] = $text;
2025    }
2026
2027    if (!$usable) {
2028      // No new commit messages, so we don't have anywhere to start from.
2029      return null;
2030    }
2031
2032    return $this->formatUsableLogs($usable);
2033  }
2034
2035  /**
2036   * Retrieve the hg messages between tip and the last update.
2037   *
2038   * @task message
2039   */
2040  private function getMercurialUpdateMessage() {
2041    $repository_api = $this->getRepositoryAPI();
2042
2043    $messages = $repository_api->getCommitMessageLog();
2044
2045    if (count($messages) == 1) {
2046      // If there's only one message, assume this is an amend-based workflow and
2047      // that using it to prefill doesn't make sense.
2048      return null;
2049    }
2050
2051    $hashes = $this->loadActiveDiffLocalCommitHashes();
2052    $hashes = array_fuse($hashes);
2053
2054    $usable = array();
2055    foreach ($messages as $rev => $message) {
2056      if (isset($hashes[$rev])) {
2057        // If this commit is currently part of the active diff on the revision,
2058        // stop using commit messages, since anything older than this isn't new.
2059        break;
2060      }
2061
2062      // Otherwise, this looks new, so it's a usable commit message.
2063      $usable[] = $message;
2064    }
2065
2066    if (!$usable) {
2067      // No new commit messages, so we don't have anywhere to start from.
2068      return null;
2069    }
2070
2071    return $this->formatUsableLogs($usable);
2072  }
2073
2074
2075  /**
2076   * Format log messages to prefill a diff update.
2077   *
2078   * @task message
2079   */
2080  private function formatUsableLogs(array $usable) {
2081    // Flip messages so they'll read chronologically (oldest-first) in the
2082    // template, e.g.:
2083    //
2084    //   - Added foobar.
2085    //   - Fixed foobar bug.
2086    //   - Documented foobar.
2087
2088    $usable = array_reverse($usable);
2089    $default = array();
2090    foreach ($usable as $message) {
2091      // Pick the first line out of each message.
2092      $text = trim($message);
2093      $text = head(explode("\n", $text));
2094      $default[] = '  - '.$text."\n";
2095    }
2096
2097    return implode('', $default);
2098  }
2099
2100  private function loadActiveDiffLocalCommitHashes() {
2101    // The older "differential.querydiffs" method includes the full diff text,
2102    // which can be very slow for large diffs. If we can, try to use
2103    // "differential.diff.search" instead.
2104
2105    // We expect this to fail if the Phabricator version on the server is
2106    // older than April 2018 (D19386), which introduced the "commits"
2107    // attachment for "differential.revision.search".
2108
2109    // TODO: This can be optimized if we're able to learn the "revisionPHID"
2110    // before we get here. See PHI1104.
2111
2112    try {
2113      $revisions_raw = $this->getConduit()->callMethodSynchronous(
2114        'differential.revision.search',
2115        array(
2116          'constraints' => array(
2117            'ids' => array(
2118              $this->revisionID,
2119            ),
2120          ),
2121        ));
2122
2123      $revisions = $revisions_raw['data'];
2124      $revision = head($revisions);
2125      if ($revision) {
2126        $revision_phid = $revision['phid'];
2127
2128        $diffs_raw = $this->getConduit()->callMethodSynchronous(
2129          'differential.diff.search',
2130          array(
2131            'constraints' => array(
2132              'revisionPHIDs' => array(
2133                $revision_phid,
2134              ),
2135            ),
2136            'attachments' => array(
2137              'commits' => true,
2138            ),
2139            'limit' => 1,
2140          ));
2141
2142        $diffs = $diffs_raw['data'];
2143        $diff = head($diffs);
2144
2145        if ($diff) {
2146          $commits = idxv($diff, array('attachments', 'commits', 'commits'));
2147          if ($commits !== null) {
2148            $hashes = ipull($commits, 'identifier');
2149            return array_values($hashes);
2150          }
2151        }
2152      }
2153    } catch (Exception $ex) {
2154      // If any of this fails, fall back to the older method below.
2155    }
2156
2157    $current_diff = $this->getConduit()->callMethodSynchronous(
2158      'differential.querydiffs',
2159      array(
2160        'revisionIDs' => array($this->revisionID),
2161      ));
2162    $current_diff = head($current_diff);
2163
2164    $properties = idx($current_diff, 'properties', array());
2165    $local = idx($properties, 'local:commits', array());
2166    $hashes = ipull($local, 'commit');
2167
2168    return array_values($hashes);
2169  }
2170
2171
2172/* -(  Diff Specification  )------------------------------------------------- */
2173
2174
2175  /**
2176   * @task diffspec
2177   */
2178  private function getLintStatus($lint_result) {
2179    $map = array(
2180      ArcanistLintWorkflow::RESULT_OKAY       => 'okay',
2181      ArcanistLintWorkflow::RESULT_ERRORS     => 'fail',
2182      ArcanistLintWorkflow::RESULT_WARNINGS   => 'warn',
2183      ArcanistLintWorkflow::RESULT_SKIP       => 'skip',
2184    );
2185    return idx($map, $lint_result, 'none');
2186  }
2187
2188
2189  /**
2190   * @task diffspec
2191   */
2192  private function getUnitStatus($unit_result) {
2193    $map = array(
2194      ArcanistUnitWorkflow::RESULT_OKAY       => 'okay',
2195      ArcanistUnitWorkflow::RESULT_FAIL       => 'fail',
2196      ArcanistUnitWorkflow::RESULT_UNSOUND    => 'warn',
2197      ArcanistUnitWorkflow::RESULT_SKIP       => 'skip',
2198    );
2199    return idx($map, $unit_result, 'none');
2200  }
2201
2202
2203  /**
2204   * @task diffspec
2205   */
2206  private function buildDiffSpecification() {
2207
2208    $base_revision  = null;
2209    $base_path      = null;
2210    $vcs            = null;
2211    $repo_uuid      = null;
2212    $parent         = null;
2213    $source_path    = null;
2214    $branch         = null;
2215    $bookmark       = null;
2216
2217    if (!$this->isRawDiffSource()) {
2218      $repository_api = $this->getRepositoryAPI();
2219
2220      $base_revision  = $repository_api->getSourceControlBaseRevision();
2221      $base_path      = $repository_api->getSourceControlPath();
2222      $vcs            = $repository_api->getSourceControlSystemName();
2223      $source_path    = $repository_api->getPath();
2224      $branch         = $repository_api->getBranchName();
2225      $repo_uuid      = $repository_api->getRepositoryUUID();
2226
2227      if ($repository_api instanceof ArcanistGitAPI) {
2228        $info = $this->getGitParentLogInfo();
2229        if ($info['parent']) {
2230          $parent = $info['parent'];
2231        }
2232        if ($info['base_revision']) {
2233          $base_revision = $info['base_revision'];
2234        }
2235        if ($info['base_path']) {
2236          $base_path = $info['base_path'];
2237        }
2238        if ($info['uuid']) {
2239          $repo_uuid = $info['uuid'];
2240        }
2241      } else if ($repository_api instanceof ArcanistMercurialAPI) {
2242
2243        $bookmark = $repository_api->getActiveBookmark();
2244        $svn_info = $repository_api->getSubversionInfo();
2245        $repo_uuid = idx($svn_info, 'uuid');
2246        $base_path = idx($svn_info, 'base_path', $base_path);
2247        $base_revision = idx($svn_info, 'base_revision', $base_revision);
2248
2249        // TODO: provide parent info
2250
2251      }
2252    }
2253
2254    $data = array(
2255      'sourceMachine'             => php_uname('n'),
2256      'sourcePath'                => $source_path,
2257      'branch'                    => $branch,
2258      'bookmark'                  => $bookmark,
2259      'sourceControlSystem'       => $vcs,
2260      'sourceControlPath'         => $base_path,
2261      'sourceControlBaseRevision' => $base_revision,
2262      'creationMethod'            => 'arc',
2263    );
2264
2265    if (!$this->isRawDiffSource()) {
2266      $repository_phid = $this->getRepositoryPHID();
2267      if ($repository_phid) {
2268        $data['repositoryPHID'] = $repository_phid;
2269      }
2270    }
2271
2272    return $data;
2273  }
2274
2275
2276/* -(  Diff Properties  )---------------------------------------------------- */
2277
2278
2279  /**
2280   * Update lint information for the diff.
2281   *
2282   * @return void
2283   *
2284   * @task diffprop
2285   */
2286  private function updateLintDiffProperty() {
2287    if (!$this->hitAutotargets) {
2288      if ($this->unresolvedLint) {
2289        $this->updateDiffProperty(
2290          'arc:lint',
2291          json_encode($this->unresolvedLint));
2292      }
2293    }
2294  }
2295
2296
2297  /**
2298   * Update unit test information for the diff.
2299   *
2300   * @return void
2301   *
2302   * @task diffprop
2303   */
2304  private function updateUnitDiffProperty() {
2305    if (!$this->hitAutotargets) {
2306      if ($this->testResults) {
2307        $this->updateDiffProperty('arc:unit', json_encode($this->testResults));
2308      }
2309    }
2310  }
2311
2312
2313  /**
2314   * Update local commit information for the diff.
2315   *
2316   * @task diffprop
2317   */
2318  private function updateLocalDiffProperty() {
2319    if ($this->isRawDiffSource()) {
2320      return;
2321    }
2322
2323    $local_info = $this->getRepositoryAPI()->getLocalCommitInformation();
2324    if (!$local_info) {
2325      return;
2326    }
2327
2328    $this->updateDiffProperty('local:commits', json_encode($local_info));
2329  }
2330
2331  private function updateOntoDiffProperty() {
2332    $onto = $this->getDiffOntoTargets();
2333
2334    if (!$onto) {
2335      return;
2336    }
2337
2338    $this->updateDiffProperty('arc:onto', json_encode($onto));
2339  }
2340
2341  private function getDiffOntoTargets() {
2342    if ($this->isRawDiffSource()) {
2343      return null;
2344    }
2345
2346    $api = $this->getRepositoryAPI();
2347
2348    if (!($api instanceof ArcanistGitAPI)) {
2349      return null;
2350    }
2351
2352    // If we track an upstream branch either directly or indirectly, use that.
2353    $branch = $api->getBranchName();
2354    if (strlen($branch)) {
2355      $upstream_path = $api->getPathToUpstream($branch);
2356      $remote_branch = $upstream_path->getRemoteBranchName();
2357      if (strlen($remote_branch)) {
2358        return array(
2359          array(
2360            'type' => 'branch',
2361            'name' => $remote_branch,
2362            'kind' => 'upstream',
2363          ),
2364        );
2365      }
2366    }
2367
2368    // If "arc.land.onto.default" is configured, use that.
2369    $config_key = 'arc.land.onto.default';
2370    $onto = $this->getConfigFromAnySource($config_key);
2371    if (strlen($onto)) {
2372      return array(
2373        array(
2374          'type' => 'branch',
2375          'name' => $onto,
2376          'kind' => 'arc.land.onto.default',
2377        ),
2378      );
2379    }
2380
2381    return null;
2382  }
2383
2384  /**
2385   * Update an arbitrary diff property.
2386   *
2387   * @param string Diff property name.
2388   * @param string Diff property value.
2389   * @return void
2390   *
2391   * @task diffprop
2392   */
2393  private function updateDiffProperty($name, $data) {
2394    $this->diffPropertyFutures[] = $this->getConduit()->callMethod(
2395      'differential.setdiffproperty',
2396      array(
2397        'diff_id' => $this->getDiffID(),
2398        'name'    => $name,
2399        'data'    => $data,
2400      ));
2401  }
2402
2403  /**
2404   * Wait for finishing all diff property updates.
2405   *
2406   * @return void
2407   *
2408   * @task diffprop
2409   */
2410  private function resolveDiffPropertyUpdates() {
2411    id(new FutureIterator($this->diffPropertyFutures))
2412      ->resolveAll();
2413    $this->diffPropertyFutures = array();
2414  }
2415
2416  private function dispatchWillCreateRevisionEvent(array $fields) {
2417    $event = $this->dispatchEvent(
2418      ArcanistEventType::TYPE_REVISION_WILLCREATEREVISION,
2419      array(
2420        'specification' => $fields,
2421      ));
2422
2423    return $event->getValue('specification');
2424  }
2425
2426  private function dispatchWillBuildEvent(array $fields) {
2427    $event = $this->dispatchEvent(
2428      ArcanistEventType::TYPE_DIFF_WILLBUILDMESSAGE,
2429      array(
2430        'fields' => $fields,
2431      ));
2432
2433    return $event->getValue('fields');
2434  }
2435
2436  private function checkRevisionOwnership(array $revision) {
2437    if ($revision['authorPHID'] == $this->getUserPHID()) {
2438      return;
2439    }
2440
2441    $id = $revision['id'];
2442    $title = $revision['title'];
2443
2444    $prompt = pht(
2445      "You don't own revision %s: \"%s\". Normally, you should ".
2446      "only update revisions you own. You can \"Commandeer\" this revision ".
2447      "from the web interface if you want to become the owner.\n\n".
2448      "Update this revision anyway?",
2449      "D{$id}",
2450      $title);
2451
2452    $ok = phutil_console_confirm($prompt, $default_no = true);
2453    if (!$ok) {
2454      throw new ArcanistUsageException(
2455        pht('Aborted update of revision: You are not the owner.'));
2456    }
2457  }
2458
2459
2460/* -(  File Uploads  )------------------------------------------------------- */
2461
2462
2463  private function uploadFilesForChanges(array $changes) {
2464    assert_instances_of($changes, 'ArcanistDiffChange');
2465
2466    // Collect all the files we need to upload.
2467
2468    $need_upload = array();
2469    foreach ($changes as $key => $change) {
2470      if ($change->getFileType() != ArcanistDiffChangeType::FILE_BINARY) {
2471        continue;
2472      }
2473
2474      if ($this->getArgument('skip-binaries')) {
2475        continue;
2476      }
2477
2478      $name = basename($change->getCurrentPath());
2479
2480      $need_upload[] = array(
2481        'type' => 'old',
2482        'name' => $name,
2483        'data' => $change->getOriginalFileData(),
2484        'change' => $change,
2485      );
2486
2487      $need_upload[] = array(
2488        'type' => 'new',
2489        'name' => $name,
2490        'data' => $change->getCurrentFileData(),
2491        'change' => $change,
2492      );
2493    }
2494
2495    if (!$need_upload) {
2496      return;
2497    }
2498
2499    // Determine mime types and file sizes. Update changes from "binary" to
2500    // "image" if the file is an image. Set image metadata.
2501
2502    $type_image = ArcanistDiffChangeType::FILE_IMAGE;
2503    foreach ($need_upload as $key => $spec) {
2504      $change = $need_upload[$key]['change'];
2505
2506      if ($spec['data'] === null) {
2507        // This covers the case where a file was added or removed; we don't
2508        // need to upload the other half of it (e.g., the old file data for
2509        // a file which was just added). This is distinct from an empty
2510        // file, which we do upload.
2511        unset($need_upload[$key]);
2512        continue;
2513      }
2514
2515      $type = $spec['type'];
2516      $size = strlen($spec['data']);
2517
2518      $change->setMetadata("{$type}:file:size", $size);
2519
2520      $mime = $this->getFileMimeType($spec['data']);
2521      if (preg_match('@^image/@', $mime)) {
2522        $change->setFileType($type_image);
2523      }
2524
2525      $change->setMetadata("{$type}:file:mime-type", $mime);
2526    }
2527
2528    $uploader = id(new ArcanistFileUploader())
2529      ->setConduitEngine($this->getConduitEngine());
2530
2531    foreach ($need_upload as $key => $spec) {
2532      $ref = id(new ArcanistFileDataRef())
2533        ->setName($spec['name'])
2534        ->setData($spec['data']);
2535
2536      $uploader->addFile($ref, $key);
2537    }
2538
2539    $files = $uploader->uploadFiles();
2540
2541    $errors = false;
2542    foreach ($files as $key => $file) {
2543      if ($file->getErrors()) {
2544        unset($files[$key]);
2545        $errors = true;
2546        echo pht(
2547          'Failed to upload binary "%s".',
2548          $file->getName());
2549      }
2550    }
2551
2552    if ($errors) {
2553      $prompt = pht('Continue?');
2554      $ok = phutil_console_confirm($prompt, $default_no = false);
2555      if (!$ok) {
2556        throw new ArcanistUsageException(
2557          pht(
2558            'Aborted due to file upload failure. You can use %s '.
2559            'to skip binary uploads.',
2560            '--skip-binaries'));
2561      }
2562    }
2563
2564    foreach ($files as $key => $file) {
2565      $spec = $need_upload[$key];
2566      $phid = $file->getPHID();
2567
2568      $change = $spec['change'];
2569      $type = $spec['type'];
2570      $change->setMetadata("{$type}:binary-phid", $phid);
2571
2572      echo pht('Uploaded binary data for "%s".', $file->getName())."\n";
2573    }
2574
2575    echo pht('Upload complete.')."\n";
2576  }
2577
2578  private function getFileMimeType($data) {
2579    $tmp = new TempFile();
2580    Filesystem::writeFile($tmp, $data);
2581    return Filesystem::getMimeType($tmp);
2582  }
2583
2584  private function shouldOpenCreatedObjectsInBrowser() {
2585    return $this->getArgument('browse');
2586  }
2587
2588  private function submitChangesToStagingArea($id) {
2589    $result = $this->pushChangesToStagingArea($id);
2590
2591    // We'll either get a failure constant on error, or a list of pushed
2592    // refs on success.
2593    $ok = is_array($result);
2594
2595    if ($ok) {
2596      $staging = array(
2597        'status' => self::STAGING_PUSHED,
2598        'refs' => $result,
2599      );
2600    } else {
2601      $staging = array(
2602        'status' => $result,
2603        'refs' => array(),
2604      );
2605    }
2606
2607    $this->updateDiffProperty(
2608      'arc.staging',
2609      phutil_json_encode($staging));
2610  }
2611
2612  private function pushChangesToStagingArea($id) {
2613    if ($this->getArgument('skip-staging')) {
2614      $this->writeInfo(
2615        pht('SKIP STAGING'),
2616        pht('Flag --skip-staging was specified.'));
2617      return self::STAGING_USER_SKIP;
2618    }
2619
2620    if ($this->isRawDiffSource()) {
2621      $this->writeInfo(
2622        pht('SKIP STAGING'),
2623        pht('Raw changes can not be pushed to a staging area.'));
2624      return self::STAGING_DIFF_RAW;
2625    }
2626
2627    if (!$this->getRepositoryPHID()) {
2628      $this->writeInfo(
2629        pht('SKIP STAGING'),
2630        pht('Unable to determine repository for this change.'));
2631      return self::STAGING_REPOSITORY_UNKNOWN;
2632    }
2633
2634    $staging = $this->getRepositoryStagingConfiguration();
2635    if ($staging === null) {
2636      $this->writeInfo(
2637        pht('SKIP STAGING'),
2638        pht('The server does not support staging areas.'));
2639      return self::STAGING_REPOSITORY_UNAVAILABLE;
2640    }
2641
2642    $supported = idx($staging, 'supported');
2643    if (!$supported) {
2644      $this->writeInfo(
2645        pht('SKIP STAGING'),
2646        pht('Phabricator does not support staging areas for this repository.'));
2647      return self::STAGING_REPOSITORY_UNSUPPORTED;
2648    }
2649
2650    $staging_uri = idx($staging, 'uri');
2651    if (!$staging_uri) {
2652      $this->writeInfo(
2653        pht('SKIP STAGING'),
2654        pht('No staging area is configured for this repository.'));
2655      return self::STAGING_REPOSITORY_UNCONFIGURED;
2656    }
2657
2658    $api = $this->getRepositoryAPI();
2659    if (!($api instanceof ArcanistGitAPI)) {
2660      $this->writeInfo(
2661        pht('SKIP STAGING'),
2662        pht('This client version does not support staging this repository.'));
2663      return self::STAGING_CLIENT_UNSUPPORTED;
2664    }
2665
2666    $commit = $api->getHeadCommit();
2667    $prefix = idx($staging, 'prefix', 'phabricator');
2668
2669    $base_tag = "refs/tags/{$prefix}/base/{$id}";
2670    $diff_tag = "refs/tags/{$prefix}/diff/{$id}";
2671
2672    $this->writeOkay(
2673      pht('PUSH STAGING'),
2674      pht('Pushing changes to staging area...'));
2675
2676    $push_flags = array();
2677    if (version_compare($api->getGitVersion(), '1.8.2', '>=')) {
2678      $push_flags[] = '--no-verify';
2679    }
2680
2681    $refs = array();
2682
2683    $remote = array(
2684      'uri' => $staging_uri,
2685    );
2686
2687    $is_lfs = $api->isGitLFSWorkingCopy();
2688
2689    // If the base commit is a real commit, we're going to push it. We don't
2690    // use this, but pushing it to a ref reduces the amount of redundant work
2691    // that Git does on later pushes by helping it figure out that the remote
2692    // already has most of the history. See T10509.
2693
2694    // In the future, we could avoid this push if the staging area is the same
2695    // as the main repository, or if the staging area is a virtual repository.
2696    // In these cases, the staging area should automatically have up-to-date
2697    // refs.
2698    $base_commit = $api->getSourceControlBaseRevision();
2699    if ($base_commit !== ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
2700      $refs[] = array(
2701        'ref' => $base_tag,
2702        'type' => 'base',
2703        'commit' => $base_commit,
2704        'remote' => $remote,
2705      );
2706    }
2707
2708    // We're always going to push the change itself.
2709    $refs[] = array(
2710      'ref' => $diff_tag,
2711      'type' => 'diff',
2712      'commit' => $is_lfs ? $base_commit : $commit,
2713      'remote' => $remote,
2714    );
2715
2716    $ref_list = array();
2717    foreach ($refs as $ref) {
2718      $ref_list[] = $ref['commit'].':'.$ref['ref'];
2719    }
2720
2721    $err = phutil_passthru(
2722      'git push %Ls -- %s %Ls',
2723      $push_flags,
2724      $staging_uri,
2725      $ref_list);
2726
2727    if ($err) {
2728      $this->writeWarn(
2729        pht('STAGING FAILED'),
2730        pht('Unable to push changes to the staging area.'));
2731
2732      throw new ArcanistUsageException(
2733        pht(
2734          'Failed to push changes to staging area. Correct the issue, or '.
2735          'use --skip-staging to skip this step.'));
2736    }
2737
2738    if ($is_lfs) {
2739      $ref = '+'.$commit.':'.$diff_tag;
2740      $err = phutil_passthru(
2741        'git push -- %s %s',
2742        $staging_uri,
2743        $ref);
2744
2745      if ($err) {
2746        $this->writeWarn(
2747          pht('STAGING FAILED'),
2748          pht('Unable to push lfs changes to the staging area.'));
2749
2750        throw new ArcanistUsageException(
2751          pht(
2752            'Failed to push lfs changes to staging area. Correct the issue, '.
2753            'or use --skip-staging to skip this step.'));
2754      }
2755    }
2756
2757    return $refs;
2758  }
2759
2760
2761  /**
2762   * Try to upload lint and unit test results into modern Harbormaster build
2763   * targets.
2764   *
2765   * @return bool True if everything was uploaded to build targets.
2766   */
2767  private function updateAutotargets($diff_phid, $unit_result) {
2768    $lint_key = 'arcanist.lint';
2769    $unit_key = 'arcanist.unit';
2770
2771    try {
2772      $result = $this->getConduit()->callMethodSynchronous(
2773        'harbormaster.queryautotargets',
2774        array(
2775          'objectPHID' => $diff_phid,
2776          'targetKeys' => array(
2777            $lint_key,
2778            $unit_key,
2779          ),
2780        ));
2781      $targets = idx($result, 'targetMap', array());
2782    } catch (Exception $ex) {
2783      return false;
2784    }
2785
2786    $futures = array();
2787
2788    $lint_target = idx($targets, $lint_key);
2789    if ($lint_target) {
2790      $lint = nonempty($this->unresolvedLint, array());
2791      foreach ($lint as $key => $message) {
2792        $lint[$key] = $this->getModernLintDictionary($message);
2793      }
2794
2795      // Consider this target to have failed if there are any unresolved
2796      // errors or warnings.
2797      $type = 'pass';
2798      foreach ($lint as $message) {
2799        switch (idx($message, 'severity')) {
2800          case ArcanistLintSeverity::SEVERITY_WARNING:
2801          case ArcanistLintSeverity::SEVERITY_ERROR:
2802            $type = 'fail';
2803            break;
2804        }
2805      }
2806
2807      $futures[] = $this->getConduit()->callMethod(
2808        'harbormaster.sendmessage',
2809        array(
2810          'buildTargetPHID' => $lint_target,
2811          'lint' => array_values($lint),
2812          'type' => $type,
2813        ));
2814    }
2815
2816    $unit_target = idx($targets, $unit_key);
2817    if ($unit_target) {
2818      $unit = nonempty($this->testResults, array());
2819      foreach ($unit as $key => $message) {
2820        $unit[$key] = $this->getModernUnitDictionary($message);
2821      }
2822
2823      $type = ArcanistUnitWorkflow::getHarbormasterTypeFromResult($unit_result);
2824
2825      $futures[] = $this->getConduit()->callMethod(
2826        'harbormaster.sendmessage',
2827        array(
2828          'buildTargetPHID' => $unit_target,
2829          'unit' => array_values($unit),
2830          'type' => $type,
2831        ));
2832    }
2833
2834    try {
2835      foreach (new FutureIterator($futures) as $future) {
2836        $future->resolve();
2837      }
2838      return true;
2839    } catch (Exception $ex) {
2840      // TODO: Eventually, we should expect these to succeed if we get this
2841      // far, but just log errors for now.
2842      phlog($ex);
2843      return false;
2844    }
2845  }
2846
2847  private function getDependsOnRevisionRef() {
2848    // TODO: Restore this behavior after updating for toolsets. Loading the
2849    // required hardpoints currently depends on a "WorkingCopy" existing.
2850    return null;
2851
2852    $api = $this->getRepositoryAPI();
2853    $base_ref = $api->getBaseCommitRef();
2854
2855    $state_ref = id(new ArcanistWorkingCopyStateRef())
2856      ->setCommitRef($base_ref);
2857
2858    $this->loadHardpoints(
2859      $state_ref,
2860      ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS);
2861
2862    $revision_refs = $state_ref->getRevisionRefs();
2863    $viewer_phid = $this->getUserPHID();
2864
2865    foreach ($revision_refs as $key => $revision_ref) {
2866      // Don't automatically depend on closed revisions.
2867      if ($revision_ref->isClosed()) {
2868        unset($revision_refs[$key]);
2869        continue;
2870      }
2871
2872      // Don't automatically depend on revisions authored by other users.
2873      if ($revision_ref->getAuthorPHID() != $viewer_phid) {
2874        unset($revision_refs[$key]);
2875        continue;
2876      }
2877    }
2878
2879    if (!$revision_refs) {
2880      return null;
2881    }
2882
2883    if (count($revision_refs) > 1) {
2884      return null;
2885    }
2886
2887    return head($revision_refs);
2888  }
2889
2890}
2891