@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 DifferentialTransaction
4 extends PhabricatorModularTransaction {
5
6 private $isCommandeerSideEffect;
7
8 const TYPE_INLINE = 'differential:inline';
9 const TYPE_ACTION = 'differential:action';
10
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';
18
19 public function getBaseTransactionClass() {
20 return DifferentialRevisionTransactionType::class;
21 }
22
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
26 // throw this away.
27
28 // NOTE: Old reviewer edits are raw edge transactions. They could be
29 // migrated to modular transactions when the rest of this migrates.
30
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();
40 }
41 }
42
43 return parent::newFallbackModularTransactionType();
44 }
45
46
47 public function setIsCommandeerSideEffect($is_side_effect) {
48 $this->isCommandeerSideEffect = $is_side_effect;
49 return $this;
50 }
51
52 public function getIsCommandeerSideEffect() {
53 return $this->isCommandeerSideEffect;
54 }
55
56 public function getApplicationName() {
57 return 'differential';
58 }
59
60 public function getApplicationTransactionType() {
61 return DifferentialRevisionPHIDType::TYPECONST;
62 }
63
64 public function getApplicationTransactionCommentObject() {
65 return new DifferentialTransactionComment();
66 }
67
68 public function shouldHide() {
69 $old = $this->getOldValue();
70 $new = $this->getNewValue();
71
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
78 // discussion.
79 return false;
80 }
81
82 return parent::shouldHide();
83 }
84
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.
90 return false;
91 }
92
93 return parent::shouldHideForMail($xactions);
94 }
95
96
97 public function isInlineCommentTransaction() {
98 switch ($this->getTransactionType()) {
99 case self::TYPE_INLINE:
100 return true;
101 }
102
103 return parent::isInlineCommentTransaction();
104 }
105
106 public function getRequiredHandlePHIDs() {
107 $phids = parent::getRequiredHandlePHIDs();
108
109 $old = $this->getOldValue();
110 $new = $this->getNewValue();
111
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');
119 }
120 if ($this->getMetadataValue('authorPHID')) {
121 $phids[] = $this->getMetadataValue('authorPHID');
122 }
123 }
124 break;
125 }
126
127 return $phids;
128 }
129
130 public function getActionStrength() {
131 switch ($this->getTransactionType()) {
132 case self::TYPE_ACTION:
133 return 300;
134 }
135
136 return parent::getActionStrength();
137 }
138
139
140 public function getActionName() {
141 switch ($this->getTransactionType()) {
142 case self::TYPE_INLINE:
143 return pht('Commented On');
144 case self::TYPE_ACTION:
145 $map = array(
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'),
156 );
157 $name = idx($map, $this->getNewValue());
158 if ($name !== null) {
159 return $name;
160 }
161 break;
162 }
163
164 return parent::getActionName();
165 }
166
167 public function getMailTags() {
168 $tags = array();
169
170 switch ($this->getTransactionType()) {
171 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
172 $tags[] = self::MAILTAG_CC;
173 break;
174 case self::TYPE_ACTION:
175 switch ($this->getNewValue()) {
176 case DifferentialAction::ACTION_CLOSE:
177 $tags[] = self::MAILTAG_CLOSED;
178 break;
179 }
180 break;
181 case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
182 $old = $this->getOldValue();
183 if ($old === null) {
184 $tags[] = self::MAILTAG_REVIEW_REQUEST;
185 } else {
186 $tags[] = self::MAILTAG_UPDATED;
187 }
188 break;
189 case PhabricatorTransactions::TYPE_COMMENT:
190 case self::TYPE_INLINE:
191 $tags[] = self::MAILTAG_COMMENT;
192 break;
193 case DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE:
194 $tags[] = self::MAILTAG_REVIEWERS;
195 break;
196 case DifferentialRevisionCloseTransaction::TRANSACTIONTYPE:
197 $tags[] = self::MAILTAG_CLOSED;
198 break;
199 }
200
201 if (!$tags) {
202 $tags[] = self::MAILTAG_OTHER;
203 }
204
205 return $tags;
206 }
207
208 public function getTitle() {
209 $author_phid = $this->getAuthorPHID();
210 $author_handle = $this->renderHandleLink($author_phid);
211
212 $old = $this->getOldValue();
213 $new = $this->getNewValue();
214
215 switch ($this->getTransactionType()) {
216 case self::TYPE_INLINE:
217 return pht(
218 '%s added inline comments.',
219 $author_handle);
220 case self::TYPE_ACTION:
221 switch ($new) {
222 case DifferentialAction::ACTION_CLOSE:
223 if (!$this->getMetadataValue('isCommitClose')) {
224 return DifferentialAction::getBasicStoryText(
225 $new,
226 $author_handle);
227 }
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);
234 } else {
235 $committer_name = $this->getMetadataValue('committerName');
236 }
237 if ($this->getHandleIfExists($author_phid)) {
238 $author_name = $this->renderHandleLink($author_phid);
239 } else {
240 $author_name = $this->getMetadataValue('authorName');
241 }
242
243 if ($committer_name && ($committer_name != $author_name)) {
244 return pht(
245 'Closed by commit %s (authored by %s, committed by %s).',
246 $commit_name,
247 $author_name,
248 $committer_name);
249 } else {
250 return pht(
251 'Closed by commit %s (authored by %s).',
252 $commit_name,
253 $author_name);
254 }
255 default:
256 return DifferentialAction::getBasicStoryText($new, $author_handle);
257 }
258 }
259
260 return parent::getTitle();
261 }
262
263 public function renderExtraInformationLink() {
264 if ($this->getMetadataValue('revisionMatchData')) {
265 $details_href =
266 '/differential/revision/closedetails/'.$this->getPHID().'/';
267 $details_link = javelin_tag(
268 'a',
269 array(
270 'href' => $details_href,
271 'sigil' => 'workflow',
272 ),
273 pht('Explain Why'));
274 return $details_link;
275 }
276 return parent::renderExtraInformationLink();
277 }
278
279 public function getTitleForFeed() {
280 $author_phid = $this->getAuthorPHID();
281 $object_phid = $this->getObjectPHID();
282
283 $old = $this->getOldValue();
284 $new = $this->getNewValue();
285
286 $author_link = $this->renderHandleLink($author_phid);
287 $object_link = $this->renderHandleLink($object_phid);
288
289 switch ($this->getTransactionType()) {
290 case self::TYPE_INLINE:
291 return pht(
292 '%s added inline comments to %s.',
293 $author_link,
294 $object_link);
295 case self::TYPE_ACTION:
296 switch ($new) {
297 case DifferentialAction::ACTION_ACCEPT:
298 return pht(
299 '%s accepted %s.',
300 $author_link,
301 $object_link);
302 case DifferentialAction::ACTION_REJECT:
303 return pht(
304 '%s requested changes to %s.',
305 $author_link,
306 $object_link);
307 case DifferentialAction::ACTION_RETHINK:
308 return pht(
309 '%s planned changes to %s.',
310 $author_link,
311 $object_link);
312 case DifferentialAction::ACTION_ABANDON:
313 return pht(
314 '%s abandoned %s.',
315 $author_link,
316 $object_link);
317 case DifferentialAction::ACTION_CLOSE:
318 if (!$this->getMetadataValue('isCommitClose')) {
319 return pht(
320 '%s closed %s.',
321 $author_link,
322 $object_link);
323 } else {
324 $commit_name = $this->renderHandleLink(
325 $this->getMetadataValue('commitPHID'));
326 $committer_phid = $this->getMetadataValue('committerPHID');
327 $author_phid = $this->getMetadataValue('authorPHID');
328
329 if ($this->getHandleIfExists($committer_phid)) {
330 $committer_name = $this->renderHandleLink($committer_phid);
331 } else {
332 $committer_name = $this->getMetadataValue('committerName');
333 }
334
335 if ($this->getHandleIfExists($author_phid)) {
336 $author_name = $this->renderHandleLink($author_phid);
337 } else {
338 $author_name = $this->getMetadataValue('authorName');
339 }
340
341 // Check if the committer and author are the same. They're the
342 // same if both resolved and are the same user, or if neither
343 // resolved and the text is identical.
344 if ($committer_phid && $author_phid) {
345 $same_author = ($committer_phid == $author_phid);
346 } else if (!$committer_phid && !$author_phid) {
347 $same_author = ($committer_name == $author_name);
348 } else {
349 $same_author = false;
350 }
351
352 if ($committer_name && !$same_author) {
353 return pht(
354 '%s closed %s by committing %s (authored by %s).',
355 $author_link,
356 $object_link,
357 $commit_name,
358 $author_name);
359 } else {
360 return pht(
361 '%s closed %s by committing %s.',
362 $author_link,
363 $object_link,
364 $commit_name);
365 }
366 }
367
368 case DifferentialAction::ACTION_REQUEST:
369 return pht(
370 '%s requested review of %s.',
371 $author_link,
372 $object_link);
373 case DifferentialAction::ACTION_RECLAIM:
374 return pht(
375 '%s reclaimed %s.',
376 $author_link,
377 $object_link);
378 case DifferentialAction::ACTION_RESIGN:
379 return pht(
380 '%s resigned from %s.',
381 $author_link,
382 $object_link);
383 case DifferentialAction::ACTION_CLAIM:
384 return pht(
385 '%s commandeered %s.',
386 $author_link,
387 $object_link);
388 case DifferentialAction::ACTION_REOPEN:
389 return pht(
390 '%s reopened %s.',
391 $author_link,
392 $object_link);
393 }
394 break;
395 }
396
397 return parent::getTitleForFeed();
398 }
399
400 public function getIcon() {
401 switch ($this->getTransactionType()) {
402 case self::TYPE_INLINE:
403 return 'fa-comment';
404 case self::TYPE_ACTION:
405 switch ($this->getNewValue()) {
406 case DifferentialAction::ACTION_CLOSE:
407 return 'fa-check';
408 case DifferentialAction::ACTION_ACCEPT:
409 return 'fa-check-circle-o';
410 case DifferentialAction::ACTION_REJECT:
411 return 'fa-times-circle-o';
412 case DifferentialAction::ACTION_ABANDON:
413 return 'fa-plane';
414 case DifferentialAction::ACTION_RETHINK:
415 return 'fa-headphones';
416 case DifferentialAction::ACTION_REQUEST:
417 return 'fa-refresh';
418 case DifferentialAction::ACTION_RECLAIM:
419 case DifferentialAction::ACTION_REOPEN:
420 return 'fa-bullhorn';
421 case DifferentialAction::ACTION_CLAIM:
422 case DifferentialAction::ACTION_RESIGN:
423 return 'fa-flag';
424 default:
425 break;
426 }
427 case PhabricatorTransactions::TYPE_EDGE:
428 switch ($this->getMetadataValue('edge:type')) {
429 case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
430 return 'fa-user';
431 }
432 }
433
434 return parent::getIcon();
435 }
436
437 public function shouldDisplayGroupWith(array $group) {
438
439 // Never group status changes with other types of actions, they're indirect
440 // and don't make sense when combined with direct actions.
441
442 if ($this->isStatusTransaction($this)) {
443 return false;
444 }
445
446 foreach ($group as $xaction) {
447 if ($this->isStatusTransaction($xaction)) {
448 return false;
449 }
450 }
451
452 return parent::shouldDisplayGroupWith($group);
453 }
454
455 private function isStatusTransaction($xaction) {
456 $status_type = DifferentialRevisionStatusTransaction::TRANSACTIONTYPE;
457 if ($xaction->getTransactionType() == $status_type) {
458 return true;
459 }
460
461 return false;
462 }
463
464
465 public function getColor() {
466 switch ($this->getTransactionType()) {
467 case self::TYPE_ACTION:
468 switch ($this->getNewValue()) {
469 case DifferentialAction::ACTION_CLOSE:
470 case DifferentialAction::ACTION_ABANDON:
471 return PhabricatorTransactions::COLOR_INDIGO;
472 case DifferentialAction::ACTION_ACCEPT:
473 return PhabricatorTransactions::COLOR_GREEN;
474 case DifferentialAction::ACTION_REJECT:
475 case DifferentialAction::ACTION_RETHINK:
476 return PhabricatorTransactions::COLOR_RED;
477 case DifferentialAction::ACTION_REQUEST:
478 case DifferentialAction::ACTION_RECLAIM:
479 case DifferentialAction::ACTION_REOPEN:
480 return PhabricatorTransactions::COLOR_SKY;
481 case DifferentialAction::ACTION_RESIGN:
482 return PhabricatorTransactions::COLOR_ORANGE;
483 case DifferentialAction::ACTION_CLAIM:
484 return PhabricatorTransactions::COLOR_YELLOW;
485 }
486 }
487
488
489 return parent::getColor();
490 }
491
492 public function getNoEffectDescription() {
493 switch ($this->getTransactionType()) {
494 case self::TYPE_ACTION:
495 switch ($this->getNewValue()) {
496 case DifferentialAction::ACTION_CLOSE:
497 return pht('This revision is already closed.');
498 case DifferentialAction::ACTION_ABANDON:
499 return pht('This revision has already been abandoned.');
500 case DifferentialAction::ACTION_RECLAIM:
501 return pht(
502 'You can not reclaim this revision because his revision is '.
503 'not abandoned.');
504 case DifferentialAction::ACTION_REOPEN:
505 return pht(
506 'You can not reopen this revision because this revision is '.
507 'not closed.');
508 case DifferentialAction::ACTION_RETHINK:
509 return pht('This revision already requires changes.');
510 case DifferentialAction::ACTION_CLAIM:
511 return pht(
512 'You can not commandeer this revision because you already own '.
513 'it.');
514 }
515 break;
516 }
517
518 return parent::getNoEffectDescription();
519 }
520
521 public function renderAsTextForDoorkeeper(
522 DoorkeeperFeedStoryPublisher $publisher,
523 PhabricatorFeedStory $story,
524 array $xactions) {
525
526 $body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions);
527
528 $inlines = array();
529 foreach ($xactions as $xaction) {
530 if ($xaction->getTransactionType() == self::TYPE_INLINE) {
531 $inlines[] = $xaction;
532 }
533 }
534
535 // TODO: This is a bit gross, but far less bad than it used to be. It
536 // could be further cleaned up at some point.
537
538 if ($inlines) {
539 $engine = PhabricatorMarkupEngine::newMarkupEngine(array())
540 ->setConfig('viewer', new PhabricatorUser())
541 ->setMode(PhutilRemarkupEngine::MODE_TEXT);
542
543 $body .= "\n\n";
544 $body .= pht('Inline Comments');
545 $body .= "\n";
546
547 $changeset_ids = array();
548 foreach ($inlines as $inline) {
549 $changeset_ids[] = $inline->getComment()->getChangesetID();
550 }
551
552 $changesets = id(new DifferentialChangeset())->loadAllWhere(
553 'id IN (%Ld)',
554 $changeset_ids);
555
556 foreach ($inlines as $inline) {
557 $comment = $inline->getComment();
558 $changeset = idx($changesets, $comment->getChangesetID());
559 if (!$changeset) {
560 continue;
561 }
562
563 $filename = $changeset->getDisplayFilename();
564 $linenumber = $comment->getLineNumber();
565 $inline_text = $engine->markupText($comment->getContent());
566 $inline_text = rtrim($inline_text);
567
568 $body .= "{$filename}:{$linenumber} {$inline_text}\n";
569 }
570 }
571
572 return $body;
573 }
574
575 public function newWarningForTransactions($object, array $xactions) {
576 $warning = new PhabricatorTransactionWarning();
577
578 switch ($this->getTransactionType()) {
579 case self::TYPE_INLINE:
580 $warning->setTitleText(pht('Warning: Editing Inlines'));
581 $warning->setContinueActionText(pht('Save Inlines and Continue'));
582
583 $count = phutil_count($xactions);
584
585 $body = array();
586 $body[] = pht(
587 'You are currently editing %s inline comment(s) on this '.
588 'revision.',
589 $count);
590 $body[] = pht(
591 'These %s inline comment(s) will be saved and published.',
592 $count);
593
594 $warning->setWarningParagraphs($body);
595 break;
596 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
597 $warning->setTitleText(pht('Warning: Draft Revision'));
598 $warning->setContinueActionText(pht('Tell No One'));
599
600 $body = array();
601
602 $body[] = pht(
603 'This is a draft revision that will not publish any '.
604 'notifications until the author requests review.');
605
606 $body[] = pht('Mentioned or subscribed users will not be notified.');
607
608 $warning->setWarningParagraphs($body);
609 break;
610 }
611
612 return $warning;
613 }
614
615
616}