Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / applications / differential / storage / DifferentialTransaction.php
blobd60fdc4bbdb20e5c96456a3040533c3e76c1eddb
1 <?php
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
26 // throw this away.
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;
49 return $this;
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
78 // discussion.
79 return false;
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.
90 return false;
93 return parent::shouldHideForMail($xactions);
97 public function isInlineCommentTransaction() {
98 switch ($this->getTransactionType()) {
99 case self::TYPE_INLINE:
100 return true;
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');
124 break;
127 return $phids;
130 public function getActionStrength() {
131 switch ($this->getTransactionType()) {
132 case self::TYPE_ACTION:
133 return 300;
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:
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'),
157 $name = idx($map, $this->getNewValue());
158 if ($name !== null) {
159 return $name;
161 break;
164 return parent::getActionName();
167 public function getMailTags() {
168 $tags = array();
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;
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;
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;
201 if (!$tags) {
202 $tags[] = self::MAILTAG_OTHER;
205 return $tags;
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:
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);
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');
237 if ($this->getHandleIfExists($author_phid)) {
238 $author_name = $this->renderHandleLink($author_phid);
239 } else {
240 $author_name = $this->getMetadataValue('authorName');
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);
255 break;
256 default:
257 return DifferentialAction::getBasicStoryText($new, $author_handle);
259 break;
262 return parent::getTitle();
265 public function renderExtraInformationLink() {
266 if ($this->getMetadataValue('revisionMatchData')) {
267 $details_href =
268 '/differential/revision/closedetails/'.$this->getPHID().'/';
269 $details_link = javelin_tag(
270 'a',
271 array(
272 'href' => $details_href,
273 'sigil' => 'workflow',
275 pht('Explain Why'));
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:
293 return pht(
294 '%s added inline comments to %s.',
295 $author_link,
296 $object_link);
297 case self::TYPE_ACTION:
298 switch ($new) {
299 case DifferentialAction::ACTION_ACCEPT:
300 return pht(
301 '%s accepted %s.',
302 $author_link,
303 $object_link);
304 case DifferentialAction::ACTION_REJECT:
305 return pht(
306 '%s requested changes to %s.',
307 $author_link,
308 $object_link);
309 case DifferentialAction::ACTION_RETHINK:
310 return pht(
311 '%s planned changes to %s.',
312 $author_link,
313 $object_link);
314 case DifferentialAction::ACTION_ABANDON:
315 return pht(
316 '%s abandoned %s.',
317 $author_link,
318 $object_link);
319 case DifferentialAction::ACTION_CLOSE:
320 if (!$this->getMetadataValue('isCommitClose')) {
321 return pht(
322 '%s closed %s.',
323 $author_link,
324 $object_link);
325 } else {
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);
333 } else {
334 $committer_name = $this->getMetadataValue('committerName');
337 if ($this->getHandleIfExists($author_phid)) {
338 $author_name = $this->renderHandleLink($author_phid);
339 } else {
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);
350 } else {
351 $same_author = false;
354 if ($committer_name && !$same_author) {
355 return pht(
356 '%s closed %s by committing %s (authored by %s).',
357 $author_link,
358 $object_link,
359 $commit_name,
360 $author_name);
361 } else {
362 return pht(
363 '%s closed %s by committing %s.',
364 $author_link,
365 $object_link,
366 $commit_name);
369 break;
371 case DifferentialAction::ACTION_REQUEST:
372 return pht(
373 '%s requested review of %s.',
374 $author_link,
375 $object_link);
376 case DifferentialAction::ACTION_RECLAIM:
377 return pht(
378 '%s reclaimed %s.',
379 $author_link,
380 $object_link);
381 case DifferentialAction::ACTION_RESIGN:
382 return pht(
383 '%s resigned from %s.',
384 $author_link,
385 $object_link);
386 case DifferentialAction::ACTION_CLAIM:
387 return pht(
388 '%s commandeered %s.',
389 $author_link,
390 $object_link);
391 case DifferentialAction::ACTION_REOPEN:
392 return pht(
393 '%s reopened %s.',
394 $author_link,
395 $object_link);
397 break;
400 return parent::getTitleForFeed();
403 public function getIcon() {
404 switch ($this->getTransactionType()) {
405 case self::TYPE_INLINE:
406 return 'fa-comment';
407 case self::TYPE_ACTION:
408 switch ($this->getNewValue()) {
409 case DifferentialAction::ACTION_CLOSE:
410 return 'fa-check';
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:
416 return 'fa-plane';
417 case DifferentialAction::ACTION_RETHINK:
418 return 'fa-headphones';
419 case DifferentialAction::ACTION_REQUEST:
420 return 'fa-refresh';
421 case DifferentialAction::ACTION_RECLAIM:
422 case DifferentialAction::ACTION_REOPEN:
423 return 'fa-bullhorn';
424 case DifferentialAction::ACTION_RESIGN:
425 return 'fa-flag';
426 case DifferentialAction::ACTION_CLAIM:
427 return 'fa-flag';
429 case PhabricatorTransactions::TYPE_EDGE:
430 switch ($this->getMetadataValue('edge:type')) {
431 case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
432 return 'fa-user';
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)) {
445 return false;
448 foreach ($group as $xaction) {
449 if ($this->isStatusTransaction($xaction)) {
450 return false;
454 return parent::shouldDisplayGroupWith($group);
457 private function isStatusTransaction($xaction) {
458 $status_type = DifferentialRevisionStatusTransaction::TRANSACTIONTYPE;
459 if ($xaction->getTransactionType() == $status_type) {
460 return true;
463 return false;
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:
507 return pht(
508 'You can not reclaim this revision because his revision is '.
509 'not abandoned.');
510 case DifferentialAction::ACTION_REOPEN:
511 return pht(
512 'You can not reopen this revision because this revision is '.
513 'not closed.');
514 case DifferentialAction::ACTION_RETHINK:
515 return pht('This revision already requires changes.');
516 case DifferentialAction::ACTION_CLAIM:
517 return pht(
518 'You can not commandeer this revision because you already own '.
519 'it.');
521 break;
524 return parent::getNoEffectDescription();
527 public function renderAsTextForDoorkeeper(
528 DoorkeeperFeedStoryPublisher $publisher,
529 PhabricatorFeedStory $story,
530 array $xactions) {
532 $body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions);
534 $inlines = array();
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.
544 if ($inlines) {
545 $engine = PhabricatorMarkupEngine::newMarkupEngine(array())
546 ->setConfig('viewer', new PhabricatorUser())
547 ->setMode(PhutilRemarkupEngine::MODE_TEXT);
549 $body .= "\n\n";
550 $body .= pht('Inline Comments');
551 $body .= "\n";
553 $changeset_ids = array();
554 foreach ($inlines as $inline) {
555 $changeset_ids[] = $inline->getComment()->getChangesetID();
558 $changesets = id(new DifferentialChangeset())->loadAllWhere(
559 'id IN (%Ld)',
560 $changeset_ids);
562 foreach ($inlines as $inline) {
563 $comment = $inline->getComment();
564 $changeset = idx($changesets, $comment->getChangesetID());
565 if (!$changeset) {
566 continue;
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";
578 return $body;
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);
591 $body = array();
592 $body[] = pht(
593 'You are currently editing %s inline comment(s) on this '.
594 'revision.',
595 $count);
596 $body[] = pht(
597 'These %s inline comment(s) will be saved and published.',
598 $count);
600 $warning->setWarningParagraphs($body);
601 break;
602 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
603 $warning->setTitleText(pht('Warning: Draft Revision'));
604 $warning->setContinueActionText(pht('Tell No One'));
606 $body = array();
608 $body[] = pht(
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);
615 break;
618 return $warning;