@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
at upstream/main 616 lines 20 kB view raw
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}