[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class DifferentialTransactionEditor 4 extends PhabricatorApplicationTransactionEditor { 5 6 private $heraldEmailPHIDs; 7 private $changedPriorToCommitURI; 8 private $isCloseByCommit; 9 private $repositoryPHIDOverride = false; 10 11 public function getEditorApplicationClass() { 12 return 'PhabricatorDifferentialApplication'; 13 } 14 15 public function getEditorObjectsDescription() { 16 return pht('Differential Revisions'); 17 } 18 19 public function getDiffUpdateTransaction(array $xactions) { 20 $type_update = DifferentialTransaction::TYPE_UPDATE; 21 22 foreach ($xactions as $xaction) { 23 if ($xaction->getTransactionType() == $type_update) { 24 return $xaction; 25 } 26 } 27 28 return null; 29 } 30 31 public function setIsCloseByCommit($is_close_by_commit) { 32 $this->isCloseByCommit = $is_close_by_commit; 33 return $this; 34 } 35 36 public function getIsCloseByCommit() { 37 return $this->isCloseByCommit; 38 } 39 40 public function setChangedPriorToCommitURI($uri) { 41 $this->changedPriorToCommitURI = $uri; 42 return $this; 43 } 44 45 public function getChangedPriorToCommitURI() { 46 return $this->changedPriorToCommitURI; 47 } 48 49 public function setRepositoryPHIDOverride($phid_or_null) { 50 $this->repositoryPHIDOverride = $phid_or_null; 51 return $this; 52 } 53 54 public function getTransactionTypes() { 55 $types = parent::getTransactionTypes(); 56 57 $types[] = PhabricatorTransactions::TYPE_COMMENT; 58 $types[] = PhabricatorTransactions::TYPE_EDGE; 59 $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 60 $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; 61 62 $types[] = DifferentialTransaction::TYPE_ACTION; 63 $types[] = DifferentialTransaction::TYPE_INLINE; 64 $types[] = DifferentialTransaction::TYPE_STATUS; 65 $types[] = DifferentialTransaction::TYPE_UPDATE; 66 67 return $types; 68 } 69 70 protected function getCustomTransactionOldValue( 71 PhabricatorLiskDAO $object, 72 PhabricatorApplicationTransaction $xaction) { 73 74 switch ($xaction->getTransactionType()) { 75 case PhabricatorTransactions::TYPE_VIEW_POLICY: 76 return $object->getViewPolicy(); 77 case PhabricatorTransactions::TYPE_EDIT_POLICY: 78 return $object->getEditPolicy(); 79 case DifferentialTransaction::TYPE_ACTION: 80 return null; 81 case DifferentialTransaction::TYPE_INLINE: 82 return null; 83 case DifferentialTransaction::TYPE_UPDATE: 84 if ($this->getIsNewObject()) { 85 return null; 86 } else { 87 return $object->getActiveDiff()->getPHID(); 88 } 89 } 90 91 return parent::getCustomTransactionOldValue($object, $xaction); 92 } 93 94 protected function getCustomTransactionNewValue( 95 PhabricatorLiskDAO $object, 96 PhabricatorApplicationTransaction $xaction) { 97 98 switch ($xaction->getTransactionType()) { 99 case PhabricatorTransactions::TYPE_VIEW_POLICY: 100 case PhabricatorTransactions::TYPE_EDIT_POLICY: 101 case DifferentialTransaction::TYPE_ACTION: 102 case DifferentialTransaction::TYPE_UPDATE: 103 return $xaction->getNewValue(); 104 case DifferentialTransaction::TYPE_INLINE: 105 return null; 106 } 107 108 return parent::getCustomTransactionNewValue($object, $xaction); 109 } 110 111 protected function transactionHasEffect( 112 PhabricatorLiskDAO $object, 113 PhabricatorApplicationTransaction $xaction) { 114 115 $actor_phid = $this->getActingAsPHID(); 116 117 switch ($xaction->getTransactionType()) { 118 case DifferentialTransaction::TYPE_INLINE: 119 return $xaction->hasComment(); 120 case DifferentialTransaction::TYPE_ACTION: 121 $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; 122 $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; 123 $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; 124 $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; 125 $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; 126 127 $action_type = $xaction->getNewValue(); 128 switch ($action_type) { 129 case DifferentialAction::ACTION_ACCEPT: 130 case DifferentialAction::ACTION_REJECT: 131 if ($action_type == DifferentialAction::ACTION_ACCEPT) { 132 $new_status = DifferentialReviewerStatus::STATUS_ACCEPTED; 133 } else { 134 $new_status = DifferentialReviewerStatus::STATUS_REJECTED; 135 } 136 137 $actor = $this->getActor(); 138 139 // These transactions can cause effects in two ways: by altering the 140 // status of an existing reviewer; or by adding the actor as a new 141 // reviewer. 142 143 $will_add_reviewer = true; 144 foreach ($object->getReviewerStatus() as $reviewer) { 145 if ($reviewer->hasAuthority($actor)) { 146 if ($reviewer->getStatus() != $new_status) { 147 return true; 148 } 149 } 150 if ($reviewer->getReviewerPHID() == $actor_phid) { 151 $will_add_reviwer = false; 152 } 153 } 154 155 return $will_add_reviewer; 156 case DifferentialAction::ACTION_CLOSE: 157 return ($object->getStatus() != $status_closed); 158 case DifferentialAction::ACTION_ABANDON: 159 return ($object->getStatus() != $status_abandoned); 160 case DifferentialAction::ACTION_RECLAIM: 161 return ($object->getStatus() == $status_abandoned); 162 case DifferentialAction::ACTION_REOPEN: 163 return ($object->getStatus() == $status_closed); 164 case DifferentialAction::ACTION_RETHINK: 165 return ($object->getStatus() != $status_plan); 166 case DifferentialAction::ACTION_REQUEST: 167 return ($object->getStatus() != $status_review); 168 case DifferentialAction::ACTION_RESIGN: 169 foreach ($object->getReviewerStatus() as $reviewer) { 170 if ($reviewer->getReviewerPHID() == $actor_phid) { 171 return true; 172 } 173 } 174 return false; 175 case DifferentialAction::ACTION_CLAIM: 176 return ($actor_phid != $object->getAuthorPHID()); 177 } 178 } 179 180 return parent::transactionHasEffect($object, $xaction); 181 } 182 183 protected function applyCustomInternalTransaction( 184 PhabricatorLiskDAO $object, 185 PhabricatorApplicationTransaction $xaction) { 186 187 $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; 188 $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; 189 $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; 190 191 switch ($xaction->getTransactionType()) { 192 case PhabricatorTransactions::TYPE_VIEW_POLICY: 193 $object->setViewPolicy($xaction->getNewValue()); 194 return; 195 case PhabricatorTransactions::TYPE_EDIT_POLICY: 196 $object->setEditPolicy($xaction->getNewValue()); 197 return; 198 case PhabricatorTransactions::TYPE_SUBSCRIBERS: 199 case PhabricatorTransactions::TYPE_COMMENT: 200 case DifferentialTransaction::TYPE_INLINE: 201 return; 202 case PhabricatorTransactions::TYPE_EDGE: 203 return; 204 case DifferentialTransaction::TYPE_UPDATE: 205 if (!$this->getIsCloseByCommit() && 206 (($object->getStatus() == $status_revision) || 207 ($object->getStatus() == $status_plan))) { 208 $object->setStatus($status_review); 209 } 210 211 $diff = $this->requireDiff($xaction->getNewValue()); 212 213 $object->setLineCount($diff->getLineCount()); 214 if ($this->repositoryPHIDOverride !== false) { 215 $object->setRepositoryPHID($this->repositoryPHIDOverride); 216 } else { 217 $object->setRepositoryPHID($diff->getRepositoryPHID()); 218 } 219 $object->setArcanistProjectPHID($diff->getArcanistProjectPHID()); 220 $object->attachActiveDiff($diff); 221 222 // TODO: Update the `diffPHID` once we add that. 223 return; 224 case DifferentialTransaction::TYPE_ACTION: 225 switch ($xaction->getNewValue()) { 226 case DifferentialAction::ACTION_RESIGN: 227 case DifferentialAction::ACTION_ACCEPT: 228 case DifferentialAction::ACTION_REJECT: 229 // These have no direct effects, and affect review status only 230 // indirectly by altering reviewers with TYPE_EDGE transactions. 231 return; 232 case DifferentialAction::ACTION_ABANDON: 233 $object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED); 234 return; 235 case DifferentialAction::ACTION_RETHINK: 236 $object->setStatus($status_plan); 237 return; 238 case DifferentialAction::ACTION_RECLAIM: 239 $object->setStatus($status_review); 240 return; 241 case DifferentialAction::ACTION_REOPEN: 242 $object->setStatus($status_review); 243 return; 244 case DifferentialAction::ACTION_REQUEST: 245 $object->setStatus($status_review); 246 return; 247 case DifferentialAction::ACTION_CLOSE: 248 $object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED); 249 return; 250 case DifferentialAction::ACTION_CLAIM: 251 $object->setAuthorPHID($this->getActingAsPHID()); 252 return; 253 } 254 break; 255 } 256 257 return parent::applyCustomInternalTransaction($object, $xaction); 258 } 259 260 protected function expandTransaction( 261 PhabricatorLiskDAO $object, 262 PhabricatorApplicationTransaction $xaction) { 263 264 $results = parent::expandTransaction($object, $xaction); 265 266 $actor = $this->getActor(); 267 $actor_phid = $this->getActingAsPHID(); 268 $type_edge = PhabricatorTransactions::TYPE_EDGE; 269 270 $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; 271 272 $edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; 273 $edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST; 274 275 $is_sticky_accept = PhabricatorEnv::getEnvConfig( 276 'differential.sticky-accept'); 277 278 $downgrade_rejects = false; 279 $downgrade_accepts = false; 280 if ($this->getIsCloseByCommit()) { 281 // Never downgrade reviewers when we're closing a revision after a 282 // commit. 283 } else { 284 switch ($xaction->getTransactionType()) { 285 case DifferentialTransaction::TYPE_UPDATE: 286 $downgrade_rejects = true; 287 if (!$is_sticky_accept) { 288 // If "sticky accept" is disabled, also downgrade the accepts. 289 $downgrade_accepts = true; 290 } 291 break; 292 case DifferentialTransaction::TYPE_ACTION: 293 switch ($xaction->getNewValue()) { 294 case DifferentialAction::ACTION_REQUEST: 295 $downgrade_rejects = true; 296 if ((!$is_sticky_accept) || 297 ($object->getStatus() != $status_plan)) { 298 // If the old state isn't "changes planned", downgrade the 299 // accepts. This exception allows an accepted revision to 300 // go through Plan Changes -> Request Review to return to 301 // "accepted" if the author didn't update the revision. 302 $downgrade_accepts = true; 303 } 304 break; 305 } 306 break; 307 } 308 } 309 310 $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED; 311 $new_reject = DifferentialReviewerStatus::STATUS_REJECTED; 312 $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER; 313 $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER; 314 315 if ($downgrade_rejects || $downgrade_accepts) { 316 // When a revision is updated, change all "reject" to "rejected older 317 // revision". This means we won't immediately push the update back into 318 // "needs review", but outstanding rejects will still block it from 319 // moving to "accepted". 320 321 // We also do this for "Request Review", even though the diff is not 322 // updated directly. Essentially, this acts like an update which doesn't 323 // actually change the diff text. 324 325 $edits = array(); 326 foreach ($object->getReviewerStatus() as $reviewer) { 327 if ($downgrade_rejects) { 328 if ($reviewer->getStatus() == $new_reject) { 329 $edits[$reviewer->getReviewerPHID()] = array( 330 'data' => array( 331 'status' => $old_reject, 332 ), 333 ); 334 } 335 } 336 337 if ($downgrade_accepts) { 338 if ($reviewer->getStatus() == $new_accept) { 339 $edits[$reviewer->getReviewerPHID()] = array( 340 'data' => array( 341 'status' => $old_accept, 342 ), 343 ); 344 } 345 } 346 } 347 348 if ($edits) { 349 $results[] = id(new DifferentialTransaction()) 350 ->setTransactionType($type_edge) 351 ->setMetadataValue('edge:type', $edge_reviewer) 352 ->setIgnoreOnNoEffect(true) 353 ->setNewValue(array('+' => $edits)); 354 } 355 } 356 357 switch ($xaction->getTransactionType()) { 358 case DifferentialTransaction::TYPE_UPDATE: 359 if ($this->getIsCloseByCommit()) { 360 // Don't bother with any of this if this update is a side effect of 361 // commit detection. 362 break; 363 } 364 365 // When a revision is updated and the diff comes from a branch named 366 // "T123" or similar, automatically associate the commit with the 367 // task that the branch names. 368 369 $maniphest = 'PhabricatorManiphestApplication'; 370 if (PhabricatorApplication::isClassInstalled($maniphest)) { 371 $diff = $this->requireDiff($xaction->getNewValue()); 372 $branch = $diff->getBranch(); 373 374 // No "$", to allow for branches like T123_demo. 375 $match = null; 376 if (preg_match('/^T(\d+)/i', $branch, $match)) { 377 $task_id = $match[1]; 378 $tasks = id(new ManiphestTaskQuery()) 379 ->setViewer($this->getActor()) 380 ->withIDs(array($task_id)) 381 ->execute(); 382 if ($tasks) { 383 $task = head($tasks); 384 $task_phid = $task->getPHID(); 385 386 $results[] = id(new DifferentialTransaction()) 387 ->setTransactionType($type_edge) 388 ->setMetadataValue('edge:type', $edge_ref_task) 389 ->setIgnoreOnNoEffect(true) 390 ->setNewValue(array('+' => array($task_phid => $task_phid))); 391 } 392 } 393 } 394 break; 395 396 case PhabricatorTransactions::TYPE_COMMENT: 397 // When a user leaves a comment, upgrade their reviewer status from 398 // "added" to "commented" if they're also a reviewer. We may further 399 // upgrade this based on other actions in the transaction group. 400 401 $status_added = DifferentialReviewerStatus::STATUS_ADDED; 402 $status_commented = DifferentialReviewerStatus::STATUS_COMMENTED; 403 404 $data = array( 405 'status' => $status_commented, 406 ); 407 408 $edits = array(); 409 foreach ($object->getReviewerStatus() as $reviewer) { 410 if ($reviewer->getReviewerPHID() == $actor_phid) { 411 if ($reviewer->getStatus() == $status_added) { 412 $edits[$actor_phid] = array( 413 'data' => $data, 414 ); 415 } 416 } 417 } 418 419 if ($edits) { 420 $results[] = id(new DifferentialTransaction()) 421 ->setTransactionType($type_edge) 422 ->setMetadataValue('edge:type', $edge_reviewer) 423 ->setIgnoreOnNoEffect(true) 424 ->setNewValue(array('+' => $edits)); 425 } 426 break; 427 428 case DifferentialTransaction::TYPE_ACTION: 429 $action_type = $xaction->getNewValue(); 430 431 switch ($action_type) { 432 case DifferentialAction::ACTION_ACCEPT: 433 case DifferentialAction::ACTION_REJECT: 434 if ($action_type == DifferentialAction::ACTION_ACCEPT) { 435 $data = array( 436 'status' => DifferentialReviewerStatus::STATUS_ACCEPTED, 437 ); 438 } else { 439 $data = array( 440 'status' => DifferentialReviewerStatus::STATUS_REJECTED, 441 ); 442 } 443 444 $edits = array(); 445 446 foreach ($object->getReviewerStatus() as $reviewer) { 447 if ($reviewer->hasAuthority($actor)) { 448 $edits[$reviewer->getReviewerPHID()] = array( 449 'data' => $data, 450 ); 451 } 452 } 453 454 // Also either update or add the actor themselves as a reviewer. 455 $edits[$actor_phid] = array( 456 'data' => $data, 457 ); 458 459 $results[] = id(new DifferentialTransaction()) 460 ->setTransactionType($type_edge) 461 ->setMetadataValue('edge:type', $edge_reviewer) 462 ->setIgnoreOnNoEffect(true) 463 ->setNewValue(array('+' => $edits)); 464 break; 465 466 case DifferentialAction::ACTION_CLAIM: 467 // If the user is commandeering, add the previous owner as a 468 // reviewer and remove the actor. 469 470 $edits = array( 471 '-' => array( 472 $actor_phid => $actor_phid, 473 ), 474 ); 475 476 $owner_phid = $object->getAuthorPHID(); 477 if ($owner_phid) { 478 $reviewer = new DifferentialReviewer( 479 $owner_phid, 480 array( 481 'status' => DifferentialReviewerStatus::STATUS_ADDED, 482 )); 483 484 $edits['+'] = array( 485 $owner_phid => array( 486 'data' => $reviewer->getEdgeData(), 487 ), 488 ); 489 } 490 491 // NOTE: We're setting setIsCommandeerSideEffect() on this because 492 // normally you can't add a revision's author as a reviewer, but 493 // this action swaps them after validation executes. 494 495 $results[] = id(new DifferentialTransaction()) 496 ->setTransactionType($type_edge) 497 ->setMetadataValue('edge:type', $edge_reviewer) 498 ->setIgnoreOnNoEffect(true) 499 ->setIsCommandeerSideEffect(true) 500 ->setNewValue($edits); 501 502 break; 503 case DifferentialAction::ACTION_RESIGN: 504 // If the user is resigning, add a separate reviewer edit 505 // transaction which removes them as a reviewer. 506 507 $results[] = id(new DifferentialTransaction()) 508 ->setTransactionType($type_edge) 509 ->setMetadataValue('edge:type', $edge_reviewer) 510 ->setIgnoreOnNoEffect(true) 511 ->setNewValue( 512 array( 513 '-' => array( 514 $actor_phid => $actor_phid, 515 ), 516 )); 517 518 break; 519 } 520 break; 521 } 522 523 return $results; 524 } 525 526 protected function applyCustomExternalTransaction( 527 PhabricatorLiskDAO $object, 528 PhabricatorApplicationTransaction $xaction) { 529 530 switch ($xaction->getTransactionType()) { 531 case PhabricatorTransactions::TYPE_VIEW_POLICY: 532 case PhabricatorTransactions::TYPE_EDIT_POLICY: 533 return; 534 case PhabricatorTransactions::TYPE_SUBSCRIBERS: 535 case PhabricatorTransactions::TYPE_EDGE: 536 case PhabricatorTransactions::TYPE_COMMENT: 537 case DifferentialTransaction::TYPE_ACTION: 538 case DifferentialTransaction::TYPE_INLINE: 539 return; 540 case DifferentialTransaction::TYPE_UPDATE: 541 // Now that we're inside the transaction, do a final check. 542 $diff = $this->requireDiff($xaction->getNewValue()); 543 544 // TODO: It would be slightly cleaner to just revalidate this 545 // transaction somehow using the same validation code, but that's 546 // not easy to do at the moment. 547 548 $revision_id = $diff->getRevisionID(); 549 if ($revision_id && ($revision_id != $object->getID())) { 550 throw new Exception( 551 pht( 552 'Diff is already attached to another revision. You lost '. 553 'a race?')); 554 } 555 556 $diff->setRevisionID($object->getID()); 557 $diff->save(); 558 return; 559 } 560 561 return parent::applyCustomExternalTransaction($object, $xaction); 562 } 563 564 protected function mergeEdgeData($type, array $u, array $v) { 565 $result = parent::mergeEdgeData($type, $u, $v); 566 567 switch ($type) { 568 case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: 569 // When the same reviewer has their status updated by multiple 570 // transactions, we want the strongest status to win. An example of 571 // this is when a user adds a comment and also accepts a revision which 572 // they are a reviewer on. The comment creates a "commented" status, 573 // while the accept creates an "accepted" status. Since accept is 574 // stronger, it should win and persist. 575 576 $u_status = idx($u, 'status'); 577 $v_status = idx($v, 'status'); 578 $u_str = DifferentialReviewerStatus::getStatusStrength($u_status); 579 $v_str = DifferentialReviewerStatus::getStatusStrength($v_status); 580 if ($u_str > $v_str) { 581 $result['status'] = $u_status; 582 } else { 583 $result['status'] = $v_status; 584 } 585 break; 586 } 587 588 return $result; 589 } 590 591 protected function applyFinalEffects( 592 PhabricatorLiskDAO $object, 593 array $xactions) { 594 595 // Load the most up-to-date version of the revision and its reviewers, 596 // so we don't need to try to deduce the state of reviewers by examining 597 // all the changes made by the transactions. Then, update the reviewers 598 // on the object to make sure we're acting on the current reviewer set 599 // (and, for example, sending mail to the right people). 600 601 $new_revision = id(new DifferentialRevisionQuery()) 602 ->setViewer($this->getActor()) 603 ->needReviewerStatus(true) 604 ->needActiveDiffs(true) 605 ->withIDs(array($object->getID())) 606 ->executeOne(); 607 if (!$new_revision) { 608 throw new Exception( 609 pht('Failed to load revision from transaction finalization.')); 610 } 611 612 $object->attachReviewerStatus($new_revision->getReviewerStatus()); 613 $object->attachActiveDiff($new_revision->getActiveDiff()); 614 $object->attachRepository($new_revision->getRepository()); 615 616 foreach ($xactions as $xaction) { 617 switch ($xaction->getTransactionType()) { 618 case DifferentialTransaction::TYPE_UPDATE: 619 $diff = $this->requireDiff($xaction->getNewValue(), true); 620 621 // Update these denormalized index tables when we attach a new 622 // diff to a revision. 623 624 $this->updateRevisionHashTable($object, $diff); 625 $this->updateAffectedPathTable($object, $diff); 626 break; 627 } 628 } 629 630 $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; 631 $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; 632 $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; 633 634 $old_status = $object->getStatus(); 635 switch ($old_status) { 636 case $status_accepted: 637 case $status_revision: 638 case $status_review: 639 // Try to move a revision to "accepted". We look for: 640 // 641 // - at least one accepting reviewer who is a user; and 642 // - no rejects; and 643 // - no rejects of older diffs; and 644 // - no blocking reviewers. 645 646 $has_accepting_user = false; 647 $has_rejecting_reviewer = false; 648 $has_rejecting_older_reviewer = false; 649 $has_blocking_reviewer = false; 650 foreach ($object->getReviewerStatus() as $reviewer) { 651 $reviewer_status = $reviewer->getStatus(); 652 switch ($reviewer_status) { 653 case DifferentialReviewerStatus::STATUS_REJECTED: 654 $has_rejecting_reviewer = true; 655 break; 656 case DifferentialReviewerStatus::STATUS_REJECTED_OLDER: 657 $has_rejecting_older_reviewer = true; 658 break; 659 case DifferentialReviewerStatus::STATUS_BLOCKING: 660 $has_blocking_reviewer = true; 661 break; 662 case DifferentialReviewerStatus::STATUS_ACCEPTED: 663 if ($reviewer->isUser()) { 664 $has_accepting_user = true; 665 } 666 break; 667 } 668 } 669 670 $new_status = null; 671 if ($has_accepting_user && 672 !$has_rejecting_reviewer && 673 !$has_rejecting_older_reviewer && 674 !$has_blocking_reviewer) { 675 $new_status = $status_accepted; 676 } else if ($has_rejecting_reviewer) { 677 // This isn't accepted, and there's at least one rejecting reviewer, 678 // so the revision needs changes. This usually happens after a 679 // "reject". 680 $new_status = $status_revision; 681 } else if ($old_status == $status_accepted) { 682 // This revision was accepted, but it no longer satisfies the 683 // conditions for acceptance. This usually happens after an accepting 684 // reviewer resigns or is removed. 685 $new_status = $status_review; 686 } 687 688 if ($new_status !== null && ($new_status != $old_status)) { 689 $xaction = id(new DifferentialTransaction()) 690 ->setTransactionType(DifferentialTransaction::TYPE_STATUS) 691 ->setOldValue($old_status) 692 ->setNewValue($new_status); 693 694 $xaction = $this->populateTransaction($object, $xaction)->save(); 695 696 $xactions[] = $xaction; 697 698 $object->setStatus($new_status)->save(); 699 } 700 break; 701 default: 702 // Revisions can't transition out of other statuses (like closed or 703 // abandoned) as a side effect of reviewer status changes. 704 break; 705 } 706 707 return $xactions; 708 } 709 710 protected function validateTransaction( 711 PhabricatorLiskDAO $object, 712 $type, 713 array $xactions) { 714 715 $errors = parent::validateTransaction($object, $type, $xactions); 716 717 $config_self_accept_key = 'differential.allow-self-accept'; 718 $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key); 719 720 foreach ($xactions as $xaction) { 721 switch ($type) { 722 case PhabricatorTransactions::TYPE_EDGE: 723 switch ($xaction->getMetadataValue('edge:type')) { 724 case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER: 725 726 // Prevent the author from becoming a reviewer. 727 728 // NOTE: This is pretty gross, but this restriction is unusual. 729 // If we end up with too much more of this, we should try to clean 730 // this up -- maybe by moving validation to after transactions 731 // are adjusted (so we can just examine the final value) or adding 732 // a second phase there? 733 734 $author_phid = $object->getAuthorPHID(); 735 $new = $xaction->getNewValue(); 736 737 $add = idx($new, '+', array()); 738 $eq = idx($new, '=', array()); 739 $phids = array_keys($add + $eq); 740 741 foreach ($phids as $phid) { 742 if (($phid == $author_phid) && 743 !$allow_self_accept && 744 !$xaction->getIsCommandeerSideEffect()) { 745 $errors[] = 746 new PhabricatorApplicationTransactionValidationError( 747 $type, 748 pht('Invalid'), 749 pht('The author of a revision can not be a reviewer.'), 750 $xaction); 751 } 752 } 753 break; 754 } 755 break; 756 case DifferentialTransaction::TYPE_UPDATE: 757 $diff = $this->loadDiff($xaction->getNewValue()); 758 if (!$diff) { 759 $errors[] = new PhabricatorApplicationTransactionValidationError( 760 $type, 761 pht('Invalid'), 762 pht('The specified diff does not exist.'), 763 $xaction); 764 } else if (($diff->getRevisionID()) && 765 ($diff->getRevisionID() != $object->getID())) { 766 $errors[] = new PhabricatorApplicationTransactionValidationError( 767 $type, 768 pht('Invalid'), 769 pht( 770 'You can not update this revision to the specified diff, '. 771 'because the diff is already attached to another revision.'), 772 $xaction); 773 } 774 break; 775 case DifferentialTransaction::TYPE_ACTION: 776 $error = $this->validateDifferentialAction( 777 $object, 778 $type, 779 $xaction, 780 $xaction->getNewValue()); 781 if ($error) { 782 $errors[] = new PhabricatorApplicationTransactionValidationError( 783 $type, 784 pht('Invalid'), 785 $error, 786 $xaction); 787 } 788 break; 789 } 790 } 791 792 return $errors; 793 } 794 795 private function validateDifferentialAction( 796 DifferentialRevision $revision, 797 $type, 798 DifferentialTransaction $xaction, 799 $action) { 800 801 $author_phid = $revision->getAuthorPHID(); 802 $actor_phid = $this->getActingAsPHID(); 803 $actor_is_author = ($author_phid == $actor_phid); 804 805 $config_abandon_key = 'differential.always-allow-abandon'; 806 $always_allow_abandon = PhabricatorEnv::getEnvConfig($config_abandon_key); 807 808 $config_close_key = 'differential.always-allow-close'; 809 $always_allow_close = PhabricatorEnv::getEnvConfig($config_close_key); 810 811 $config_reopen_key = 'differential.allow-reopen'; 812 $allow_reopen = PhabricatorEnv::getEnvConfig($config_reopen_key); 813 814 $config_self_accept_key = 'differential.allow-self-accept'; 815 $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key); 816 817 $revision_status = $revision->getStatus(); 818 819 $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; 820 $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; 821 $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; 822 823 switch ($action) { 824 case DifferentialAction::ACTION_ACCEPT: 825 if ($actor_is_author && !$allow_self_accept) { 826 return pht( 827 'You can not accept this revision because you are the owner.'); 828 } 829 830 if ($revision_status == $status_abandoned) { 831 return pht( 832 'You can not accept this revision because it has been '. 833 'abandoned.'); 834 } 835 836 if ($revision_status == $status_closed) { 837 return pht( 838 'You can not accept this revision because it has already been '. 839 'closed.'); 840 } 841 842 // TODO: It would be nice to make this generic at some point. 843 $signatures = DifferentialRequiredSignaturesField::loadForRevision( 844 $revision); 845 foreach ($signatures as $phid => $signed) { 846 if (!$signed) { 847 return pht( 848 'You can not accept this revision because the author has '. 849 'not signed all of the required legal documents.'); 850 } 851 } 852 853 break; 854 855 case DifferentialAction::ACTION_REJECT: 856 if ($actor_is_author) { 857 return pht( 858 'You can not request changes to your own revision.'); 859 } 860 861 if ($revision_status == $status_abandoned) { 862 return pht( 863 'You can not request changes to this revision because it has been '. 864 'abandoned.'); 865 } 866 867 if ($revision_status == $status_closed) { 868 return pht( 869 'You can not request changes to this revision because it has '. 870 'already been closed.'); 871 } 872 break; 873 874 case DifferentialAction::ACTION_RESIGN: 875 // You can always resign from a revision if you're a reviewer. If you 876 // aren't, this is a no-op rather than invalid. 877 break; 878 879 case DifferentialAction::ACTION_CLAIM: 880 // You can claim a revision if you're not the owner. If you are, this 881 // is a no-op rather than invalid. 882 883 if ($revision_status == $status_closed) { 884 return pht( 885 'You can not commandeer this revision because it has already been '. 886 'closed.'); 887 } 888 break; 889 890 case DifferentialAction::ACTION_ABANDON: 891 if (!$actor_is_author && !$always_allow_abandon) { 892 return pht( 893 'You can not abandon this revision because you do not own it. '. 894 'You can only abandon revisions you own.'); 895 } 896 897 if ($revision_status == $status_closed) { 898 return pht( 899 'You can not abandon this revision because it has already been '. 900 'closed.'); 901 } 902 903 // NOTE: Abandons of already-abandoned revisions are treated as no-op 904 // instead of invalid. Other abandons are OK. 905 906 break; 907 908 case DifferentialAction::ACTION_RECLAIM: 909 if (!$actor_is_author) { 910 return pht( 911 'You can not reclaim this revision because you do not own '. 912 'it. You can only reclaim revisions you own.'); 913 } 914 915 if ($revision_status == $status_closed) { 916 return pht( 917 'You can not reclaim this revision because it has already been '. 918 'closed.'); 919 } 920 921 // NOTE: Reclaims of other non-abandoned revisions are treated as no-op 922 // instead of invalid. 923 924 break; 925 926 case DifferentialAction::ACTION_REOPEN: 927 if (!$allow_reopen) { 928 return pht( 929 'The reopen action is not enabled on this Phabricator install. '. 930 'Adjust your configuration to enable it.'); 931 } 932 933 // NOTE: If the revision is not closed, this is caught as a no-op 934 // instead of an invalid transaction. 935 936 break; 937 938 case DifferentialAction::ACTION_RETHINK: 939 if (!$actor_is_author) { 940 return pht( 941 'You can not plan changes to this revision because you do not '. 942 'own it. To plan changes to a revision, you must be its owner.'); 943 } 944 945 switch ($revision_status) { 946 case ArcanistDifferentialRevisionStatus::ACCEPTED: 947 case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: 948 case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: 949 // These are OK. 950 break; 951 case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: 952 // Let this through, it's a no-op. 953 break; 954 case ArcanistDifferentialRevisionStatus::ABANDONED: 955 return pht( 956 'You can not plan changes to this revision because it has '. 957 'been abandoned.'); 958 case ArcanistDifferentialRevisionStatus::CLOSED: 959 return pht( 960 'You can not plan changes to this revision because it has '. 961 'already been closed.'); 962 default: 963 throw new Exception( 964 pht( 965 'Encountered unexpected revision status ("%s") when '. 966 'validating "%s" action.', 967 $revision_status, 968 $action)); 969 } 970 break; 971 972 case DifferentialAction::ACTION_REQUEST: 973 if (!$actor_is_author) { 974 return pht( 975 'You can not request review of this revision because you do '. 976 'not own it. To request review of a revision, you must be its '. 977 'owner.'); 978 } 979 980 switch ($revision_status) { 981 case ArcanistDifferentialRevisionStatus::ACCEPTED: 982 case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: 983 case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: 984 // These are OK. 985 break; 986 case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: 987 // This will be caught as "no effect" later on. 988 break; 989 case ArcanistDifferentialRevisionStatus::ABANDONED: 990 return pht( 991 'You can not request review of this revision because it has '. 992 'been abandoned. Instead, reclaim it.'); 993 case ArcanistDifferentialRevisionStatus::CLOSED: 994 return pht( 995 'You can not request review of this revision because it has '. 996 'already been closed.'); 997 default: 998 throw new Exception( 999 pht( 1000 'Encountered unexpected revision status ("%s") when '. 1001 'validating "%s" action.', 1002 $revision_status, 1003 $action)); 1004 } 1005 break; 1006 1007 case DifferentialAction::ACTION_CLOSE: 1008 // We force revisions closed when we discover a corresponding commit. 1009 // In this case, revisions are allowed to transition to closed from 1010 // any state. This is an automated action taken by the daemons. 1011 1012 if (!$this->getIsCloseByCommit()) { 1013 if (!$actor_is_author && !$always_allow_close) { 1014 return pht( 1015 'You can not close this revision because you do not own it. To '. 1016 'close a revision, you must be its owner.'); 1017 } 1018 1019 if ($revision_status != $status_accepted) { 1020 return pht( 1021 'You can not close this revision because it has not been '. 1022 'accepted. You can only close accepted revisions.'); 1023 } 1024 } 1025 break; 1026 } 1027 1028 return null; 1029 } 1030 1031 protected function sortTransactions(array $xactions) { 1032 $xactions = parent::sortTransactions($xactions); 1033 1034 $head = array(); 1035 $tail = array(); 1036 1037 foreach ($xactions as $xaction) { 1038 $type = $xaction->getTransactionType(); 1039 if ($type == DifferentialTransaction::TYPE_INLINE) { 1040 $tail[] = $xaction; 1041 } else { 1042 $head[] = $xaction; 1043 } 1044 } 1045 1046 return array_values(array_merge($head, $tail)); 1047 } 1048 1049 protected function requireCapabilities( 1050 PhabricatorLiskDAO $object, 1051 PhabricatorApplicationTransaction $xaction) { 1052 1053 switch ($xaction->getTransactionType()) {} 1054 1055 return parent::requireCapabilities($object, $xaction); 1056 } 1057 1058 protected function shouldPublishFeedStory( 1059 PhabricatorLiskDAO $object, 1060 array $xactions) { 1061 return true; 1062 } 1063 1064 protected function shouldSendMail( 1065 PhabricatorLiskDAO $object, 1066 array $xactions) { 1067 return true; 1068 } 1069 1070 protected function getMailTo(PhabricatorLiskDAO $object) { 1071 $phids = array(); 1072 $phids[] = $object->getAuthorPHID(); 1073 foreach ($object->getReviewerStatus() as $reviewer) { 1074 $phids[] = $reviewer->getReviewerPHID(); 1075 } 1076 return $phids; 1077 } 1078 1079 protected function getMailCC(PhabricatorLiskDAO $object) { 1080 $phids = parent::getMailCC($object); 1081 1082 if ($this->heraldEmailPHIDs) { 1083 foreach ($this->heraldEmailPHIDs as $phid) { 1084 $phids[] = $phid; 1085 } 1086 } 1087 1088 return $phids; 1089 } 1090 1091 protected function getMailAction( 1092 PhabricatorLiskDAO $object, 1093 array $xactions) { 1094 $action = parent::getMailAction($object, $xactions); 1095 1096 $strongest = $this->getStrongestAction($object, $xactions); 1097 switch ($strongest->getTransactionType()) { 1098 case DifferentialTransaction::TYPE_UPDATE: 1099 $count = new PhutilNumber($object->getLineCount()); 1100 $action = pht('%s, %s line(s)', $action, $count); 1101 break; 1102 } 1103 1104 return $action; 1105 } 1106 1107 protected function getMailSubjectPrefix() { 1108 return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); 1109 } 1110 1111 protected function getMailThreadID(PhabricatorLiskDAO $object) { 1112 // This is nonstandard, but retains threading with older messages. 1113 $phid = $object->getPHID(); 1114 return "differential-rev-{$phid}-req"; 1115 } 1116 1117 protected function buildReplyHandler(PhabricatorLiskDAO $object) { 1118 return id(new DifferentialReplyHandler()) 1119 ->setMailReceiver($object); 1120 } 1121 1122 protected function buildMailTemplate(PhabricatorLiskDAO $object) { 1123 $id = $object->getID(); 1124 $title = $object->getTitle(); 1125 1126 $original_title = $object->getOriginalTitle(); 1127 1128 $subject = "D{$id}: {$title}"; 1129 $thread_topic = "D{$id}: {$original_title}"; 1130 1131 return id(new PhabricatorMetaMTAMail()) 1132 ->setSubject($subject) 1133 ->addHeader('Thread-Topic', $thread_topic); 1134 } 1135 1136 protected function buildMailBody( 1137 PhabricatorLiskDAO $object, 1138 array $xactions) { 1139 1140 $body = parent::buildMailBody($object, $xactions); 1141 1142 $type_inline = DifferentialTransaction::TYPE_INLINE; 1143 1144 $inlines = array(); 1145 foreach ($xactions as $xaction) { 1146 if ($xaction->getTransactionType() == $type_inline) { 1147 $inlines[] = $xaction; 1148 } 1149 } 1150 1151 $changed_uri = $this->getChangedPriorToCommitURI(); 1152 if ($changed_uri) { 1153 $body->addLinkSection( 1154 pht('CHANGED PRIOR TO COMMIT'), 1155 $changed_uri); 1156 } 1157 1158 if ($inlines) { 1159 $body->addTextSection( 1160 pht('INLINE COMMENTS'), 1161 $this->renderInlineCommentsForMail($object, $inlines)); 1162 } 1163 1164 $body->addLinkSection( 1165 pht('REVISION DETAIL'), 1166 PhabricatorEnv::getProductionURI('/D'.$object->getID())); 1167 1168 $update_xaction = null; 1169 foreach ($xactions as $xaction) { 1170 switch ($xaction->getTransactionType()) { 1171 case DifferentialTransaction::TYPE_UPDATE: 1172 $update_xaction = $xaction; 1173 break; 1174 } 1175 } 1176 1177 if ($update_xaction) { 1178 $diff = $this->requireDiff($update_xaction->getNewValue(), true); 1179 1180 $body->addTextSection( 1181 pht('AFFECTED FILES'), 1182 $this->renderAffectedFilesForMail($diff)); 1183 1184 $config_key_inline = 'metamta.differential.inline-patches'; 1185 $config_inline = PhabricatorEnv::getEnvConfig($config_key_inline); 1186 1187 $config_key_attach = 'metamta.differential.attach-patches'; 1188 $config_attach = PhabricatorEnv::getEnvConfig($config_key_attach); 1189 1190 if ($config_inline || $config_attach) { 1191 $patch_section = $this->renderPatchForMail($diff); 1192 $lines = count(phutil_split_lines($patch_section->getPlaintext())); 1193 1194 if ($config_inline && ($lines <= $config_inline)) { 1195 $body->addTextSection( 1196 pht('CHANGE DETAILS'), 1197 $patch_section); 1198 } 1199 1200 if ($config_attach) { 1201 $name = pht('D%s.%s.patch', $object->getID(), $diff->getID()); 1202 $mime_type = 'text/x-patch; charset=utf-8'; 1203 $body->addAttachment( 1204 new PhabricatorMetaMTAAttachment( 1205 $patch_section->getPlaintext(), $name, $mime_type)); 1206 } 1207 } 1208 } 1209 1210 return $body; 1211 } 1212 1213 public function getMailTagsMap() { 1214 return array( 1215 MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST => 1216 pht('A revision is created.'), 1217 MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED => 1218 pht('A revision is updated.'), 1219 MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT => 1220 pht('Someone comments on a revision.'), 1221 MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED => 1222 pht('A revision is closed.'), 1223 MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS => 1224 pht("A revision's reviewers change."), 1225 MetaMTANotificationType::TYPE_DIFFERENTIAL_CC => 1226 pht("A revision's CCs change."), 1227 MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER => 1228 pht('Other revision activity not listed above occurs.'), 1229 ); 1230 } 1231 1232 protected function supportsSearch() { 1233 return true; 1234 } 1235 1236 protected function extractFilePHIDsFromCustomTransaction( 1237 PhabricatorLiskDAO $object, 1238 PhabricatorApplicationTransaction $xaction) { 1239 1240 switch ($xaction->getTransactionType()) {} 1241 1242 return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); 1243 } 1244 1245 protected function expandCustomRemarkupBlockTransactions( 1246 PhabricatorLiskDAO $object, 1247 array $xactions, 1248 $blocks, 1249 PhutilMarkupEngine $engine) { 1250 1251 $flat_blocks = array_mergev($blocks); 1252 $huge_block = implode("\n\n", $flat_blocks); 1253 1254 $task_map = array(); 1255 $task_refs = id(new ManiphestCustomFieldStatusParser()) 1256 ->parseCorpus($huge_block); 1257 foreach ($task_refs as $match) { 1258 foreach ($match['monograms'] as $monogram) { 1259 $task_id = (int)trim($monogram, 'tT'); 1260 $task_map[$task_id] = true; 1261 } 1262 } 1263 1264 $rev_map = array(); 1265 $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) 1266 ->parseCorpus($huge_block); 1267 foreach ($rev_refs as $match) { 1268 foreach ($match['monograms'] as $monogram) { 1269 $rev_id = (int)trim($monogram, 'dD'); 1270 $rev_map[$rev_id] = true; 1271 } 1272 } 1273 1274 $edges = array(); 1275 1276 if ($task_map) { 1277 $tasks = id(new ManiphestTaskQuery()) 1278 ->setViewer($this->getActor()) 1279 ->withIDs(array_keys($task_map)) 1280 ->execute(); 1281 1282 if ($tasks) { 1283 $phid_map = mpull($tasks, 'getPHID', 'getPHID'); 1284 $edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST; 1285 $edges[$edge_related] = $phid_map; 1286 $this->setUnmentionablePHIDMap($phid_map); 1287 } 1288 } 1289 1290 if ($rev_map) { 1291 $revs = id(new DifferentialRevisionQuery()) 1292 ->setViewer($this->getActor()) 1293 ->withIDs(array_keys($rev_map)) 1294 ->execute(); 1295 $rev_phids = mpull($revs, 'getPHID', 'getPHID'); 1296 1297 // NOTE: Skip any write attempts if a user cleverly implies a revision 1298 // depends upon itself. 1299 unset($rev_phids[$object->getPHID()]); 1300 1301 if ($revs) { 1302 $edge_depends = PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV; 1303 $edges[$edge_depends] = $rev_phids; 1304 } 1305 } 1306 1307 $result = array(); 1308 foreach ($edges as $type => $specs) { 1309 $result[] = id(new DifferentialTransaction()) 1310 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 1311 ->setMetadataValue('edge:type', $type) 1312 ->setNewValue(array('+' => $specs)); 1313 } 1314 1315 return $result; 1316 } 1317 1318 protected function indentForMail(array $lines) { 1319 $indented = array(); 1320 foreach ($lines as $line) { 1321 $indented[] = '> '.$line; 1322 } 1323 return $indented; 1324 } 1325 1326 protected function nestCommentHistory( 1327 DifferentialTransactionComment $comment, array $comments_by_line_number, 1328 array $users_by_phid) { 1329 1330 $nested = array(); 1331 $previous_comments = $comments_by_line_number[$comment->getChangesetID()] 1332 [$comment->getLineNumber()]; 1333 foreach ($previous_comments as $previous_comment) { 1334 if ($previous_comment->getID() >= $comment->getID()) 1335 break; 1336 $nested = $this->indentForMail( 1337 array_merge( 1338 $nested, 1339 explode("\n", $previous_comment->getContent()))); 1340 $user = idx($users_by_phid, $previous_comment->getAuthorPHID(), null); 1341 if ($user) { 1342 array_unshift($nested, pht('%s wrote:', $user->getUserName())); 1343 } 1344 } 1345 1346 $nested = array_merge($nested, explode("\n", $comment->getContent())); 1347 return implode("\n", $nested); 1348 } 1349 1350 private function renderInlineCommentsForMail( 1351 PhabricatorLiskDAO $object, 1352 array $inlines) { 1353 1354 $context_key = 'metamta.differential.unified-comment-context'; 1355 $show_context = PhabricatorEnv::getEnvConfig($context_key); 1356 1357 $changeset_ids = array(); 1358 $line_numbers_by_changeset = array(); 1359 foreach ($inlines as $inline) { 1360 $id = $inline->getComment()->getChangesetID(); 1361 $changeset_ids[$id] = $id; 1362 $line_numbers_by_changeset[$id][] = 1363 $inline->getComment()->getLineNumber(); 1364 } 1365 1366 $changesets = id(new DifferentialChangesetQuery()) 1367 ->setViewer($this->getActor()) 1368 ->withIDs($changeset_ids) 1369 ->needHunks(true) 1370 ->execute(); 1371 1372 $inline_groups = DifferentialTransactionComment::sortAndGroupInlines( 1373 $inlines, 1374 $changesets); 1375 1376 if ($show_context) { 1377 $hunk_parser = new DifferentialHunkParser(); 1378 $table = new DifferentialTransactionComment(); 1379 $conn_r = $table->establishConnection('r'); 1380 $queries = array(); 1381 foreach ($line_numbers_by_changeset as $id => $line_numbers) { 1382 $queries[] = qsprintf( 1383 $conn_r, 1384 '(changesetID = %d AND lineNumber IN (%Ld))', 1385 $id, $line_numbers); 1386 } 1387 $all_comments = id(new DifferentialTransactionComment())->loadAllWhere( 1388 'transactionPHID IS NOT NULL AND (%Q)', implode(' OR ', $queries)); 1389 $comments_by_line_number = array(); 1390 foreach ($all_comments as $comment) { 1391 $comments_by_line_number 1392 [$comment->getChangesetID()] 1393 [$comment->getLineNumber()] 1394 [$comment->getID()] = $comment; 1395 } 1396 $author_phids = mpull($all_comments, 'getAuthorPHID'); 1397 $authors = id(new PhabricatorPeopleQuery()) 1398 ->setViewer($this->getActor()) 1399 ->withPHIDs($author_phids) 1400 ->execute(); 1401 $authors_by_phid = mpull($authors, null, 'getPHID'); 1402 } 1403 1404 $section = new PhabricatorMetaMTAMailSection(); 1405 foreach ($inline_groups as $changeset_id => $group) { 1406 $changeset = idx($changesets, $changeset_id); 1407 if (!$changeset) { 1408 continue; 1409 } 1410 1411 foreach ($group as $inline) { 1412 $comment = $inline->getComment(); 1413 $file = $changeset->getFilename(); 1414 $start = $comment->getLineNumber(); 1415 $len = $comment->getLineLength(); 1416 if ($len) { 1417 $range = $start.'-'.($start + $len); 1418 } else { 1419 $range = $start; 1420 } 1421 1422 $inline_content = $comment->getContent(); 1423 1424 if (!$show_context) { 1425 $section->addFragment("{$file}:{$range} {$inline_content}"); 1426 } else { 1427 $patch = $hunk_parser->makeContextDiff( 1428 $changeset->getHunks(), 1429 $comment->getIsNewFile(), 1430 $comment->getLineNumber(), 1431 $comment->getLineLength(), 1432 1); 1433 $nested_comments = $this->nestCommentHistory( 1434 $inline->getComment(), $comments_by_line_number, $authors_by_phid); 1435 1436 $section->addFragment('================') 1437 ->addFragment('Comment at: '.$file.':'.$range) 1438 ->addPlaintextFragment($patch) 1439 ->addHTMLFragment($this->renderPatchHTMLForMail($patch)) 1440 ->addFragment('----------------') 1441 ->addFragment($nested_comments) 1442 ->addFragment(null); 1443 } 1444 } 1445 } 1446 1447 return $section; 1448 } 1449 1450 private function loadDiff($phid, $need_changesets = false) { 1451 $query = id(new DifferentialDiffQuery()) 1452 ->withPHIDs(array($phid)) 1453 ->setViewer($this->getActor()); 1454 1455 if ($need_changesets) { 1456 $query->needChangesets(true); 1457 } 1458 1459 return $query->executeOne(); 1460 } 1461 1462 private function requireDiff($phid, $need_changesets = false) { 1463 $diff = $this->loadDiff($phid, $need_changesets); 1464 if (!$diff) { 1465 throw new Exception(pht('Diff "%s" does not exist!', $phid)); 1466 } 1467 1468 return $diff; 1469 } 1470 1471 /* -( Herald Integration )------------------------------------------------- */ 1472 1473 protected function shouldApplyHeraldRules( 1474 PhabricatorLiskDAO $object, 1475 array $xactions) { 1476 1477 if ($this->getIsNewObject()) { 1478 return true; 1479 } 1480 1481 foreach ($xactions as $xaction) { 1482 switch ($xaction->getTransactionType()) { 1483 case DifferentialTransaction::TYPE_UPDATE: 1484 if (!$this->getIsCloseByCommit()) { 1485 return true; 1486 } 1487 break; 1488 case DifferentialTransaction::TYPE_ACTION: 1489 switch ($xaction->getNewValue()) { 1490 case DifferentialAction::ACTION_CLAIM: 1491 // When users commandeer revisions, we may need to trigger 1492 // signatures or author-based rules. 1493 return true; 1494 } 1495 break; 1496 } 1497 } 1498 1499 return parent::shouldApplyHeraldRules($object, $xactions); 1500 } 1501 1502 protected function buildHeraldAdapter( 1503 PhabricatorLiskDAO $object, 1504 array $xactions) { 1505 1506 $unsubscribed_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 1507 $object->getPHID(), 1508 PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); 1509 1510 $subscribed_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( 1511 $object->getPHID()); 1512 1513 $revision = id(new DifferentialRevisionQuery()) 1514 ->setViewer($this->getActor()) 1515 ->withPHIDs(array($object->getPHID())) 1516 ->needActiveDiffs(true) 1517 ->needReviewerStatus(true) 1518 ->executeOne(); 1519 if (!$revision) { 1520 throw new Exception( 1521 pht( 1522 'Failed to load revision for Herald adapter construction!')); 1523 } 1524 1525 $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( 1526 $revision, 1527 $revision->getActiveDiff()); 1528 1529 $reviewers = $revision->getReviewerStatus(); 1530 $reviewer_phids = mpull($reviewers, 'getReviewerPHID'); 1531 1532 $adapter->setExplicitCCs($subscribed_phids); 1533 $adapter->setExplicitReviewers($reviewer_phids); 1534 $adapter->setForbiddenCCs($unsubscribed_phids); 1535 1536 return $adapter; 1537 } 1538 1539 protected function didApplyHeraldRules( 1540 PhabricatorLiskDAO $object, 1541 HeraldAdapter $adapter, 1542 HeraldTranscript $transcript) { 1543 1544 $xactions = array(); 1545 1546 // Build a transaction to adjust CCs. 1547 $ccs = array( 1548 '+' => array_keys($adapter->getCCsAddedByHerald()), 1549 '-' => array_keys($adapter->getCCsRemovedByHerald()), 1550 ); 1551 $value = array(); 1552 foreach ($ccs as $type => $phids) { 1553 foreach ($phids as $phid) { 1554 $value[$type][$phid] = $phid; 1555 } 1556 } 1557 1558 if ($value) { 1559 $xactions[] = id(new DifferentialTransaction()) 1560 ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 1561 ->setNewValue($value); 1562 } 1563 1564 // Build a transaction to adjust reviewers. 1565 $reviewers = array( 1566 DifferentialReviewerStatus::STATUS_ADDED => 1567 array_keys($adapter->getReviewersAddedByHerald()), 1568 DifferentialReviewerStatus::STATUS_BLOCKING => 1569 array_keys($adapter->getBlockingReviewersAddedByHerald()), 1570 ); 1571 1572 $old_reviewers = $object->getReviewerStatus(); 1573 $old_reviewers = mpull($old_reviewers, null, 'getReviewerPHID'); 1574 1575 $value = array(); 1576 foreach ($reviewers as $status => $phids) { 1577 foreach ($phids as $phid) { 1578 if ($phid == $object->getAuthorPHID()) { 1579 // Don't try to add the revision's author as a reviewer, since this 1580 // isn't valid and doesn't make sense. 1581 continue; 1582 } 1583 1584 // If the target is already a reviewer, don't try to change anything 1585 // if their current status is at least as strong as the new status. 1586 // For example, don't downgrade an "Accepted" to a "Blocking Reviewer". 1587 $old_reviewer = idx($old_reviewers, $phid); 1588 if ($old_reviewer) { 1589 $old_status = $old_reviewer->getStatus(); 1590 1591 $old_strength = DifferentialReviewerStatus::getStatusStrength( 1592 $old_status); 1593 $new_strength = DifferentialReviewerStatus::getStatusStrength( 1594 $status); 1595 1596 if ($new_strength <= $old_strength) { 1597 continue; 1598 } 1599 } 1600 1601 $value['+'][$phid] = array( 1602 'data' => array( 1603 'status' => $status, 1604 ), 1605 ); 1606 } 1607 } 1608 1609 if ($value) { 1610 $edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; 1611 1612 $xactions[] = id(new DifferentialTransaction()) 1613 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 1614 ->setMetadataValue('edge:type', $edge_reviewer) 1615 ->setNewValue($value); 1616 } 1617 1618 // Require legalpad document signatures. 1619 $legal_phids = $adapter->getRequiredSignatureDocumentPHIDs(); 1620 if ($legal_phids) { 1621 // We only require signatures of documents which have not already 1622 // been signed. In general, this reduces the amount of churn that 1623 // signature rules cause. 1624 1625 $signatures = id(new LegalpadDocumentSignatureQuery()) 1626 ->setViewer(PhabricatorUser::getOmnipotentUser()) 1627 ->withDocumentPHIDs($legal_phids) 1628 ->withSignerPHIDs(array($object->getAuthorPHID())) 1629 ->execute(); 1630 $signed_phids = mpull($signatures, 'getDocumentPHID'); 1631 $legal_phids = array_diff($legal_phids, $signed_phids); 1632 1633 // If we still have something to trigger, add the edges. 1634 if ($legal_phids) { 1635 $edge_legal = PhabricatorEdgeConfig::TYPE_OBJECT_NEEDS_SIGNATURE; 1636 $xactions[] = id(new DifferentialTransaction()) 1637 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 1638 ->setMetadataValue('edge:type', $edge_legal) 1639 ->setNewValue( 1640 array( 1641 '+' => array_fuse($legal_phids), 1642 )); 1643 } 1644 } 1645 1646 // Save extra email PHIDs for later. 1647 $email_phids = $adapter->getEmailPHIDsAddedByHerald(); 1648 $this->heraldEmailPHIDs = array_keys($email_phids); 1649 1650 // Apply build plans. 1651 HarbormasterBuildable::applyBuildPlans( 1652 $adapter->getDiff()->getPHID(), 1653 $adapter->getPHID(), 1654 $adapter->getBuildPlans()); 1655 1656 return $xactions; 1657 } 1658 1659 /** 1660 * Update the table which links Differential revisions to paths they affect, 1661 * so Diffusion can efficiently find pending revisions for a given file. 1662 */ 1663 private function updateAffectedPathTable( 1664 DifferentialRevision $revision, 1665 DifferentialDiff $diff) { 1666 1667 $repository = $revision->getRepository(); 1668 if (!$repository) { 1669 // The repository where the code lives is untracked. 1670 return; 1671 } 1672 1673 $path_prefix = null; 1674 1675 $local_root = $diff->getSourceControlPath(); 1676 if ($local_root) { 1677 // We're in a working copy which supports subdirectory checkouts (e.g., 1678 // SVN) so we need to figure out what prefix we should add to each path 1679 // (e.g., trunk/projects/example/) to get the absolute path from the 1680 // root of the repository. DVCS systems like Git and Mercurial are not 1681 // affected. 1682 1683 // Normalize both paths and check if the repository root is a prefix of 1684 // the local root. If so, throw it away. Note that this correctly handles 1685 // the case where the remote path is "/". 1686 $local_root = id(new PhutilURI($local_root))->getPath(); 1687 $local_root = rtrim($local_root, '/'); 1688 1689 $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); 1690 $repo_root = rtrim($repo_root, '/'); 1691 1692 if (!strncmp($repo_root, $local_root, strlen($repo_root))) { 1693 $path_prefix = substr($local_root, strlen($repo_root)); 1694 } 1695 } 1696 1697 $changesets = $diff->getChangesets(); 1698 $paths = array(); 1699 foreach ($changesets as $changeset) { 1700 $paths[] = $path_prefix.'/'.$changeset->getFilename(); 1701 } 1702 1703 // Mark this as also touching all parent paths, so you can see all pending 1704 // changes to any file within a directory. 1705 $all_paths = array(); 1706 foreach ($paths as $local) { 1707 foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { 1708 $all_paths[$path] = true; 1709 } 1710 } 1711 $all_paths = array_keys($all_paths); 1712 1713 $path_ids = 1714 PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( 1715 $all_paths); 1716 1717 $table = new DifferentialAffectedPath(); 1718 $conn_w = $table->establishConnection('w'); 1719 1720 $sql = array(); 1721 foreach ($path_ids as $path_id) { 1722 $sql[] = qsprintf( 1723 $conn_w, 1724 '(%d, %d, %d, %d)', 1725 $repository->getID(), 1726 $path_id, 1727 time(), 1728 $revision->getID()); 1729 } 1730 1731 queryfx( 1732 $conn_w, 1733 'DELETE FROM %T WHERE revisionID = %d', 1734 $table->getTableName(), 1735 $revision->getID()); 1736 foreach (array_chunk($sql, 256) as $chunk) { 1737 queryfx( 1738 $conn_w, 1739 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', 1740 $table->getTableName(), 1741 implode(', ', $chunk)); 1742 } 1743 } 1744 1745 /** 1746 * Update the table connecting revisions to DVCS local hashes, so we can 1747 * identify revisions by commit/tree hashes. 1748 */ 1749 private function updateRevisionHashTable( 1750 DifferentialRevision $revision, 1751 DifferentialDiff $diff) { 1752 1753 $vcs = $diff->getSourceControlSystem(); 1754 if ($vcs == DifferentialRevisionControlSystem::SVN) { 1755 // Subversion has no local commit or tree hash information, so we don't 1756 // have to do anything. 1757 return; 1758 } 1759 1760 $property = id(new DifferentialDiffProperty())->loadOneWhere( 1761 'diffID = %d AND name = %s', 1762 $diff->getID(), 1763 'local:commits'); 1764 if (!$property) { 1765 return; 1766 } 1767 1768 $hashes = array(); 1769 1770 $data = $property->getData(); 1771 switch ($vcs) { 1772 case DifferentialRevisionControlSystem::GIT: 1773 foreach ($data as $commit) { 1774 $hashes[] = array( 1775 ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT, 1776 $commit['commit'], 1777 ); 1778 $hashes[] = array( 1779 ArcanistDifferentialRevisionHash::HASH_GIT_TREE, 1780 $commit['tree'], 1781 ); 1782 } 1783 break; 1784 case DifferentialRevisionControlSystem::MERCURIAL: 1785 foreach ($data as $commit) { 1786 $hashes[] = array( 1787 ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT, 1788 $commit['rev'], 1789 ); 1790 } 1791 break; 1792 } 1793 1794 $conn_w = $revision->establishConnection('w'); 1795 1796 $sql = array(); 1797 foreach ($hashes as $info) { 1798 list($type, $hash) = $info; 1799 $sql[] = qsprintf( 1800 $conn_w, 1801 '(%d, %s, %s)', 1802 $revision->getID(), 1803 $type, 1804 $hash); 1805 } 1806 1807 queryfx( 1808 $conn_w, 1809 'DELETE FROM %T WHERE revisionID = %d', 1810 ArcanistDifferentialRevisionHash::TABLE_NAME, 1811 $revision->getID()); 1812 1813 if ($sql) { 1814 queryfx( 1815 $conn_w, 1816 'INSERT INTO %T (revisionID, type, hash) VALUES %Q', 1817 ArcanistDifferentialRevisionHash::TABLE_NAME, 1818 implode(', ', $sql)); 1819 } 1820 } 1821 1822 private function renderAffectedFilesForMail(DifferentialDiff $diff) { 1823 $changesets = $diff->getChangesets(); 1824 1825 $filenames = mpull($changesets, 'getDisplayFilename'); 1826 sort($filenames); 1827 1828 $count = count($filenames); 1829 $max = 250; 1830 if ($count > $max) { 1831 $filenames = array_slice($filenames, 0, $max); 1832 $filenames[] = pht('(%d more files...)', ($count - $max)); 1833 } 1834 1835 return implode("\n", $filenames); 1836 } 1837 1838 private function renderPatchHTMLForMail($patch) { 1839 return phutil_tag('pre', 1840 array('style' => 'font-family: monospace;'), $patch); 1841 } 1842 1843 private function renderPatchForMail(DifferentialDiff $diff) { 1844 $format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format'); 1845 1846 $patch = id(new DifferentialRawDiffRenderer()) 1847 ->setViewer($this->getActor()) 1848 ->setFormat($format) 1849 ->setChangesets($diff->getChangesets()) 1850 ->buildPatch(); 1851 1852 $section = new PhabricatorMetaMTAMailSection(); 1853 $section->addHTMLFragment($this->renderPatchHTMLForMail($patch)); 1854 $section->addPlaintextFragment($patch); 1855 1856 return $section; 1857 } 1858 1859 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |