3 abstract class DifferentialRevisionReviewTransaction
4 extends DifferentialRevisionActionTransaction
{
6 protected function getRevisionActionGroupKey() {
7 return DifferentialRevisionEditEngine
::ACTIONGROUP_REVIEW
;
10 public function generateNewValue($object, $value) {
11 if (!is_array($value)) {
15 // If the list of options is the same as the default list, just treat this
16 // as a "take the default action" transaction.
17 $viewer = $this->getActor();
18 list($options, $default) = $this->getActionOptions($viewer, $object);
20 // Remove reviewers which aren't actionable. In the case of "Accept", we
21 // may allow the transaction to proceed with some reviewers who have
22 // already accepted, to avoid race conditions where two reviewers fill
23 // out the form at the same time and accept on behalf of the same package.
24 // It's okay for these reviewers to survive validation, but they should
25 // not survive beyond this point.
26 $value = array_fuse($value);
27 $value = array_intersect($value, array_keys($options));
28 $value = array_values($value);
33 if ($default === $value) {
40 protected function isViewerAnyReviewer(
41 DifferentialRevision
$revision,
42 PhabricatorUser
$viewer) {
43 return ($this->getViewerReviewerStatus($revision, $viewer) !== null);
46 protected function isViewerAnyAuthority(
47 DifferentialRevision
$revision,
48 PhabricatorUser
$viewer) {
50 $reviewers = $revision->getReviewers();
51 foreach ($revision->getReviewers() as $reviewer) {
52 if ($reviewer->hasAuthority($viewer)) {
60 protected function isViewerFullyAccepted(
61 DifferentialRevision
$revision,
62 PhabricatorUser
$viewer) {
63 return $this->isViewerReviewerStatusFully(
66 DifferentialReviewerStatus
::STATUS_ACCEPTED
);
69 protected function isViewerFullyRejected(
70 DifferentialRevision
$revision,
71 PhabricatorUser
$viewer) {
72 return $this->isViewerReviewerStatusFully(
75 DifferentialReviewerStatus
::STATUS_REJECTED
);
78 protected function getViewerReviewerStatus(
79 DifferentialRevision
$revision,
80 PhabricatorUser
$viewer) {
82 if (!$viewer->getPHID()) {
86 foreach ($revision->getReviewers() as $reviewer) {
87 if ($reviewer->getReviewerPHID() != $viewer->getPHID()) {
91 return $reviewer->getReviewerStatus();
97 private function isViewerReviewerStatusFully(
98 DifferentialRevision
$revision,
99 PhabricatorUser
$viewer,
102 // If the user themselves is not a reviewer, the reviews they have
103 // authority over can not all be in any set of states since their own
104 // personal review has no state.
105 $status = $this->getViewerReviewerStatus($revision, $viewer);
106 if ($status === null) {
110 $active_phid = $this->getActiveDiffPHID($revision);
112 $status_accepted = DifferentialReviewerStatus
::STATUS_ACCEPTED
;
113 $status_rejected = DifferentialReviewerStatus
::STATUS_REJECTED
;
115 $is_accepted = ($require_status == $status_accepted);
116 $is_rejected = ($require_status == $status_rejected);
118 // Otherwise, check that all reviews they have authority over are in
119 // the desired set of states.
120 foreach ($revision->getReviewers() as $reviewer) {
121 if (!$reviewer->hasAuthority($viewer)) {
125 if ($revision->canReviewerForceAccept($viewer, $reviewer)) {
135 $status = $reviewer->getReviewerStatus();
136 if ($status != $require_status) {
140 // Here, we're primarily testing if we can remove a void on the review.
142 if (!$reviewer->isAccepted($active_phid)) {
148 if (!$reviewer->isRejected($active_phid)) {
153 // This is a broader check to see if we can update the diff where the
154 // last action occurred.
155 if ($reviewer->getLastActionDiffPHID() != $active_phid) {
163 protected function applyReviewerEffect(
164 DifferentialRevision
$revision,
165 PhabricatorUser
$viewer,
168 array $reviewer_options = array()) {
170 PhutilTypeSpec
::checkMap(
173 'sticky' => 'optional bool',
178 // When you accept or reject, you may accept or reject on behalf of all
179 // reviewers you have authority for. When you resign, you only affect
181 $with_authority = ($status != DifferentialReviewerStatus
::STATUS_RESIGNED
);
182 $with_force = ($status == DifferentialReviewerStatus
::STATUS_ACCEPTED
);
184 if ($with_authority) {
185 foreach ($revision->getReviewers() as $reviewer) {
186 if (!$reviewer->hasAuthority($viewer)) {
191 if (!$revision->canReviewerForceAccept($viewer, $reviewer)) {
196 $map[$reviewer->getReviewerPHID()] = $status;
200 // In all cases, you affect yourself.
201 $map[$viewer->getPHID()] = $status;
203 // If we're applying an "accept the defaults" transaction, and this
204 // transaction type uses checkboxes, replace the value with the list of
206 if (!is_array($value)) {
207 list($options, $default) = $this->getActionOptions($viewer, $revision);
213 // If we have a specific list of reviewers to act on, usually because the
214 // user has submitted a specific list of reviewers to act as by
215 // unchecking some checkboxes under "Accept", only affect those reviewers.
216 if (is_array($value)) {
217 $map = array_select_keys($map, $value);
220 // Now, do the new write.
223 $diff = $this->getEditor()->getActiveDiff($revision);
225 $diff_phid = $diff->getPHID();
230 $table = new DifferentialReviewer();
231 $src_phid = $revision->getPHID();
233 $reviewers = $table->loadAllWhere(
234 'revisionPHID = %s AND reviewerPHID IN (%Ls)',
237 $reviewers = mpull($reviewers, null, 'getReviewerPHID');
239 foreach (array_keys($map) as $dst_phid) {
240 $reviewer = idx($reviewers, $dst_phid);
242 $reviewer = id(new DifferentialReviewer())
243 ->setRevisionPHID($src_phid)
244 ->setReviewerPHID($dst_phid);
247 $old_status = $reviewer->getReviewerStatus();
248 $reviewer->setReviewerStatus($status);
251 $reviewer->setLastActionDiffPHID($diff_phid);
254 if ($old_status !== $status) {
255 $reviewer->setLastActorPHID($this->getActingAsPHID());
258 // Clear any outstanding void on this reviewer. A void may be placed
259 // by the author using "Request Review" when a reviewer has already
261 $reviewer->setVoidedPHID(null);
263 $reviewer->setOption('sticky', idx($reviewer_options, 'sticky'));
267 } catch (AphrontDuplicateKeyQueryException
$ex) {
268 // At least for now, just ignore it if we lost a race.