private function loadReviewers(AphrontDatabaseConnection $conn_r, array $revisions)
 {
     assert_instances_of($revisions, 'DifferentialRevision');
     $edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
     $edges = id(new PhabricatorEdgeQuery())->withSourcePHIDs(mpull($revisions, 'getPHID'))->withEdgeTypes(array($edge_type))->needEdgeData(true)->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST)->execute();
     $viewer = $this->getViewer();
     $viewer_phid = $viewer->getPHID();
     $allow_key = 'differential.allow-self-accept';
     $allow_self = PhabricatorEnv::getEnvConfig($allow_key);
     // Figure out which of these reviewers the viewer has authority to act as.
     if ($this->needReviewerAuthority && $viewer_phid) {
         $authority = $this->loadReviewerAuthority($revisions, $edges, $allow_self);
     }
     foreach ($revisions as $revision) {
         $revision_edges = $edges[$revision->getPHID()][$edge_type];
         $reviewers = array();
         foreach ($revision_edges as $reviewer_phid => $edge) {
             $reviewer = new DifferentialReviewer($reviewer_phid, $edge['data']);
             if ($this->needReviewerAuthority) {
                 if (!$viewer_phid) {
                     // Logged-out users never have authority.
                     $has_authority = false;
                 } else {
                     if (!$allow_self && $revision->getAuthorPHID() == $viewer_phid) {
                         // The author can never have authority unless we allow self-accept.
                         $has_authority = false;
                     } else {
                         // Otherwise, look up whether th viewer has authority.
                         $has_authority = isset($authority[$reviewer_phid]);
                     }
                 }
                 $reviewer->attachAuthority($viewer, $has_authority);
             }
             $reviewers[$reviewer_phid] = $reviewer;
         }
         $revision->attachReviewerStatus($reviewers);
     }
 }
 protected function expandTransaction(PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction)
 {
     $results = parent::expandTransaction($object, $xaction);
     $actor = $this->getActor();
     $actor_phid = $this->getActingAsPHID();
     $type_edge = PhabricatorTransactions::TYPE_EDGE;
     $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
     $edge_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
     $edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
     $is_sticky_accept = PhabricatorEnv::getEnvConfig('differential.sticky-accept');
     $downgrade_rejects = false;
     $downgrade_accepts = false;
     if ($this->getIsCloseByCommit()) {
         // Never downgrade reviewers when we're closing a revision after a
         // commit.
     } else {
         switch ($xaction->getTransactionType()) {
             case DifferentialTransaction::TYPE_UPDATE:
                 $downgrade_rejects = true;
                 if (!$is_sticky_accept) {
                     // If "sticky accept" is disabled, also downgrade the accepts.
                     $downgrade_accepts = true;
                 }
                 break;
             case DifferentialTransaction::TYPE_ACTION:
                 switch ($xaction->getNewValue()) {
                     case DifferentialAction::ACTION_REQUEST:
                         $downgrade_rejects = true;
                         if (!$is_sticky_accept || $object->getStatus() != $status_plan) {
                             // If the old state isn't "changes planned", downgrade the
                             // accepts. This exception allows an accepted revision to
                             // go through Plan Changes -> Request Review to return to
                             // "accepted" if the author didn't update the revision.
                             $downgrade_accepts = true;
                         }
                         break;
                 }
                 break;
         }
     }
     $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED;
     $new_reject = DifferentialReviewerStatus::STATUS_REJECTED;
     $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER;
     $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER;
     if ($downgrade_rejects || $downgrade_accepts) {
         // When a revision is updated, change all "reject" to "rejected older
         // revision". This means we won't immediately push the update back into
         // "needs review", but outstanding rejects will still block it from
         // moving to "accepted".
         // We also do this for "Request Review", even though the diff is not
         // updated directly. Essentially, this acts like an update which doesn't
         // actually change the diff text.
         $edits = array();
         foreach ($object->getReviewerStatus() as $reviewer) {
             if ($downgrade_rejects) {
                 if ($reviewer->getStatus() == $new_reject) {
                     $edits[$reviewer->getReviewerPHID()] = array('data' => array('status' => $old_reject));
                 }
             }
             if ($downgrade_accepts) {
                 if ($reviewer->getStatus() == $new_accept) {
                     $edits[$reviewer->getReviewerPHID()] = array('data' => array('status' => $old_accept));
                 }
             }
         }
         if ($edits) {
             $results[] = id(new DifferentialTransaction())->setTransactionType($type_edge)->setMetadataValue('edge:type', $edge_reviewer)->setIgnoreOnNoEffect(true)->setNewValue(array('+' => $edits));
         }
     }
     switch ($xaction->getTransactionType()) {
         case DifferentialTransaction::TYPE_UPDATE:
             if ($this->getIsCloseByCommit()) {
                 // Don't bother with any of this if this update is a side effect of
                 // commit detection.
                 break;
             }
             // When a revision is updated and the diff comes from a branch named
             // "T123" or similar, automatically associate the commit with the
             // task that the branch names.
             $maniphest = 'PhabricatorManiphestApplication';
             if (PhabricatorApplication::isClassInstalled($maniphest)) {
                 $diff = $this->requireDiff($xaction->getNewValue());
                 $branch = $diff->getBranch();
                 // No "$", to allow for branches like T123_demo.
                 $match = null;
                 if (preg_match('/^T(\\d+)/i', $branch, $match)) {
                     $task_id = $match[1];
                     $tasks = id(new ManiphestTaskQuery())->setViewer($this->getActor())->withIDs(array($task_id))->execute();
                     if ($tasks) {
                         $task = head($tasks);
                         $task_phid = $task->getPHID();
                         $results[] = id(new DifferentialTransaction())->setTransactionType($type_edge)->setMetadataValue('edge:type', $edge_ref_task)->setIgnoreOnNoEffect(true)->setNewValue(array('+' => array($task_phid => $task_phid)));
                     }
                 }
             }
             break;
         case PhabricatorTransactions::TYPE_COMMENT:
             // When a user leaves a comment, upgrade their reviewer status from
             // "added" to "commented" if they're also a reviewer. We may further
             // upgrade this based on other actions in the transaction group.
             $status_added = DifferentialReviewerStatus::STATUS_ADDED;
             $status_commented = DifferentialReviewerStatus::STATUS_COMMENTED;
             $data = array('status' => $status_commented);
             $edits = array();
             foreach ($object->getReviewerStatus() as $reviewer) {
                 if ($reviewer->getReviewerPHID() == $actor_phid) {
                     if ($reviewer->getStatus() == $status_added) {
                         $edits[$actor_phid] = array('data' => $data);
                     }
                 }
             }
             if ($edits) {
                 $results[] = id(new DifferentialTransaction())->setTransactionType($type_edge)->setMetadataValue('edge:type', $edge_reviewer)->setIgnoreOnNoEffect(true)->setNewValue(array('+' => $edits));
             }
             break;
         case DifferentialTransaction::TYPE_ACTION:
             $action_type = $xaction->getNewValue();
             switch ($action_type) {
                 case DifferentialAction::ACTION_ACCEPT:
                 case DifferentialAction::ACTION_REJECT:
                     if ($action_type == DifferentialAction::ACTION_ACCEPT) {
                         $data = array('status' => DifferentialReviewerStatus::STATUS_ACCEPTED);
                     } else {
                         $data = array('status' => DifferentialReviewerStatus::STATUS_REJECTED);
                     }
                     $edits = array();
                     foreach ($object->getReviewerStatus() as $reviewer) {
                         if ($reviewer->hasAuthority($actor)) {
                             $edits[$reviewer->getReviewerPHID()] = array('data' => $data);
                         }
                     }
                     // Also either update or add the actor themselves as a reviewer.
                     $edits[$actor_phid] = array('data' => $data);
                     $results[] = id(new DifferentialTransaction())->setTransactionType($type_edge)->setMetadataValue('edge:type', $edge_reviewer)->setIgnoreOnNoEffect(true)->setNewValue(array('+' => $edits));
                     break;
                 case DifferentialAction::ACTION_CLAIM:
                     // If the user is commandeering, add the previous owner as a
                     // reviewer and remove the actor.
                     $edits = array('-' => array($actor_phid => $actor_phid));
                     $owner_phid = $object->getAuthorPHID();
                     if ($owner_phid) {
                         $reviewer = new DifferentialReviewer($owner_phid, array('status' => DifferentialReviewerStatus::STATUS_ADDED));
                         $edits['+'] = array($owner_phid => array('data' => $reviewer->getEdgeData()));
                     }
                     // NOTE: We're setting setIsCommandeerSideEffect() on this because
                     // normally you can't add a revision's author as a reviewer, but
                     // this action swaps them after validation executes.
                     $results[] = id(new DifferentialTransaction())->setTransactionType($type_edge)->setMetadataValue('edge:type', $edge_reviewer)->setIgnoreOnNoEffect(true)->setIsCommandeerSideEffect(true)->setNewValue($edits);
                     break;
                 case DifferentialAction::ACTION_RESIGN:
                     // If the user is resigning, add a separate reviewer edit
                     // transaction which removes them as a reviewer.
                     $results[] = id(new DifferentialTransaction())->setTransactionType($type_edge)->setMetadataValue('edge:type', $edge_reviewer)->setIgnoreOnNoEffect(true)->setNewValue(array('-' => array($actor_phid => $actor_phid)));
                     break;
             }
             break;
     }
     if (!$this->didExpandInlineState) {
         switch ($xaction->getTransactionType()) {
             case PhabricatorTransactions::TYPE_COMMENT:
             case DifferentialTransaction::TYPE_ACTION:
             case DifferentialTransaction::TYPE_UPDATE:
             case DifferentialTransaction::TYPE_INLINE:
                 $this->didExpandInlineState = true;
                 $actor_phid = $this->getActingAsPHID();
                 $actor_is_author = $object->getAuthorPHID() == $actor_phid;
                 if (!$actor_is_author) {
                     break;
                 }
                 $state_map = PhabricatorTransactions::getInlineStateMap();
                 $inlines = id(new DifferentialDiffInlineCommentQuery())->setViewer($this->getActor())->withRevisionPHIDs(array($object->getPHID()))->withFixedStates(array_keys($state_map))->execute();
                 if (!$inlines) {
                     break;
                 }
                 $old_value = mpull($inlines, 'getFixedState', 'getPHID');
                 $new_value = array();
                 foreach ($old_value as $key => $state) {
                     $new_value[$key] = $state_map[$state];
                 }
                 $results[] = id(new DifferentialTransaction())->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)->setIgnoreOnNoEffect(true)->setOldValue($old_value)->setNewValue($new_value);
                 break;
         }
     }
     return $results;
 }
 public function processRequest()
 {
     $request = $this->getRequest();
     $viewer = $request->getUser();
     if (!$request->isFormPost()) {
         return new Aphront400Response();
     }
     $revision = id(new DifferentialRevisionQuery())->setViewer($viewer)->withIDs(array($this->id))->needReviewerStatus(true)->needReviewerAuthority(true)->executeOne();
     if (!$revision) {
         return new Aphront404Response();
     }
     $type_action = DifferentialTransaction::TYPE_ACTION;
     $type_subscribers = PhabricatorTransactions::TYPE_SUBSCRIBERS;
     $type_edge = PhabricatorTransactions::TYPE_EDGE;
     $type_comment = PhabricatorTransactions::TYPE_COMMENT;
     $type_inline = DifferentialTransaction::TYPE_INLINE;
     $edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
     $xactions = array();
     $action = $request->getStr('action');
     switch ($action) {
         case DifferentialAction::ACTION_COMMENT:
         case DifferentialAction::ACTION_ADDREVIEWERS:
         case DifferentialAction::ACTION_ADDCCS:
             // These transaction types have no direct effect, they just
             // accompany other transaction types which can have an effect.
             break;
         default:
             $xactions[] = id(new DifferentialTransaction())->setTransactionType($type_action)->setNewValue($request->getStr('action'));
             break;
     }
     $ccs = $request->getArr('ccs');
     if ($ccs) {
         $xactions[] = id(new DifferentialTransaction())->setTransactionType($type_subscribers)->setNewValue(array('+' => $ccs));
     }
     $current_reviewers = mpull($revision->getReviewerStatus(), null, 'getReviewerPHID');
     $reviewer_edges = array();
     $add_reviewers = $request->getArr('reviewers');
     foreach ($add_reviewers as $reviewer_phid) {
         if (isset($current_reviewers[$reviewer_phid])) {
             continue;
         }
         $reviewer = new DifferentialReviewer($reviewer_phid, array('status' => DifferentialReviewerStatus::STATUS_ADDED));
         $reviewer_edges[$reviewer_phid] = array('data' => $reviewer->getEdgeData());
     }
     if ($add_reviewers) {
         $xactions[] = id(new DifferentialTransaction())->setTransactionType($type_edge)->setMetadataValue('edge:type', $edge_reviewer)->setNewValue(array('+' => $reviewer_edges));
     }
     $inlines = DifferentialTransactionQuery::loadUnsubmittedInlineComments($viewer, $revision);
     foreach ($inlines as $inline) {
         $xactions[] = id(new DifferentialTransaction())->setTransactionType($type_inline)->attachComment($inline);
     }
     // NOTE: If there are no other transactions, add an empty comment
     // transaction so that we'll raise a more user-friendly error message,
     // to the effect of "you can not post an empty comment".
     $no_xactions = !$xactions;
     $comment = $request->getStr('comment');
     if (strlen($comment) || $no_xactions) {
         $xactions[] = id(new DifferentialTransaction())->setTransactionType($type_comment)->attachComment(id(new DifferentialTransactionComment())->setRevisionPHID($revision->getPHID())->setContent($comment));
     }
     $editor = id(new DifferentialTransactionEditor())->setActor($viewer)->setContentSourceFromRequest($request)->setContinueOnMissingFields(true)->setContinueOnNoEffect($request->isContinueRequest());
     $revision_uri = '/D' . $revision->getID();
     try {
         $editor->applyTransactions($revision, $xactions);
     } catch (PhabricatorApplicationTransactionNoEffectException $ex) {
         return id(new PhabricatorApplicationTransactionNoEffectResponse())->setCancelURI($revision_uri)->setException($ex);
     } catch (PhabricatorApplicationTransactionValidationException $ex) {
         // TODO: Provide a nice Response for rendering these in a clean way.
         throw $ex;
     }
     $user = $request->getUser();
     $draft = id(new PhabricatorDraft())->loadOneWhere('authorPHID = %s AND draftKey = %s', $user->getPHID(), 'differential-comment-' . $revision->getID());
     if ($draft) {
         $draft->delete();
     }
     DifferentialDraft::deleteAllDrafts($user->getPHID(), $revision->getPHID());
     return id(new AphrontRedirectResponse())->setURI('/D' . $revision->getID());
 }