[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/diffusion/query/ -> DiffusionCommitQuery.php (source)

   1  <?php
   2  
   3  final class DiffusionCommitQuery
   4    extends PhabricatorCursorPagedPolicyAwareQuery {
   5  
   6    private $ids;
   7    private $phids;
   8    private $authorPHIDs;
   9    private $defaultRepository;
  10    private $identifiers;
  11    private $repositoryIDs;
  12    private $repositoryPHIDs;
  13    private $identifierMap;
  14  
  15    private $needAuditRequests;
  16    private $auditIDs;
  17    private $auditorPHIDs;
  18    private $auditAwaitingUser;
  19    private $auditStatus;
  20  
  21    const AUDIT_STATUS_ANY       = 'audit-status-any';
  22    const AUDIT_STATUS_OPEN      = 'audit-status-open';
  23    const AUDIT_STATUS_CONCERN   = 'audit-status-concern';
  24    const AUDIT_STATUS_ACCEPTED  = 'audit-status-accepted';
  25    const AUDIT_STATUS_PARTIAL   = 'audit-status-partial';
  26  
  27    private $needCommitData;
  28  
  29    public function withIDs(array $ids) {
  30      $this->ids = $ids;
  31      return $this;
  32    }
  33  
  34    public function withPHIDs(array $phids) {
  35      $this->phids = $phids;
  36      return $this;
  37    }
  38  
  39    public function withAuthorPHIDs(array $phids) {
  40      $this->authorPHIDs = $phids;
  41      return $this;
  42    }
  43  
  44    /**
  45     * Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234",
  46     * or "a9caf12". When an identifier matches multiple commits, they will all
  47     * be returned; callers should be prepared to deal with more results than
  48     * they queried for.
  49     */
  50    public function withIdentifiers(array $identifiers) {
  51      $this->identifiers = $identifiers;
  52      return $this;
  53    }
  54  
  55    /**
  56     * Look up commits in a specific repository. This is a shorthand for calling
  57     * @{method:withDefaultRepository} and @{method:withRepositoryIDs}.
  58     */
  59    public function withRepository(PhabricatorRepository $repository) {
  60      $this->withDefaultRepository($repository);
  61      $this->withRepositoryIDs(array($repository->getID()));
  62      return $this;
  63    }
  64  
  65    /**
  66     * Look up commits in a specific repository. Prefer
  67     * @{method:withRepositoryIDs}; the underyling table is keyed by ID such
  68     * that this method requires a separate initial query to map PHID to ID.
  69     */
  70    public function withRepositoryPHIDs(array $phids) {
  71      $this->repositoryPHIDs = $phids;
  72    }
  73  
  74    /**
  75     * If a default repository is provided, ambiguous commit identifiers will
  76     * be assumed to belong to the default repository.
  77     *
  78     * For example, "r123" appearing in a commit message in repository X is
  79     * likely to be unambiguously "rX123". Normally the reference would be
  80     * considered ambiguous, but if you provide a default repository it will
  81     * be correctly resolved.
  82     */
  83    public function withDefaultRepository(PhabricatorRepository $repository) {
  84      $this->defaultRepository = $repository;
  85      return $this;
  86    }
  87  
  88    public function withRepositoryIDs(array $repository_ids) {
  89      $this->repositoryIDs = $repository_ids;
  90      return $this;
  91    }
  92  
  93    public function needCommitData($need) {
  94      $this->needCommitData = $need;
  95      return $this;
  96    }
  97  
  98    public function needAuditRequests($need) {
  99      $this->needAuditRequests = $need;
 100      return $this;
 101    }
 102  
 103    /**
 104     * Returns true if we should join the audit table, either because we're
 105     * interested in the information if it's available or because matching rows
 106     * must always have it.
 107     */
 108    private function shouldJoinAudits() {
 109      return $this->auditStatus ||
 110             $this->rowsMustHaveAudits();
 111    }
 112  
 113    /**
 114     * Return true if we should `JOIN` (vs `LEFT JOIN`) the audit table, because
 115     * matching commits will always have audit rows.
 116     */
 117    private function rowsMustHaveAudits() {
 118      return
 119        $this->auditIDs ||
 120        $this->auditorPHIDs ||
 121        $this->auditAwaitingUser;
 122    }
 123  
 124    public function withAuditIDs(array $ids) {
 125      $this->auditIDs = $ids;
 126      return $this;
 127    }
 128  
 129    public function withAuditorPHIDs(array $auditor_phids) {
 130      $this->auditorPHIDs = $auditor_phids;
 131      return $this;
 132    }
 133  
 134    public function withAuditAwaitingUser(PhabricatorUser $user) {
 135      $this->auditAwaitingUser = $user;
 136      return $this;
 137    }
 138  
 139    public function withAuditStatus($status) {
 140      $this->auditStatus = $status;
 141      return $this;
 142    }
 143  
 144    public function getIdentifierMap() {
 145      if ($this->identifierMap === null) {
 146        throw new Exception(
 147          'You must execute() the query before accessing the identifier map.');
 148      }
 149      return $this->identifierMap;
 150    }
 151  
 152    protected function getPagingColumn() {
 153      return 'commit.id';
 154    }
 155  
 156    protected function willExecute() {
 157      if ($this->identifierMap === null) {
 158        $this->identifierMap = array();
 159      }
 160    }
 161  
 162    protected function loadPage() {
 163      $table = new PhabricatorRepositoryCommit();
 164      $conn_r = $table->establishConnection('r');
 165  
 166      $data = queryfx_all(
 167        $conn_r,
 168        'SELECT commit.* FROM %T commit %Q %Q %Q %Q %Q',
 169        $table->getTableName(),
 170        $this->buildJoinClause($conn_r),
 171        $this->buildWhereClause($conn_r),
 172        $this->buildGroupClause($conn_r),
 173        $this->buildOrderClause($conn_r),
 174        $this->buildLimitClause($conn_r));
 175  
 176      return $table->loadAllFromArray($data);
 177    }
 178  
 179    protected function willFilterPage(array $commits) {
 180      $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID');
 181      $repos = id(new PhabricatorRepositoryQuery())
 182        ->setViewer($this->getViewer())
 183        ->withIDs($repository_ids)
 184        ->execute();
 185  
 186      foreach ($commits as $key => $commit) {
 187        $repo = idx($repos, $commit->getRepositoryID());
 188        if ($repo) {
 189          $commit->attachRepository($repo);
 190        } else {
 191          unset($commits[$key]);
 192        }
 193      }
 194  
 195      if ($this->identifiers !== null) {
 196        $ids = array_fuse($this->identifiers);
 197        $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
 198  
 199        $result = array();
 200        foreach ($commits as $commit) {
 201          $prefix = 'r'.$commit->getRepository()->getCallsign();
 202          $suffix = $commit->getCommitIdentifier();
 203  
 204          if ($commit->getRepository()->isSVN()) {
 205            if (isset($ids[$prefix.$suffix])) {
 206              $result[$prefix.$suffix][] = $commit;
 207            }
 208          } else {
 209            // This awkward construction is so we can link the commits up in O(N)
 210            // time instead of O(N^2).
 211            for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) {
 212              $part = substr($suffix, 0, $ii);
 213              if (isset($ids[$prefix.$part])) {
 214                $result[$prefix.$part][] = $commit;
 215              }
 216              if (isset($ids[$part])) {
 217                $result[$part][] = $commit;
 218              }
 219            }
 220          }
 221        }
 222  
 223        foreach ($result as $identifier => $matching_commits) {
 224          if (count($matching_commits) == 1) {
 225            $result[$identifier] = head($matching_commits);
 226          } else {
 227            // This reference is ambiguous (it matches more than one commit) so
 228            // don't link it.
 229            unset($result[$identifier]);
 230          }
 231        }
 232  
 233        $this->identifierMap += $result;
 234      }
 235  
 236      return $commits;
 237    }
 238  
 239    protected function didFilterPage(array $commits) {
 240      if ($this->needCommitData) {
 241        $data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
 242          'commitID in (%Ld)',
 243          mpull($commits, 'getID'));
 244        $data = mpull($data, null, 'getCommitID');
 245        foreach ($commits as $commit) {
 246          $commit_data = idx($data, $commit->getID());
 247          if (!$commit_data) {
 248            $commit_data = new PhabricatorRepositoryCommitData();
 249          }
 250          $commit->attachCommitData($commit_data);
 251        }
 252      }
 253  
 254      // TODO: This should just be `needAuditRequests`, not `shouldJoinAudits()`,
 255      // but leave that for a future diff.
 256  
 257      if ($this->needAuditRequests || $this->shouldJoinAudits()) {
 258        $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
 259          'commitPHID IN (%Ls)',
 260          mpull($commits, 'getPHID'));
 261  
 262        $requests = mgroup($requests, 'getCommitPHID');
 263        foreach ($commits as $commit) {
 264          $audit_requests = idx($requests, $commit->getPHID(), array());
 265          $commit->attachAudits($audit_requests);
 266          foreach ($audit_requests as $audit_request) {
 267            $audit_request->attachCommit($commit);
 268          }
 269        }
 270      }
 271  
 272      return $commits;
 273    }
 274  
 275    private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
 276      $where = array();
 277  
 278      if ($this->repositoryPHIDs !== null) {
 279        $map_repositories = id (new PhabricatorRepositoryQuery())
 280          ->setViewer($this->getViewer())
 281          ->withPHIDs($this->repositoryPHIDs)
 282          ->execute();
 283  
 284        if (!$map_repositories) {
 285          throw new PhabricatorEmptyQueryException();
 286        }
 287        $repository_ids = mpull($map_repositories, 'getID');
 288        if ($this->repositoryIDs !== null) {
 289          $repository_ids = array_merge($repository_ids, $this->repositoryIDs);
 290        }
 291        $this->withRepositoryIDs($repository_ids);
 292      }
 293  
 294      if ($this->ids !== null) {
 295        $where[] = qsprintf(
 296          $conn_r,
 297          'commit.id IN (%Ld)',
 298          $this->ids);
 299      }
 300  
 301      if ($this->phids !== null) {
 302        $where[] = qsprintf(
 303          $conn_r,
 304          'commit.phid IN (%Ls)',
 305          $this->phids);
 306      }
 307  
 308      if ($this->repositoryIDs !== null) {
 309        $where[] = qsprintf(
 310          $conn_r,
 311          'commit.repositoryID IN (%Ld)',
 312          $this->repositoryIDs);
 313      }
 314  
 315      if ($this->authorPHIDs !== null) {
 316        $where[] = qsprintf(
 317          $conn_r,
 318          'commit.authorPHID IN (%Ls)',
 319          $this->authorPHIDs);
 320      }
 321  
 322      if ($this->identifiers !== null) {
 323        $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
 324        $min_qualified   = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
 325  
 326        $refs = array();
 327        $bare = array();
 328        foreach ($this->identifiers as $identifier) {
 329          $matches = null;
 330          preg_match('/^(?:r([A-Z]+))?(.*)$/', $identifier, $matches);
 331          $repo = nonempty($matches[1], null);
 332          $identifier = nonempty($matches[2], null);
 333  
 334          if ($repo === null) {
 335            if ($this->defaultRepository) {
 336              $repo = $this->defaultRepository->getCallsign();
 337            }
 338          }
 339  
 340          if ($repo === null) {
 341            if (strlen($identifier) < $min_unqualified) {
 342              continue;
 343            }
 344            $bare[] = $identifier;
 345          } else {
 346            $refs[] = array(
 347              'callsign' => $repo,
 348              'identifier' => $identifier,
 349            );
 350          }
 351        }
 352  
 353        $sql = array();
 354  
 355        foreach ($bare as $identifier) {
 356          $sql[] = qsprintf(
 357            $conn_r,
 358            '(commit.commitIdentifier LIKE %> AND '.
 359            'LENGTH(commit.commitIdentifier) = 40)',
 360            $identifier);
 361        }
 362  
 363        if ($refs) {
 364          $callsigns = ipull($refs, 'callsign');
 365          $repos = id(new PhabricatorRepositoryQuery())
 366            ->setViewer($this->getViewer())
 367            ->withCallsigns($callsigns)
 368            ->execute();
 369          $repos = mpull($repos, null, 'getCallsign');
 370  
 371          foreach ($refs as $key => $ref) {
 372            $repo = idx($repos, $ref['callsign']);
 373            if (!$repo) {
 374              continue;
 375            }
 376  
 377            if ($repo->isSVN()) {
 378              if (!ctype_digit($ref['identifier'])) {
 379                continue;
 380              }
 381              $sql[] = qsprintf(
 382                $conn_r,
 383                '(commit.repositoryID = %d AND commit.commitIdentifier = %s)',
 384                $repo->getID(),
 385                // NOTE: Because the 'commitIdentifier' column is a string, MySQL
 386                // ignores the index if we hand it an integer. Hand it a string.
 387                // See T3377.
 388                (int)$ref['identifier']);
 389            } else {
 390              if (strlen($ref['identifier']) < $min_qualified) {
 391                continue;
 392              }
 393              $sql[] = qsprintf(
 394                $conn_r,
 395                '(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)',
 396                $repo->getID(),
 397                $ref['identifier']);
 398            }
 399          }
 400        }
 401  
 402        if (!$sql) {
 403          // If we discarded all possible identifiers (e.g., they all referenced
 404          // bogus repositories or were all too short), make sure the query finds
 405          // nothing.
 406          throw new PhabricatorEmptyQueryException(
 407            pht('No commit identifiers.'));
 408        }
 409  
 410        $where[] = '('.implode(' OR ', $sql).')';
 411      }
 412  
 413      if ($this->auditIDs !== null) {
 414        $where[] = qsprintf(
 415          $conn_r,
 416          'audit.id IN (%Ld)',
 417          $this->auditIDs);
 418      }
 419  
 420      if ($this->auditorPHIDs !== null) {
 421        $where[] = qsprintf(
 422          $conn_r,
 423          'audit.auditorPHID IN (%Ls)',
 424          $this->auditorPHIDs);
 425      }
 426  
 427      if ($this->auditAwaitingUser) {
 428        $awaiting_user_phid = $this->auditAwaitingUser->getPHID();
 429        // Exclude package and project audits associated with commits where
 430        // the user is the author.
 431        $where[] = qsprintf(
 432          $conn_r,
 433          '(commit.authorPHID IS NULL OR commit.authorPHID != %s)
 434            OR (audit.auditorPHID = %s)',
 435          $awaiting_user_phid,
 436          $awaiting_user_phid);
 437      }
 438  
 439      $status = $this->auditStatus;
 440      if ($status !== null) {
 441        switch ($status) {
 442          case self::AUDIT_STATUS_PARTIAL:
 443            $where[] = qsprintf(
 444              $conn_r,
 445              'commit.auditStatus = %d',
 446              PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED);
 447            break;
 448          case self::AUDIT_STATUS_ACCEPTED:
 449            $where[] = qsprintf(
 450              $conn_r,
 451              'commit.auditStatus = %d',
 452              PhabricatorAuditCommitStatusConstants::FULLY_AUDITED);
 453            break;
 454          case self::AUDIT_STATUS_CONCERN:
 455            $where[] = qsprintf(
 456              $conn_r,
 457              'audit.auditStatus = %s',
 458              PhabricatorAuditStatusConstants::CONCERNED);
 459            break;
 460          case self::AUDIT_STATUS_OPEN:
 461            $where[] = qsprintf(
 462              $conn_r,
 463              'audit.auditStatus in (%Ls)',
 464              PhabricatorAuditStatusConstants::getOpenStatusConstants());
 465            if ($this->auditAwaitingUser) {
 466              $where[] = qsprintf(
 467                $conn_r,
 468                'awaiting.auditStatus IS NULL OR awaiting.auditStatus != %s',
 469                PhabricatorAuditStatusConstants::RESIGNED);
 470            }
 471            break;
 472          case self::AUDIT_STATUS_ANY:
 473            break;
 474          default:
 475            $valid = array(
 476              self::AUDIT_STATUS_ANY,
 477              self::AUDIT_STATUS_OPEN,
 478              self::AUDIT_STATUS_CONCERN,
 479              self::AUDIT_STATUS_ACCEPTED,
 480              self::AUDIT_STATUS_PARTIAL,
 481            );
 482            throw new Exception(
 483              "Unknown audit status '{$status}'! Valid statuses are: ".
 484              implode(', ', $valid));
 485        }
 486      }
 487  
 488      $where[] = $this->buildPagingClause($conn_r);
 489  
 490      return $this->formatWhereClause($where);
 491    }
 492  
 493    public function didFilterResults(array $filtered) {
 494      if ($this->identifierMap) {
 495        foreach ($this->identifierMap as $name => $commit) {
 496          if (isset($filtered[$commit->getPHID()])) {
 497            unset($this->identifierMap[$name]);
 498          }
 499        }
 500      }
 501    }
 502  
 503    private function buildJoinClause($conn_r) {
 504      $joins = array();
 505      $audit_request = new PhabricatorRepositoryAuditRequest();
 506  
 507      if ($this->shouldJoinAudits()) {
 508        $joins[] = qsprintf(
 509          $conn_r,
 510          '%Q %T audit ON commit.phid = audit.commitPHID',
 511          ($this->rowsMustHaveAudits() ? 'JOIN' : 'LEFT JOIN'),
 512          $audit_request->getTableName());
 513      }
 514  
 515      if ($this->auditAwaitingUser) {
 516        // Join the request table on the awaiting user's requests, so we can
 517        // filter out package and project requests which the user has resigned
 518        // from.
 519        $joins[] = qsprintf(
 520          $conn_r,
 521          'LEFT JOIN %T awaiting ON audit.commitPHID = awaiting.commitPHID AND
 522          awaiting.auditorPHID = %s',
 523          $audit_request->getTableName(),
 524          $this->auditAwaitingUser->getPHID());
 525      }
 526  
 527      if ($joins) {
 528        return implode(' ', $joins);
 529      } else {
 530        return '';
 531      }
 532    }
 533  
 534    private function buildGroupClause(AphrontDatabaseConnection $conn_r) {
 535      $should_group = $this->shouldJoinAudits();
 536  
 537      // TODO: Currently, the audit table is missing a unique key, so we may
 538      // require a GROUP BY if we perform this join. See T1768. This can be
 539      // removed once the table has the key.
 540      if ($this->auditAwaitingUser) {
 541        $should_group = true;
 542      }
 543  
 544      if ($should_group) {
 545        return 'GROUP BY commit.id';
 546      } else {
 547        return '';
 548      }
 549    }
 550  
 551    public function getQueryApplicationClass() {
 552      return 'PhabricatorDiffusionApplication';
 553    }
 554  
 555  }


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