[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/differential/editor/ -> DifferentialTransactionEditor.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1