[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/differential/query/ -> DifferentialRevisionQuery.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1