@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
at upstream/main 1112 lines 30 kB view raw
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}