3 final class PhabricatorRepositoryQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery
{
12 private $datasourceQuery;
14 private $almanacServicePHIDs;
16 private $numericIdentifiers;
17 private $callsignIdentifiers;
18 private $phidIdentifiers;
19 private $monogramIdentifiers;
20 private $slugIdentifiers;
22 private $identifierMap;
24 const STATUS_OPEN
= 'status-open';
25 const STATUS_CLOSED
= 'status-closed';
26 const STATUS_ALL
= 'status-all';
27 private $status = self
::STATUS_ALL
;
29 const HOSTED_PHABRICATOR
= 'hosted-phab';
30 const HOSTED_REMOTE
= 'hosted-remote';
31 const HOSTED_ALL
= 'hosted-all';
32 private $hosted = self
::HOSTED_ALL
;
34 private $needMostRecentCommits;
35 private $needCommitCounts;
36 private $needProjectPHIDs;
38 private $needProfileImage;
40 public function withIDs(array $ids) {
45 public function withPHIDs(array $phids) {
46 $this->phids
= $phids;
50 public function withCallsigns(array $callsigns) {
51 $this->callsigns
= $callsigns;
55 public function withIdentifiers(array $identifiers) {
56 $identifiers = array_fuse($identifiers);
64 foreach ($identifiers as $identifier) {
65 if (ctype_digit((string)$identifier)) {
66 $ids[$identifier] = $identifier;
70 if (preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $identifier)) {
71 $monograms[$identifier] = $identifier;
75 $repository_type = PhabricatorRepositoryRepositoryPHIDType
::TYPECONST
;
76 if (phid_get_type($identifier) === $repository_type) {
77 $phids[$identifier] = $identifier;
81 if (preg_match('/^[A-Z]+\z/', $identifier)) {
82 $callsigns[$identifier] = $identifier;
86 $slugs[$identifier] = $identifier;
89 $this->numericIdentifiers
= $ids;
90 $this->callsignIdentifiers
= $callsigns;
91 $this->phidIdentifiers
= $phids;
92 $this->monogramIdentifiers
= $monograms;
93 $this->slugIdentifiers
= $slugs;
98 public function withStatus($status) {
99 $this->status
= $status;
103 public function withHosted($hosted) {
104 $this->hosted
= $hosted;
108 public function withTypes(array $types) {
109 $this->types
= $types;
113 public function withUUIDs(array $uuids) {
114 $this->uuids
= $uuids;
118 public function withURIs(array $uris) {
123 public function withDatasourceQuery($query) {
124 $this->datasourceQuery
= $query;
128 public function withSlugs(array $slugs) {
129 $this->slugs
= $slugs;
133 public function withAlmanacServicePHIDs(array $phids) {
134 $this->almanacServicePHIDs
= $phids;
138 public function needCommitCounts($need_counts) {
139 $this->needCommitCounts
= $need_counts;
143 public function needMostRecentCommits($need_commits) {
144 $this->needMostRecentCommits
= $need_commits;
148 public function needProjectPHIDs($need_phids) {
149 $this->needProjectPHIDs
= $need_phids;
153 public function needURIs($need_uris) {
154 $this->needURIs
= $need_uris;
158 public function needProfileImage($need) {
159 $this->needProfileImage
= $need;
163 public function getBuiltinOrders() {
165 'committed' => array(
166 'vector' => array('committed', 'id'),
167 'name' => pht('Most Recent Commit'),
170 'vector' => array('name', 'id'),
171 'name' => pht('Name'),
174 'vector' => array('callsign'),
175 'name' => pht('Callsign'),
178 'vector' => array('size', 'id'),
179 'name' => pht('Size'),
181 ) + parent
::getBuiltinOrders();
184 public function getIdentifierMap() {
185 if ($this->identifierMap
=== null) {
186 throw new PhutilInvalidStateException('execute');
188 return $this->identifierMap
;
191 protected function willExecute() {
192 $this->identifierMap
= array();
195 public function newResultObject() {
196 return new PhabricatorRepository();
199 protected function loadPage() {
200 $table = $this->newResultObject();
201 $data = $this->loadStandardPageRows($table);
202 $repositories = $table->loadAllFromArray($data);
204 if ($this->needCommitCounts
) {
205 $sizes = ipull($data, 'size', 'id');
206 foreach ($repositories as $id => $repository) {
207 $repository->attachCommitCount(nonempty($sizes[$id], 0));
211 if ($this->needMostRecentCommits
) {
212 $commit_ids = ipull($data, 'lastCommitID', 'id');
213 $commit_ids = array_filter($commit_ids);
215 $commits = id(new DiffusionCommitQuery())
216 ->setViewer($this->getViewer())
217 ->withIDs($commit_ids)
218 ->needCommitData(true)
219 ->needIdentities(true)
224 foreach ($repositories as $id => $repository) {
226 if (idx($commit_ids, $id)) {
227 $commit = idx($commits, $commit_ids[$id]);
229 $repository->attachMostRecentCommit($commit);
233 return $repositories;
236 protected function willFilterPage(array $repositories) {
237 assert_instances_of($repositories, 'PhabricatorRepository');
239 // TODO: Denormalize repository status into the PhabricatorRepository
240 // table so we can do this filtering in the database.
241 foreach ($repositories as $key => $repo) {
242 $status = $this->status
;
244 case self
::STATUS_OPEN
:
245 if (!$repo->isTracked()) {
246 unset($repositories[$key]);
249 case self
::STATUS_CLOSED
:
250 if ($repo->isTracked()) {
251 unset($repositories[$key]);
254 case self
::STATUS_ALL
:
257 throw new Exception("Unknown status '{$status}'!");
260 // TODO: This should also be denormalized.
261 $hosted = $this->hosted
;
263 case self
::HOSTED_PHABRICATOR
:
264 if (!$repo->isHosted()) {
265 unset($repositories[$key]);
268 case self
::HOSTED_REMOTE
:
269 if ($repo->isHosted()) {
270 unset($repositories[$key]);
273 case self
::HOSTED_ALL
:
276 throw new Exception(pht("Unknown hosted failed '%s'!", $hosted));
280 // Build the identifierMap
281 if ($this->numericIdentifiers
) {
282 foreach ($this->numericIdentifiers
as $id) {
283 if (isset($repositories[$id])) {
284 $this->identifierMap
[$id] = $repositories[$id];
289 if ($this->callsignIdentifiers
) {
290 $repository_callsigns = mpull($repositories, null, 'getCallsign');
292 foreach ($this->callsignIdentifiers
as $callsign) {
293 if (isset($repository_callsigns[$callsign])) {
294 $this->identifierMap
[$callsign] = $repository_callsigns[$callsign];
299 if ($this->phidIdentifiers
) {
300 $repository_phids = mpull($repositories, null, 'getPHID');
302 foreach ($this->phidIdentifiers
as $phid) {
303 if (isset($repository_phids[$phid])) {
304 $this->identifierMap
[$phid] = $repository_phids[$phid];
309 if ($this->monogramIdentifiers
) {
310 $monogram_map = array();
311 foreach ($repositories as $repository) {
312 foreach ($repository->getAllMonograms() as $monogram) {
313 $monogram_map[$monogram] = $repository;
317 foreach ($this->monogramIdentifiers
as $monogram) {
318 if (isset($monogram_map[$monogram])) {
319 $this->identifierMap
[$monogram] = $monogram_map[$monogram];
324 if ($this->slugIdentifiers
) {
326 foreach ($repositories as $repository) {
327 $slug = $repository->getRepositorySlug();
328 if ($slug === null) {
332 $normal = phutil_utf8_strtolower($slug);
333 $slug_map[$normal] = $repository;
336 foreach ($this->slugIdentifiers
as $slug) {
337 $normal = phutil_utf8_strtolower($slug);
338 if (isset($slug_map[$normal])) {
339 $this->identifierMap
[$slug] = $slug_map[$normal];
344 return $repositories;
347 protected function didFilterPage(array $repositories) {
348 if ($this->needProjectPHIDs
) {
349 $type_project = PhabricatorProjectObjectHasProjectEdgeType
::EDGECONST
;
351 $edge_query = id(new PhabricatorEdgeQuery())
352 ->withSourcePHIDs(mpull($repositories, 'getPHID'))
353 ->withEdgeTypes(array($type_project));
354 $edge_query->execute();
356 foreach ($repositories as $repository) {
357 $project_phids = $edge_query->getDestinationPHIDs(
359 $repository->getPHID(),
361 $repository->attachProjectPHIDs($project_phids);
365 $viewer = $this->getViewer();
367 if ($this->needURIs
) {
368 $uris = id(new PhabricatorRepositoryURIQuery())
370 ->withRepositories($repositories)
372 $uri_groups = mgroup($uris, 'getRepositoryPHID');
373 foreach ($repositories as $repository) {
374 $repository_uris = idx($uri_groups, $repository->getPHID(), array());
375 $repository->attachURIs($repository_uris);
379 if ($this->needProfileImage
) {
382 $file_phids = mpull($repositories, 'getProfileImagePHID');
383 $file_phids = array_filter($file_phids);
385 $files = id(new PhabricatorFileQuery())
386 ->setParentQuery($this)
387 ->setViewer($this->getViewer())
388 ->withPHIDs($file_phids)
390 $files = mpull($files, null, 'getPHID');
395 foreach ($repositories as $repository) {
396 $file = idx($files, $repository->getProfileImagePHID());
399 $default = PhabricatorFile
::loadBuiltin(
405 $repository->attachProfileImageFile($file);
409 return $repositories;
412 protected function getPrimaryTableAlias() {
416 public function getOrderableColumns() {
417 return parent
::getOrderableColumns() +
array(
418 'committed' => array(
426 'column' => 'callsign',
447 protected function newPagingMapFromCursorObject(
448 PhabricatorQueryCursor
$cursor,
451 $repository = $cursor->getObject();
454 'id' => (int)$repository->getID(),
455 'callsign' => $repository->getCallsign(),
456 'name' => $repository->getName(),
459 if (isset($keys['committed'])) {
460 $map['committed'] = $cursor->getRawRowProperty('epoch');
463 if (isset($keys['size'])) {
464 $map['size'] = $cursor->getRawRowProperty('size');
470 protected function buildSelectClauseParts(AphrontDatabaseConnection
$conn) {
471 $parts = parent
::buildSelectClauseParts($conn);
473 if ($this->shouldJoinSummaryTable()) {
474 $parts[] = qsprintf($conn, 's.*');
480 protected function buildJoinClauseParts(AphrontDatabaseConnection
$conn) {
481 $joins = parent
::buildJoinClauseParts($conn);
483 if ($this->shouldJoinSummaryTable()) {
486 'LEFT JOIN %T s ON r.id = s.repositoryID',
487 PhabricatorRepository
::TABLE_SUMMARY
);
490 if ($this->shouldJoinURITable()) {
493 'LEFT JOIN %R uri ON r.phid = uri.repositoryPHID',
494 new PhabricatorRepositoryURIIndex());
500 protected function shouldGroupQueryResultRows() {
501 if ($this->shouldJoinURITable()) {
505 return parent
::shouldGroupQueryResultRows();
508 private function shouldJoinURITable() {
509 return ($this->uris
!== null);
512 private function shouldJoinSummaryTable() {
513 if ($this->needCommitCounts
) {
517 if ($this->needMostRecentCommits
) {
521 $vector = $this->getOrderVector();
522 if ($vector->containsKey('committed')) {
526 if ($vector->containsKey('size')) {
533 protected function buildWhereClauseParts(AphrontDatabaseConnection
$conn) {
534 $where = parent
::buildWhereClauseParts($conn);
536 if ($this->ids
!== null) {
543 if ($this->phids
!== null) {
550 if ($this->callsigns
!== null) {
553 'r.callsign IN (%Ls)',
557 if ($this->numericIdentifiers ||
558 $this->callsignIdentifiers ||
559 $this->phidIdentifiers ||
560 $this->monogramIdentifiers ||
561 $this->slugIdentifiers
) {
562 $identifier_clause = array();
564 if ($this->numericIdentifiers
) {
565 $identifier_clause[] = qsprintf(
568 $this->numericIdentifiers
);
571 if ($this->callsignIdentifiers
) {
572 $identifier_clause[] = qsprintf(
574 'r.callsign IN (%Ls)',
575 $this->callsignIdentifiers
);
578 if ($this->phidIdentifiers
) {
579 $identifier_clause[] = qsprintf(
582 $this->phidIdentifiers
);
585 if ($this->monogramIdentifiers
) {
586 $monogram_callsigns = array();
587 $monogram_ids = array();
589 foreach ($this->monogramIdentifiers
as $identifier) {
590 if ($identifier[0] == 'r') {
591 $monogram_callsigns[] = substr($identifier, 1);
593 $monogram_ids[] = substr($identifier, 1);
598 $identifier_clause[] = qsprintf(
604 if ($monogram_callsigns) {
605 $identifier_clause[] = qsprintf(
607 'r.callsign IN (%Ls)',
608 $monogram_callsigns);
612 if ($this->slugIdentifiers
) {
613 $identifier_clause[] = qsprintf(
615 'r.repositorySlug IN (%Ls)',
616 $this->slugIdentifiers
);
619 $where[] = qsprintf($conn, '%LO', $identifier_clause);
625 'r.versionControlSystem IN (%Ls)',
636 if (phutil_nonempty_string($this->datasourceQuery
)) {
637 // This handles having "rP" match callsigns starting with "P...".
638 $query = trim($this->datasourceQuery
);
639 if (preg_match('/^r/', $query)) {
640 $callsign = substr($query, 1);
646 'r.name LIKE %> OR r.callsign LIKE %> OR r.repositorySlug LIKE %>',
652 if ($this->slugs
!== null) {
655 'r.repositorySlug IN (%Ls)',
659 if ($this->uris
!== null) {
660 $try_uris = $this->getNormalizedURIs();
661 $try_uris = array_fuse($try_uris);
665 'uri.repositoryURI IN (%Ls)',
669 if ($this->almanacServicePHIDs
!== null) {
672 'r.almanacServicePHID IN (%Ls)',
673 $this->almanacServicePHIDs
);
679 public function getQueryApplicationClass() {
680 return 'PhabricatorDiffusionApplication';
683 private function getNormalizedURIs() {
684 $normalized_uris = array();
686 // Since we don't know which type of repository this URI is in the general
687 // case, just generate all the normalizations. We could refine this in some
688 // cases: if the query specifies VCS types, or the URI is a git-style URI
689 // or an `svn+ssh` URI, we could deduce how to normalize it. However, this
690 // would be more complicated and it's not clear if it matters in practice.
692 $domain_map = PhabricatorRepositoryURI
::getURINormalizerDomainMap();
694 $types = ArcanistRepositoryURINormalizer
::getAllURITypes();
695 foreach ($this->uris
as $uri) {
696 foreach ($types as $type) {
697 $normalized_uri = new ArcanistRepositoryURINormalizer($type, $uri);
698 $normalized_uri->setDomainMap($domain_map);
699 $normalized_uris[] = $normalized_uri->getNormalizedURI();
703 return array_unique($normalized_uris);