@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @task config Query Configuration
5 * @task exec Query Execution
6 * @task internal Internals
7 *
8 * @extends PhabricatorCursorPagedPolicyAwareQuery<DifferentialRevision>
9 */
10final class DifferentialRevisionQuery
11 extends PhabricatorCursorPagedPolicyAwareQuery {
12
13 private $authors = array();
14 private $draftAuthors = array();
15 private $ccs = array();
16 private $reviewers = array();
17 private $revIDs = array();
18 private $commitHashes = array();
19 private $phids = array();
20 private $responsibles = array();
21 private $branches = array();
22 private $repositoryPHIDs;
23 private $updatedEpochMin;
24 private $updatedEpochMax;
25 private $statuses;
26 private $isOpen;
27 private $createdEpochMin;
28 private $createdEpochMax;
29 private $noReviewers;
30 private $paths;
31
32 const ORDER_MODIFIED = 'order-modified';
33 const ORDER_CREATED = 'order-created';
34
35 private $needActiveDiffs = false;
36 private $needDiffIDs = false;
37 private $needCommitPHIDs = false;
38 private $needHashes = false;
39 private $needReviewers = false;
40 private $needReviewerAuthority;
41 private $needDrafts;
42 private $needFlags;
43
44
45/* -( Query Configuration )------------------------------------------------ */
46
47 /**
48 * Find revisions affecting one or more items in a list of paths.
49 *
50 * @param list<string> $paths List of file paths.
51 * @return $this
52 * @task config
53 */
54 public function withPaths(array $paths) {
55 $this->paths = $paths;
56 return $this;
57 }
58
59 /**
60 * Filter results to revisions authored by one of the given PHIDs. Calling
61 * this function will clear anything set by previous calls to
62 * @{method:withAuthors}.
63 *
64 * @param array $author_phids List of PHIDs of authors
65 * @return $this
66 * @task config
67 */
68 public function withAuthors(array $author_phids) {
69 $this->authors = $author_phids;
70 return $this;
71 }
72
73 /**
74 * Filter results to revisions which CC one of the listed people. Calling this
75 * function will clear anything set by previous calls to @{method:withCCs}.
76 *
77 * @param array $cc_phids List of PHIDs of subscribers.
78 * @return $this
79 * @task config
80 */
81 public function withCCs(array $cc_phids) {
82 $this->ccs = $cc_phids;
83 return $this;
84 }
85
86 /**
87 * Filter results to revisions that have one of the provided PHIDs as
88 * reviewers. Calling this function will clear anything set by previous calls
89 * to @{method:withReviewers}.
90 *
91 * @param array $reviewer_phids List of PHIDs of reviewers
92 * @return $this
93 * @task config
94 */
95 public function withReviewers(array $reviewer_phids) {
96 if ($reviewer_phids === array()) {
97 throw new Exception(
98 pht(
99 'Empty "withReviewers()" constraint is invalid. Provide one or '.
100 'more values, or remove the constraint.'));
101 }
102
103 $with_none = false;
104
105 foreach ($reviewer_phids as $key => $phid) {
106 switch ($phid) {
107 case DifferentialNoReviewersDatasource::FUNCTION_TOKEN:
108 $with_none = true;
109 unset($reviewer_phids[$key]);
110 break;
111 default:
112 break;
113 }
114 }
115
116 $this->noReviewers = $with_none;
117 if ($reviewer_phids) {
118 $this->reviewers = array_values($reviewer_phids);
119 }
120
121 return $this;
122 }
123
124 /**
125 * Filter results to revisions that have one of the provided commit hashes.
126 * Calling this function will clear anything set by previous calls to
127 * @{method:withCommitHashes}.
128 *
129 * @param array $commit_hashes List of pairs <Class
130 * ArcanistDifferentialRevisionHash::HASH_$type constant,
131 * hash>
132 * @return $this
133 * @task config
134 */
135 public function withCommitHashes(array $commit_hashes) {
136 $this->commitHashes = $commit_hashes;
137 return $this;
138 }
139
140 public function withStatuses(array $statuses) {
141 $this->statuses = $statuses;
142 return $this;
143 }
144
145 public function withIsOpen($is_open) {
146 $this->isOpen = $is_open;
147 return $this;
148 }
149
150
151 /**
152 * Filter results to revisions on given branches.
153 *
154 * @param list $branches List of branch names.
155 * @return $this
156 * @task config
157 */
158 public function withBranches(array $branches) {
159 $this->branches = $branches;
160 return $this;
161 }
162
163
164 /**
165 * Filter results to only return revisions whose ids are in the given set.
166 *
167 * @param array $ids List of revision ids
168 * @return $this
169 * @task config
170 */
171 public function withIDs(array $ids) {
172 $this->revIDs = $ids;
173 return $this;
174 }
175
176
177 /**
178 * Filter results to only return revisions whose PHIDs are in the given set.
179 *
180 * @param array $phids List of revision PHIDs
181 * @return $this
182 * @task config
183 */
184 public function withPHIDs(array $phids) {
185 $this->phids = $phids;
186 return $this;
187 }
188
189
190 /**
191 * Given a set of users, filter results to return only revisions they are
192 * responsible for (i.e., they are either authors or reviewers).
193 *
194 * @param array $responsible_phids List of user PHIDs.
195 * @return $this
196 * @task config
197 */
198 public function withResponsibleUsers(array $responsible_phids) {
199 $this->responsibles = $responsible_phids;
200 return $this;
201 }
202
203
204 public function withRepositoryPHIDs(array $repository_phids) {
205 $this->repositoryPHIDs = $repository_phids;
206 return $this;
207 }
208
209 public function withUpdatedEpochBetween($min, $max) {
210 $this->updatedEpochMin = $min;
211 $this->updatedEpochMax = $max;
212 return $this;
213 }
214
215 public function withCreatedEpochBetween($min, $max) {
216 $this->createdEpochMin = $min;
217 $this->createdEpochMax = $max;
218 return $this;
219 }
220
221
222 /**
223 * Set whether or not the query should load the active diff for each
224 * revision.
225 *
226 * @param bool $need_active_diffs True to load and attach diffs.
227 * @return $this
228 * @task config
229 */
230 public function needActiveDiffs($need_active_diffs) {
231 $this->needActiveDiffs = $need_active_diffs;
232 return $this;
233 }
234
235
236 /**
237 * Set whether or not the query should load the associated commit PHIDs for
238 * each revision.
239 *
240 * @param bool $need_commit_phids True to load and attach diffs.
241 * @return $this
242 * @task config
243 */
244 public function needCommitPHIDs($need_commit_phids) {
245 $this->needCommitPHIDs = $need_commit_phids;
246 return $this;
247 }
248
249
250 /**
251 * Set whether or not the query should load associated diff IDs for each
252 * revision.
253 *
254 * @param bool $need_diff_ids True to load and attach diff IDs.
255 * @return $this
256 * @task config
257 */
258 public function needDiffIDs($need_diff_ids) {
259 $this->needDiffIDs = $need_diff_ids;
260 return $this;
261 }
262
263
264 /**
265 * Set whether or not the query should load associated commit hashes for each
266 * revision.
267 *
268 * @param bool $need_hashes True to load and attach commit hashes.
269 * @return $this
270 * @task config
271 */
272 public function needHashes($need_hashes) {
273 $this->needHashes = $need_hashes;
274 return $this;
275 }
276
277
278 /**
279 * Set whether or not the query should load associated reviewers.
280 *
281 * @param bool $need_reviewers True to load and attach reviewers.
282 * @return $this
283 * @task config
284 */
285 public function needReviewers($need_reviewers) {
286 $this->needReviewers = $need_reviewers;
287 return $this;
288 }
289
290
291 /**
292 * Request information about the viewer's authority to act on behalf of each
293 * reviewer. In particular, they have authority to act on behalf of projects
294 * they are a member of.
295 *
296 * @param bool $need_reviewer_authority True to load and attach authority.
297 * @return $this
298 * @task config
299 */
300 public function needReviewerAuthority($need_reviewer_authority) {
301 $this->needReviewerAuthority = $need_reviewer_authority;
302 return $this;
303 }
304
305 public function needFlags($need_flags) {
306 $this->needFlags = $need_flags;
307 return $this;
308 }
309
310 public function needDrafts($need_drafts) {
311 $this->needDrafts = $need_drafts;
312 return $this;
313 }
314
315
316/* -( Query Execution )---------------------------------------------------- */
317
318
319 public function newResultObject() {
320 return new DifferentialRevision();
321 }
322
323
324 /**
325 * Execute the query as configured, returning matching
326 * @{class:DifferentialRevision} objects.
327 *
328 * @return list List of matching DifferentialRevision objects.
329 * @task exec
330 */
331 protected function loadPage() {
332 $data = $this->loadData();
333 $data = $this->didLoadRawRows($data);
334 $table = $this->newResultObject();
335 return $table->loadAllFromArray($data);
336 }
337
338 protected function willFilterPage(array $revisions) {
339 $viewer = $this->getViewer();
340
341 $repository_phids = mpull($revisions, 'getRepositoryPHID');
342 $repository_phids = array_filter($repository_phids);
343
344 $repositories = array();
345 if ($repository_phids) {
346 $repositories = id(new PhabricatorRepositoryQuery())
347 ->setViewer($this->getViewer())
348 ->withPHIDs($repository_phids)
349 ->execute();
350 $repositories = mpull($repositories, null, 'getPHID');
351 }
352
353 // If a revision is associated with a repository:
354 //
355 // - the viewer must be able to see the repository; or
356 // - the viewer must have an automatic view capability.
357 //
358 // In the latter case, we'll load the revision but not load the repository.
359
360 $can_view = PhabricatorPolicyCapability::CAN_VIEW;
361 foreach ($revisions as $key => $revision) {
362 $repo_phid = $revision->getRepositoryPHID();
363 if (!$repo_phid) {
364 // The revision has no associated repository. Attach `null` and move on.
365 $revision->attachRepository(null);
366 continue;
367 }
368
369 $repository = idx($repositories, $repo_phid);
370 if ($repository) {
371 // The revision has an associated repository, and the viewer can see
372 // it. Attach it and move on.
373 $revision->attachRepository($repository);
374 continue;
375 }
376
377 if ($revision->hasAutomaticCapability($can_view, $viewer)) {
378 // The revision has an associated repository which the viewer can not
379 // see, but the viewer has an automatic capability on this revision.
380 // Load the revision without attaching a repository.
381 $revision->attachRepository(null);
382 continue;
383 }
384
385 if ($this->getViewer()->isOmnipotent()) {
386 // The viewer is omnipotent. Allow the revision to load even without
387 // a repository.
388 $revision->attachRepository(null);
389 continue;
390 }
391
392 // The revision has an associated repository, and the viewer can't see
393 // it, and the viewer has no special capabilities. Filter out this
394 // revision.
395 $this->didRejectResult($revision);
396 unset($revisions[$key]);
397 }
398
399 if (!$revisions) {
400 return array();
401 }
402
403 $table = new DifferentialRevision();
404 $conn_r = $table->establishConnection('r');
405
406 if ($this->needCommitPHIDs) {
407 $this->loadCommitPHIDs($revisions);
408 }
409
410 $need_active = $this->needActiveDiffs;
411 $need_ids = $need_active || $this->needDiffIDs;
412
413 if ($need_ids) {
414 $this->loadDiffIDs($conn_r, $revisions);
415 }
416
417 if ($need_active) {
418 $this->loadActiveDiffs($conn_r, $revisions);
419 }
420
421 if ($this->needHashes) {
422 $this->loadHashes($conn_r, $revisions);
423 }
424
425 if ($this->needReviewers || $this->needReviewerAuthority) {
426 $this->loadReviewers($conn_r, $revisions);
427 }
428
429 return $revisions;
430 }
431
432 protected function didFilterPage(array $revisions) {
433 $viewer = $this->getViewer();
434
435 if ($this->needFlags) {
436 $flags = id(new PhabricatorFlagQuery())
437 ->setViewer($viewer)
438 ->withOwnerPHIDs(array($viewer->getPHID()))
439 ->withObjectPHIDs(mpull($revisions, 'getPHID'))
440 ->execute();
441 $flags = mpull($flags, null, 'getObjectPHID');
442 foreach ($revisions as $revision) {
443 $revision->attachFlag(
444 $viewer,
445 idx($flags, $revision->getPHID()));
446 }
447 }
448
449 if ($this->needDrafts) {
450 PhabricatorDraftEngine::attachDrafts(
451 $viewer,
452 $revisions);
453 }
454
455 return $revisions;
456 }
457
458 private function loadData() {
459 $table = $this->newResultObject();
460 $conn = $table->establishConnection('r');
461
462 $selects = array();
463
464 // NOTE: If the query includes "responsiblePHIDs", we execute it as a
465 // UNION of revisions they own and revisions they're reviewing. This has
466 // much better performance than doing it with JOIN/WHERE.
467 if ($this->responsibles) {
468 $basic_authors = $this->authors;
469 $basic_reviewers = $this->reviewers;
470
471 try {
472 // Build the query where the responsible users are authors.
473 $this->authors = array_merge($basic_authors, $this->responsibles);
474
475 $this->reviewers = $basic_reviewers;
476 $selects[] = $this->buildSelectStatement($conn);
477
478 // Build the query where the responsible users are reviewers, or
479 // projects they are members of are reviewers.
480 $this->authors = $basic_authors;
481 $this->reviewers = array_merge($basic_reviewers, $this->responsibles);
482 $selects[] = $this->buildSelectStatement($conn);
483
484 // Put everything back like it was.
485 $this->authors = $basic_authors;
486 $this->reviewers = $basic_reviewers;
487 } catch (Exception $ex) {
488 $this->authors = $basic_authors;
489 $this->reviewers = $basic_reviewers;
490 throw $ex;
491 }
492 } else {
493 $selects[] = $this->buildSelectStatement($conn);
494 }
495
496 if (count($selects) > 1) {
497 $unions = null;
498 foreach ($selects as $select) {
499 if (!$unions) {
500 $unions = $select;
501 continue;
502 }
503
504 $unions = qsprintf(
505 $conn,
506 '%Q UNION DISTINCT %Q',
507 $unions,
508 $select);
509 }
510
511 $query = qsprintf(
512 $conn,
513 '%Q %Q %Q',
514 $unions,
515 $this->buildOrderClause($conn, true),
516 $this->buildLimitClause($conn));
517 } else {
518 $query = head($selects);
519 }
520
521 return queryfx_all($conn, '%Q', $query);
522 }
523
524 private function buildSelectStatement(AphrontDatabaseConnection $conn_r) {
525 $table = new DifferentialRevision();
526
527 $select = $this->buildSelectClause($conn_r);
528
529 $from = qsprintf(
530 $conn_r,
531 'FROM %T r',
532 $table->getTableName());
533
534 $joins = $this->buildJoinsClause($conn_r);
535 $where = $this->buildWhereClause($conn_r);
536 $group_by = $this->buildGroupClause($conn_r);
537 $having = $this->buildHavingClause($conn_r);
538
539 $order_by = $this->buildOrderClause($conn_r);
540
541 $limit = $this->buildLimitClause($conn_r);
542
543 return qsprintf(
544 $conn_r,
545 '(%Q %Q %Q %Q %Q %Q %Q %Q)',
546 $select,
547 $from,
548 $joins,
549 $where,
550 $group_by,
551 $having,
552 $order_by,
553 $limit);
554 }
555
556
557/* -( Internals )---------------------------------------------------------- */
558
559
560 /**
561 * @task internal
562 */
563 private function buildJoinsClause(AphrontDatabaseConnection $conn) {
564 $joins = array();
565
566 if ($this->paths) {
567 $path_table = new DifferentialAffectedPath();
568 $joins[] = qsprintf(
569 $conn,
570 'JOIN %R paths ON paths.revisionID = r.id',
571 $path_table);
572 }
573
574 if ($this->commitHashes) {
575 $joins[] = qsprintf(
576 $conn,
577 'JOIN %T hash_rel ON hash_rel.revisionID = r.id',
578 ArcanistDifferentialRevisionHash::TABLE_NAME);
579 }
580
581 if ($this->ccs) {
582 $joins[] = qsprintf(
583 $conn,
584 'JOIN %T e_ccs ON e_ccs.src = r.phid '.
585 'AND e_ccs.type = %s '.
586 'AND e_ccs.dst in (%Ls)',
587 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
588 PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
589 $this->ccs);
590 }
591
592 if ($this->reviewers) {
593 $joins[] = qsprintf(
594 $conn,
595 'LEFT JOIN %T reviewer ON reviewer.revisionPHID = r.phid
596 AND reviewer.reviewerStatus != %s
597 AND reviewer.reviewerPHID in (%Ls)',
598 id(new DifferentialReviewer())->getTableName(),
599 DifferentialReviewerStatus::STATUS_RESIGNED,
600 $this->reviewers);
601 }
602
603 if ($this->noReviewers) {
604 $joins[] = qsprintf(
605 $conn,
606 'LEFT JOIN %T no_reviewer ON no_reviewer.revisionPHID = r.phid
607 AND no_reviewer.reviewerStatus != %s',
608 id(new DifferentialReviewer())->getTableName(),
609 DifferentialReviewerStatus::STATUS_RESIGNED);
610 }
611
612 if ($this->draftAuthors) {
613 $joins[] = qsprintf(
614 $conn,
615 'JOIN %T has_draft ON has_draft.srcPHID = r.phid
616 AND has_draft.type = %s
617 AND has_draft.dstPHID IN (%Ls)',
618 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
619 PhabricatorObjectHasDraftEdgeType::EDGECONST,
620 $this->draftAuthors);
621 }
622
623 $joins[] = $this->buildJoinClauseParts($conn);
624
625 return $this->formatJoinClause($conn, $joins);
626 }
627
628
629 /**
630 * @task internal
631 */
632 protected function buildWhereClause(AphrontDatabaseConnection $conn) {
633 $viewer = $this->getViewer();
634 $where = array();
635
636 if ($this->paths !== null) {
637 $paths = $this->paths;
638
639 $path_map = id(new DiffusionPathIDQuery($paths))
640 ->loadPathIDs();
641
642 if (!$path_map) {
643 // If none of the paths have entries in the PathID table, we can not
644 // possibly find any revisions affecting them.
645 throw new PhabricatorEmptyQueryException();
646 }
647
648 $where[] = qsprintf(
649 $conn,
650 'paths.pathID IN (%Ld)',
651 array_fuse($path_map));
652
653 // If we have repository PHIDs, additionally constrain this query to
654 // try to help MySQL execute it efficiently.
655 if ($this->repositoryPHIDs !== null) {
656 $repositories = id(new PhabricatorRepositoryQuery())
657 ->setViewer($viewer)
658 ->setParentQuery($this)
659 ->withPHIDs($this->repositoryPHIDs)
660 ->execute();
661
662 if (!$repositories) {
663 throw new PhabricatorEmptyQueryException();
664 }
665
666 $repository_ids = mpull($repositories, 'getID');
667
668 $where[] = qsprintf(
669 $conn,
670 'paths.repositoryID IN (%Ld)',
671 $repository_ids);
672 }
673 }
674
675 if ($this->authors) {
676 $where[] = qsprintf(
677 $conn,
678 'r.authorPHID IN (%Ls)',
679 $this->authors);
680 }
681
682 if ($this->revIDs) {
683 $where[] = qsprintf(
684 $conn,
685 'r.id IN (%Ld)',
686 $this->revIDs);
687 }
688
689 if ($this->repositoryPHIDs) {
690 $where[] = qsprintf(
691 $conn,
692 'r.repositoryPHID IN (%Ls)',
693 $this->repositoryPHIDs);
694 }
695
696 if ($this->commitHashes) {
697 $hash_clauses = array();
698 foreach ($this->commitHashes as $info) {
699 list($type, $hash) = $info;
700 $hash_clauses[] = qsprintf(
701 $conn,
702 '(hash_rel.type = %s AND hash_rel.hash = %s)',
703 $type,
704 $hash);
705 }
706 $hash_clauses = qsprintf($conn, '%LO', $hash_clauses);
707 $where[] = $hash_clauses;
708 }
709
710 if ($this->phids) {
711 $where[] = qsprintf(
712 $conn,
713 'r.phid IN (%Ls)',
714 $this->phids);
715 }
716
717 if ($this->branches) {
718 $where[] = qsprintf(
719 $conn,
720 'r.branchName in (%Ls)',
721 $this->branches);
722 }
723
724 if ($this->updatedEpochMin !== null) {
725 $where[] = qsprintf(
726 $conn,
727 'r.dateModified >= %d',
728 $this->updatedEpochMin);
729 }
730
731 if ($this->updatedEpochMax !== null) {
732 $where[] = qsprintf(
733 $conn,
734 'r.dateModified <= %d',
735 $this->updatedEpochMax);
736 }
737
738 if ($this->createdEpochMin !== null) {
739 $where[] = qsprintf(
740 $conn,
741 'r.dateCreated >= %d',
742 $this->createdEpochMin);
743 }
744
745 if ($this->createdEpochMax !== null) {
746 $where[] = qsprintf(
747 $conn,
748 'r.dateCreated <= %d',
749 $this->createdEpochMax);
750 }
751
752 if ($this->statuses !== null) {
753 $where[] = qsprintf(
754 $conn,
755 'r.status in (%Ls)',
756 $this->statuses);
757 }
758
759 if ($this->isOpen !== null) {
760 if ($this->isOpen) {
761 $statuses = DifferentialLegacyQuery::getModernValues(
762 DifferentialLegacyQuery::STATUS_OPEN);
763 } else {
764 $statuses = DifferentialLegacyQuery::getModernValues(
765 DifferentialLegacyQuery::STATUS_CLOSED);
766 }
767 $where[] = qsprintf(
768 $conn,
769 'r.status in (%Ls)',
770 $statuses);
771 }
772
773 $reviewer_subclauses = array();
774
775 if ($this->noReviewers) {
776 $reviewer_subclauses[] = qsprintf(
777 $conn,
778 'no_reviewer.reviewerPHID IS NULL');
779 }
780
781 if ($this->reviewers) {
782 $reviewer_subclauses[] = qsprintf(
783 $conn,
784 'reviewer.reviewerPHID IS NOT NULL');
785 }
786
787 if ($reviewer_subclauses) {
788 $where[] = qsprintf($conn, '%LO', $reviewer_subclauses);
789 }
790
791 $where[] = $this->buildWhereClauseParts($conn);
792
793 return $this->formatWhereClause($conn, $where);
794 }
795
796
797 /**
798 * @task internal
799 */
800 protected function shouldGroupQueryResultRows() {
801
802 if ($this->paths) {
803 // (If we have exactly one repository and exactly one path, we don't
804 // technically need to group, but it's simpler to always group.)
805 return true;
806 }
807
808 if (count($this->ccs) > 1) {
809 return true;
810 }
811
812 if (count($this->reviewers) > 1) {
813 return true;
814 }
815
816 if (count($this->commitHashes) > 1) {
817 return true;
818 }
819
820 if ($this->noReviewers) {
821 return true;
822 }
823
824 return parent::shouldGroupQueryResultRows();
825 }
826
827 public function getBuiltinOrders() {
828 $orders = parent::getBuiltinOrders() + array(
829 'updated' => array(
830 'vector' => array('updated', 'id'),
831 'name' => pht('Date Updated (Latest First)'),
832 'aliases' => array(self::ORDER_MODIFIED),
833 ),
834 'outdated' => array(
835 'vector' => array('-updated', '-id'),
836 'name' => pht('Date Updated (Oldest First)'),
837 ),
838 );
839
840 // Alias the "newest" builtin to the historical key for it.
841 $orders['newest']['aliases'][] = self::ORDER_CREATED;
842
843 return $orders;
844 }
845
846 protected function getDefaultOrderVector() {
847 return array('updated', 'id');
848 }
849
850 public function getOrderableColumns() {
851 return array(
852 'updated' => array(
853 'table' => $this->getPrimaryTableAlias(),
854 'column' => 'dateModified',
855 'type' => 'int',
856 ),
857 ) + parent::getOrderableColumns();
858 }
859
860 protected function newPagingMapFromPartialObject($object) {
861 return array(
862 'id' => (int)$object->getID(),
863 'updated' => (int)$object->getDateModified(),
864 );
865 }
866
867 /**
868 * @param array<DifferentialRevision> $revisions
869 */
870 private function loadCommitPHIDs(array $revisions) {
871 assert_instances_of($revisions, DifferentialRevision::class);
872
873 if (!$revisions) {
874 return;
875 }
876
877 $revisions = mpull($revisions, null, 'getPHID');
878
879 $edge_query = id(new PhabricatorEdgeQuery())
880 ->withSourcePHIDs(array_keys($revisions))
881 ->withEdgeTypes(
882 array(
883 DifferentialRevisionHasCommitEdgeType::EDGECONST,
884 ));
885 $edge_query->execute();
886
887 foreach ($revisions as $phid => $revision) {
888 $commit_phids = $edge_query->getDestinationPHIDs(array($phid));
889 $revision->attachCommitPHIDs($commit_phids);
890 }
891 }
892
893 /**
894 * @param AphrontDatabaseConnection $conn_r
895 * @param array<DifferentialRevision> $revisions
896 */
897 private function loadDiffIDs($conn_r, array $revisions) {
898 assert_instances_of($revisions, DifferentialRevision::class);
899
900 $diff_table = new DifferentialDiff();
901
902 $diff_ids = queryfx_all(
903 $conn_r,
904 'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld)
905 ORDER BY id DESC',
906 $diff_table->getTableName(),
907 mpull($revisions, 'getID'));
908 $diff_ids = igroup($diff_ids, 'revisionID');
909
910 foreach ($revisions as $revision) {
911 $ids = idx($diff_ids, $revision->getID(), array());
912 $ids = ipull($ids, 'id');
913 $revision->attachDiffIDs($ids);
914 }
915 }
916
917 /**
918 * @param AphrontDatabaseConnection $conn_r
919 * @param array<DifferentialRevision> $revisions
920 */
921 private function loadActiveDiffs($conn_r, array $revisions) {
922 assert_instances_of($revisions, DifferentialRevision::class);
923
924 $diff_table = new DifferentialDiff();
925
926 $load_ids = array();
927 foreach ($revisions as $revision) {
928 $diffs = $revision->getDiffIDs();
929 if ($diffs) {
930 $load_ids[] = max($diffs);
931 }
932 }
933
934 $active_diffs = array();
935 if ($load_ids) {
936 $active_diffs = $diff_table->loadAllWhere(
937 'id IN (%Ld)',
938 $load_ids);
939 }
940
941 $active_diffs = mpull($active_diffs, null, 'getRevisionID');
942 foreach ($revisions as $revision) {
943 $revision->attachActiveDiff(idx($active_diffs, $revision->getID()));
944 }
945 }
946
947 /**
948 * @param AphrontDatabaseConnection $conn_r
949 * @param array<DifferentialRevision> $revisions
950 */
951 private function loadHashes(
952 AphrontDatabaseConnection $conn_r,
953 array $revisions) {
954 assert_instances_of($revisions, DifferentialRevision::class);
955
956 $data = queryfx_all(
957 $conn_r,
958 'SELECT * FROM %T WHERE revisionID IN (%Ld)',
959 'differential_revisionhash',
960 mpull($revisions, 'getID'));
961
962 $data = igroup($data, 'revisionID');
963 foreach ($revisions as $revision) {
964 $hashes = idx($data, $revision->getID(), array());
965 $list = array();
966 foreach ($hashes as $hash) {
967 $list[] = array($hash['type'], $hash['hash']);
968 }
969 $revision->attachHashes($list);
970 }
971 }
972
973 /**
974 * @param AphrontDatabaseConnection $conn
975 * @param array<DifferentialRevision> $revisions
976 */
977 private function loadReviewers(
978 AphrontDatabaseConnection $conn,
979 array $revisions) {
980
981 assert_instances_of($revisions, DifferentialRevision::class);
982
983 $reviewer_table = new DifferentialReviewer();
984 $reviewer_rows = queryfx_all(
985 $conn,
986 'SELECT * FROM %T WHERE revisionPHID IN (%Ls)
987 ORDER BY id ASC',
988 $reviewer_table->getTableName(),
989 mpull($revisions, 'getPHID'));
990 $reviewer_list = $reviewer_table->loadAllFromArray($reviewer_rows);
991 $reviewer_map = mgroup($reviewer_list, 'getRevisionPHID');
992
993 foreach ($reviewer_map as $key => $reviewers) {
994 $reviewer_map[$key] = mpull($reviewers, null, 'getReviewerPHID');
995 }
996
997 $viewer = $this->getViewer();
998 $viewer_phid = $viewer->getPHID();
999
1000 $allow_key = 'differential.allow-self-accept';
1001 $allow_self = PhabricatorEnv::getEnvConfig($allow_key);
1002
1003 // Figure out which of these reviewers the viewer has authority to act as.
1004 if ($this->needReviewerAuthority && $viewer_phid) {
1005 $authority = $this->loadReviewerAuthority(
1006 $revisions,
1007 $reviewer_map,
1008 $allow_self);
1009 }
1010
1011 foreach ($revisions as $revision) {
1012 $reviewers = idx($reviewer_map, $revision->getPHID(), array());
1013 foreach ($reviewers as $reviewer_phid => $reviewer) {
1014 if ($this->needReviewerAuthority) {
1015 if (!$viewer_phid) {
1016 // Logged-out users never have authority.
1017 $has_authority = false;
1018 } else if ((!$allow_self) &&
1019 ($revision->getAuthorPHID() == $viewer_phid)) {
1020 // The author can never have authority unless we allow self-accept.
1021 $has_authority = false;
1022 } else {
1023 // Otherwise, look up whether the viewer has authority.
1024 $has_authority = isset($authority[$reviewer_phid]);
1025 }
1026
1027 $reviewer->attachAuthority($viewer, $has_authority);
1028 }
1029
1030 $reviewers[$reviewer_phid] = $reviewer;
1031 }
1032
1033 $revision->attachReviewers($reviewers);
1034 }
1035 }
1036
1037 private function loadReviewerAuthority(
1038 array $revisions,
1039 array $reviewers,
1040 $allow_self) {
1041
1042 $revision_map = mpull($revisions, null, 'getPHID');
1043 $viewer_phid = $this->getViewer()->getPHID();
1044
1045 // Find all the project/package reviewers which the user may have authority
1046 // over.
1047 $project_phids = array();
1048 $package_phids = array();
1049 $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
1050 $package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
1051
1052 foreach ($reviewers as $revision_phid => $reviewer_list) {
1053 if (!$allow_self) {
1054 if ($revision_map[$revision_phid]->getAuthorPHID() == $viewer_phid) {
1055 // If self-review isn't permitted, the user will never have
1056 // authority over projects on revisions they authored because you
1057 // can't accept your own revisions, so we don't need to load any
1058 // data about these reviewers.
1059 continue;
1060 }
1061 }
1062
1063 foreach ($reviewer_list as $reviewer_phid => $reviewer) {
1064 $phid_type = phid_get_type($reviewer_phid);
1065 if ($phid_type == $project_type) {
1066 $project_phids[] = $reviewer_phid;
1067 }
1068 if ($phid_type == $package_type) {
1069 $package_phids[] = $reviewer_phid;
1070 }
1071 }
1072 }
1073
1074 // The viewer has authority over themselves.
1075 $user_authority = array_fuse(array($viewer_phid));
1076
1077 // And over any projects they are a member of.
1078 $project_authority = array();
1079 if ($project_phids) {
1080 $project_authority = id(new PhabricatorProjectQuery())
1081 ->setViewer($this->getViewer())
1082 ->withPHIDs($project_phids)
1083 ->withMemberPHIDs(array($viewer_phid))
1084 ->execute();
1085 $project_authority = mpull($project_authority, 'getPHID');
1086 $project_authority = array_fuse($project_authority);
1087 }
1088
1089 // And over any packages they own.
1090 $package_authority = array();
1091 if ($package_phids) {
1092 $package_authority = id(new PhabricatorOwnersPackageQuery())
1093 ->setViewer($this->getViewer())
1094 ->withPHIDs($package_phids)
1095 ->withAuthorityPHIDs(array($viewer_phid))
1096 ->execute();
1097 $package_authority = mpull($package_authority, 'getPHID');
1098 $package_authority = array_fuse($package_authority);
1099 }
1100
1101 return $user_authority + $project_authority + $package_authority;
1102 }
1103
1104 public function getQueryApplicationClass() {
1105 return PhabricatorDifferentialApplication::class;
1106 }
1107
1108 protected function getPrimaryTableAlias() {
1109 return 'r';
1110 }
1111
1112}