Path: blob/master/src/applications/differential/xaction/DifferentialRevisionReviewersTransaction.php
12256 views
<?php12final class DifferentialRevisionReviewersTransaction3extends DifferentialRevisionTransactionType {45const TRANSACTIONTYPE = 'differential.revision.reviewers';6const EDITKEY = 'reviewers';78public function generateOldValue($object) {9$reviewers = $object->getReviewers();10$reviewers = mpull($reviewers, 'getReviewerStatus', 'getReviewerPHID');11return $reviewers;12}1314public function generateNewValue($object, $value) {15$actor = $this->getActor();1617$datasource = id(new DifferentialBlockingReviewerDatasource())18->setViewer($actor);1920$reviewers = $this->generateOldValue($object);21$old_reviewers = $reviewers;2223// First, remove any reviewers we're getting rid of.24$rem = idx($value, '-', array());25$rem = $datasource->evaluateTokens($rem);26foreach ($rem as $spec) {27if (!is_array($spec)) {28$phid = $spec;29} else {30$phid = $spec['phid'];31}3233unset($reviewers[$phid]);34}3536$add = idx($value, '+', array());37$add = $datasource->evaluateTokens($add);38$add_map = array();39foreach ($add as $spec) {40if (!is_array($spec)) {41$phid = $spec;42$status = DifferentialReviewerStatus::STATUS_ADDED;43} else {44$phid = $spec['phid'];45$status = $spec['type'];46}4748$add_map[$phid] = $status;49}5051$set = idx($value, '=', null);52if ($set !== null) {53$set = $datasource->evaluateTokens($set);54foreach ($set as $spec) {55if (!is_array($spec)) {56$phid = $spec;57$status = DifferentialReviewerStatus::STATUS_ADDED;58} else {59$phid = $spec['phid'];60$status = $spec['type'];61}6263$add_map[$phid] = $status;64}6566// We treat setting reviewers as though they were being added to an67// empty list, so we can share more code between pathways.68$reviewers = array();69}7071$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;72foreach ($add_map as $phid => $new_status) {73$old_status = idx($old_reviewers, $phid);7475// If we have an old status and this didn't make the reviewer blocking76// or nonblocking, just retain the old status. This makes sure we don't77// throw away rejects, accepts, etc.78if ($old_status) {79$was_blocking = ($old_status == $status_blocking);80$now_blocking = ($new_status == $status_blocking);8182$is_block = ($now_blocking && !$was_blocking);83$is_unblock = (!$now_blocking && $was_blocking);8485if (!$is_block && !$is_unblock) {86$reviewers[$phid] = $old_status;87continue;88}89}9091$reviewers[$phid] = $new_status;92}9394return $reviewers;95}9697public function getTransactionHasEffect($object, $old, $new) {98// At least for now, we ignore transactions which ONLY reorder reviewers99// without making any actual changes.100ksort($old);101ksort($new);102return ($old !== $new);103}104105public function applyExternalEffects($object, $value) {106$src_phid = $object->getPHID();107108$old = $this->generateOldValue($object);109$new = $value;110$rem = array_diff_key($old, $new);111112$table = new DifferentialReviewer();113$table_name = $table->getTableName();114$conn = $table->establishConnection('w');115116if ($rem) {117queryfx(118$conn,119'DELETE FROM %T WHERE revisionPHID = %s AND reviewerPHID IN (%Ls)',120$table_name,121$src_phid,122array_keys($rem));123}124125if ($new) {126$reviewers = $table->loadAllWhere(127'revisionPHID = %s AND reviewerPHID IN (%Ls)',128$src_phid,129array_keys($new));130$reviewers = mpull($reviewers, null, 'getReviewerPHID');131132foreach ($new as $dst_phid => $status) {133$old_status = idx($old, $dst_phid);134if ($old_status === $status) {135continue;136}137138$reviewer = idx($reviewers, $dst_phid);139if (!$reviewer) {140$reviewer = id(new DifferentialReviewer())141->setRevisionPHID($src_phid)142->setReviewerPHID($dst_phid);143}144145$reviewer->setReviewerStatus($status);146147try {148$reviewer->save();149} catch (AphrontDuplicateKeyQueryException $ex) {150// At least for now, just ignore it if we lost a race.151}152}153}154}155156public function getTitle() {157return $this->renderReviewerEditTitle(false);158}159160public function getTitleForFeed() {161return $this->renderReviewerEditTitle(true);162}163164private function renderReviewerEditTitle($is_feed) {165$old = $this->getOldValue();166$new = $this->getNewValue();167168$rem = array_diff_key($old, $new);169$add = array_diff_key($new, $old);170$rem_phids = array_keys($rem);171$add_phids = array_keys($add);172$total_count = count($rem) + count($add);173174$parts = array();175176if ($rem && $add) {177if ($is_feed) {178$parts[] = pht(179'%s edited %s reviewer(s) for %s, added %s: %s; removed %s: %s.',180$this->renderAuthor(),181new PhutilNumber($total_count),182$this->renderObject(),183phutil_count($add_phids),184$this->renderHandleList($add_phids),185phutil_count($rem_phids),186$this->renderHandleList($rem_phids));187} else {188$parts[] = pht(189'%s edited %s reviewer(s), added %s: %s; removed %s: %s.',190$this->renderAuthor(),191new PhutilNumber($total_count),192phutil_count($add_phids),193$this->renderHandleList($add_phids),194phutil_count($rem_phids),195$this->renderHandleList($rem_phids));196}197} else if ($add) {198if ($is_feed) {199$parts[] = pht(200'%s added %s reviewer(s) for %s: %s.',201$this->renderAuthor(),202phutil_count($add_phids),203$this->renderObject(),204$this->renderHandleList($add_phids));205} else {206$parts[] = pht(207'%s added %s reviewer(s): %s.',208$this->renderAuthor(),209phutil_count($add_phids),210$this->renderHandleList($add_phids));211}212} else if ($rem) {213if ($is_feed) {214$parts[] = pht(215'%s removed %s reviewer(s) for %s: %s.',216$this->renderAuthor(),217phutil_count($rem_phids),218$this->renderObject(),219$this->renderHandleList($rem_phids));220} else {221$parts[] = pht(222'%s removed %s reviewer(s): %s.',223$this->renderAuthor(),224phutil_count($rem_phids),225$this->renderHandleList($rem_phids));226}227}228229$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;230$blocks = array();231$unblocks = array();232foreach ($new as $phid => $new_status) {233$old_status = idx($old, $phid);234if (!$old_status) {235continue;236}237238$was_blocking = ($old_status == $status_blocking);239$now_blocking = ($new_status == $status_blocking);240241$is_block = ($now_blocking && !$was_blocking);242$is_unblock = (!$now_blocking && $was_blocking);243244if ($is_block) {245$blocks[] = $phid;246}247if ($is_unblock) {248$unblocks[] = $phid;249}250}251252$total_count = count($blocks) + count($unblocks);253254if ($blocks && $unblocks) {255if ($is_feed) {256$parts[] = pht(257'%s changed %s blocking reviewer(s) for %s, added %s: %s; removed '.258'%s: %s.',259$this->renderAuthor(),260new PhutilNumber($total_count),261$this->renderObject(),262phutil_count($blocks),263$this->renderHandleList($blocks),264phutil_count($unblocks),265$this->renderHandleList($unblocks));266} else {267$parts[] = pht(268'%s changed %s blocking reviewer(s), added %s: %s; removed %s: %s.',269$this->renderAuthor(),270new PhutilNumber($total_count),271phutil_count($blocks),272$this->renderHandleList($blocks),273phutil_count($unblocks),274$this->renderHandleList($unblocks));275}276} else if ($blocks) {277if ($is_feed) {278$parts[] = pht(279'%s added %s blocking reviewer(s) for %s: %s.',280$this->renderAuthor(),281phutil_count($blocks),282$this->renderObject(),283$this->renderHandleList($blocks));284} else {285$parts[] = pht(286'%s added %s blocking reviewer(s): %s.',287$this->renderAuthor(),288phutil_count($blocks),289$this->renderHandleList($blocks));290}291} else if ($unblocks) {292if ($is_feed) {293$parts[] = pht(294'%s removed %s blocking reviewer(s) for %s: %s.',295$this->renderAuthor(),296phutil_count($unblocks),297$this->renderObject(),298$this->renderHandleList($unblocks));299} else {300$parts[] = pht(301'%s removed %s blocking reviewer(s): %s.',302$this->renderAuthor(),303phutil_count($unblocks),304$this->renderHandleList($unblocks));305}306}307308if ($this->isTextMode()) {309return implode(' ', $parts);310} else {311return phutil_implode_html(' ', $parts);312}313}314315public function validateTransactions($object, array $xactions) {316$actor = $this->getActor();317$errors = array();318319if (!$xactions) {320// If we aren't applying new reviewer transactions, just bail. We need321// reviewers to be attached to the revision continue validation, and322// they won't always be (for example, when mentioning a revision).323return $errors;324}325326$author_phid = $object->getAuthorPHID();327$config_self_accept_key = 'differential.allow-self-accept';328$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);329330$old = $this->generateOldValue($object);331foreach ($xactions as $xaction) {332$new = $this->generateNewValue($object, $xaction->getNewValue());333334$add = array_diff_key($new, $old);335if (!$add) {336continue;337}338339$objects = id(new PhabricatorObjectQuery())340->setViewer($actor)341->withPHIDs(array_keys($add))342->execute();343$objects = mpull($objects, null, 'getPHID');344345foreach ($add as $phid => $status) {346if (!isset($objects[$phid])) {347$errors[] = $this->newInvalidError(348pht(349'Reviewer "%s" is not a valid object.',350$phid),351$xaction);352continue;353}354355switch (phid_get_type($phid)) {356case PhabricatorPeopleUserPHIDType::TYPECONST:357case PhabricatorOwnersPackagePHIDType::TYPECONST:358case PhabricatorProjectProjectPHIDType::TYPECONST:359break;360default:361$errors[] = $this->newInvalidError(362pht(363'Reviewer "%s" must be a user, a package, or a project.',364$phid),365$xaction);366continue 2;367}368369// NOTE: This weird behavior around commandeering is a bit unorthodox,370// but this restriction is an unusual one.371372$is_self = ($phid === $author_phid);373if ($is_self && !$allow_self_accept) {374if (!$xaction->getIsCommandeerSideEffect()) {375$errors[] = $this->newInvalidError(376pht('The author of a revision can not be a reviewer.'),377$xaction);378continue;379}380}381}382}383384return $errors;385}386387388public function getTransactionTypeForConduit($xaction) {389return 'reviewers';390}391392public function getFieldValuesForConduit($xaction, $data) {393$old_value = $xaction->getOldValue();394$new_value = $xaction->getNewValue();395396$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;397398$add_phids = array_diff_key($new_value, $old_value);399foreach ($add_phids as $add_phid => $value) {400$add_phids[$add_phid] = array(401'operation' => 'add',402'phid' => $add_phid,403'oldStatus' => null,404'newStatus' => $value,405'isBlocking' => ($value === $status_blocking),406);407}408409$rem_phids = array_diff_key($old_value, $new_value);410foreach ($rem_phids as $rem_phid => $value) {411$rem_phids[$rem_phid] = array(412'operation' => 'remove',413'phid' => $rem_phid,414'oldStatus' => $value,415'newStatus' => null,416'isBlocking' => false,417);418}419420$mod_phids = array_intersect_key($old_value, $new_value);421foreach ($mod_phids as $mod_phid => $ignored) {422$old = $old_value[$mod_phid];423$new = $new_value[$mod_phid];424425if ($old === $new) {426unset($mod_phids[$mod_phid]);427continue;428}429430$mod_phids[$mod_phid] = array(431'operation' => 'update',432'phid' => $mod_phid,433'oldStatus' => $old,434'newStatus' => $new,435'isBlocking' => ($new === $status_blocking),436);437}438439$all_ops = $add_phids + $rem_phids + $mod_phids;440$all_ops = array_select_keys($all_ops, $new_value) + $all_ops;441$all_ops = array_values($all_ops);442443return array(444'operations' => $all_ops,445);446}447}448449450