@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final 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::class;
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_class = PhabricatorManiphestApplication::class;
215 if (PhabricatorApplication::isClassInstalled($maniphest_class)) {
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 buildPatchForMail(DifferentialDiff $diff, $byte_limit) {
1388 $format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
1389
1390 return id(new DifferentialRawDiffRenderer())
1391 ->setViewer($this->getActor())
1392 ->setFormat($format)
1393 ->setChangesets($diff->getChangesets())
1394 ->setByteLimit($byte_limit)
1395 ->buildPatch();
1396 }
1397
1398 protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
1399 // Reload to pick up the active diff and reviewer status.
1400 return id(new DifferentialRevisionQuery())
1401 ->setViewer($this->getActor())
1402 ->needReviewers(true)
1403 ->needActiveDiffs(true)
1404 ->withIDs(array($object->getID()))
1405 ->executeOne();
1406 }
1407
1408 protected function getCustomWorkerState() {
1409 return array(
1410 'changedPriorToCommitURI' => $this->changedPriorToCommitURI,
1411 'firstBroadcast' => $this->firstBroadcast,
1412 'isDraftDemotion' => $this->isDraftDemotion,
1413 );
1414 }
1415
1416 protected function loadCustomWorkerState(array $state) {
1417 $this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI');
1418 $this->firstBroadcast = idx($state, 'firstBroadcast');
1419 $this->isDraftDemotion = idx($state, 'isDraftDemotion');
1420 return $this;
1421 }
1422
1423 private function newSwapReviewersTransaction(
1424 DifferentialRevision $revision,
1425 $new_author_phid) {
1426
1427 $old_author_phid = $revision->getAuthorPHID();
1428
1429 if ($old_author_phid === $new_author_phid) {
1430 return;
1431 }
1432
1433 // If the revision is changing authorship, add the previous author as a
1434 // reviewer and remove the new author.
1435
1436 $edits = array(
1437 '-' => array(
1438 $new_author_phid,
1439 ),
1440 '+' => array(
1441 $old_author_phid,
1442 ),
1443 );
1444
1445 // NOTE: We're setting setIsCommandeerSideEffect() on this because normally
1446 // you can't add a revision's author as a reviewer, but this action swaps
1447 // them after validation executes.
1448
1449 $xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
1450
1451 return id(new DifferentialTransaction())
1452 ->setTransactionType($xaction_type)
1453 ->setIgnoreOnNoEffect(true)
1454 ->setIsCommandeerSideEffect(true)
1455 ->setNewValue($edits);
1456 }
1457
1458
1459 public function getActiveDiff($object) {
1460 if ($this->getIsNewObject()) {
1461 return null;
1462 } else {
1463 return $object->getActiveDiff();
1464 }
1465 }
1466
1467 /**
1468 * When a reviewer makes a comment, mark the last revision they commented
1469 * on.
1470 *
1471 * This allows us to show a hint to help authors and other reviewers quickly
1472 * distinguish between reviewers who have participated in the discussion and
1473 * reviewers who haven't been part of it.
1474 */
1475 private function markReviewerComments($object, array $xactions) {
1476 $acting_phid = $this->getActingAsPHID();
1477 if (!$acting_phid) {
1478 return;
1479 }
1480
1481 $diff = $this->getActiveDiff($object);
1482 if (!$diff) {
1483 return;
1484 }
1485
1486 $has_comment = false;
1487 foreach ($xactions as $xaction) {
1488 if ($xaction->hasComment()) {
1489 $has_comment = true;
1490 break;
1491 }
1492 }
1493
1494 if (!$has_comment) {
1495 return;
1496 }
1497
1498 $reviewer_table = new DifferentialReviewer();
1499 $conn = $reviewer_table->establishConnection('w');
1500
1501 queryfx(
1502 $conn,
1503 'UPDATE %T SET lastCommentDiffPHID = %s
1504 WHERE revisionPHID = %s
1505 AND reviewerPHID = %s',
1506 $reviewer_table->getTableName(),
1507 $diff->getPHID(),
1508 $object->getPHID(),
1509 $acting_phid);
1510 }
1511
1512 private function loadUnbroadcastTransactions($object) {
1513 $viewer = $this->requireActor();
1514
1515 $xactions = id(new DifferentialTransactionQuery())
1516 ->setViewer($viewer)
1517 ->withObjectPHIDs(array($object->getPHID()))
1518 ->execute();
1519
1520 return array_reverse($xactions);
1521 }
1522
1523
1524 protected function didApplyTransactions($object, array $xactions) {
1525 // In a moment, we're going to try to publish draft revisions which have
1526 // completed all their builds. However, we only want to do that if the
1527 // actor is either the revision author or an omnipotent user (generally,
1528 // the Harbormaster application).
1529
1530 // If we let any actor publish the revision as a side effect of other
1531 // changes then an unlucky third party who innocently comments on the draft
1532 // can end up racing Harbormaster and promoting the revision. At best, this
1533 // is confusing. It can also run into validation problems with the "Request
1534 // Review" transaction. See PHI309 for some discussion.
1535 $author_phid = $object->getAuthorPHID();
1536 $viewer = $this->requireActor();
1537 $can_undraft =
1538 ($this->getActingAsPHID() === $author_phid) ||
1539 ($viewer->isOmnipotent());
1540
1541 // If a draft revision has no outstanding builds and we're automatically
1542 // making drafts public after builds finish, make the revision public.
1543 if ($can_undraft) {
1544 $auto_undraft = !$object->getHoldAsDraft();
1545 } else {
1546 $auto_undraft = false;
1547 }
1548
1549 $can_promote = false;
1550 $can_demote = false;
1551
1552 // "Draft" revisions can promote to "Review Requested" after builds pass,
1553 // or demote to "Changes Planned" after builds fail.
1554 if ($object->isDraft()) {
1555 $can_promote = true;
1556 $can_demote = true;
1557 }
1558
1559 // See PHI584. "Changes Planned" revisions which are not yet broadcasting
1560 // can promote to "Review Requested" if builds pass.
1561
1562 // This pass is presumably the result of someone restarting the builds and
1563 // having them work this time, perhaps because the builds are not perfectly
1564 // reliable or perhaps because someone fixed some issue with build hardware
1565 // or some other dependency.
1566
1567 // Currently, there's no legitimate way to end up in this state except
1568 // through automatic demotion, so this behavior should not generate an
1569 // undue level of confusion or ambiguity. Also note that these changes can
1570 // not demote again since they've already been demoted once.
1571 if ($object->isChangePlanned()) {
1572 if (!$object->getShouldBroadcast()) {
1573 $can_promote = true;
1574 }
1575 }
1576
1577 if (($can_promote || $can_demote) && $auto_undraft) {
1578 $status = $this->loadCompletedBuildableStatus($object);
1579
1580 $is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED);
1581 $is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED);
1582
1583 if ($is_passed && $can_promote) {
1584 // When Harbormaster moves a revision out of the draft state, we
1585 // attribute the action to the revision author since this is more
1586 // natural and more useful.
1587
1588 // Additionally, we change the acting PHID for the transaction set
1589 // to the author if it isn't already a user so that mail comes from
1590 // the natural author.
1591 $acting_phid = $this->getActingAsPHID();
1592 $user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
1593 if (phid_get_type($acting_phid) != $user_type) {
1594 $this->setActingAsPHID($author_phid);
1595 }
1596
1597 $xaction = $object->getApplicationTransactionTemplate()
1598 ->setAuthorPHID($author_phid)
1599 ->setTransactionType(
1600 DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE)
1601 ->setNewValue(true);
1602
1603 // If we're creating this revision and immediately moving it out of
1604 // the draft state, mark this as a create transaction so it gets
1605 // hidden in the timeline and mail, since it isn't interesting: it
1606 // is as though the draft phase never happened.
1607 if ($this->getIsNewObject()) {
1608 $xaction->setIsCreateTransaction(true);
1609 }
1610
1611 // Queue this transaction and apply it separately after the current
1612 // batch of transactions finishes so that Herald can fire on the new
1613 // revision state. See T13027 for discussion.
1614 $this->queueTransaction($xaction);
1615 } else if ($is_failed && $can_demote) {
1616 // When demoting a revision, we act as "Harbormaster" instead of
1617 // the author since this feels a little more natural.
1618 $harbormaster_phid = id(new PhabricatorHarbormasterApplication())
1619 ->getPHID();
1620
1621 $xaction = $object->getApplicationTransactionTemplate()
1622 ->setAuthorPHID($harbormaster_phid)
1623 ->setMetadataValue('draft.demote', true)
1624 ->setTransactionType(
1625 DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE)
1626 ->setNewValue(true);
1627
1628 $this->queueTransaction($xaction);
1629 }
1630 }
1631
1632 // If the revision is new or was a draft, and is no longer a draft, we
1633 // might be sending the first email about it.
1634
1635 // This might mean it was created directly into a non-draft state, or
1636 // it just automatically undrafted after builds finished, or a user
1637 // explicitly promoted it out of the draft state with an action like
1638 // "Request Review".
1639
1640 // If we haven't sent any email about it yet, mark this email as the first
1641 // email so the mail gets enriched with "SUMMARY" and "TEST PLAN".
1642
1643 $is_new = $this->getIsNewObject();
1644 $was_broadcasting = $this->wasBroadcasting;
1645
1646 if ($object->getShouldBroadcast()) {
1647 if (!$was_broadcasting || $is_new) {
1648 // Mark this as the first broadcast we're sending about the revision
1649 // so mail can generate specially.
1650 $this->firstBroadcast = true;
1651 }
1652 }
1653
1654 return $xactions;
1655 }
1656
1657 private function loadCompletedBuildableStatus(
1658 DifferentialRevision $revision) {
1659 $viewer = $this->requireActor();
1660 $builds = $revision->loadImpactfulBuilds($viewer);
1661 return $revision->newBuildableStatusForBuilds($builds);
1662 }
1663
1664 private function requireReviewers(DifferentialRevision $revision) {
1665 if ($revision->hasAttachedReviewers()) {
1666 return;
1667 }
1668
1669 $with_reviewers = id(new DifferentialRevisionQuery())
1670 ->setViewer($this->getActor())
1671 ->needReviewers(true)
1672 ->withPHIDs(array($revision->getPHID()))
1673 ->executeOne();
1674 if (!$with_reviewers) {
1675 throw new Exception(
1676 pht(
1677 'Failed to reload revision ("%s").',
1678 $revision->getPHID()));
1679 }
1680
1681 $revision->attachReviewers($with_reviewers->getReviewers());
1682 }
1683
1684
1685}