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