Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/differential/editor/DifferentialTransactionEditor.php
12256 views
1
<?php
2
3
final class DifferentialTransactionEditor
4
extends PhabricatorApplicationTransactionEditor {
5
6
private $changedPriorToCommitURI;
7
private $isCloseByCommit;
8
private $repositoryPHIDOverride = false;
9
private $didExpandInlineState = false;
10
private $firstBroadcast = false;
11
private $wasBroadcasting;
12
private $isDraftDemotion;
13
14
private $ownersDiff;
15
private $ownersChangesets;
16
17
public function getEditorApplicationClass() {
18
return 'PhabricatorDifferentialApplication';
19
}
20
21
public function getEditorObjectsDescription() {
22
return pht('Differential Revisions');
23
}
24
25
public function getCreateObjectTitle($author, $object) {
26
return pht('%s created this revision.', $author);
27
}
28
29
public function getCreateObjectTitleForFeed($author, $object) {
30
return pht('%s created %s.', $author, $object);
31
}
32
33
public function isFirstBroadcast() {
34
return $this->firstBroadcast;
35
}
36
37
public function getDiffUpdateTransaction(array $xactions) {
38
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
39
40
foreach ($xactions as $xaction) {
41
if ($xaction->getTransactionType() == $type_update) {
42
return $xaction;
43
}
44
}
45
46
return null;
47
}
48
49
public function setIsCloseByCommit($is_close_by_commit) {
50
$this->isCloseByCommit = $is_close_by_commit;
51
return $this;
52
}
53
54
public function getIsCloseByCommit() {
55
return $this->isCloseByCommit;
56
}
57
58
public function setChangedPriorToCommitURI($uri) {
59
$this->changedPriorToCommitURI = $uri;
60
return $this;
61
}
62
63
public function getChangedPriorToCommitURI() {
64
return $this->changedPriorToCommitURI;
65
}
66
67
public function setRepositoryPHIDOverride($phid_or_null) {
68
$this->repositoryPHIDOverride = $phid_or_null;
69
return $this;
70
}
71
72
public function getTransactionTypes() {
73
$types = parent::getTransactionTypes();
74
75
$types[] = PhabricatorTransactions::TYPE_COMMENT;
76
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
77
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
78
$types[] = PhabricatorTransactions::TYPE_INLINESTATE;
79
80
$types[] = DifferentialTransaction::TYPE_INLINE;
81
82
return $types;
83
}
84
85
protected function getCustomTransactionOldValue(
86
PhabricatorLiskDAO $object,
87
PhabricatorApplicationTransaction $xaction) {
88
89
switch ($xaction->getTransactionType()) {
90
case DifferentialTransaction::TYPE_INLINE:
91
return null;
92
}
93
94
return parent::getCustomTransactionOldValue($object, $xaction);
95
}
96
97
protected function getCustomTransactionNewValue(
98
PhabricatorLiskDAO $object,
99
PhabricatorApplicationTransaction $xaction) {
100
101
switch ($xaction->getTransactionType()) {
102
case DifferentialTransaction::TYPE_INLINE:
103
return null;
104
}
105
106
return parent::getCustomTransactionNewValue($object, $xaction);
107
}
108
109
protected function applyCustomInternalTransaction(
110
PhabricatorLiskDAO $object,
111
PhabricatorApplicationTransaction $xaction) {
112
113
switch ($xaction->getTransactionType()) {
114
case DifferentialTransaction::TYPE_INLINE:
115
$comment = $xaction->getComment();
116
117
$comment->setAttribute('editing', false);
118
119
PhabricatorVersionedDraft::purgeDrafts(
120
$comment->getPHID(),
121
$this->getActingAsPHID());
122
return;
123
}
124
125
return parent::applyCustomInternalTransaction($object, $xaction);
126
}
127
128
protected function expandTransactions(
129
PhabricatorLiskDAO $object,
130
array $xactions) {
131
132
foreach ($xactions as $xaction) {
133
switch ($xaction->getTransactionType()) {
134
case PhabricatorTransactions::TYPE_INLINESTATE:
135
// If we have an "Inline State" transaction already, the caller
136
// built it for us so we don't need to expand it again.
137
$this->didExpandInlineState = true;
138
break;
139
case DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE:
140
if ($xaction->getMetadataValue('draft.demote')) {
141
$this->isDraftDemotion = true;
142
}
143
break;
144
}
145
}
146
147
$this->wasBroadcasting = $object->getShouldBroadcast();
148
149
return parent::expandTransactions($object, $xactions);
150
}
151
152
protected function expandTransaction(
153
PhabricatorLiskDAO $object,
154
PhabricatorApplicationTransaction $xaction) {
155
156
$results = parent::expandTransaction($object, $xaction);
157
158
$actor = $this->getActor();
159
$actor_phid = $this->getActingAsPHID();
160
$type_edge = PhabricatorTransactions::TYPE_EDGE;
161
162
$edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
163
164
$want_downgrade = array();
165
$must_downgrade = array();
166
if ($this->getIsCloseByCommit()) {
167
// Never downgrade reviewers when we're closing a revision after a
168
// commit.
169
} else {
170
switch ($xaction->getTransactionType()) {
171
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
172
$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
173
$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
174
break;
175
case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:
176
if (!$object->isChangePlanned()) {
177
// If the old state isn't "Changes Planned", downgrade the accepts
178
// even if they're sticky.
179
180
// We don't downgrade for "Changes Planned" to allow an author to
181
// undo a "Plan Changes" by immediately following it up with a
182
// "Request Review".
183
$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
184
$must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
185
}
186
$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
187
break;
188
}
189
}
190
191
if ($want_downgrade) {
192
$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;
193
194
$results[] = id(new DifferentialTransaction())
195
->setTransactionType($void_type)
196
->setIgnoreOnNoEffect(true)
197
->setMetadataValue('void.force', $must_downgrade)
198
->setNewValue($want_downgrade);
199
}
200
201
$new_author_phid = null;
202
switch ($xaction->getTransactionType()) {
203
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
204
if ($this->getIsCloseByCommit()) {
205
// Don't bother with any of this if this update is a side effect of
206
// commit detection.
207
break;
208
}
209
210
// When a revision is updated and the diff comes from a branch named
211
// "T123" or similar, automatically associate the commit with the
212
// task that the branch names.
213
214
$maniphest = 'PhabricatorManiphestApplication';
215
if (PhabricatorApplication::isClassInstalled($maniphest)) {
216
$diff = $this->requireDiff($xaction->getNewValue());
217
$branch = $diff->getBranch();
218
219
// No "$", to allow for branches like T123_demo.
220
$match = null;
221
if ($branch !== null && preg_match('/^T(\d+)/i', $branch, $match)) {
222
$task_id = $match[1];
223
$tasks = id(new ManiphestTaskQuery())
224
->setViewer($this->getActor())
225
->withIDs(array($task_id))
226
->execute();
227
if ($tasks) {
228
$task = head($tasks);
229
$task_phid = $task->getPHID();
230
231
$results[] = id(new DifferentialTransaction())
232
->setTransactionType($type_edge)
233
->setMetadataValue('edge:type', $edge_ref_task)
234
->setIgnoreOnNoEffect(true)
235
->setNewValue(array('+' => array($task_phid => $task_phid)));
236
}
237
}
238
}
239
break;
240
241
case DifferentialRevisionCommandeerTransaction::TRANSACTIONTYPE:
242
$new_author_phid = $actor_phid;
243
break;
244
245
case DifferentialRevisionAuthorTransaction::TRANSACTIONTYPE:
246
$new_author_phid = $xaction->getNewValue();
247
break;
248
249
}
250
251
if ($new_author_phid) {
252
$swap_xaction = $this->newSwapReviewersTransaction(
253
$object,
254
$new_author_phid);
255
if ($swap_xaction) {
256
$results[] = $swap_xaction;
257
}
258
}
259
260
if (!$this->didExpandInlineState) {
261
switch ($xaction->getTransactionType()) {
262
case PhabricatorTransactions::TYPE_COMMENT:
263
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
264
case DifferentialTransaction::TYPE_INLINE:
265
$this->didExpandInlineState = true;
266
267
$query_template = id(new DifferentialDiffInlineCommentQuery())
268
->withRevisionPHIDs(array($object->getPHID()));
269
270
$state_xaction = $this->newInlineStateTransaction(
271
$object,
272
$query_template);
273
274
if ($state_xaction) {
275
$results[] = $state_xaction;
276
}
277
break;
278
}
279
}
280
281
return $results;
282
}
283
284
protected function applyCustomExternalTransaction(
285
PhabricatorLiskDAO $object,
286
PhabricatorApplicationTransaction $xaction) {
287
288
switch ($xaction->getTransactionType()) {
289
case DifferentialTransaction::TYPE_INLINE:
290
$reply = $xaction->getComment()->getReplyToComment();
291
if ($reply && !$reply->getHasReplies()) {
292
$reply->setHasReplies(1)->save();
293
}
294
return;
295
}
296
297
return parent::applyCustomExternalTransaction($object, $xaction);
298
}
299
300
protected function applyBuiltinExternalTransaction(
301
PhabricatorLiskDAO $object,
302
PhabricatorApplicationTransaction $xaction) {
303
304
switch ($xaction->getTransactionType()) {
305
case PhabricatorTransactions::TYPE_INLINESTATE:
306
$table = new DifferentialTransactionComment();
307
$conn_w = $table->establishConnection('w');
308
foreach ($xaction->getNewValue() as $phid => $state) {
309
queryfx(
310
$conn_w,
311
'UPDATE %T SET fixedState = %s WHERE phid = %s',
312
$table->getTableName(),
313
$state,
314
$phid);
315
}
316
break;
317
}
318
319
return parent::applyBuiltinExternalTransaction($object, $xaction);
320
}
321
322
protected function applyFinalEffects(
323
PhabricatorLiskDAO $object,
324
array $xactions) {
325
326
// Load the most up-to-date version of the revision and its reviewers,
327
// so we don't need to try to deduce the state of reviewers by examining
328
// all the changes made by the transactions. Then, update the reviewers
329
// on the object to make sure we're acting on the current reviewer set
330
// (and, for example, sending mail to the right people).
331
332
$new_revision = id(new DifferentialRevisionQuery())
333
->setViewer($this->getActor())
334
->needReviewers(true)
335
->needActiveDiffs(true)
336
->withIDs(array($object->getID()))
337
->executeOne();
338
if (!$new_revision) {
339
throw new Exception(
340
pht('Failed to load revision from transaction finalization.'));
341
}
342
343
$active_diff = $new_revision->getActiveDiff();
344
$new_diff_phid = $active_diff->getPHID();
345
346
$object->attachReviewers($new_revision->getReviewers());
347
$object->attachActiveDiff($active_diff);
348
$object->attachRepository($new_revision->getRepository());
349
350
$has_new_diff = false;
351
$should_index_paths = false;
352
$should_index_hashes = false;
353
$need_changesets = false;
354
355
foreach ($xactions as $xaction) {
356
switch ($xaction->getTransactionType()) {
357
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
358
$need_changesets = true;
359
360
$new_diff_phid = $xaction->getNewValue();
361
$has_new_diff = true;
362
363
$should_index_paths = true;
364
$should_index_hashes = true;
365
break;
366
case DifferentialRevisionRepositoryTransaction::TRANSACTIONTYPE:
367
// The "AffectedPath" table denormalizes the repository, so we
368
// want to update the index if the repository changes.
369
370
$need_changesets = true;
371
372
$should_index_paths = true;
373
break;
374
}
375
}
376
377
if ($need_changesets) {
378
$new_diff = $this->requireDiff($new_diff_phid, true);
379
380
if ($should_index_paths) {
381
id(new DifferentialAffectedPathEngine())
382
->setRevision($object)
383
->setDiff($new_diff)
384
->updateAffectedPaths();
385
}
386
387
if ($should_index_hashes) {
388
$this->updateRevisionHashTable($object, $new_diff);
389
}
390
391
if ($has_new_diff) {
392
$this->ownersDiff = $new_diff;
393
$this->ownersChangesets = $new_diff->getChangesets();
394
}
395
}
396
397
$xactions = $this->updateReviewStatus($object, $xactions);
398
$this->markReviewerComments($object, $xactions);
399
400
return $xactions;
401
}
402
403
private function updateReviewStatus(
404
DifferentialRevision $revision,
405
array $xactions) {
406
407
$was_accepted = $revision->isAccepted();
408
$was_revision = $revision->isNeedsRevision();
409
$was_review = $revision->isNeedsReview();
410
if (!$was_accepted && !$was_revision && !$was_review) {
411
// Revisions can't transition out of other statuses (like closed or
412
// abandoned) as a side effect of reviewer status changes.
413
return $xactions;
414
}
415
416
// Try to move a revision to "accepted". We look for:
417
//
418
// - at least one accepting reviewer who is a user; and
419
// - no rejects; and
420
// - no rejects of older diffs; and
421
// - no blocking reviewers.
422
423
$has_accepting_user = false;
424
$has_rejecting_reviewer = false;
425
$has_rejecting_older_reviewer = false;
426
$has_blocking_reviewer = false;
427
428
$active_diff = $revision->getActiveDiff();
429
foreach ($revision->getReviewers() as $reviewer) {
430
$reviewer_status = $reviewer->getReviewerStatus();
431
switch ($reviewer_status) {
432
case DifferentialReviewerStatus::STATUS_REJECTED:
433
$active_phid = $active_diff->getPHID();
434
if ($reviewer->isRejected($active_phid)) {
435
$has_rejecting_reviewer = true;
436
} else {
437
$has_rejecting_older_reviewer = true;
438
}
439
break;
440
case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:
441
$has_rejecting_older_reviewer = true;
442
break;
443
case DifferentialReviewerStatus::STATUS_BLOCKING:
444
$has_blocking_reviewer = true;
445
break;
446
case DifferentialReviewerStatus::STATUS_ACCEPTED:
447
if ($reviewer->isUser()) {
448
$active_phid = $active_diff->getPHID();
449
if ($reviewer->isAccepted($active_phid)) {
450
$has_accepting_user = true;
451
}
452
}
453
break;
454
}
455
}
456
457
$new_status = null;
458
if ($has_accepting_user &&
459
!$has_rejecting_reviewer &&
460
!$has_rejecting_older_reviewer &&
461
!$has_blocking_reviewer) {
462
$new_status = DifferentialRevisionStatus::ACCEPTED;
463
} else if ($has_rejecting_reviewer) {
464
// This isn't accepted, and there's at least one rejecting reviewer,
465
// so the revision needs changes. This usually happens after a
466
// "reject".
467
$new_status = DifferentialRevisionStatus::NEEDS_REVISION;
468
} else if ($was_accepted) {
469
// This revision was accepted, but it no longer satisfies the
470
// conditions for acceptance. This usually happens after an accepting
471
// reviewer resigns or is removed.
472
$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;
473
} else if ($was_revision) {
474
// This revision was "Needs Revision", but no longer has any rejecting
475
// reviewers. This usually happens after the last rejecting reviewer
476
// resigns or is removed. Put the revision back in "Needs Review".
477
$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;
478
}
479
480
if ($new_status === null) {
481
return $xactions;
482
}
483
484
$old_status = $revision->getModernRevisionStatus();
485
if ($new_status == $old_status) {
486
return $xactions;
487
}
488
489
$xaction = id(new DifferentialTransaction())
490
->setTransactionType(
491
DifferentialRevisionStatusTransaction::TRANSACTIONTYPE)
492
->setOldValue($old_status)
493
->setNewValue($new_status);
494
495
$xaction = $this->populateTransaction($revision, $xaction)
496
->save();
497
$xactions[] = $xaction;
498
499
// Save the status adjustment we made earlier.
500
$revision
501
->setModernRevisionStatus($new_status)
502
->save();
503
504
return $xactions;
505
}
506
507
protected function sortTransactions(array $xactions) {
508
$xactions = parent::sortTransactions($xactions);
509
510
$head = array();
511
$tail = array();
512
513
foreach ($xactions as $xaction) {
514
$type = $xaction->getTransactionType();
515
if ($type == DifferentialTransaction::TYPE_INLINE) {
516
$tail[] = $xaction;
517
} else {
518
$head[] = $xaction;
519
}
520
}
521
522
return array_values(array_merge($head, $tail));
523
}
524
525
protected function shouldPublishFeedStory(
526
PhabricatorLiskDAO $object,
527
array $xactions) {
528
529
if (!$object->getShouldBroadcast()) {
530
return false;
531
}
532
533
return true;
534
}
535
536
protected function shouldSendMail(
537
PhabricatorLiskDAO $object,
538
array $xactions) {
539
return true;
540
}
541
542
protected function getMailTo(PhabricatorLiskDAO $object) {
543
if ($object->getShouldBroadcast()) {
544
$this->requireReviewers($object);
545
546
$phids = array();
547
$phids[] = $object->getAuthorPHID();
548
foreach ($object->getReviewers() as $reviewer) {
549
if ($reviewer->isResigned()) {
550
continue;
551
}
552
553
$phids[] = $reviewer->getReviewerPHID();
554
}
555
return $phids;
556
}
557
558
// If we're demoting a draft after a build failure, just notify the author.
559
if ($this->isDraftDemotion) {
560
$author_phid = $object->getAuthorPHID();
561
return array(
562
$author_phid,
563
);
564
}
565
566
return array();
567
}
568
569
protected function getMailCC(PhabricatorLiskDAO $object) {
570
if (!$object->getShouldBroadcast()) {
571
return array();
572
}
573
574
return parent::getMailCC($object);
575
}
576
577
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
578
$this->requireReviewers($object);
579
580
$phids = array();
581
582
foreach ($object->getReviewers() as $reviewer) {
583
if ($reviewer->isResigned()) {
584
$phids[] = $reviewer->getReviewerPHID();
585
}
586
}
587
588
return $phids;
589
}
590
591
protected function getMailAction(
592
PhabricatorLiskDAO $object,
593
array $xactions) {
594
595
$show_lines = false;
596
if ($this->isFirstBroadcast()) {
597
$action = pht('Request');
598
599
$show_lines = true;
600
} else {
601
$action = parent::getMailAction($object, $xactions);
602
603
$strongest = $this->getStrongestAction($object, $xactions);
604
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
605
if ($strongest->getTransactionType() == $type_update) {
606
$show_lines = true;
607
}
608
}
609
610
if ($show_lines) {
611
$count = new PhutilNumber($object->getLineCount());
612
$action = pht('%s] [%s', $action, $object->getRevisionScaleGlyphs());
613
}
614
615
return $action;
616
}
617
618
protected function getMailSubjectPrefix() {
619
return pht('[Differential]');
620
}
621
622
protected function getMailThreadID(PhabricatorLiskDAO $object) {
623
// This is nonstandard, but retains threading with older messages.
624
$phid = $object->getPHID();
625
return "differential-rev-{$phid}-req";
626
}
627
628
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
629
return id(new DifferentialReplyHandler())
630
->setMailReceiver($object);
631
}
632
633
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
634
$monogram = $object->getMonogram();
635
$title = $object->getTitle();
636
637
return id(new PhabricatorMetaMTAMail())
638
->setSubject(pht('%s: %s', $monogram, $title))
639
->setMustEncryptSubject(pht('%s: Revision Updated', $monogram))
640
->setMustEncryptURI($object->getURI());
641
}
642
643
protected function getTransactionsForMail(
644
PhabricatorLiskDAO $object,
645
array $xactions) {
646
// If this is the first time we're sending mail about this revision, we
647
// generate mail for all prior transactions, not just whatever is being
648
// applied now. This gets the "added reviewers" lines and other relevant
649
// information into the mail.
650
if ($this->isFirstBroadcast()) {
651
return $this->loadUnbroadcastTransactions($object);
652
}
653
654
return $xactions;
655
}
656
657
protected function getObjectLinkButtonLabelForMail(
658
PhabricatorLiskDAO $object) {
659
return pht('View Revision');
660
}
661
662
protected function buildMailBody(
663
PhabricatorLiskDAO $object,
664
array $xactions) {
665
666
$viewer = $this->requireActor();
667
668
$body = id(new PhabricatorMetaMTAMailBody())
669
->setViewer($viewer);
670
671
$revision_uri = $this->getObjectLinkButtonURIForMail($object);
672
$new_uri = $revision_uri.'/new/';
673
674
$this->addHeadersAndCommentsToMailBody(
675
$body,
676
$xactions,
677
$this->getObjectLinkButtonLabelForMail($object),
678
$revision_uri);
679
680
$type_inline = DifferentialTransaction::TYPE_INLINE;
681
682
$inlines = array();
683
foreach ($xactions as $xaction) {
684
if ($xaction->getTransactionType() == $type_inline) {
685
$inlines[] = $xaction;
686
}
687
}
688
689
if ($inlines) {
690
$this->appendInlineCommentsForMail($object, $inlines, $body);
691
}
692
693
$update_xaction = null;
694
foreach ($xactions as $xaction) {
695
switch ($xaction->getTransactionType()) {
696
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
697
$update_xaction = $xaction;
698
break;
699
}
700
}
701
702
if ($update_xaction) {
703
$diff = $this->requireDiff($update_xaction->getNewValue(), true);
704
} else {
705
$diff = null;
706
}
707
708
$changed_uri = $this->getChangedPriorToCommitURI();
709
if ($changed_uri) {
710
$body->addLinkSection(
711
pht('CHANGED PRIOR TO COMMIT'),
712
$changed_uri);
713
}
714
715
$this->addCustomFieldsToMailBody($body, $object, $xactions);
716
717
if (!$this->isFirstBroadcast()) {
718
$body->addLinkSection(pht('CHANGES SINCE LAST ACTION'), $new_uri);
719
}
720
721
$body->addLinkSection(
722
pht('REVISION DETAIL'),
723
$revision_uri);
724
725
if ($update_xaction) {
726
$body->addTextSection(
727
pht('AFFECTED FILES'),
728
$this->renderAffectedFilesForMail($diff));
729
730
$config_key_inline = 'metamta.differential.inline-patches';
731
$config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);
732
733
$config_key_attach = 'metamta.differential.attach-patches';
734
$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
735
736
if ($config_inline || $config_attach) {
737
$body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
738
739
try {
740
$patch = $this->buildPatchForMail($diff, $body_limit);
741
} catch (ArcanistDiffByteSizeException $ex) {
742
$patch = null;
743
}
744
745
if (($patch !== null) && $config_inline) {
746
$lines = substr_count($patch, "\n");
747
$bytes = strlen($patch);
748
749
// Limit the patch size to the smaller of 256 bytes per line or
750
// the mail body limit. This prevents degenerate behavior for patches
751
// with one line that is 10MB long. See T11748.
752
$byte_limits = array();
753
$byte_limits[] = (256 * $config_inline);
754
$byte_limits[] = $body_limit;
755
$byte_limit = min($byte_limits);
756
757
$lines_ok = ($lines <= $config_inline);
758
$bytes_ok = ($bytes <= $byte_limit);
759
760
if ($lines_ok && $bytes_ok) {
761
$this->appendChangeDetailsForMail($object, $diff, $patch, $body);
762
} else {
763
// TODO: Provide a helpful message about the patch being too
764
// large or lengthy here.
765
}
766
}
767
768
if (($patch !== null) && $config_attach) {
769
// See T12033, T11767, and PHI55. This is a crude fix to stop the
770
// major concrete problems that lackluster email size limits cause.
771
if (strlen($patch) < $body_limit) {
772
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
773
$mime_type = 'text/x-patch; charset=utf-8';
774
$body->addAttachment(
775
new PhabricatorMailAttachment($patch, $name, $mime_type));
776
}
777
}
778
}
779
}
780
781
return $body;
782
}
783
784
public function getMailTagsMap() {
785
return array(
786
DifferentialTransaction::MAILTAG_REVIEW_REQUEST =>
787
pht('A revision is created.'),
788
DifferentialTransaction::MAILTAG_UPDATED =>
789
pht('A revision is updated.'),
790
DifferentialTransaction::MAILTAG_COMMENT =>
791
pht('Someone comments on a revision.'),
792
DifferentialTransaction::MAILTAG_CLOSED =>
793
pht('A revision is closed.'),
794
DifferentialTransaction::MAILTAG_REVIEWERS =>
795
pht("A revision's reviewers change."),
796
DifferentialTransaction::MAILTAG_CC =>
797
pht("A revision's CCs change."),
798
DifferentialTransaction::MAILTAG_OTHER =>
799
pht('Other revision activity not listed above occurs.'),
800
);
801
}
802
803
protected function supportsSearch() {
804
return true;
805
}
806
807
protected function expandCustomRemarkupBlockTransactions(
808
PhabricatorLiskDAO $object,
809
array $xactions,
810
array $changes,
811
PhutilMarkupEngine $engine) {
812
813
// For "Fixes ..." and "Depends on ...", we're only going to look at
814
// content blocks which are part of the revision itself (like "Summary"
815
// and "Test Plan"), not comments.
816
$content_parts = array();
817
foreach ($changes as $change) {
818
if ($change->getTransaction()->isCommentTransaction()) {
819
continue;
820
}
821
$content_parts[] = $change->getNewValue();
822
}
823
if (!$content_parts) {
824
return array();
825
}
826
$content_block = implode("\n\n", $content_parts);
827
$task_map = array();
828
$task_refs = id(new ManiphestCustomFieldStatusParser())
829
->parseCorpus($content_block);
830
foreach ($task_refs as $match) {
831
foreach ($match['monograms'] as $monogram) {
832
$task_id = (int)trim($monogram, 'tT');
833
$task_map[$task_id] = true;
834
}
835
}
836
837
$rev_map = array();
838
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
839
->parseCorpus($content_block);
840
foreach ($rev_refs as $match) {
841
foreach ($match['monograms'] as $monogram) {
842
$rev_id = (int)trim($monogram, 'dD');
843
$rev_map[$rev_id] = true;
844
}
845
}
846
847
$edges = array();
848
$task_phids = array();
849
$rev_phids = array();
850
851
if ($task_map) {
852
$tasks = id(new ManiphestTaskQuery())
853
->setViewer($this->getActor())
854
->withIDs(array_keys($task_map))
855
->execute();
856
857
if ($tasks) {
858
$task_phids = mpull($tasks, 'getPHID', 'getPHID');
859
$edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;
860
$edges[$edge_related] = $task_phids;
861
}
862
}
863
864
if ($rev_map) {
865
$revs = id(new DifferentialRevisionQuery())
866
->setViewer($this->getActor())
867
->withIDs(array_keys($rev_map))
868
->execute();
869
$rev_phids = mpull($revs, 'getPHID', 'getPHID');
870
871
// NOTE: Skip any write attempts if a user cleverly implies a revision
872
// depends upon itself.
873
unset($rev_phids[$object->getPHID()]);
874
875
if ($revs) {
876
$depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
877
$edges[$depends] = $rev_phids;
878
}
879
}
880
881
$revert_refs = id(new DifferentialCustomFieldRevertsParser())
882
->parseCorpus($content_block);
883
884
$revert_monograms = array();
885
foreach ($revert_refs as $match) {
886
foreach ($match['monograms'] as $monogram) {
887
$revert_monograms[] = $monogram;
888
}
889
}
890
891
if ($revert_monograms) {
892
$revert_objects = DiffusionCommitRevisionQuery::loadRevertedObjects(
893
$this->getActor(),
894
$object,
895
$revert_monograms,
896
null);
897
898
$revert_phids = mpull($revert_objects, 'getPHID', 'getPHID');
899
900
$revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST;
901
$edges[$revert_type] = $revert_phids;
902
} else {
903
$revert_phids = array();
904
}
905
906
$this->addUnmentionablePHIDs($task_phids);
907
$this->addUnmentionablePHIDs($rev_phids);
908
$this->addUnmentionablePHIDs($revert_phids);
909
910
$result = array();
911
foreach ($edges as $type => $specs) {
912
$result[] = id(new DifferentialTransaction())
913
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
914
->setMetadataValue('edge:type', $type)
915
->setNewValue(array('+' => $specs));
916
}
917
918
return $result;
919
}
920
921
private function appendInlineCommentsForMail(
922
PhabricatorLiskDAO $object,
923
array $inlines,
924
PhabricatorMetaMTAMailBody $body) {
925
926
$limit = 100;
927
$limit_note = null;
928
if (count($inlines) > $limit) {
929
$limit_note = pht(
930
'(Showing first %s of %s inline comments.)',
931
new PhutilNumber($limit),
932
phutil_count($inlines));
933
934
$inlines = array_slice($inlines, 0, $limit, true);
935
}
936
937
$section = id(new DifferentialInlineCommentMailView())
938
->setViewer($this->getActor())
939
->setInlines($inlines)
940
->buildMailSection();
941
942
$header = pht('INLINE COMMENTS');
943
944
$section_text = "\n".$section->getPlaintext();
945
if ($limit_note) {
946
$section_text = $limit_note."\n".$section_text;
947
}
948
949
$style = array(
950
'margin: 6px 0 12px 0;',
951
);
952
953
$section_html = phutil_tag(
954
'div',
955
array(
956
'style' => implode(' ', $style),
957
),
958
$section->getHTML());
959
960
if ($limit_note) {
961
$section_html = array(
962
phutil_tag(
963
'em',
964
array(),
965
$limit_note),
966
$section_html,
967
);
968
}
969
970
$body->addPlaintextSection($header, $section_text, false);
971
$body->addHTMLSection($header, $section_html);
972
}
973
974
private function appendChangeDetailsForMail(
975
PhabricatorLiskDAO $object,
976
DifferentialDiff $diff,
977
$patch,
978
PhabricatorMetaMTAMailBody $body) {
979
980
$section = id(new DifferentialChangeDetailMailView())
981
->setViewer($this->getActor())
982
->setDiff($diff)
983
->setPatch($patch)
984
->buildMailSection();
985
986
$header = pht('CHANGE DETAILS');
987
988
$section_text = "\n".$section->getPlaintext();
989
990
$style = array(
991
'margin: 6px 0 12px 0;',
992
);
993
994
$section_html = phutil_tag(
995
'div',
996
array(
997
'style' => implode(' ', $style),
998
),
999
$section->getHTML());
1000
1001
$body->addPlaintextSection($header, $section_text, false);
1002
$body->addHTMLSection($header, $section_html);
1003
}
1004
1005
private function loadDiff($phid, $need_changesets = false) {
1006
$query = id(new DifferentialDiffQuery())
1007
->withPHIDs(array($phid))
1008
->setViewer($this->getActor());
1009
1010
if ($need_changesets) {
1011
$query->needChangesets(true);
1012
}
1013
1014
return $query->executeOne();
1015
}
1016
1017
public function requireDiff($phid, $need_changesets = false) {
1018
$diff = $this->loadDiff($phid, $need_changesets);
1019
if (!$diff) {
1020
throw new Exception(pht('Diff "%s" does not exist!', $phid));
1021
}
1022
1023
return $diff;
1024
}
1025
1026
/* -( Herald Integration )------------------------------------------------- */
1027
1028
protected function shouldApplyHeraldRules(
1029
PhabricatorLiskDAO $object,
1030
array $xactions) {
1031
return true;
1032
}
1033
1034
protected function didApplyHeraldRules(
1035
PhabricatorLiskDAO $object,
1036
HeraldAdapter $adapter,
1037
HeraldTranscript $transcript) {
1038
1039
$repository = $object->getRepository();
1040
if (!$repository) {
1041
return array();
1042
}
1043
1044
$diff = $this->ownersDiff;
1045
$changesets = $this->ownersChangesets;
1046
1047
$this->ownersDiff = null;
1048
$this->ownersChangesets = null;
1049
1050
if (!$changesets) {
1051
return array();
1052
}
1053
1054
$packages = PhabricatorOwnersPackage::loadAffectedPackagesForChangesets(
1055
$repository,
1056
$diff,
1057
$changesets);
1058
if (!$packages) {
1059
return array();
1060
}
1061
1062
// Identify the packages with "Non-Owner Author" review rules and remove
1063
// them if the author has authority over the package.
1064
1065
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
1066
$need_authority = array();
1067
foreach ($packages as $package) {
1068
$autoreview_setting = $package->getAutoReview();
1069
1070
$spec = idx($autoreview_map, $autoreview_setting);
1071
if (!$spec) {
1072
continue;
1073
}
1074
1075
if (idx($spec, 'authority')) {
1076
$need_authority[$package->getPHID()] = $package->getPHID();
1077
}
1078
}
1079
1080
if ($need_authority) {
1081
$authority = id(new PhabricatorOwnersPackageQuery())
1082
->setViewer(PhabricatorUser::getOmnipotentUser())
1083
->withPHIDs($need_authority)
1084
->withAuthorityPHIDs(array($object->getAuthorPHID()))
1085
->execute();
1086
$authority = mpull($authority, null, 'getPHID');
1087
1088
foreach ($packages as $key => $package) {
1089
$package_phid = $package->getPHID();
1090
if (isset($authority[$package_phid])) {
1091
unset($packages[$key]);
1092
continue;
1093
}
1094
}
1095
1096
if (!$packages) {
1097
return array();
1098
}
1099
}
1100
1101
$auto_subscribe = array();
1102
$auto_review = array();
1103
$auto_block = array();
1104
1105
foreach ($packages as $package) {
1106
switch ($package->getAutoReview()) {
1107
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW:
1108
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW_ALWAYS:
1109
$auto_review[] = $package;
1110
break;
1111
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK:
1112
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK_ALWAYS:
1113
$auto_block[] = $package;
1114
break;
1115
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE:
1116
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE_ALWAYS:
1117
$auto_subscribe[] = $package;
1118
break;
1119
case PhabricatorOwnersPackage::AUTOREVIEW_NONE:
1120
default:
1121
break;
1122
}
1123
}
1124
1125
$owners_phid = id(new PhabricatorOwnersApplication())
1126
->getPHID();
1127
1128
$xactions = array();
1129
if ($auto_subscribe) {
1130
$xactions[] = $object->getApplicationTransactionTemplate()
1131
->setAuthorPHID($owners_phid)
1132
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
1133
->setNewValue(
1134
array(
1135
'+' => mpull($auto_subscribe, 'getPHID'),
1136
));
1137
}
1138
1139
$specs = array(
1140
array($auto_review, false),
1141
array($auto_block, true),
1142
);
1143
1144
foreach ($specs as $spec) {
1145
list($reviewers, $blocking) = $spec;
1146
if (!$reviewers) {
1147
continue;
1148
}
1149
1150
$phids = mpull($reviewers, 'getPHID');
1151
$xaction = $this->newAutoReviewTransaction($object, $phids, $blocking);
1152
if ($xaction) {
1153
$xactions[] = $xaction;
1154
}
1155
}
1156
1157
return $xactions;
1158
}
1159
1160
private function newAutoReviewTransaction(
1161
PhabricatorLiskDAO $object,
1162
array $phids,
1163
$is_blocking) {
1164
1165
// TODO: This is substantially similar to DifferentialReviewersHeraldAction
1166
// and both are needlessly complex. This logic should live in the normal
1167
// transaction application pipeline. See T10967.
1168
1169
$reviewers = $object->getReviewers();
1170
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
1171
1172
if ($is_blocking) {
1173
$new_status = DifferentialReviewerStatus::STATUS_BLOCKING;
1174
} else {
1175
$new_status = DifferentialReviewerStatus::STATUS_ADDED;
1176
}
1177
1178
$new_strength = DifferentialReviewerStatus::getStatusStrength(
1179
$new_status);
1180
1181
$current = array();
1182
foreach ($phids as $phid) {
1183
if (!isset($reviewers[$phid])) {
1184
continue;
1185
}
1186
1187
// If we're applying a stronger status (usually, upgrading a reviewer
1188
// into a blocking reviewer), skip this check so we apply the change.
1189
$old_strength = DifferentialReviewerStatus::getStatusStrength(
1190
$reviewers[$phid]->getReviewerStatus());
1191
if ($old_strength <= $new_strength) {
1192
continue;
1193
}
1194
1195
$current[] = $phid;
1196
}
1197
1198
$phids = array_diff($phids, $current);
1199
1200
if (!$phids) {
1201
return null;
1202
}
1203
1204
$phids = array_fuse($phids);
1205
1206
$value = array();
1207
foreach ($phids as $phid) {
1208
if ($is_blocking) {
1209
$value[] = 'blocking('.$phid.')';
1210
} else {
1211
$value[] = $phid;
1212
}
1213
}
1214
1215
$owners_phid = id(new PhabricatorOwnersApplication())
1216
->getPHID();
1217
1218
$reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
1219
1220
return $object->getApplicationTransactionTemplate()
1221
->setAuthorPHID($owners_phid)
1222
->setTransactionType($reviewers_type)
1223
->setNewValue(
1224
array(
1225
'+' => $value,
1226
));
1227
}
1228
1229
protected function buildHeraldAdapter(
1230
PhabricatorLiskDAO $object,
1231
array $xactions) {
1232
1233
$revision = id(new DifferentialRevisionQuery())
1234
->setViewer($this->getActor())
1235
->withPHIDs(array($object->getPHID()))
1236
->needActiveDiffs(true)
1237
->needReviewers(true)
1238
->executeOne();
1239
if (!$revision) {
1240
throw new Exception(
1241
pht('Failed to load revision for Herald adapter construction!'));
1242
}
1243
1244
$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
1245
$revision,
1246
$revision->getActiveDiff());
1247
1248
// If the object is still a draft, prevent "Send me an email" and other
1249
// similar rules from acting yet.
1250
if (!$object->getShouldBroadcast()) {
1251
$adapter->setForbiddenAction(
1252
HeraldMailableState::STATECONST,
1253
DifferentialHeraldStateReasons::REASON_DRAFT);
1254
}
1255
1256
// If this edit didn't actually change the diff (for example, a user
1257
// edited the title or changed subscribers), prevent "Run build plan"
1258
// and other similar rules from acting yet, since the build results will
1259
// not (or, at least, should not) change unless the actual source changes.
1260
// We also don't run Differential builds if the update was caused by
1261
// discovering a commit, as the expectation is that Diffusion builds take
1262
// over once things land.
1263
$has_update = false;
1264
$has_commit = false;
1265
1266
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
1267
foreach ($xactions as $xaction) {
1268
if ($xaction->getTransactionType() != $type_update) {
1269
continue;
1270
}
1271
1272
if ($xaction->getMetadataValue('isCommitUpdate')) {
1273
$has_commit = true;
1274
} else {
1275
$has_update = true;
1276
}
1277
1278
break;
1279
}
1280
1281
if ($has_commit) {
1282
$adapter->setForbiddenAction(
1283
HeraldBuildableState::STATECONST,
1284
DifferentialHeraldStateReasons::REASON_LANDED);
1285
} else if (!$has_update) {
1286
$adapter->setForbiddenAction(
1287
HeraldBuildableState::STATECONST,
1288
DifferentialHeraldStateReasons::REASON_UNCHANGED);
1289
}
1290
1291
return $adapter;
1292
}
1293
1294
/**
1295
* Update the table connecting revisions to DVCS local hashes, so we can
1296
* identify revisions by commit/tree hashes.
1297
*/
1298
private function updateRevisionHashTable(
1299
DifferentialRevision $revision,
1300
DifferentialDiff $diff) {
1301
1302
$vcs = $diff->getSourceControlSystem();
1303
if ($vcs == DifferentialRevisionControlSystem::SVN) {
1304
// Subversion has no local commit or tree hash information, so we don't
1305
// have to do anything.
1306
return;
1307
}
1308
1309
$property = id(new DifferentialDiffProperty())->loadOneWhere(
1310
'diffID = %d AND name = %s',
1311
$diff->getID(),
1312
'local:commits');
1313
if (!$property) {
1314
return;
1315
}
1316
1317
$hashes = array();
1318
1319
$data = $property->getData();
1320
switch ($vcs) {
1321
case DifferentialRevisionControlSystem::GIT:
1322
foreach ($data as $commit) {
1323
$hashes[] = array(
1324
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
1325
$commit['commit'],
1326
);
1327
$hashes[] = array(
1328
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
1329
$commit['tree'],
1330
);
1331
}
1332
break;
1333
case DifferentialRevisionControlSystem::MERCURIAL:
1334
foreach ($data as $commit) {
1335
$hashes[] = array(
1336
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
1337
$commit['rev'],
1338
);
1339
}
1340
break;
1341
}
1342
1343
$conn_w = $revision->establishConnection('w');
1344
1345
$sql = array();
1346
foreach ($hashes as $info) {
1347
list($type, $hash) = $info;
1348
$sql[] = qsprintf(
1349
$conn_w,
1350
'(%d, %s, %s)',
1351
$revision->getID(),
1352
$type,
1353
$hash);
1354
}
1355
1356
queryfx(
1357
$conn_w,
1358
'DELETE FROM %T WHERE revisionID = %d',
1359
ArcanistDifferentialRevisionHash::TABLE_NAME,
1360
$revision->getID());
1361
1362
if ($sql) {
1363
queryfx(
1364
$conn_w,
1365
'INSERT INTO %T (revisionID, type, hash) VALUES %LQ',
1366
ArcanistDifferentialRevisionHash::TABLE_NAME,
1367
$sql);
1368
}
1369
}
1370
1371
private function renderAffectedFilesForMail(DifferentialDiff $diff) {
1372
$changesets = $diff->getChangesets();
1373
1374
$filenames = mpull($changesets, 'getDisplayFilename');
1375
sort($filenames);
1376
1377
$count = count($filenames);
1378
$max = 250;
1379
if ($count > $max) {
1380
$filenames = array_slice($filenames, 0, $max);
1381
$filenames[] = pht('(%d more files...)', ($count - $max));
1382
}
1383
1384
return implode("\n", $filenames);
1385
}
1386
1387
private function renderPatchHTMLForMail($patch) {
1388
return phutil_tag('pre',
1389
array('style' => 'font-family: monospace;'), $patch);
1390
}
1391
1392
private function buildPatchForMail(DifferentialDiff $diff, $byte_limit) {
1393
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
1394
1395
return id(new DifferentialRawDiffRenderer())
1396
->setViewer($this->getActor())
1397
->setFormat($format)
1398
->setChangesets($diff->getChangesets())
1399
->setByteLimit($byte_limit)
1400
->buildPatch();
1401
}
1402
1403
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
1404
// Reload to pick up the active diff and reviewer status.
1405
return id(new DifferentialRevisionQuery())
1406
->setViewer($this->getActor())
1407
->needReviewers(true)
1408
->needActiveDiffs(true)
1409
->withIDs(array($object->getID()))
1410
->executeOne();
1411
}
1412
1413
protected function getCustomWorkerState() {
1414
return array(
1415
'changedPriorToCommitURI' => $this->changedPriorToCommitURI,
1416
'firstBroadcast' => $this->firstBroadcast,
1417
'isDraftDemotion' => $this->isDraftDemotion,
1418
);
1419
}
1420
1421
protected function loadCustomWorkerState(array $state) {
1422
$this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI');
1423
$this->firstBroadcast = idx($state, 'firstBroadcast');
1424
$this->isDraftDemotion = idx($state, 'isDraftDemotion');
1425
return $this;
1426
}
1427
1428
private function newSwapReviewersTransaction(
1429
DifferentialRevision $revision,
1430
$new_author_phid) {
1431
1432
$old_author_phid = $revision->getAuthorPHID();
1433
1434
if ($old_author_phid === $new_author_phid) {
1435
return;
1436
}
1437
1438
// If the revision is changing authorship, add the previous author as a
1439
// reviewer and remove the new author.
1440
1441
$edits = array(
1442
'-' => array(
1443
$new_author_phid,
1444
),
1445
'+' => array(
1446
$old_author_phid,
1447
),
1448
);
1449
1450
// NOTE: We're setting setIsCommandeerSideEffect() on this because normally
1451
// you can't add a revision's author as a reviewer, but this action swaps
1452
// them after validation executes.
1453
1454
$xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
1455
1456
return id(new DifferentialTransaction())
1457
->setTransactionType($xaction_type)
1458
->setIgnoreOnNoEffect(true)
1459
->setIsCommandeerSideEffect(true)
1460
->setNewValue($edits);
1461
}
1462
1463
1464
public function getActiveDiff($object) {
1465
if ($this->getIsNewObject()) {
1466
return null;
1467
} else {
1468
return $object->getActiveDiff();
1469
}
1470
}
1471
1472
/**
1473
* When a reviewer makes a comment, mark the last revision they commented
1474
* on.
1475
*
1476
* This allows us to show a hint to help authors and other reviewers quickly
1477
* distinguish between reviewers who have participated in the discussion and
1478
* reviewers who haven't been part of it.
1479
*/
1480
private function markReviewerComments($object, array $xactions) {
1481
$acting_phid = $this->getActingAsPHID();
1482
if (!$acting_phid) {
1483
return;
1484
}
1485
1486
$diff = $this->getActiveDiff($object);
1487
if (!$diff) {
1488
return;
1489
}
1490
1491
$has_comment = false;
1492
foreach ($xactions as $xaction) {
1493
if ($xaction->hasComment()) {
1494
$has_comment = true;
1495
break;
1496
}
1497
}
1498
1499
if (!$has_comment) {
1500
return;
1501
}
1502
1503
$reviewer_table = new DifferentialReviewer();
1504
$conn = $reviewer_table->establishConnection('w');
1505
1506
queryfx(
1507
$conn,
1508
'UPDATE %T SET lastCommentDiffPHID = %s
1509
WHERE revisionPHID = %s
1510
AND reviewerPHID = %s',
1511
$reviewer_table->getTableName(),
1512
$diff->getPHID(),
1513
$object->getPHID(),
1514
$acting_phid);
1515
}
1516
1517
private function loadUnbroadcastTransactions($object) {
1518
$viewer = $this->requireActor();
1519
1520
$xactions = id(new DifferentialTransactionQuery())
1521
->setViewer($viewer)
1522
->withObjectPHIDs(array($object->getPHID()))
1523
->execute();
1524
1525
return array_reverse($xactions);
1526
}
1527
1528
1529
protected function didApplyTransactions($object, array $xactions) {
1530
// In a moment, we're going to try to publish draft revisions which have
1531
// completed all their builds. However, we only want to do that if the
1532
// actor is either the revision author or an omnipotent user (generally,
1533
// the Harbormaster application).
1534
1535
// If we let any actor publish the revision as a side effect of other
1536
// changes then an unlucky third party who innocently comments on the draft
1537
// can end up racing Harbormaster and promoting the revision. At best, this
1538
// is confusing. It can also run into validation problems with the "Request
1539
// Review" transaction. See PHI309 for some discussion.
1540
$author_phid = $object->getAuthorPHID();
1541
$viewer = $this->requireActor();
1542
$can_undraft =
1543
($this->getActingAsPHID() === $author_phid) ||
1544
($viewer->isOmnipotent());
1545
1546
// If a draft revision has no outstanding builds and we're automatically
1547
// making drafts public after builds finish, make the revision public.
1548
if ($can_undraft) {
1549
$auto_undraft = !$object->getHoldAsDraft();
1550
} else {
1551
$auto_undraft = false;
1552
}
1553
1554
$can_promote = false;
1555
$can_demote = false;
1556
1557
// "Draft" revisions can promote to "Review Requested" after builds pass,
1558
// or demote to "Changes Planned" after builds fail.
1559
if ($object->isDraft()) {
1560
$can_promote = true;
1561
$can_demote = true;
1562
}
1563
1564
// See PHI584. "Changes Planned" revisions which are not yet broadcasting
1565
// can promote to "Review Requested" if builds pass.
1566
1567
// This pass is presumably the result of someone restarting the builds and
1568
// having them work this time, perhaps because the builds are not perfectly
1569
// reliable or perhaps because someone fixed some issue with build hardware
1570
// or some other dependency.
1571
1572
// Currently, there's no legitimate way to end up in this state except
1573
// through automatic demotion, so this behavior should not generate an
1574
// undue level of confusion or ambiguity. Also note that these changes can
1575
// not demote again since they've already been demoted once.
1576
if ($object->isChangePlanned()) {
1577
if (!$object->getShouldBroadcast()) {
1578
$can_promote = true;
1579
}
1580
}
1581
1582
if (($can_promote || $can_demote) && $auto_undraft) {
1583
$status = $this->loadCompletedBuildableStatus($object);
1584
1585
$is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED);
1586
$is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED);
1587
1588
if ($is_passed && $can_promote) {
1589
// When Harbormaster moves a revision out of the draft state, we
1590
// attribute the action to the revision author since this is more
1591
// natural and more useful.
1592
1593
// Additionally, we change the acting PHID for the transaction set
1594
// to the author if it isn't already a user so that mail comes from
1595
// the natural author.
1596
$acting_phid = $this->getActingAsPHID();
1597
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
1598
if (phid_get_type($acting_phid) != $user_type) {
1599
$this->setActingAsPHID($author_phid);
1600
}
1601
1602
$xaction = $object->getApplicationTransactionTemplate()
1603
->setAuthorPHID($author_phid)
1604
->setTransactionType(
1605
DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE)
1606
->setNewValue(true);
1607
1608
// If we're creating this revision and immediately moving it out of
1609
// the draft state, mark this as a create transaction so it gets
1610
// hidden in the timeline and mail, since it isn't interesting: it
1611
// is as though the draft phase never happened.
1612
if ($this->getIsNewObject()) {
1613
$xaction->setIsCreateTransaction(true);
1614
}
1615
1616
// Queue this transaction and apply it separately after the current
1617
// batch of transactions finishes so that Herald can fire on the new
1618
// revision state. See T13027 for discussion.
1619
$this->queueTransaction($xaction);
1620
} else if ($is_failed && $can_demote) {
1621
// When demoting a revision, we act as "Harbormaster" instead of
1622
// the author since this feels a little more natural.
1623
$harbormaster_phid = id(new PhabricatorHarbormasterApplication())
1624
->getPHID();
1625
1626
$xaction = $object->getApplicationTransactionTemplate()
1627
->setAuthorPHID($harbormaster_phid)
1628
->setMetadataValue('draft.demote', true)
1629
->setTransactionType(
1630
DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE)
1631
->setNewValue(true);
1632
1633
$this->queueTransaction($xaction);
1634
}
1635
}
1636
1637
// If the revision is new or was a draft, and is no longer a draft, we
1638
// might be sending the first email about it.
1639
1640
// This might mean it was created directly into a non-draft state, or
1641
// it just automatically undrafted after builds finished, or a user
1642
// explicitly promoted it out of the draft state with an action like
1643
// "Request Review".
1644
1645
// If we haven't sent any email about it yet, mark this email as the first
1646
// email so the mail gets enriched with "SUMMARY" and "TEST PLAN".
1647
1648
$is_new = $this->getIsNewObject();
1649
$was_broadcasting = $this->wasBroadcasting;
1650
1651
if ($object->getShouldBroadcast()) {
1652
if (!$was_broadcasting || $is_new) {
1653
// Mark this as the first broadcast we're sending about the revision
1654
// so mail can generate specially.
1655
$this->firstBroadcast = true;
1656
}
1657
}
1658
1659
return $xactions;
1660
}
1661
1662
private function loadCompletedBuildableStatus(
1663
DifferentialRevision $revision) {
1664
$viewer = $this->requireActor();
1665
$builds = $revision->loadImpactfulBuilds($viewer);
1666
return $revision->newBuildableStatusForBuilds($builds);
1667
}
1668
1669
private function requireReviewers(DifferentialRevision $revision) {
1670
if ($revision->hasAttachedReviewers()) {
1671
return;
1672
}
1673
1674
$with_reviewers = id(new DifferentialRevisionQuery())
1675
->setViewer($this->getActor())
1676
->needReviewers(true)
1677
->withPHIDs(array($revision->getPHID()))
1678
->executeOne();
1679
if (!$with_reviewers) {
1680
throw new Exception(
1681
pht(
1682
'Failed to reload revision ("%s").',
1683
$revision->getPHID()));
1684
}
1685
1686
$revision->attachReviewers($with_reviewers->getReviewers());
1687
}
1688
1689
1690
}
1691
1692