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()); }