3 final class DifferentialTransaction
4 extends PhabricatorModularTransaction
{
6 private $isCommandeerSideEffect;
8 const TYPE_INLINE
= 'differential:inline';
9 const TYPE_ACTION
= 'differential:action';
11 const MAILTAG_REVIEWERS
= 'differential-reviewers';
12 const MAILTAG_CLOSED
= 'differential-committed';
13 const MAILTAG_CC
= 'differential-cc';
14 const MAILTAG_COMMENT
= 'differential-comment';
15 const MAILTAG_UPDATED
= 'differential-updated';
16 const MAILTAG_REVIEW_REQUEST
= 'differential-review-request';
17 const MAILTAG_OTHER
= 'differential-other';
19 public function getBaseTransactionClass() {
20 return 'DifferentialRevisionTransactionType';
23 protected function newFallbackModularTransactionType() {
24 // TODO: This allows us to render modern strings for older transactions
25 // without doing a migration. At some point, we should do a migration and
28 // NOTE: Old reviewer edits are raw edge transactions. They could be
29 // migrated to modular transactions when the rest of this migrates.
31 $xaction_type = $this->getTransactionType();
32 if ($xaction_type == PhabricatorTransactions
::TYPE_CUSTOMFIELD
) {
33 switch ($this->getMetadataValue('customfield:key')) {
34 case 'differential:title':
35 return new DifferentialRevisionTitleTransaction();
36 case 'differential:test-plan':
37 return new DifferentialRevisionTestPlanTransaction();
38 case 'differential:repository':
39 return new DifferentialRevisionRepositoryTransaction();
43 return parent
::newFallbackModularTransactionType();
47 public function setIsCommandeerSideEffect($is_side_effect) {
48 $this->isCommandeerSideEffect
= $is_side_effect;
52 public function getIsCommandeerSideEffect() {
53 return $this->isCommandeerSideEffect
;
56 public function getApplicationName() {
57 return 'differential';
60 public function getApplicationTransactionType() {
61 return DifferentialRevisionPHIDType
::TYPECONST
;
64 public function getApplicationTransactionCommentObject() {
65 return new DifferentialTransactionComment();
68 public function shouldHide() {
69 $old = $this->getOldValue();
70 $new = $this->getNewValue();
72 switch ($this->getTransactionType()) {
73 case DifferentialRevisionRequestReviewTransaction
::TRANSACTIONTYPE
:
74 // Don't hide the initial "X requested review: ..." transaction from
75 // mail or feed even when it occurs during creation. We need this
76 // transaction to survive so we'll generate mail and feed stories when
77 // revisions immediately leave the draft state. See T13035 for
82 return parent
::shouldHide();
85 public function shouldHideForMail(array $xactions) {
86 switch ($this->getTransactionType()) {
87 case DifferentialRevisionReviewersTransaction
::TRANSACTIONTYPE
:
88 // Don't hide the initial "X added reviewers: ..." transaction during
89 // object creation from mail. See T12118 and PHI54.
93 return parent
::shouldHideForMail($xactions);
97 public function isInlineCommentTransaction() {
98 switch ($this->getTransactionType()) {
99 case self
::TYPE_INLINE
:
103 return parent
::isInlineCommentTransaction();
106 public function getRequiredHandlePHIDs() {
107 $phids = parent
::getRequiredHandlePHIDs();
109 $old = $this->getOldValue();
110 $new = $this->getNewValue();
112 switch ($this->getTransactionType()) {
113 case self
::TYPE_ACTION
:
114 if ($new == DifferentialAction
::ACTION_CLOSE
&&
115 $this->getMetadataValue('isCommitClose')) {
116 $phids[] = $this->getMetadataValue('commitPHID');
117 if ($this->getMetadataValue('committerPHID')) {
118 $phids[] = $this->getMetadataValue('committerPHID');
120 if ($this->getMetadataValue('authorPHID')) {
121 $phids[] = $this->getMetadataValue('authorPHID');
130 public function getActionStrength() {
131 switch ($this->getTransactionType()) {
132 case self
::TYPE_ACTION
:
136 return parent
::getActionStrength();
140 public function getActionName() {
141 switch ($this->getTransactionType()) {
142 case self
::TYPE_INLINE
:
143 return pht('Commented On');
144 case self
::TYPE_ACTION
:
146 DifferentialAction
::ACTION_ACCEPT
=> pht('Accepted'),
147 DifferentialAction
::ACTION_REJECT
=> pht('Requested Changes To'),
148 DifferentialAction
::ACTION_RETHINK
=> pht('Planned Changes To'),
149 DifferentialAction
::ACTION_ABANDON
=> pht('Abandoned'),
150 DifferentialAction
::ACTION_CLOSE
=> pht('Closed'),
151 DifferentialAction
::ACTION_REQUEST
=> pht('Requested A Review Of'),
152 DifferentialAction
::ACTION_RESIGN
=> pht('Resigned From'),
153 DifferentialAction
::ACTION_ADDREVIEWERS
=> pht('Added Reviewers'),
154 DifferentialAction
::ACTION_CLAIM
=> pht('Commandeered'),
155 DifferentialAction
::ACTION_REOPEN
=> pht('Reopened'),
157 $name = idx($map, $this->getNewValue());
158 if ($name !== null) {
164 return parent
::getActionName();
167 public function getMailTags() {
170 switch ($this->getTransactionType()) {
171 case PhabricatorTransactions
::TYPE_SUBSCRIBERS
;
172 $tags[] = self
::MAILTAG_CC
;
174 case self
::TYPE_ACTION
:
175 switch ($this->getNewValue()) {
176 case DifferentialAction
::ACTION_CLOSE
:
177 $tags[] = self
::MAILTAG_CLOSED
;
181 case DifferentialRevisionUpdateTransaction
::TRANSACTIONTYPE
:
182 $old = $this->getOldValue();
184 $tags[] = self
::MAILTAG_REVIEW_REQUEST
;
186 $tags[] = self
::MAILTAG_UPDATED
;
189 case PhabricatorTransactions
::TYPE_COMMENT
:
190 case self
::TYPE_INLINE
:
191 $tags[] = self
::MAILTAG_COMMENT
;
193 case DifferentialRevisionReviewersTransaction
::TRANSACTIONTYPE
:
194 $tags[] = self
::MAILTAG_REVIEWERS
;
196 case DifferentialRevisionCloseTransaction
::TRANSACTIONTYPE
:
197 $tags[] = self
::MAILTAG_CLOSED
;
202 $tags[] = self
::MAILTAG_OTHER
;
208 public function getTitle() {
209 $author_phid = $this->getAuthorPHID();
210 $author_handle = $this->renderHandleLink($author_phid);
212 $old = $this->getOldValue();
213 $new = $this->getNewValue();
215 switch ($this->getTransactionType()) {
216 case self
::TYPE_INLINE
:
218 '%s added inline comments.',
220 case self
::TYPE_ACTION
:
222 case DifferentialAction
::ACTION_CLOSE
:
223 if (!$this->getMetadataValue('isCommitClose')) {
224 return DifferentialAction
::getBasicStoryText(
228 $commit_name = $this->renderHandleLink(
229 $this->getMetadataValue('commitPHID'));
230 $committer_phid = $this->getMetadataValue('committerPHID');
231 $author_phid = $this->getMetadataValue('authorPHID');
232 if ($this->getHandleIfExists($committer_phid)) {
233 $committer_name = $this->renderHandleLink($committer_phid);
235 $committer_name = $this->getMetadataValue('committerName');
237 if ($this->getHandleIfExists($author_phid)) {
238 $author_name = $this->renderHandleLink($author_phid);
240 $author_name = $this->getMetadataValue('authorName');
243 if ($committer_name && ($committer_name != $author_name)) {
245 'Closed by commit %s (authored by %s, committed by %s).',
251 'Closed by commit %s (authored by %s).',
257 return DifferentialAction
::getBasicStoryText($new, $author_handle);
262 return parent
::getTitle();
265 public function renderExtraInformationLink() {
266 if ($this->getMetadataValue('revisionMatchData')) {
268 '/differential/revision/closedetails/'.$this->getPHID().'/';
269 $details_link = javelin_tag(
272 'href' => $details_href,
273 'sigil' => 'workflow',
276 return $details_link;
278 return parent
::renderExtraInformationLink();
281 public function getTitleForFeed() {
282 $author_phid = $this->getAuthorPHID();
283 $object_phid = $this->getObjectPHID();
285 $old = $this->getOldValue();
286 $new = $this->getNewValue();
288 $author_link = $this->renderHandleLink($author_phid);
289 $object_link = $this->renderHandleLink($object_phid);
291 switch ($this->getTransactionType()) {
292 case self
::TYPE_INLINE
:
294 '%s added inline comments to %s.',
297 case self
::TYPE_ACTION
:
299 case DifferentialAction
::ACTION_ACCEPT
:
304 case DifferentialAction
::ACTION_REJECT
:
306 '%s requested changes to %s.',
309 case DifferentialAction
::ACTION_RETHINK
:
311 '%s planned changes to %s.',
314 case DifferentialAction
::ACTION_ABANDON
:
319 case DifferentialAction
::ACTION_CLOSE
:
320 if (!$this->getMetadataValue('isCommitClose')) {
326 $commit_name = $this->renderHandleLink(
327 $this->getMetadataValue('commitPHID'));
328 $committer_phid = $this->getMetadataValue('committerPHID');
329 $author_phid = $this->getMetadataValue('authorPHID');
331 if ($this->getHandleIfExists($committer_phid)) {
332 $committer_name = $this->renderHandleLink($committer_phid);
334 $committer_name = $this->getMetadataValue('committerName');
337 if ($this->getHandleIfExists($author_phid)) {
338 $author_name = $this->renderHandleLink($author_phid);
340 $author_name = $this->getMetadataValue('authorName');
343 // Check if the committer and author are the same. They're the
344 // same if both resolved and are the same user, or if neither
345 // resolved and the text is identical.
346 if ($committer_phid && $author_phid) {
347 $same_author = ($committer_phid == $author_phid);
348 } else if (!$committer_phid && !$author_phid) {
349 $same_author = ($committer_name == $author_name);
351 $same_author = false;
354 if ($committer_name && !$same_author) {
356 '%s closed %s by committing %s (authored by %s).',
363 '%s closed %s by committing %s.',
371 case DifferentialAction
::ACTION_REQUEST
:
373 '%s requested review of %s.',
376 case DifferentialAction
::ACTION_RECLAIM
:
381 case DifferentialAction
::ACTION_RESIGN
:
383 '%s resigned from %s.',
386 case DifferentialAction
::ACTION_CLAIM
:
388 '%s commandeered %s.',
391 case DifferentialAction
::ACTION_REOPEN
:
400 return parent
::getTitleForFeed();
403 public function getIcon() {
404 switch ($this->getTransactionType()) {
405 case self
::TYPE_INLINE
:
407 case self
::TYPE_ACTION
:
408 switch ($this->getNewValue()) {
409 case DifferentialAction
::ACTION_CLOSE
:
411 case DifferentialAction
::ACTION_ACCEPT
:
412 return 'fa-check-circle-o';
413 case DifferentialAction
::ACTION_REJECT
:
414 return 'fa-times-circle-o';
415 case DifferentialAction
::ACTION_ABANDON
:
417 case DifferentialAction
::ACTION_RETHINK
:
418 return 'fa-headphones';
419 case DifferentialAction
::ACTION_REQUEST
:
421 case DifferentialAction
::ACTION_RECLAIM
:
422 case DifferentialAction
::ACTION_REOPEN
:
423 return 'fa-bullhorn';
424 case DifferentialAction
::ACTION_RESIGN
:
426 case DifferentialAction
::ACTION_CLAIM
:
429 case PhabricatorTransactions
::TYPE_EDGE
:
430 switch ($this->getMetadataValue('edge:type')) {
431 case DifferentialRevisionHasReviewerEdgeType
::EDGECONST
:
436 return parent
::getIcon();
439 public function shouldDisplayGroupWith(array $group) {
441 // Never group status changes with other types of actions, they're indirect
442 // and don't make sense when combined with direct actions.
444 if ($this->isStatusTransaction($this)) {
448 foreach ($group as $xaction) {
449 if ($this->isStatusTransaction($xaction)) {
454 return parent
::shouldDisplayGroupWith($group);
457 private function isStatusTransaction($xaction) {
458 $status_type = DifferentialRevisionStatusTransaction
::TRANSACTIONTYPE
;
459 if ($xaction->getTransactionType() == $status_type) {
467 public function getColor() {
468 switch ($this->getTransactionType()) {
469 case self
::TYPE_ACTION
:
470 switch ($this->getNewValue()) {
471 case DifferentialAction
::ACTION_CLOSE
:
472 return PhabricatorTransactions
::COLOR_INDIGO
;
473 case DifferentialAction
::ACTION_ACCEPT
:
474 return PhabricatorTransactions
::COLOR_GREEN
;
475 case DifferentialAction
::ACTION_REJECT
:
476 return PhabricatorTransactions
::COLOR_RED
;
477 case DifferentialAction
::ACTION_ABANDON
:
478 return PhabricatorTransactions
::COLOR_INDIGO
;
479 case DifferentialAction
::ACTION_RETHINK
:
480 return PhabricatorTransactions
::COLOR_RED
;
481 case DifferentialAction
::ACTION_REQUEST
:
482 return PhabricatorTransactions
::COLOR_SKY
;
483 case DifferentialAction
::ACTION_RECLAIM
:
484 return PhabricatorTransactions
::COLOR_SKY
;
485 case DifferentialAction
::ACTION_REOPEN
:
486 return PhabricatorTransactions
::COLOR_SKY
;
487 case DifferentialAction
::ACTION_RESIGN
:
488 return PhabricatorTransactions
::COLOR_ORANGE
;
489 case DifferentialAction
::ACTION_CLAIM
:
490 return PhabricatorTransactions
::COLOR_YELLOW
;
495 return parent
::getColor();
498 public function getNoEffectDescription() {
499 switch ($this->getTransactionType()) {
500 case self
::TYPE_ACTION
:
501 switch ($this->getNewValue()) {
502 case DifferentialAction
::ACTION_CLOSE
:
503 return pht('This revision is already closed.');
504 case DifferentialAction
::ACTION_ABANDON
:
505 return pht('This revision has already been abandoned.');
506 case DifferentialAction
::ACTION_RECLAIM
:
508 'You can not reclaim this revision because his revision is '.
510 case DifferentialAction
::ACTION_REOPEN
:
512 'You can not reopen this revision because this revision is '.
514 case DifferentialAction
::ACTION_RETHINK
:
515 return pht('This revision already requires changes.');
516 case DifferentialAction
::ACTION_CLAIM
:
518 'You can not commandeer this revision because you already own '.
524 return parent
::getNoEffectDescription();
527 public function renderAsTextForDoorkeeper(
528 DoorkeeperFeedStoryPublisher
$publisher,
529 PhabricatorFeedStory
$story,
532 $body = parent
::renderAsTextForDoorkeeper($publisher, $story, $xactions);
535 foreach ($xactions as $xaction) {
536 if ($xaction->getTransactionType() == self
::TYPE_INLINE
) {
537 $inlines[] = $xaction;
541 // TODO: This is a bit gross, but far less bad than it used to be. It
542 // could be further cleaned up at some point.
545 $engine = PhabricatorMarkupEngine
::newMarkupEngine(array())
546 ->setConfig('viewer', new PhabricatorUser())
547 ->setMode(PhutilRemarkupEngine
::MODE_TEXT
);
550 $body .= pht('Inline Comments');
553 $changeset_ids = array();
554 foreach ($inlines as $inline) {
555 $changeset_ids[] = $inline->getComment()->getChangesetID();
558 $changesets = id(new DifferentialChangeset())->loadAllWhere(
562 foreach ($inlines as $inline) {
563 $comment = $inline->getComment();
564 $changeset = idx($changesets, $comment->getChangesetID());
569 $filename = $changeset->getDisplayFilename();
570 $linenumber = $comment->getLineNumber();
571 $inline_text = $engine->markupText($comment->getContent());
572 $inline_text = rtrim($inline_text);
574 $body .= "{$filename}:{$linenumber} {$inline_text}\n";
581 public function newWarningForTransactions($object, array $xactions) {
582 $warning = new PhabricatorTransactionWarning();
584 switch ($this->getTransactionType()) {
585 case self
::TYPE_INLINE
:
586 $warning->setTitleText(pht('Warning: Editing Inlines'));
587 $warning->setContinueActionText(pht('Save Inlines and Continue'));
589 $count = phutil_count($xactions);
593 'You are currently editing %s inline comment(s) on this '.
597 'These %s inline comment(s) will be saved and published.',
600 $warning->setWarningParagraphs($body);
602 case PhabricatorTransactions
::TYPE_SUBSCRIBERS
:
603 $warning->setTitleText(pht('Warning: Draft Revision'));
604 $warning->setContinueActionText(pht('Tell No One'));
609 'This is a draft revision that will not publish any '.
610 'notifications until the author requests review.');
612 $body[] = pht('Mentioned or subscribed users will not be notified.');
614 $warning->setWarningParagraphs($body);