~ubuntu-branches/ubuntu/wily/phabricator/wily

« back to all changes in this revision

Viewing changes to src/workflow/ArcanistDiffWorkflow.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-11-01 23:20:06 UTC
  • mto: This revision was merged to the branch mainline in revision 4.
  • Revision ID: package-import@ubuntu.com-20141101232006-mvlnp0cil67tsboe
Tags: upstream-0~git20141101/arcanist
Import upstream version 0~git20141101, component arcanist

Show diffs side-by-side

added added

removed removed

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