Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / repository / query / PhabricatorRepositoryQuery.php
blob05b011e85a152fd9b4d67808951e7378a130933c
1 <?php
3 final class PhabricatorRepositoryQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery {
6 private $ids;
7 private $phids;
8 private $callsigns;
9 private $types;
10 private $uuids;
11 private $uris;
12 private $datasourceQuery;
13 private $slugs;
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;
37 private $needURIs;
38 private $needProfileImage;
40 public function withIDs(array $ids) {
41 $this->ids = $ids;
42 return $this;
45 public function withPHIDs(array $phids) {
46 $this->phids = $phids;
47 return $this;
50 public function withCallsigns(array $callsigns) {
51 $this->callsigns = $callsigns;
52 return $this;
55 public function withIdentifiers(array $identifiers) {
56 $identifiers = array_fuse($identifiers);
58 $ids = array();
59 $callsigns = array();
60 $phids = array();
61 $monograms = array();
62 $slugs = array();
64 foreach ($identifiers as $identifier) {
65 if (ctype_digit((string)$identifier)) {
66 $ids[$identifier] = $identifier;
67 continue;
70 if (preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $identifier)) {
71 $monograms[$identifier] = $identifier;
72 continue;
75 $repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST;
76 if (phid_get_type($identifier) === $repository_type) {
77 $phids[$identifier] = $identifier;
78 continue;
81 if (preg_match('/^[A-Z]+\z/', $identifier)) {
82 $callsigns[$identifier] = $identifier;
83 continue;
86 $slugs[$identifier] = $identifier;
89 $this->numericIdentifiers = $ids;
90 $this->callsignIdentifiers = $callsigns;
91 $this->phidIdentifiers = $phids;
92 $this->monogramIdentifiers = $monograms;
93 $this->slugIdentifiers = $slugs;
95 return $this;
98 public function withStatus($status) {
99 $this->status = $status;
100 return $this;
103 public function withHosted($hosted) {
104 $this->hosted = $hosted;
105 return $this;
108 public function withTypes(array $types) {
109 $this->types = $types;
110 return $this;
113 public function withUUIDs(array $uuids) {
114 $this->uuids = $uuids;
115 return $this;
118 public function withURIs(array $uris) {
119 $this->uris = $uris;
120 return $this;
123 public function withDatasourceQuery($query) {
124 $this->datasourceQuery = $query;
125 return $this;
128 public function withSlugs(array $slugs) {
129 $this->slugs = $slugs;
130 return $this;
133 public function withAlmanacServicePHIDs(array $phids) {
134 $this->almanacServicePHIDs = $phids;
135 return $this;
138 public function needCommitCounts($need_counts) {
139 $this->needCommitCounts = $need_counts;
140 return $this;
143 public function needMostRecentCommits($need_commits) {
144 $this->needMostRecentCommits = $need_commits;
145 return $this;
148 public function needProjectPHIDs($need_phids) {
149 $this->needProjectPHIDs = $need_phids;
150 return $this;
153 public function needURIs($need_uris) {
154 $this->needURIs = $need_uris;
155 return $this;
158 public function needProfileImage($need) {
159 $this->needProfileImage = $need;
160 return $this;
163 public function getBuiltinOrders() {
164 return array(
165 'committed' => array(
166 'vector' => array('committed', 'id'),
167 'name' => pht('Most Recent Commit'),
169 'name' => array(
170 'vector' => array('name', 'id'),
171 'name' => pht('Name'),
173 'callsign' => array(
174 'vector' => array('callsign'),
175 'name' => pht('Callsign'),
177 'size' => array(
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);
214 if ($commit_ids) {
215 $commits = id(new DiffusionCommitQuery())
216 ->setViewer($this->getViewer())
217 ->withIDs($commit_ids)
218 ->needCommitData(true)
219 ->needIdentities(true)
220 ->execute();
221 } else {
222 $commits = array();
224 foreach ($repositories as $id => $repository) {
225 $commit = null;
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;
243 switch ($status) {
244 case self::STATUS_OPEN:
245 if (!$repo->isTracked()) {
246 unset($repositories[$key]);
248 break;
249 case self::STATUS_CLOSED:
250 if ($repo->isTracked()) {
251 unset($repositories[$key]);
253 break;
254 case self::STATUS_ALL:
255 break;
256 default:
257 throw new Exception("Unknown status '{$status}'!");
260 // TODO: This should also be denormalized.
261 $hosted = $this->hosted;
262 switch ($hosted) {
263 case self::HOSTED_PHABRICATOR:
264 if (!$repo->isHosted()) {
265 unset($repositories[$key]);
267 break;
268 case self::HOSTED_REMOTE:
269 if ($repo->isHosted()) {
270 unset($repositories[$key]);
272 break;
273 case self::HOSTED_ALL:
274 break;
275 default:
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) {
325 $slug_map = array();
326 foreach ($repositories as $repository) {
327 $slug = $repository->getRepositorySlug();
328 if ($slug === null) {
329 continue;
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(
358 array(
359 $repository->getPHID(),
361 $repository->attachProjectPHIDs($project_phids);
365 $viewer = $this->getViewer();
367 if ($this->needURIs) {
368 $uris = id(new PhabricatorRepositoryURIQuery())
369 ->setViewer($viewer)
370 ->withRepositories($repositories)
371 ->execute();
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) {
380 $default = null;
382 $file_phids = mpull($repositories, 'getProfileImagePHID');
383 $file_phids = array_filter($file_phids);
384 if ($file_phids) {
385 $files = id(new PhabricatorFileQuery())
386 ->setParentQuery($this)
387 ->setViewer($this->getViewer())
388 ->withPHIDs($file_phids)
389 ->execute();
390 $files = mpull($files, null, 'getPHID');
391 } else {
392 $files = array();
395 foreach ($repositories as $repository) {
396 $file = idx($files, $repository->getProfileImagePHID());
397 if (!$file) {
398 if (!$default) {
399 $default = PhabricatorFile::loadBuiltin(
400 $this->getViewer(),
401 'repo/code.png');
403 $file = $default;
405 $repository->attachProfileImageFile($file);
409 return $repositories;
412 protected function getPrimaryTableAlias() {
413 return 'r';
416 public function getOrderableColumns() {
417 return parent::getOrderableColumns() + array(
418 'committed' => array(
419 'table' => 's',
420 'column' => 'epoch',
421 'type' => 'int',
422 'null' => 'tail',
424 'callsign' => array(
425 'table' => 'r',
426 'column' => 'callsign',
427 'type' => 'string',
428 'unique' => true,
429 'reverse' => true,
430 'null' => 'tail',
432 'name' => array(
433 'table' => 'r',
434 'column' => 'name',
435 'type' => 'string',
436 'reverse' => true,
438 'size' => array(
439 'table' => 's',
440 'column' => 'size',
441 'type' => 'int',
442 'null' => 'tail',
447 protected function newPagingMapFromCursorObject(
448 PhabricatorQueryCursor $cursor,
449 array $keys) {
451 $repository = $cursor->getObject();
453 $map = array(
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');
467 return $map;
470 protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
471 $parts = parent::buildSelectClauseParts($conn);
473 if ($this->shouldJoinSummaryTable()) {
474 $parts[] = qsprintf($conn, 's.*');
477 return $parts;
480 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
481 $joins = parent::buildJoinClauseParts($conn);
483 if ($this->shouldJoinSummaryTable()) {
484 $joins[] = qsprintf(
485 $conn,
486 'LEFT JOIN %T s ON r.id = s.repositoryID',
487 PhabricatorRepository::TABLE_SUMMARY);
490 if ($this->shouldJoinURITable()) {
491 $joins[] = qsprintf(
492 $conn,
493 'LEFT JOIN %R uri ON r.phid = uri.repositoryPHID',
494 new PhabricatorRepositoryURIIndex());
497 return $joins;
500 protected function shouldGroupQueryResultRows() {
501 if ($this->shouldJoinURITable()) {
502 return true;
505 return parent::shouldGroupQueryResultRows();
508 private function shouldJoinURITable() {
509 return ($this->uris !== null);
512 private function shouldJoinSummaryTable() {
513 if ($this->needCommitCounts) {
514 return true;
517 if ($this->needMostRecentCommits) {
518 return true;
521 $vector = $this->getOrderVector();
522 if ($vector->containsKey('committed')) {
523 return true;
526 if ($vector->containsKey('size')) {
527 return true;
530 return false;
533 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
534 $where = parent::buildWhereClauseParts($conn);
536 if ($this->ids !== null) {
537 $where[] = qsprintf(
538 $conn,
539 'r.id IN (%Ld)',
540 $this->ids);
543 if ($this->phids !== null) {
544 $where[] = qsprintf(
545 $conn,
546 'r.phid IN (%Ls)',
547 $this->phids);
550 if ($this->callsigns !== null) {
551 $where[] = qsprintf(
552 $conn,
553 'r.callsign IN (%Ls)',
554 $this->callsigns);
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(
566 $conn,
567 'r.id IN (%Ld)',
568 $this->numericIdentifiers);
571 if ($this->callsignIdentifiers) {
572 $identifier_clause[] = qsprintf(
573 $conn,
574 'r.callsign IN (%Ls)',
575 $this->callsignIdentifiers);
578 if ($this->phidIdentifiers) {
579 $identifier_clause[] = qsprintf(
580 $conn,
581 'r.phid IN (%Ls)',
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);
592 } else {
593 $monogram_ids[] = substr($identifier, 1);
597 if ($monogram_ids) {
598 $identifier_clause[] = qsprintf(
599 $conn,
600 'r.id IN (%Ld)',
601 $monogram_ids);
604 if ($monogram_callsigns) {
605 $identifier_clause[] = qsprintf(
606 $conn,
607 'r.callsign IN (%Ls)',
608 $monogram_callsigns);
612 if ($this->slugIdentifiers) {
613 $identifier_clause[] = qsprintf(
614 $conn,
615 'r.repositorySlug IN (%Ls)',
616 $this->slugIdentifiers);
619 $where[] = qsprintf($conn, '%LO', $identifier_clause);
622 if ($this->types) {
623 $where[] = qsprintf(
624 $conn,
625 'r.versionControlSystem IN (%Ls)',
626 $this->types);
629 if ($this->uuids) {
630 $where[] = qsprintf(
631 $conn,
632 'r.uuid IN (%Ls)',
633 $this->uuids);
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);
641 } else {
642 $callsign = $query;
644 $where[] = qsprintf(
645 $conn,
646 'r.name LIKE %> OR r.callsign LIKE %> OR r.repositorySlug LIKE %>',
647 $query,
648 $callsign,
649 $query);
652 if ($this->slugs !== null) {
653 $where[] = qsprintf(
654 $conn,
655 'r.repositorySlug IN (%Ls)',
656 $this->slugs);
659 if ($this->uris !== null) {
660 $try_uris = $this->getNormalizedURIs();
661 $try_uris = array_fuse($try_uris);
663 $where[] = qsprintf(
664 $conn,
665 'uri.repositoryURI IN (%Ls)',
666 $try_uris);
669 if ($this->almanacServicePHIDs !== null) {
670 $where[] = qsprintf(
671 $conn,
672 'r.almanacServicePHID IN (%Ls)',
673 $this->almanacServicePHIDs);
676 return $where;
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);