@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 1685 lines 51 kB view raw
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}