Remove product literal strings in "pht()", part 6
[phabricator.git] / src / applications / differential / query / DifferentialRevisionQuery.php
blob75d3e052e400413fcc8471e6cfbcbf02e6b4677b
1 <?php
3 /**
4 * @task config Query Configuration
5 * @task exec Query Execution
6 * @task internal Internals
7 */
8 final class DifferentialRevisionQuery
9 extends PhabricatorCursorPagedPolicyAwareQuery {
11 private $authors = array();
12 private $draftAuthors = array();
13 private $ccs = array();
14 private $reviewers = array();
15 private $revIDs = array();
16 private $commitHashes = array();
17 private $phids = array();
18 private $responsibles = array();
19 private $branches = array();
20 private $repositoryPHIDs;
21 private $updatedEpochMin;
22 private $updatedEpochMax;
23 private $statuses;
24 private $isOpen;
25 private $createdEpochMin;
26 private $createdEpochMax;
27 private $noReviewers;
28 private $paths;
30 const ORDER_MODIFIED = 'order-modified';
31 const ORDER_CREATED = 'order-created';
33 private $needActiveDiffs = false;
34 private $needDiffIDs = false;
35 private $needCommitPHIDs = false;
36 private $needHashes = false;
37 private $needReviewers = false;
38 private $needReviewerAuthority;
39 private $needDrafts;
40 private $needFlags;
43 /* -( Query Configuration )------------------------------------------------ */
45 /**
46 * Find revisions affecting one or more items in a list of paths.
48 * @param list<string> List of file paths.
49 * @return this
50 * @task config
52 public function withPaths(array $paths) {
53 $this->paths = $paths;
54 return $this;
57 /**
58 * Filter results to revisions authored by one of the given PHIDs. Calling
59 * this function will clear anything set by previous calls to
60 * @{method:withAuthors}.
62 * @param array List of PHIDs of authors
63 * @return this
64 * @task config
66 public function withAuthors(array $author_phids) {
67 $this->authors = $author_phids;
68 return $this;
71 /**
72 * Filter results to revisions which CC one of the listed people. Calling this
73 * function will clear anything set by previous calls to @{method:withCCs}.
75 * @param array List of PHIDs of subscribers.
76 * @return this
77 * @task config
79 public function withCCs(array $cc_phids) {
80 $this->ccs = $cc_phids;
81 return $this;
84 /**
85 * Filter results to revisions that have one of the provided PHIDs as
86 * reviewers. Calling this function will clear anything set by previous calls
87 * to @{method:withReviewers}.
89 * @param array List of PHIDs of reviewers
90 * @return this
91 * @task config
93 public function withReviewers(array $reviewer_phids) {
94 if ($reviewer_phids === array()) {
95 throw new Exception(
96 pht(
97 'Empty "withReviewers()" constraint is invalid. Provide one or '.
98 'more values, or remove the constraint.'));
101 $with_none = false;
103 foreach ($reviewer_phids as $key => $phid) {
104 switch ($phid) {
105 case DifferentialNoReviewersDatasource::FUNCTION_TOKEN:
106 $with_none = true;
107 unset($reviewer_phids[$key]);
108 break;
109 default:
110 break;
114 $this->noReviewers = $with_none;
115 if ($reviewer_phids) {
116 $this->reviewers = array_values($reviewer_phids);
119 return $this;
123 * Filter results to revisions that have one of the provided commit hashes.
124 * Calling this function will clear anything set by previous calls to
125 * @{method:withCommitHashes}.
127 * @param array List of pairs <Class
128 * ArcanistDifferentialRevisionHash::HASH_$type constant,
129 * hash>
130 * @return this
131 * @task config
133 public function withCommitHashes(array $commit_hashes) {
134 $this->commitHashes = $commit_hashes;
135 return $this;
138 public function withStatuses(array $statuses) {
139 $this->statuses = $statuses;
140 return $this;
143 public function withIsOpen($is_open) {
144 $this->isOpen = $is_open;
145 return $this;
150 * Filter results to revisions on given branches.
152 * @param list List of branch names.
153 * @return this
154 * @task config
156 public function withBranches(array $branches) {
157 $this->branches = $branches;
158 return $this;
163 * Filter results to only return revisions whose ids are in the given set.
165 * @param array List of revision ids
166 * @return this
167 * @task config
169 public function withIDs(array $ids) {
170 $this->revIDs = $ids;
171 return $this;
176 * Filter results to only return revisions whose PHIDs are in the given set.
178 * @param array List of revision PHIDs
179 * @return this
180 * @task config
182 public function withPHIDs(array $phids) {
183 $this->phids = $phids;
184 return $this;
189 * Given a set of users, filter results to return only revisions they are
190 * responsible for (i.e., they are either authors or reviewers).
192 * @param array List of user PHIDs.
193 * @return this
194 * @task config
196 public function withResponsibleUsers(array $responsible_phids) {
197 $this->responsibles = $responsible_phids;
198 return $this;
202 public function withRepositoryPHIDs(array $repository_phids) {
203 $this->repositoryPHIDs = $repository_phids;
204 return $this;
207 public function withUpdatedEpochBetween($min, $max) {
208 $this->updatedEpochMin = $min;
209 $this->updatedEpochMax = $max;
210 return $this;
213 public function withCreatedEpochBetween($min, $max) {
214 $this->createdEpochMin = $min;
215 $this->createdEpochMax = $max;
216 return $this;
221 * Set whether or not the query should load the active diff for each
222 * revision.
224 * @param bool True to load and attach diffs.
225 * @return this
226 * @task config
228 public function needActiveDiffs($need_active_diffs) {
229 $this->needActiveDiffs = $need_active_diffs;
230 return $this;
235 * Set whether or not the query should load the associated commit PHIDs for
236 * each revision.
238 * @param bool True to load and attach diffs.
239 * @return this
240 * @task config
242 public function needCommitPHIDs($need_commit_phids) {
243 $this->needCommitPHIDs = $need_commit_phids;
244 return $this;
249 * Set whether or not the query should load associated diff IDs for each
250 * revision.
252 * @param bool True to load and attach diff IDs.
253 * @return this
254 * @task config
256 public function needDiffIDs($need_diff_ids) {
257 $this->needDiffIDs = $need_diff_ids;
258 return $this;
263 * Set whether or not the query should load associated commit hashes for each
264 * revision.
266 * @param bool True to load and attach commit hashes.
267 * @return this
268 * @task config
270 public function needHashes($need_hashes) {
271 $this->needHashes = $need_hashes;
272 return $this;
277 * Set whether or not the query should load associated reviewers.
279 * @param bool True to load and attach reviewers.
280 * @return this
281 * @task config
283 public function needReviewers($need_reviewers) {
284 $this->needReviewers = $need_reviewers;
285 return $this;
290 * Request information about the viewer's authority to act on behalf of each
291 * reviewer. In particular, they have authority to act on behalf of projects
292 * they are a member of.
294 * @param bool True to load and attach authority.
295 * @return this
296 * @task config
298 public function needReviewerAuthority($need_reviewer_authority) {
299 $this->needReviewerAuthority = $need_reviewer_authority;
300 return $this;
303 public function needFlags($need_flags) {
304 $this->needFlags = $need_flags;
305 return $this;
308 public function needDrafts($need_drafts) {
309 $this->needDrafts = $need_drafts;
310 return $this;
314 /* -( Query Execution )---------------------------------------------------- */
317 public function newResultObject() {
318 return new DifferentialRevision();
323 * Execute the query as configured, returning matching
324 * @{class:DifferentialRevision} objects.
326 * @return list List of matching DifferentialRevision objects.
327 * @task exec
329 protected function loadPage() {
330 $data = $this->loadData();
331 $data = $this->didLoadRawRows($data);
332 $table = $this->newResultObject();
333 return $table->loadAllFromArray($data);
336 protected function willFilterPage(array $revisions) {
337 $viewer = $this->getViewer();
339 $repository_phids = mpull($revisions, 'getRepositoryPHID');
340 $repository_phids = array_filter($repository_phids);
342 $repositories = array();
343 if ($repository_phids) {
344 $repositories = id(new PhabricatorRepositoryQuery())
345 ->setViewer($this->getViewer())
346 ->withPHIDs($repository_phids)
347 ->execute();
348 $repositories = mpull($repositories, null, 'getPHID');
351 // If a revision is associated with a repository:
353 // - the viewer must be able to see the repository; or
354 // - the viewer must have an automatic view capability.
356 // In the latter case, we'll load the revision but not load the repository.
358 $can_view = PhabricatorPolicyCapability::CAN_VIEW;
359 foreach ($revisions as $key => $revision) {
360 $repo_phid = $revision->getRepositoryPHID();
361 if (!$repo_phid) {
362 // The revision has no associated repository. Attach `null` and move on.
363 $revision->attachRepository(null);
364 continue;
367 $repository = idx($repositories, $repo_phid);
368 if ($repository) {
369 // The revision has an associated repository, and the viewer can see
370 // it. Attach it and move on.
371 $revision->attachRepository($repository);
372 continue;
375 if ($revision->hasAutomaticCapability($can_view, $viewer)) {
376 // The revision has an associated repository which the viewer can not
377 // see, but the viewer has an automatic capability on this revision.
378 // Load the revision without attaching a repository.
379 $revision->attachRepository(null);
380 continue;
383 if ($this->getViewer()->isOmnipotent()) {
384 // The viewer is omnipotent. Allow the revision to load even without
385 // a repository.
386 $revision->attachRepository(null);
387 continue;
390 // The revision has an associated repository, and the viewer can't see
391 // it, and the viewer has no special capabilities. Filter out this
392 // revision.
393 $this->didRejectResult($revision);
394 unset($revisions[$key]);
397 if (!$revisions) {
398 return array();
401 $table = new DifferentialRevision();
402 $conn_r = $table->establishConnection('r');
404 if ($this->needCommitPHIDs) {
405 $this->loadCommitPHIDs($revisions);
408 $need_active = $this->needActiveDiffs;
409 $need_ids = $need_active || $this->needDiffIDs;
411 if ($need_ids) {
412 $this->loadDiffIDs($conn_r, $revisions);
415 if ($need_active) {
416 $this->loadActiveDiffs($conn_r, $revisions);
419 if ($this->needHashes) {
420 $this->loadHashes($conn_r, $revisions);
423 if ($this->needReviewers || $this->needReviewerAuthority) {
424 $this->loadReviewers($conn_r, $revisions);
427 return $revisions;
430 protected function didFilterPage(array $revisions) {
431 $viewer = $this->getViewer();
433 if ($this->needFlags) {
434 $flags = id(new PhabricatorFlagQuery())
435 ->setViewer($viewer)
436 ->withOwnerPHIDs(array($viewer->getPHID()))
437 ->withObjectPHIDs(mpull($revisions, 'getPHID'))
438 ->execute();
439 $flags = mpull($flags, null, 'getObjectPHID');
440 foreach ($revisions as $revision) {
441 $revision->attachFlag(
442 $viewer,
443 idx($flags, $revision->getPHID()));
447 if ($this->needDrafts) {
448 PhabricatorDraftEngine::attachDrafts(
449 $viewer,
450 $revisions);
453 return $revisions;
456 private function loadData() {
457 $table = $this->newResultObject();
458 $conn = $table->establishConnection('r');
460 $selects = array();
462 // NOTE: If the query includes "responsiblePHIDs", we execute it as a
463 // UNION of revisions they own and revisions they're reviewing. This has
464 // much better performance than doing it with JOIN/WHERE.
465 if ($this->responsibles) {
466 $basic_authors = $this->authors;
467 $basic_reviewers = $this->reviewers;
469 try {
470 // Build the query where the responsible users are authors.
471 $this->authors = array_merge($basic_authors, $this->responsibles);
473 $this->reviewers = $basic_reviewers;
474 $selects[] = $this->buildSelectStatement($conn);
476 // Build the query where the responsible users are reviewers, or
477 // projects they are members of are reviewers.
478 $this->authors = $basic_authors;
479 $this->reviewers = array_merge($basic_reviewers, $this->responsibles);
480 $selects[] = $this->buildSelectStatement($conn);
482 // Put everything back like it was.
483 $this->authors = $basic_authors;
484 $this->reviewers = $basic_reviewers;
485 } catch (Exception $ex) {
486 $this->authors = $basic_authors;
487 $this->reviewers = $basic_reviewers;
488 throw $ex;
490 } else {
491 $selects[] = $this->buildSelectStatement($conn);
494 if (count($selects) > 1) {
495 $unions = null;
496 foreach ($selects as $select) {
497 if (!$unions) {
498 $unions = $select;
499 continue;
502 $unions = qsprintf(
503 $conn,
504 '%Q UNION DISTINCT %Q',
505 $unions,
506 $select);
509 $query = qsprintf(
510 $conn,
511 '%Q %Q %Q',
512 $unions,
513 $this->buildOrderClause($conn, true),
514 $this->buildLimitClause($conn));
515 } else {
516 $query = head($selects);
519 return queryfx_all($conn, '%Q', $query);
522 private function buildSelectStatement(AphrontDatabaseConnection $conn_r) {
523 $table = new DifferentialRevision();
525 $select = $this->buildSelectClause($conn_r);
527 $from = qsprintf(
528 $conn_r,
529 'FROM %T r',
530 $table->getTableName());
532 $joins = $this->buildJoinsClause($conn_r);
533 $where = $this->buildWhereClause($conn_r);
534 $group_by = $this->buildGroupClause($conn_r);
535 $having = $this->buildHavingClause($conn_r);
537 $order_by = $this->buildOrderClause($conn_r);
539 $limit = $this->buildLimitClause($conn_r);
541 return qsprintf(
542 $conn_r,
543 '(%Q %Q %Q %Q %Q %Q %Q %Q)',
544 $select,
545 $from,
546 $joins,
547 $where,
548 $group_by,
549 $having,
550 $order_by,
551 $limit);
555 /* -( Internals )---------------------------------------------------------- */
559 * @task internal
561 private function buildJoinsClause(AphrontDatabaseConnection $conn) {
562 $joins = array();
564 if ($this->paths) {
565 $path_table = new DifferentialAffectedPath();
566 $joins[] = qsprintf(
567 $conn,
568 'JOIN %R paths ON paths.revisionID = r.id',
569 $path_table);
572 if ($this->commitHashes) {
573 $joins[] = qsprintf(
574 $conn,
575 'JOIN %T hash_rel ON hash_rel.revisionID = r.id',
576 ArcanistDifferentialRevisionHash::TABLE_NAME);
579 if ($this->ccs) {
580 $joins[] = qsprintf(
581 $conn,
582 'JOIN %T e_ccs ON e_ccs.src = r.phid '.
583 'AND e_ccs.type = %s '.
584 'AND e_ccs.dst in (%Ls)',
585 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
586 PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
587 $this->ccs);
590 if ($this->reviewers) {
591 $joins[] = qsprintf(
592 $conn,
593 'LEFT JOIN %T reviewer ON reviewer.revisionPHID = r.phid
594 AND reviewer.reviewerStatus != %s
595 AND reviewer.reviewerPHID in (%Ls)',
596 id(new DifferentialReviewer())->getTableName(),
597 DifferentialReviewerStatus::STATUS_RESIGNED,
598 $this->reviewers);
601 if ($this->noReviewers) {
602 $joins[] = qsprintf(
603 $conn,
604 'LEFT JOIN %T no_reviewer ON no_reviewer.revisionPHID = r.phid
605 AND no_reviewer.reviewerStatus != %s',
606 id(new DifferentialReviewer())->getTableName(),
607 DifferentialReviewerStatus::STATUS_RESIGNED);
610 if ($this->draftAuthors) {
611 $joins[] = qsprintf(
612 $conn,
613 'JOIN %T has_draft ON has_draft.srcPHID = r.phid
614 AND has_draft.type = %s
615 AND has_draft.dstPHID IN (%Ls)',
616 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
617 PhabricatorObjectHasDraftEdgeType::EDGECONST,
618 $this->draftAuthors);
621 $joins[] = $this->buildJoinClauseParts($conn);
623 return $this->formatJoinClause($conn, $joins);
628 * @task internal
630 protected function buildWhereClause(AphrontDatabaseConnection $conn) {
631 $viewer = $this->getViewer();
632 $where = array();
634 if ($this->paths !== null) {
635 $paths = $this->paths;
637 $path_map = id(new DiffusionPathIDQuery($paths))
638 ->loadPathIDs();
640 if (!$path_map) {
641 // If none of the paths have entries in the PathID table, we can not
642 // possibly find any revisions affecting them.
643 throw new PhabricatorEmptyQueryException();
646 $where[] = qsprintf(
647 $conn,
648 'paths.pathID IN (%Ld)',
649 array_fuse($path_map));
651 // If we have repository PHIDs, additionally constrain this query to
652 // try to help MySQL execute it efficiently.
653 if ($this->repositoryPHIDs !== null) {
654 $repositories = id(new PhabricatorRepositoryQuery())
655 ->setViewer($viewer)
656 ->setParentQuery($this)
657 ->withPHIDs($this->repositoryPHIDs)
658 ->execute();
660 if (!$repositories) {
661 throw new PhabricatorEmptyQueryException();
664 $repository_ids = mpull($repositories, 'getID');
666 $where[] = qsprintf(
667 $conn,
668 'paths.repositoryID IN (%Ld)',
669 $repository_ids);
673 if ($this->authors) {
674 $where[] = qsprintf(
675 $conn,
676 'r.authorPHID IN (%Ls)',
677 $this->authors);
680 if ($this->revIDs) {
681 $where[] = qsprintf(
682 $conn,
683 'r.id IN (%Ld)',
684 $this->revIDs);
687 if ($this->repositoryPHIDs) {
688 $where[] = qsprintf(
689 $conn,
690 'r.repositoryPHID IN (%Ls)',
691 $this->repositoryPHIDs);
694 if ($this->commitHashes) {
695 $hash_clauses = array();
696 foreach ($this->commitHashes as $info) {
697 list($type, $hash) = $info;
698 $hash_clauses[] = qsprintf(
699 $conn,
700 '(hash_rel.type = %s AND hash_rel.hash = %s)',
701 $type,
702 $hash);
704 $hash_clauses = qsprintf($conn, '%LO', $hash_clauses);
705 $where[] = $hash_clauses;
708 if ($this->phids) {
709 $where[] = qsprintf(
710 $conn,
711 'r.phid IN (%Ls)',
712 $this->phids);
715 if ($this->branches) {
716 $where[] = qsprintf(
717 $conn,
718 'r.branchName in (%Ls)',
719 $this->branches);
722 if ($this->updatedEpochMin !== null) {
723 $where[] = qsprintf(
724 $conn,
725 'r.dateModified >= %d',
726 $this->updatedEpochMin);
729 if ($this->updatedEpochMax !== null) {
730 $where[] = qsprintf(
731 $conn,
732 'r.dateModified <= %d',
733 $this->updatedEpochMax);
736 if ($this->createdEpochMin !== null) {
737 $where[] = qsprintf(
738 $conn,
739 'r.dateCreated >= %d',
740 $this->createdEpochMin);
743 if ($this->createdEpochMax !== null) {
744 $where[] = qsprintf(
745 $conn,
746 'r.dateCreated <= %d',
747 $this->createdEpochMax);
750 if ($this->statuses !== null) {
751 $where[] = qsprintf(
752 $conn,
753 'r.status in (%Ls)',
754 $this->statuses);
757 if ($this->isOpen !== null) {
758 if ($this->isOpen) {
759 $statuses = DifferentialLegacyQuery::getModernValues(
760 DifferentialLegacyQuery::STATUS_OPEN);
761 } else {
762 $statuses = DifferentialLegacyQuery::getModernValues(
763 DifferentialLegacyQuery::STATUS_CLOSED);
765 $where[] = qsprintf(
766 $conn,
767 'r.status in (%Ls)',
768 $statuses);
771 $reviewer_subclauses = array();
773 if ($this->noReviewers) {
774 $reviewer_subclauses[] = qsprintf(
775 $conn,
776 'no_reviewer.reviewerPHID IS NULL');
779 if ($this->reviewers) {
780 $reviewer_subclauses[] = qsprintf(
781 $conn,
782 'reviewer.reviewerPHID IS NOT NULL');
785 if ($reviewer_subclauses) {
786 $where[] = qsprintf($conn, '%LO', $reviewer_subclauses);
789 $where[] = $this->buildWhereClauseParts($conn);
791 return $this->formatWhereClause($conn, $where);
796 * @task internal
798 protected function shouldGroupQueryResultRows() {
800 if ($this->paths) {
801 // (If we have exactly one repository and exactly one path, we don't
802 // technically need to group, but it's simpler to always group.)
803 return true;
806 if (count($this->ccs) > 1) {
807 return true;
810 if (count($this->reviewers) > 1) {
811 return true;
814 if (count($this->commitHashes) > 1) {
815 return true;
818 if ($this->noReviewers) {
819 return true;
822 return parent::shouldGroupQueryResultRows();
825 public function getBuiltinOrders() {
826 $orders = parent::getBuiltinOrders() + array(
827 'updated' => array(
828 'vector' => array('updated', 'id'),
829 'name' => pht('Date Updated (Latest First)'),
830 'aliases' => array(self::ORDER_MODIFIED),
832 'outdated' => array(
833 'vector' => array('-updated', '-id'),
834 'name' => pht('Date Updated (Oldest First)'),
838 // Alias the "newest" builtin to the historical key for it.
839 $orders['newest']['aliases'][] = self::ORDER_CREATED;
841 return $orders;
844 protected function getDefaultOrderVector() {
845 return array('updated', 'id');
848 public function getOrderableColumns() {
849 return array(
850 'updated' => array(
851 'table' => $this->getPrimaryTableAlias(),
852 'column' => 'dateModified',
853 'type' => 'int',
855 ) + parent::getOrderableColumns();
858 protected function newPagingMapFromPartialObject($object) {
859 return array(
860 'id' => (int)$object->getID(),
861 'updated' => (int)$object->getDateModified(),
865 private function loadCommitPHIDs(array $revisions) {
866 assert_instances_of($revisions, 'DifferentialRevision');
868 if (!$revisions) {
869 return;
872 $revisions = mpull($revisions, null, 'getPHID');
874 $edge_query = id(new PhabricatorEdgeQuery())
875 ->withSourcePHIDs(array_keys($revisions))
876 ->withEdgeTypes(
877 array(
878 DifferentialRevisionHasCommitEdgeType::EDGECONST,
880 $edge_query->execute();
882 foreach ($revisions as $phid => $revision) {
883 $commit_phids = $edge_query->getDestinationPHIDs(array($phid));
884 $revision->attachCommitPHIDs($commit_phids);
888 private function loadDiffIDs($conn_r, array $revisions) {
889 assert_instances_of($revisions, 'DifferentialRevision');
891 $diff_table = new DifferentialDiff();
893 $diff_ids = queryfx_all(
894 $conn_r,
895 'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld)
896 ORDER BY id DESC',
897 $diff_table->getTableName(),
898 mpull($revisions, 'getID'));
899 $diff_ids = igroup($diff_ids, 'revisionID');
901 foreach ($revisions as $revision) {
902 $ids = idx($diff_ids, $revision->getID(), array());
903 $ids = ipull($ids, 'id');
904 $revision->attachDiffIDs($ids);
908 private function loadActiveDiffs($conn_r, array $revisions) {
909 assert_instances_of($revisions, 'DifferentialRevision');
911 $diff_table = new DifferentialDiff();
913 $load_ids = array();
914 foreach ($revisions as $revision) {
915 $diffs = $revision->getDiffIDs();
916 if ($diffs) {
917 $load_ids[] = max($diffs);
921 $active_diffs = array();
922 if ($load_ids) {
923 $active_diffs = $diff_table->loadAllWhere(
924 'id IN (%Ld)',
925 $load_ids);
928 $active_diffs = mpull($active_diffs, null, 'getRevisionID');
929 foreach ($revisions as $revision) {
930 $revision->attachActiveDiff(idx($active_diffs, $revision->getID()));
934 private function loadHashes(
935 AphrontDatabaseConnection $conn_r,
936 array $revisions) {
937 assert_instances_of($revisions, 'DifferentialRevision');
939 $data = queryfx_all(
940 $conn_r,
941 'SELECT * FROM %T WHERE revisionID IN (%Ld)',
942 'differential_revisionhash',
943 mpull($revisions, 'getID'));
945 $data = igroup($data, 'revisionID');
946 foreach ($revisions as $revision) {
947 $hashes = idx($data, $revision->getID(), array());
948 $list = array();
949 foreach ($hashes as $hash) {
950 $list[] = array($hash['type'], $hash['hash']);
952 $revision->attachHashes($list);
956 private function loadReviewers(
957 AphrontDatabaseConnection $conn,
958 array $revisions) {
960 assert_instances_of($revisions, 'DifferentialRevision');
962 $reviewer_table = new DifferentialReviewer();
963 $reviewer_rows = queryfx_all(
964 $conn,
965 'SELECT * FROM %T WHERE revisionPHID IN (%Ls)
966 ORDER BY id ASC',
967 $reviewer_table->getTableName(),
968 mpull($revisions, 'getPHID'));
969 $reviewer_list = $reviewer_table->loadAllFromArray($reviewer_rows);
970 $reviewer_map = mgroup($reviewer_list, 'getRevisionPHID');
972 foreach ($reviewer_map as $key => $reviewers) {
973 $reviewer_map[$key] = mpull($reviewers, null, 'getReviewerPHID');
976 $viewer = $this->getViewer();
977 $viewer_phid = $viewer->getPHID();
979 $allow_key = 'differential.allow-self-accept';
980 $allow_self = PhabricatorEnv::getEnvConfig($allow_key);
982 // Figure out which of these reviewers the viewer has authority to act as.
983 if ($this->needReviewerAuthority && $viewer_phid) {
984 $authority = $this->loadReviewerAuthority(
985 $revisions,
986 $reviewer_map,
987 $allow_self);
990 foreach ($revisions as $revision) {
991 $reviewers = idx($reviewer_map, $revision->getPHID(), array());
992 foreach ($reviewers as $reviewer_phid => $reviewer) {
993 if ($this->needReviewerAuthority) {
994 if (!$viewer_phid) {
995 // Logged-out users never have authority.
996 $has_authority = false;
997 } else if ((!$allow_self) &&
998 ($revision->getAuthorPHID() == $viewer_phid)) {
999 // The author can never have authority unless we allow self-accept.
1000 $has_authority = false;
1001 } else {
1002 // Otherwise, look up whether the viewer has authority.
1003 $has_authority = isset($authority[$reviewer_phid]);
1006 $reviewer->attachAuthority($viewer, $has_authority);
1009 $reviewers[$reviewer_phid] = $reviewer;
1012 $revision->attachReviewers($reviewers);
1016 private function loadReviewerAuthority(
1017 array $revisions,
1018 array $reviewers,
1019 $allow_self) {
1021 $revision_map = mpull($revisions, null, 'getPHID');
1022 $viewer_phid = $this->getViewer()->getPHID();
1024 // Find all the project/package reviewers which the user may have authority
1025 // over.
1026 $project_phids = array();
1027 $package_phids = array();
1028 $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
1029 $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
1031 foreach ($reviewers as $revision_phid => $reviewer_list) {
1032 if (!$allow_self) {
1033 if ($revision_map[$revision_phid]->getAuthorPHID() == $viewer_phid) {
1034 // If self-review isn't permitted, the user will never have
1035 // authority over projects on revisions they authored because you
1036 // can't accept your own revisions, so we don't need to load any
1037 // data about these reviewers.
1038 continue;
1042 foreach ($reviewer_list as $reviewer_phid => $reviewer) {
1043 $phid_type = phid_get_type($reviewer_phid);
1044 if ($phid_type == $project_type) {
1045 $project_phids[] = $reviewer_phid;
1047 if ($phid_type == $package_type) {
1048 $package_phids[] = $reviewer_phid;
1053 // The viewer has authority over themselves.
1054 $user_authority = array_fuse(array($viewer_phid));
1056 // And over any projects they are a member of.
1057 $project_authority = array();
1058 if ($project_phids) {
1059 $project_authority = id(new PhabricatorProjectQuery())
1060 ->setViewer($this->getViewer())
1061 ->withPHIDs($project_phids)
1062 ->withMemberPHIDs(array($viewer_phid))
1063 ->execute();
1064 $project_authority = mpull($project_authority, 'getPHID');
1065 $project_authority = array_fuse($project_authority);
1068 // And over any packages they own.
1069 $package_authority = array();
1070 if ($package_phids) {
1071 $package_authority = id(new PhabricatorOwnersPackageQuery())
1072 ->setViewer($this->getViewer())
1073 ->withPHIDs($package_phids)
1074 ->withAuthorityPHIDs(array($viewer_phid))
1075 ->execute();
1076 $package_authority = mpull($package_authority, 'getPHID');
1077 $package_authority = array_fuse($package_authority);
1080 return $user_authority + $project_authority + $package_authority;
1083 public function getQueryApplicationClass() {
1084 return 'PhabricatorDifferentialApplication';
1087 protected function getPrimaryTableAlias() {
1088 return 'r';