4
* Sends changes from your working copy to Differential for code review.
6
* @task lintunit Lint and Unit Tests
7
* @task message Commit and Update Messages
8
* @task diffspec Diff Specification
9
* @task diffprop Diff Properties
11
final class ArcanistDiffWorkflow extends ArcanistWorkflow {
14
private $hasWarnedExternals = false;
15
private $unresolvedLint;
16
private $excuses = array('lint' => null, 'unit' => null);
20
private $postponedLinters;
21
private $haveUncommittedChanges = false;
22
private $diffPropertyFutures = array();
23
private $commitMessageFromRevision;
25
public function getWorkflowName() {
29
public function getCommandSynopses() {
30
return phutil_console_format(<<<EOTEXT
31
**diff** [__paths__] (svn)
32
**diff** [__commit__] (git, hg)
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.
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.
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.
53
public function requiresWorkingCopy() {
54
return !$this->isRawDiffSource();
57
public function requiresConduit() {
61
public function requiresAuthentication() {
65
public function requiresRepositoryAPI() {
66
if (!$this->isRawDiffSource()) {
70
if ($this->getArgument('use-commit-message')) {
77
public function getDiffID() {
81
public function getArguments() {
87
'When updating a revision, use the specified message instead of '.
90
'message-file' => array(
93
'paramtype' => 'file',
94
'help' => 'When creating a revision, read revision information '.
97
'use-commit-message' => array(
100
// TODO: Support mercurial.
104
'help' => 'Read revision information from a specific commit.',
105
'conflicts' => array(
116
'nosupport' => array(
117
'svn' => 'Edit revisions via the web interface when using SVN.',
120
'When updating a revision under git, edit revision information '.
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.',
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,
142
'raw-command' => array(
143
'param' => 'command',
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.',
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.',
166
'param' => 'revision_id',
167
'help' => 'Always update a specific revision.',
171
'Do not run unit tests.',
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.',
185
'Only generate a diff, without running lint, unit tests, or other '.
186
'auxiliary steps. See also --preview.',
187
'conflicts' => array(
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.',
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(
204
'edit' => '--preview does affect revisions.',
205
'message' => '--preview does not update any revision.',
208
'plan-changes' => array(
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.',
217
'param' => 'encoding',
219
'Attempt to convert non UTF-8 hunks into specified encoding.',
221
'allow-untracked' => array(
223
'Skip checks for untracked files in the working copy.',
227
'help' => 'Provide a prepared in advance excuse for any lints/tests'.
230
'less-context' => array(
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.",
240
'Raise all lint warnings, not just those on lines you changed.',
247
'Require excuse for lint advice in addition to lint warnings and '.
253
'Display only lint messages not present in the original code.',
258
'apply-patches' => array(
260
'Apply patches suggested by lint to the working copy without '.
262
'conflicts' => array(
263
'never-apply-patches' => true,
269
'never-apply-patches' => array(
270
'help' => 'Never apply patches suggested by lint.',
271
'conflicts' => array(
272
'apply-patches' => true,
278
'amend-all' => array(
280
'When linting git repositories, amend HEAD with all patches '.
281
'suggested by lint without prompting.',
286
'amend-autofixes' => array(
288
'When linting git repositories, amend HEAD with autofix '.
289
'patches suggested by lint without prompting.',
297
'Automatically add all untracked, unstaged and uncommitted files to '.
302
'Emit machine-readable JSON. EXPERIMENTAL! Probably does not work!',
305
'help' => 'Never amend commits in the working copy with lint patches.',
307
'uncommitted' => array(
308
'help' => 'Suppress warning about uncommitted changes.',
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.',
322
'conflicts' => array(
323
'use-commit-message' => true,
328
'raw-command' => true,
329
'message-file' => true,
332
'reviewers' => array(
333
'param' => 'usernames',
334
'help' => 'When creating a revision, add reviewers.',
335
'conflicts' => array(
342
'param' => 'usernames',
343
'help' => 'When creating a revision, add CCs.',
344
'conflicts' => array(
350
'skip-binaries' => array(
351
'help' => 'Do not upload binaries (like images).',
353
'ignore-unsound-tests' => array(
354
'help' => 'Ignore unsound test failures without prompting.',
358
'help' => 'Additional rules for determining base revision.',
359
'nosupport' => array(
360
'svn' => 'Subversion does not use base commits.',
362
'supports' => array('git', 'hg'),
365
'help' => 'Only run lint and unit tests. Intended for internal use.',
369
'help' => '0 to disable lint cache, 1 to enable (default).',
375
'help' => 'Always enable coverage information.',
376
'conflicts' => array(
377
'no-coverage' => null,
383
'no-coverage' => array(
384
'help' => 'Always disable coverage information.',
391
'After creating a diff or revision, open it in a web browser.'),
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.'),
405
'conflicts' => array(
406
'lintall' => '--head suppresses lint.',
407
'advice' => '--head suppresses lint.',
415
public function isRawDiffSource() {
416
return $this->getArgument('raw') || $this->getArgument('raw-command');
419
public function run() {
420
$this->console = PhutilConsole::getConsole();
422
$this->runRepositoryAPISetup();
424
if ($this->getArgument('no-diff')) {
425
$this->removeScratchFile('diff-result.json');
426
$data = $this->runLintUnit();
427
$this->writeScratchJSONFile('diff-result.json', $data);
431
$this->runDiffSetupBasics();
433
$commit_message = $this->buildCommitMessage();
435
$this->dispatchEvent(
436
ArcanistEventType::TYPE_DIFF_DIDBUILDMESSAGE,
438
'message' => $commit_message,
441
if (!$this->shouldOnlyCreateDiff()) {
442
$revision = $this->buildRevisionFromCommitMessage($commit_message);
445
$server = $this->console->getServer();
446
$server->setHandler(array($this, 'handleServerMessage'));
447
$data = $this->runLintUnit();
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'];
455
if ($this->getArgument('nolint')) {
456
$this->excuses['lint'] = $this->getSkipExcuse(
457
'Provide explanation for skipping lint or press Enter to abort:',
461
if ($this->getArgument('nounit')) {
462
$this->excuses['unit'] = $this->getSkipExcuse(
463
'Provide explanation for skipping unit tests or press Enter to abort:',
467
$changes = $this->generateChanges();
469
throw new ArcanistUsageException(
470
'There are no changes to generate a diff from!');
474
'changes' => mpull($changes, 'toDictionary'),
475
'lintStatus' => $this->getLintStatus($lint_result),
476
'unitStatus' => $this->getUnitStatus($unit_result),
477
) + $this->buildDiffSpecification();
479
$conduit = $this->getConduit();
480
$diff_info = $conduit->callMethodSynchronous(
481
'differential.creatediff',
484
$this->diffID = $diff_info['diffid'];
486
$event = $this->dispatchEvent(
487
ArcanistEventType::TYPE_DIFF_WASCREATED,
489
'diffID' => $diff_info['diffid'],
490
'lintResult' => $lint_result,
491
'unitResult' => $unit_result,
494
$this->updateLintDiffProperty();
495
$this->updateUnitDiffProperty();
496
$this->updateLocalDiffProperty();
497
$this->resolveDiffPropertyUpdates();
499
$output_json = $this->getArgument('json');
501
if ($this->shouldOnlyCreateDiff()) {
503
echo phutil_console_format(
504
"Created a new Differential diff:\n".
505
" **Diff URI:** __%s__\n\n",
508
$human = ob_get_clean();
509
echo json_encode(array(
510
'diffURI' => $diff_info['uri'],
511
'diffID' => $this->getDiffID(),
517
if ($this->shouldOpenCreatedObjectsInBrowser()) {
518
$this->openURIsInBrowser(array($diff_info['uri']));
521
$revision['diffid'] = $this->getDiffID();
523
if ($commit_message->getRevisionID()) {
524
$result = $conduit->callMethodSynchronous(
525
'differential.updaterevision',
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);
534
echo "Updated an existing Differential revision:\n";
536
$revision = $this->dispatchWillCreateRevisionEvent($revision);
538
$result = $conduit->callMethodSynchronous(
539
'differential.createrevision',
542
$revised_message = $conduit->callMethodSynchronous(
543
'differential.getcommitmessage',
545
'revision_id' => $result['revisionid'],
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);
554
echo 'Commit message was not amended. Amending commit message is '.
555
'only supported in git and hg (version 2.2 or newer)';
559
echo "Created a new Differential revision:\n";
562
$uri = $result['uri'];
563
echo phutil_console_format(
564
" **Revision URI:** __%s__\n\n",
567
if ($this->getArgument('plan-changes')) {
568
$conduit->callMethodSynchronous(
569
'differential.createcomment',
571
'revision_id' => $result['revisionid'],
572
'action' => 'rethink',
574
echo "Planned changes to the revision.\n";
577
if ($this->shouldOpenCreatedObjectsInBrowser()) {
578
$this->openURIsInBrowser(array($uri));
582
echo "Included changes:\n";
583
foreach ($changes as $change) {
584
echo ' '.$change->renderTextSummary()."\n";
591
$this->removeScratchFile('create-message');
596
private function runRepositoryAPISetup() {
597
if (!$this->requiresRepositoryAPI()) {
601
$repository_api = $this->getRepositoryAPI();
602
if ($this->getArgument('less-context')) {
603
$repository_api->setDiffLinesOfContext(3);
606
$repository_api->setBaseCommitArgumentRules(
607
$this->getArgument('base', ''));
609
if ($repository_api->supportsCommitRanges()) {
610
$this->parseBaseCommitArgument($this->getArgument('paths'));
613
$head_commit = $this->getArgument('head');
614
if ($head_commit !== null) {
615
$repository_api->setHeadCommit($head_commit);
620
private function runDiffSetupBasics() {
621
$output_json = $this->getArgument('json');
623
// TODO: We should move this to a higher-level and put an indirection
624
// layer between echoing stuff and stdout.
628
if ($this->requiresWorkingCopy()) {
629
$repository_api = $this->getRepositoryAPI();
631
if ($this->getArgument('add-all')) {
632
$this->setCommitMode(self::COMMIT_ENABLE);
633
} else if ($this->getArgument('uncommitted')) {
634
$this->setCommitMode(self::COMMIT_DISABLE);
636
$this->setCommitMode(self::COMMIT_ALLOW);
638
if ($repository_api instanceof ArcanistSubversionAPI) {
639
$repository_api->limitStatusToPaths($this->getArgument('paths'));
641
if (!$this->getArgument('head')) {
642
$this->requireCleanWorkingCopy();
644
} catch (ArcanistUncommittedChangesException $ex) {
645
if ($repository_api instanceof ArcanistMercurialAPI) {
646
$use_dirty_changes = false;
647
if ($this->getArgument('uncommitted')) {
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?");
660
$this->haveUncommittedChanges = true;
667
$this->dispatchEvent(
668
ArcanistEventType::TYPE_DIFF_DIDCOLLECTCHANGES,
672
private function buildRevisionFromCommitMessage(
673
ArcanistDifferentialCommitMessage $message) {
675
$conduit = $this->getConduit();
677
$revision_id = $message->getRevisionID();
679
'fields' => $message->getFields(),
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();
692
$use_fields = array();
695
$should_edit = $this->getArgument('edit');
696
$edit_messages = $this->readScratchJSONFile('edit-messages.json');
697
$remote_corpus = idx($edit_messages, $revision_id);
699
if (!$should_edit || !$remote_corpus || $use_fields) {
700
if ($this->commitMessageFromRevision) {
701
$remote_corpus = $this->commitMessageFromRevision;
703
$remote_corpus = $conduit->callMethodSynchronous(
704
'differential.getcommitmessage',
706
'revision_id' => $revision_id,
708
'fields' => $use_fields,
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);
724
if ($this->commitMessageFromRevision == $remote_corpus) {
725
$new_message = $message;
727
$remote_corpus = ArcanistCommentRemover::removeComments(
729
$new_message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
731
$new_message->pullDataFromConduit($conduit);
734
$revision['fields'] = $new_message->getFields();
736
$revision['id'] = $revision_id;
737
$this->revisionID = $revision_id;
739
$revision['message'] = $this->getArgument('message');
740
if (!strlen($revision['message'])) {
741
$update_messages = $this->readScratchJSONFile('update-messages.json');
743
$update_messages[$revision_id] = $this->getUpdateMessage(
745
idx($update_messages, $revision_id));
747
$revision['message'] = ArcanistCommentRemover::removeComments(
748
$update_messages[$revision_id]);
749
if (!strlen(trim($revision['message']))) {
750
throw new ArcanistUserAbortException();
753
$this->writeScratchJSONFile('update-messages.json', $update_messages);
760
protected function shouldOnlyCreateDiff() {
762
if ($this->getArgument('create')) {
766
if ($this->getArgument('update')) {
770
if ($this->getArgument('use-commit-message')) {
774
if ($this->isRawDiffSource()) {
778
return $this->getArgument('preview') ||
779
$this->getArgument('only');
782
private function generateAffectedPaths() {
783
if ($this->isRawDiffSource()) {
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]);
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]);
805
$warn_externals[] = $path;
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.".
816
"Modified 'svn:externals' files:".
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();
823
$this->hasWarnedExternals = true;
828
$paths = $repository_api->getWorkingCopyStatus();
831
foreach ($paths as $path => $mask) {
832
if ($mask & ArcanistRepositoryAPI::FLAG_UNTRACKED) {
833
unset($paths[$path]);
841
protected function generateChanges() {
842
$parser = $this->newDiffParser();
844
$is_raw = $this->isRawDiffSource();
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'));
853
throw new Exception('Unknown raw diff source.');
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]);
866
$repository_api = $this->getRepositoryAPI();
868
if ($repository_api instanceof ArcanistSubversionAPI) {
869
$paths = $this->generateAffectedPaths();
870
$this->primeSubversionWorkingCopyData($paths);
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();
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]);
886
$rev = reset($bases);
889
foreach ($bases as $path => $baserev) {
890
$revlist[] = " Revision {$baserev}, {$path}";
892
$revlist = implode("\n", $revlist);
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: ".
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);
915
$changes = $parser->parseSubversionDiff(
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?)');
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?)');
933
$changes = $parser->parseDiff($diff);
935
throw new Exception('Repository API is not supported.');
938
if (count($changes) > 250) {
939
$count = number_format(count($changes));
941
'http://www.phabricator.com/docs/phabricator/article/'.
942
'Differential_User_Guide_Large_Changes.html';
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.');
955
$limit = 1024 * 1024 * 4;
956
foreach ($changes as $change) {
958
foreach ($change->getHunks() as $hunk) {
959
$size += strlen($hunk->getCorpus());
961
if ($size > $limit) {
962
$file_name = $change->getCurrentPath();
963
$change_size = number_format($size);
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')) {
969
" If this file is a huge text file, try using the ".
970
"'--less-context' flag.";
972
if ($repository_api instanceof ArcanistSubversionAPI) {
973
throw new ArcanistUsageException(
974
"{$byte_warning} If the file is not a text file, mark it as ".
977
" $ svn propset svn:mime-type application/octet-stream <filename>".
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);
986
throw new ArcanistUsageException(
987
'Aborted generation of gigantic diff.');
993
$try_encoding = nonempty($this->getArgument('encoding'), null);
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)) {
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);
1007
if (!$try_encoding) {
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".
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}' ".
1028
$hunk->setCorpus($corpus);
1033
$utf8_problems[] = $change;
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) {
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";
1056
'Do you want to mark these files as binary and continue?',
1057
count($utf8_problems));
1059
echo phutil_console_format("**Invalid Content Encoding (Non-UTF8)**\n");
1060
echo phutil_console_wrap($utf8_warning);
1062
$file_list = mpull($utf8_problems, 'getCurrentPath');
1063
$file_list = ' '.implode("\n ", $file_list);
1066
if (!phutil_console_confirm($confirm, $default_no = false)) {
1067
throw new ArcanistUsageException('Aborted workflow to fix UTF-8.');
1069
foreach ($utf8_problems as $change) {
1070
$change->convertToBinaryChange($repository_api);
1075
$this->uploadFilesForChanges($changes);
1080
private function getGitParentLogInfo() {
1083
'base_revision' => null,
1084
'base_path' => null,
1088
$repository_api = $this->getRepositoryAPI();
1090
$parser = $this->newDiffParser();
1091
$history_messages = $repository_api->getGitHistoryLog();
1092
if (!$history_messages) {
1093
// This can occur on the initial commit.
1096
$history_messages = $parser->parseDiff($history_messages);
1098
foreach ($history_messages as $key => $change) {
1100
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
1101
$change->getMetadata('message'));
1102
if ($message->getRevisionID() && $info['parent'] === null) {
1103
$info['parent'] = $message->getRevisionID();
1105
if ($message->getGitSVNBaseRevision() &&
1106
$info['base_revision'] === null) {
1107
$info['base_revision'] = $message->getGitSVNBaseRevision();
1108
$info['base_path'] = $message->getGitSVNBasePath();
1110
if ($message->getGitSVNUUID()) {
1111
$info['uuid'] = $message->getGitSVNUUID();
1113
if ($info['parent'] && $info['base_revision']) {
1116
} catch (ArcanistDifferentialCommitMessageParserException $ex) {
1118
} catch (ArcanistUsageException $ex) {
1119
// Ignore an invalid Differential Revision field in the parent commit
1126
protected function primeSubversionWorkingCopyData($paths) {
1127
$repository_api = $this->getRepositoryAPI();
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);
1138
foreach (Futures($futures)->limit(8) as $key => $future) {
1139
$target = $targets[$key];
1140
if ($target['command'] == 'diff') {
1141
$repository_api->primeSVNDiffResult(
1143
$future->resolve());
1145
$repository_api->primeSVNInfoResult(
1147
$future->resolve());
1152
private function shouldAmend() {
1153
if ($this->isRawDiffSource()) {
1157
if ($this->haveUncommittedChanges) {
1161
if ($this->getArgument('no-amend')) {
1165
if ($this->getArgument('head') !== null) {
1169
// Run this last: with --raw or --raw-command, we won't have a repository
1171
if ($this->isHistoryImmutable()) {
1179
/* -( Lint and Unit Tests )------------------------------------------------ */
1185
private function runLintUnit() {
1186
$lint_result = $this->runLint();
1187
$unit_result = $this->runUnit();
1189
'lintResult' => $lint_result,
1190
'unresolvedLint' => $this->unresolvedLint,
1191
'postponedLinters' => $this->postponedLinters,
1192
'unitResult' => $unit_result,
1193
'testResults' => $this->testResults,
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;
1209
$repository_api = $this->getRepositoryAPI();
1211
$this->console->writeOut("Linting...\n");
1213
$argv = $this->getPassthruArgumentsAsArgv('lint');
1214
if ($repository_api->supportsCommitRanges()) {
1216
$argv[] = $repository_api->getBaseCommit();
1219
$lint_workflow = $this->buildChildWorkflow('lint', $argv);
1221
if ($this->shouldAmend()) {
1222
// TODO: We should offer to create a checkpoint commit.
1223
$lint_workflow->setShouldAmendChanges(true);
1226
$lint_result = $lint_workflow->run();
1228
switch ($lint_result) {
1229
case ArcanistLintWorkflow::RESULT_OKAY:
1230
if ($this->getArgument('advice') &&
1231
$lint_workflow->getUnresolvedMessages()) {
1232
$this->getErrorExcuse(
1234
'Lint issued unresolved advice.',
1237
$this->console->writeOut(
1238
"<bg:green>** LINT OKAY **</bg> No lint problems.\n");
1241
case ArcanistLintWorkflow::RESULT_WARNINGS:
1242
$this->getErrorExcuse(
1244
'Lint issued unresolved warnings.',
1247
case ArcanistLintWorkflow::RESULT_ERRORS:
1248
$this->console->writeOut(
1249
"<bg:red>** LINT ERRORS **</bg> Lint raised errors!\n");
1250
$this->getErrorExcuse(
1252
'Lint issued unresolved errors!',
1255
case ArcanistLintWorkflow::RESULT_POSTPONED:
1256
$this->console->writeOut(
1257
"<bg:yellow>** LINT POSTPONED **</bg> ".
1258
"Lint results are postponed.\n");
1262
$this->unresolvedLint = array();
1263
foreach ($lint_workflow->getUnresolvedMessages() as $message) {
1264
$this->unresolvedLint[] = $message->toDictionary();
1267
$this->postponedLinters = $lint_workflow->getPostponedLinters();
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");
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;
1291
$repository_api = $this->getRepositoryAPI();
1293
$this->console->writeOut("Running unit tests...\n");
1295
$argv = $this->getPassthruArgumentsAsArgv('unit');
1296
if ($repository_api->supportsCommitRanges()) {
1298
$argv[] = $repository_api->getBaseCommit();
1300
$unit_workflow = $this->buildChildWorkflow('unit', $argv);
1301
$unit_result = $unit_workflow->run();
1303
switch ($unit_result) {
1304
case ArcanistUnitWorkflow::RESULT_OKAY:
1305
$this->console->writeOut(
1306
"<bg:green>** UNIT OKAY **</bg> No unit test failures.\n");
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");
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?');
1318
throw new ArcanistUserAbortException();
1322
case ArcanistUnitWorkflow::RESULT_FAIL:
1323
$this->console->writeOut(
1324
"<bg:red>** UNIT ERRORS **</bg> Unit testing raised errors!\n");
1325
$this->getErrorExcuse(
1327
'Unit test results include failures!',
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(),
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");
1355
public function getTestResults() {
1356
return $this->testResults;
1359
private function getSkipExcuse($prompt, $history) {
1360
$excuse = $this->getArgument('excuse');
1362
if ($excuse === null) {
1363
$history = $this->getRepositoryAPI()->getScratchFilePath($history);
1364
$excuse = phutil_console_prompt($prompt, $history);
1365
if ($excuse == '') {
1366
throw new ArcanistUserAbortException();
1373
private function getErrorExcuse($type, $prompt, $history) {
1374
if ($this->getArgument('excuse')) {
1375
$this->console->sendMessage(array(
1377
'confirm' => $prompt.' Ignore them?',
1382
$history = $this->getRepositoryAPI()->getScratchFilePath($history);
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(
1388
'prompt' => 'Explanation:',
1389
'history' => $history,
1393
public function handleServerMessage(PhutilConsoleMessage $message) {
1394
$data = $message->getData();
1396
if ($this->getArgument('excuse')) {
1398
phutil_console_require_tty();
1399
} catch (PhutilConsoleStdinNotInteractiveException $ex) {
1400
$this->excuses[$data['type']] = $this->getArgument('excuse');
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');
1411
if ($response == '') {
1412
throw new ArcanistUserAbortException();
1414
$this->excuses[$data['type']] = $response;
1419
/* -( Commit and Update Messages )----------------------------------------- */
1425
private function buildCommitMessage() {
1426
if ($this->getArgument('preview') || $this->getArgument('only')) {
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');
1437
return $this->getCommitMessageFromCommit($is_message);
1441
return $this->getCommitMessageFromUser();
1445
if (!$is_raw && !$is_create && !$is_update) {
1446
$repository_api = $this->getRepositoryAPI();
1447
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
1448
$this->getConduit(),
1450
'authors' => array($this->getUserPHID()),
1451
'status' => 'status-open',
1455
} else if (count($revisions) == 1) {
1456
$revision = head($revisions);
1457
$is_update = $revision['id'];
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 ".
1469
$message_file = $this->getArgument('message-file');
1470
if ($message_file) {
1471
return $this->getCommitMessageFromFile($message_file);
1473
return $this->getCommitMessageFromUser();
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');
1481
return $this->getCommitMessageFromRevision($revision_id);
1483
// This is --raw without enough info to create a revision, so force just
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);
1505
private function getCommitMessageFromUser() {
1506
$conduit = $this->getConduit();
1510
if (!$this->getArgument('verbatim')) {
1511
$saved = $this->readScratchFile('create-message');
1513
$where = $this->getReadableScratchFilePath('create-message');
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);
1523
$preview = "Message begins:\n\n {$preview}\n\n";
1529
"You have a saved revision message in '{$where}'.\n".
1531
"You can use this message, or discard it.";
1533
$use = phutil_console_confirm(
1534
'Do you want to use this message?',
1535
$default_no = false);
1539
$this->removeScratchFile('create-message');
1544
$template_is_default = false;
1546
$included = array();
1548
list($fields, $notes, $included_commits) = $this->getDefaultCreateFields();
1554
$template_is_default = true;
1558
$commit = head($this->getRepositoryAPI()->getLocalCommitInformation());
1559
$template = $commit['message'];
1561
$template = $conduit->callMethodSynchronous(
1562
'differential.getcommitmessage',
1564
'revision_id' => null,
1566
'fields' => $fields,
1571
$old_message = $template;
1573
$included = array();
1574
if ($included_commits) {
1575
foreach ($included_commits as $commit) {
1576
$included[] = ' '.$commit;
1579
if (!$this->isRawDiffSource()) {
1580
$in_branch = ' in branch '.$this->getRepositoryAPI()->getBranchName();
1582
$included = array_merge(
1585
"Included commits{$in_branch}:",
1591
$issues = array_merge(
1593
'NEW DIFFERENTIAL REVISION',
1594
'Describe the changes in this new revision.',
1599
'arc could not identify any existing revision in your working copy.',
1600
'If you intended to update an existing revision, use:',
1602
' $ arc diff --update <revision>',
1605
$issues = array_merge($issues, array(''), $notes);
1611
$template = rtrim($template, "\r\n")."\n\n";
1612
foreach ($issues as $issue) {
1613
$template .= '# '.$issue."\n";
1617
if ($first && $this->getArgument('verbatim') && !$template_is_default) {
1618
$new_template = $template;
1620
$new_template = $this->newInteractiveEditor($template)
1621
->setName('new-commit')
1622
->editInteractively();
1626
if ($template_is_default && ($new_template == $template)) {
1627
throw new ArcanistUsageException('Template not edited.');
1630
$template = ArcanistCommentRemover::removeComments($new_template);
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());
1642
$should_amend = false;
1645
if ($should_amend) {
1646
$wrote = (rtrim($old_message) != rtrim($template));
1648
$repository_api->amendCommit($template);
1649
$where = 'commit message';
1652
$wrote = $this->writeScratchFile('create-message', $template);
1653
$where = "'".$this->getReadableScratchFilePath('create-message')."'";
1657
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
1659
$message->pullDataFromConduit($conduit);
1660
$this->validateCommitMessage($message);
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;
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);
1679
$saved = "A copy was saved to {$where}.";
1681
throw new ArcanistUsageException(
1682
"Message has unresolved errrors. {$saved}");
1684
} catch (Exception $ex) {
1686
echo phutil_console_wrap("(Message saved to {$where}.)\n");
1699
private function getCommitMessageFromFile($file) {
1700
$conduit = $this->getConduit();
1702
$data = Filesystem::readFile($file);
1703
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus($data);
1704
$message->pullDataFromConduit($conduit);
1706
$this->validateCommitMessage($message);
1715
private function getCommitMessageFromRevision($revision_id) {
1718
$revision = $this->getConduit()->callMethodSynchronous(
1719
'differential.query',
1721
'ids' => array($id),
1723
$revision = head($revision);
1726
throw new ArcanistUsageException(
1727
"Revision '{$revision_id}' does not exist!");
1730
$this->checkRevisionOwnership($revision);
1732
$message = $this->getConduit()->callMethodSynchronous(
1733
'differential.getcommitmessage',
1735
'revision_id' => $id,
1738
$this->commitMessageFromRevision = $message;
1740
$obj = ArcanistDifferentialCommitMessage::newFromRawCorpus($message);
1741
$obj->pullDataFromConduit($this->getConduit());
1750
private function validateCommitMessage(
1751
ArcanistDifferentialCommitMessage $message) {
1754
$revision_id = $message->getRevisionID();
1756
$futures['revision'] = $this->getConduit()->callMethod(
1757
'differential.query',
1759
'ids' => array($revision_id),
1763
$reviewers = $message->getFieldValue('reviewerPHIDs');
1765
$confirm = 'You have not specified any reviewers. Continue anyway?';
1766
if (!phutil_console_confirm($confirm)) {
1767
throw new ArcanistUsageException('Specify reviewers and retry.');
1770
$futures['reviewers'] = $this->getConduit()->callMethod(
1773
'phids' => $reviewers,
1777
foreach (Futures($futures) as $key => $future) {
1778
$result = $future->resolve();
1781
if (empty($result)) {
1782
throw new ArcanistUsageException(
1783
"There is no revision D{$revision_id}.");
1785
$this->checkRevisionOwnership(head($result));
1789
foreach ($result as $user) {
1790
if (idx($user, 'currentStatus') == 'away') {
1791
$untils[] = $user['currentStatusUntil'];
1794
if (count($untils) == count($reviewers)) {
1795
$until = date('l, M j Y', min($untils));
1796
$confirm = "All reviewers are away until {$until}. ".
1798
if (!phutil_console_confirm($confirm)) {
1799
throw new ArcanistUsageException(
1800
'Specify available reviewers and retry.');
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.)");
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:
1825
// $ git commit -a -m 'fix some junk'
1828
// ...you shouldn't have to retype the update message. Similar things apply
1831
if ($template == '') {
1832
$comments = $this->getDefaultUpdateMessage();
1837
"# Updating D{$fields['revisionID']}: {$fields['title']}\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".
1842
"# If you intended to create a new revision, use:\n".
1843
"# $ arc diff --create\n".
1847
$comments = $this->newInteractiveEditor($template)
1848
->setName('differential-update-comments')
1849
->editInteractively();
1854
private function getDefaultCreateFields() {
1855
$result = array(array(), array(), array());
1857
if ($this->isRawDiffSource()) {
1861
$repository_api = $this->getRepositoryAPI();
1862
$local = $repository_api->getLocalCommitInformation();
1864
$result = $this->parseCommitMessagesIntoFields($local);
1865
if ($this->getArgument('create')) {
1866
unset($result[0]['revisionID']);
1870
$result[0] = $this->dispatchWillBuildEvent($result[0]);
1876
* Convert a list of commits from `getLocalCommitInformation()` into
1877
* a format usable by arc to create a new diff. Specifically, we emit:
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.
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.
1890
* @param dict Local commit information.
1891
* @return list Complex output, see summary.
1894
private function parseCommitMessagesIntoFields(array $local) {
1895
$conduit = $this->getConduit();
1896
$local = ipull($local, null, 'commit');
1898
// If the user provided "--reviewers" or "--ccs", add a faux message to
1899
// the list with the implied fields.
1901
$faux_message = array();
1902
if ($this->getArgument('reviewers')) {
1903
$faux_message[] = 'Reviewers: '.$this->getArgument('reviewers');
1905
if ($this->getArgument('cc')) {
1906
$faux_message[] = 'CC: '.$this->getArgument('cc');
1909
if ($faux_message) {
1910
$faux_message = implode("\n\n", $faux_message);
1912
'(Flags) ' => array(
1913
'message' => $faux_message,
1914
'summary' => 'Command-Line Flags',
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'];
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) {
1933
$obj = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
1934
$messages[$hash] = $obj;
1939
foreach ($messages as $hash => $message) {
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.
1950
$fields[$hash] = $message->getFields();
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}";
1960
// Merge commit message fields. We do this somewhat-intelligently so that
1961
// multiple "Reviewers" or "CC" fields will merge into the concatenation
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.
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);
1976
foreach ($fields as $hash => $dict) {
1977
$title = idx($dict, 'title');
1978
if (!strlen($title)) {
1982
if (!isset($result['title'])) {
1983
// We don't have a title yet, so use this one.
1984
$result['title'] = $title;
1986
// We already have a title, so merge this new title into the summary.
1987
$summary = idx($dict, 'summary');
1989
$summary = $title."\n\n".$summary;
1993
$fields[$hash]['summary'] = $summary;
1997
// Now, merge all the other fields in a general sort of way.
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.
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;
2016
if (!strlen(trim($value))) {
2017
// Ignore empty fields.
2021
// For string values, append the new field to the old field with
2022
// a blank line separating them. Examples are "Test Plan" and
2024
$cur = idx($result, $key, '');
2026
$new = $cur."\n\n".$value;
2030
$result[$key] = $new;
2035
return array($result, $notes, $included);
2038
private function getDefaultUpdateMessage() {
2039
if ($this->isRawDiffSource()) {
2043
$repository_api = $this->getRepositoryAPI();
2044
if ($repository_api instanceof ArcanistGitAPI) {
2045
return $this->getGitUpdateMessage();
2048
if ($repository_api instanceof ArcanistMercurialAPI) {
2049
return $this->getMercurialUpdateMessage();
2056
* Retrieve the git messages between HEAD and the last update.
2060
private function getGitUpdateMessage() {
2061
$repository_api = $this->getRepositoryAPI();
2063
$parser = $this->newDiffParser();
2064
$commit_messages = $repository_api->getGitCommitLog();
2065
$commit_messages = $parser->parseDiff($commit_messages);
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.
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
2079
$local = $this->loadActiveLocalCommitInfo();
2080
$hashes = ipull($local, null, 'commit');
2083
foreach ($commit_messages as $message) {
2084
$text = $message->getMetadata('message');
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.
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.
2099
// Otherwise, this looks new, so it's a usable commit message.
2104
// No new commit messages, so we don't have anywhere to start from.
2108
return $this->formatUsableLogs($usable);
2112
* Retrieve the hg messages between tip and the last update.
2116
private function getMercurialUpdateMessage() {
2117
$repository_api = $this->getRepositoryAPI();
2119
$messages = $repository_api->getCommitMessageLog();
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.
2127
$local = $this->loadActiveLocalCommitInfo();
2128
$hashes = ipull($local, null, 'commit');
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.
2138
// Otherwise, this looks new, so it's a usable commit message.
2139
$usable[] = $message;
2143
// No new commit messages, so we don't have anywhere to start from.
2147
return $this->formatUsableLogs($usable);
2152
* Format log messages to prefill a diff update.
2156
private function formatUsableLogs(array $usable) {
2157
// Flip messages so they'll read chronologically (oldest-first) in the
2161
// - Fixed foobar bug.
2162
// - Documented foobar.
2164
$usable = array_reverse($usable);
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) {
2172
$text = head(explode("\n", $text));
2173
$default[] = ' - '.$text."\n";
2176
return implode('', $default);
2179
private function loadActiveLocalCommitInfo() {
2180
$current_diff = $this->getConduit()->callMethodSynchronous(
2181
'differential.getdiff',
2183
'revision_id' => $this->revisionID,
2186
$properties = idx($current_diff, 'properties', array());
2187
return idx($properties, 'local:commits', array());
2191
/* -( Diff Specification )------------------------------------------------- */
2197
private function getLintStatus($lint_result) {
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',
2205
return idx($map, $lint_result, 'none');
2212
private function getUnitStatus($unit_result) {
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',
2220
return idx($map, $unit_result, 'none');
2227
private function buildDiffSpecification() {
2229
$base_revision = null;
2234
$source_path = null;
2238
if (!$this->isRawDiffSource()) {
2239
$repository_api = $this->getRepositoryAPI();
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();
2248
if ($repository_api instanceof ArcanistGitAPI) {
2249
$info = $this->getGitParentLogInfo();
2250
if ($info['parent']) {
2251
$parent = $info['parent'];
2253
if ($info['base_revision']) {
2254
$base_revision = $info['base_revision'];
2256
if ($info['base_path']) {
2257
$base_path = $info['base_path'];
2259
if ($info['uuid']) {
2260
$repo_uuid = $info['uuid'];
2262
} else if ($repository_api instanceof ArcanistMercurialAPI) {
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);
2270
// TODO: provide parent info
2276
if ($this->requiresWorkingCopy()) {
2277
$project_id = $this->getWorkingCopy()->getProjectID();
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,
2292
if (!$this->isRawDiffSource()) {
2293
$repository_phid = $this->getRepositoryPHID();
2294
if ($repository_phid) {
2295
$data['repositoryPHID'] = $repository_phid;
2303
/* -( Diff Properties )---------------------------------------------------- */
2307
* Update lint information for the diff.
2313
private function updateLintDiffProperty() {
2314
if (strlen($this->excuses['lint'])) {
2315
$this->updateDiffProperty('arc:lint-excuse',
2316
json_encode($this->excuses['lint']));
2319
if ($this->unresolvedLint) {
2320
$this->updateDiffProperty('arc:lint', json_encode($this->unresolvedLint));
2323
$postponed = $this->postponedLinters;
2325
$this->updateDiffProperty('arc:lint-postponed', json_encode($postponed));
2332
* Update unit test information for the diff.
2338
private function updateUnitDiffProperty() {
2339
if (strlen($this->excuses['unit'])) {
2340
$this->updateDiffProperty('arc:unit-excuse',
2341
json_encode($this->excuses['unit']));
2344
if ($this->testResults) {
2345
$this->updateDiffProperty('arc:unit', json_encode($this->testResults));
2351
* Update local commit information for the diff.
2355
private function updateLocalDiffProperty() {
2356
if ($this->isRawDiffSource()) {
2360
$local_info = $this->getRepositoryAPI()->getLocalCommitInformation();
2365
$this->updateDiffProperty('local:commits', json_encode($local_info));
2370
* Update an arbitrary diff property.
2372
* @param string Diff property name.
2373
* @param string Diff property value.
2378
private function updateDiffProperty($name, $data) {
2379
$this->diffPropertyFutures[] = $this->getConduit()->callMethod(
2380
'differential.setdiffproperty',
2382
'diff_id' => $this->getDiffID(),
2389
* Wait for finishing all diff property updates.
2395
private function resolveDiffPropertyUpdates() {
2396
Futures($this->diffPropertyFutures)->resolveAll();
2397
$this->diffPropertyFutures = array();
2400
private function dispatchWillCreateRevisionEvent(array $fields) {
2401
$event = $this->dispatchEvent(
2402
ArcanistEventType::TYPE_REVISION_WILLCREATEREVISION,
2404
'specification' => $fields,
2407
return $event->getValue('specification');
2410
private function dispatchWillBuildEvent(array $fields) {
2411
$event = $this->dispatchEvent(
2412
ArcanistEventType::TYPE_DIFF_WILLBUILDMESSAGE,
2414
'fields' => $fields,
2417
return $event->getValue('fields');
2420
private function checkRevisionOwnership(array $revision) {
2421
if ($revision['authorPHID'] == $this->getUserPHID()) {
2425
$id = $revision['id'];
2426
$title = $revision['title'];
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.");
2435
/* -( File Uploads )------------------------------------------------------- */
2438
private function uploadFilesForChanges(array $changes) {
2439
assert_instances_of($changes, 'ArcanistDiffChange');
2441
// Collect all the files we need to upload.
2443
$need_upload = array();
2444
foreach ($changes as $key => $change) {
2445
if ($change->getFileType() != ArcanistDiffChangeType::FILE_BINARY) {
2449
if ($this->getArgument('skip-binaries')) {
2453
$name = basename($change->getCurrentPath());
2455
$need_upload[] = array(
2458
'data' => $change->getOriginalFileData(),
2459
'change' => $change,
2462
$need_upload[] = array(
2465
'data' => $change->getCurrentFileData(),
2466
'change' => $change,
2470
if (!$need_upload) {
2474
// Determine mime types and file sizes. Update changes from "binary" to
2475
// "image" if the file is an image. Set image metadata.
2477
$type_image = ArcanistDiffChangeType::FILE_IMAGE;
2478
foreach ($need_upload as $key => $spec) {
2479
$change = $need_upload[$key]['change'];
2481
$type = $spec['type'];
2482
$size = strlen($spec['data']);
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]);
2494
$mime = $this->getFileMimeType($spec['data']);
2495
if (preg_match('@^image/@', $mime)) {
2496
$change->setFileType($type_image);
2499
$change->setMetadata("{$type}:file:mime-type", $mime);
2502
echo pht('Uploading %d files...', count($need_upload))."\n";
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(
2512
$can_use_hash_upload = isset($conduit_methods['file.uploadhash']);
2514
if ($can_use_hash_upload) {
2515
$hash_futures = array();
2516
foreach ($need_upload as $key => $spec) {
2517
$hash_futures[$key] = $this->getConduit()->callMethod(
2520
'name' => $spec['name'],
2521
'hash' => sha1($spec['data']),
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'];
2532
$phid = $future->resolve();
2533
} catch (Exception $e) {
2534
// Just try uploading normally if the hash upload failed.
2539
$change->setMetadata("{$type}:binary-phid", $phid);
2540
unset($need_upload[$key]);
2541
echo pht("Uploaded '%s' (%s).", $name, $type)."\n";
2546
$upload_futures = array();
2547
foreach ($need_upload as $key => $spec) {
2548
$upload_futures[$key] = $this->getConduit()->callMethod(
2551
'name' => $spec['name'],
2552
'data_base64' => base64_encode($spec['data']),
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'];
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.');
2576
echo pht('Upload complete.')."\n";
2579
private function getFileMimeType($data) {
2580
$tmp = new TempFile();
2581
Filesystem::writeFile($tmp, $data);
2582
return Filesystem::getMimeType($tmp);
2585
private function shouldOpenCreatedObjectsInBrowser() {
2586
return $this->getArgument('browse');