@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhabricatorRepository>
5 */
6final class PhabricatorRepositoryQuery
7 extends PhabricatorCursorPagedPolicyAwareQuery {
8
9 private $ids;
10 private $phids;
11 private $callsigns;
12 private $types;
13 private $uuids;
14 private $uris;
15 private $slugs;
16 private $almanacServicePHIDs;
17
18 private $numericIdentifiers;
19 private $callsignIdentifiers;
20 private $phidIdentifiers;
21 private $monogramIdentifiers;
22 private $slugIdentifiers;
23
24 private $identifierMap;
25
26 const STATUS_OPEN = 'status-open';
27 const STATUS_CLOSED = 'status-closed';
28 const STATUS_ALL = 'status-all';
29 private $status = self::STATUS_ALL;
30
31 const HOSTED_PHABRICATOR = 'hosted-phab';
32 const HOSTED_REMOTE = 'hosted-remote';
33 const HOSTED_ALL = 'hosted-all';
34 private $hosted = self::HOSTED_ALL;
35
36 private $needMostRecentCommits;
37 private $needCommitCounts;
38 private $needProjectPHIDs;
39 private $needURIs;
40 private $needProfileImage;
41
42 public function withIDs(array $ids) {
43 $this->ids = $ids;
44 return $this;
45 }
46
47 public function withPHIDs(array $phids) {
48 $this->phids = $phids;
49 return $this;
50 }
51
52 public function withCallsigns(array $callsigns) {
53 $this->callsigns = $callsigns;
54 return $this;
55 }
56
57 public function withIdentifiers(array $identifiers) {
58 $identifiers = array_fuse($identifiers);
59
60 $ids = array();
61 $callsigns = array();
62 $phids = array();
63 $monograms = array();
64 $slugs = array();
65
66 foreach ($identifiers as $identifier) {
67 if ($identifier === null) {
68 continue;
69 }
70
71 if (ctype_digit((string)$identifier)) {
72 $ids[$identifier] = $identifier;
73 continue;
74 }
75
76 if (preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $identifier)) {
77 $monograms[$identifier] = $identifier;
78 continue;
79 }
80
81 $repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST;
82 if (phid_get_type($identifier) === $repository_type) {
83 $phids[$identifier] = $identifier;
84 continue;
85 }
86
87 if (preg_match('/^[A-Z]+\z/', $identifier)) {
88 $callsigns[$identifier] = $identifier;
89 continue;
90 }
91
92 $slugs[$identifier] = $identifier;
93 }
94
95 $this->numericIdentifiers = $ids;
96 $this->callsignIdentifiers = $callsigns;
97 $this->phidIdentifiers = $phids;
98 $this->monogramIdentifiers = $monograms;
99 $this->slugIdentifiers = $slugs;
100
101 return $this;
102 }
103
104 public function withStatus($status) {
105 $this->status = $status;
106 return $this;
107 }
108
109 public function withHosted($hosted) {
110 $this->hosted = $hosted;
111 return $this;
112 }
113
114 public function withTypes(array $types) {
115 $this->types = $types;
116 return $this;
117 }
118
119 public function withUUIDs(array $uuids) {
120 $this->uuids = $uuids;
121 return $this;
122 }
123
124 public function withURIs(array $uris) {
125 $this->uris = $uris;
126 return $this;
127 }
128
129 public function withSlugs(array $slugs) {
130 $this->slugs = $slugs;
131 return $this;
132 }
133
134 public function withAlmanacServicePHIDs(array $phids) {
135 $this->almanacServicePHIDs = $phids;
136 return $this;
137 }
138
139 public function needCommitCounts($need_counts) {
140 $this->needCommitCounts = $need_counts;
141 return $this;
142 }
143
144 public function needMostRecentCommits($need_commits) {
145 $this->needMostRecentCommits = $need_commits;
146 return $this;
147 }
148
149 public function needProjectPHIDs($need_phids) {
150 $this->needProjectPHIDs = $need_phids;
151 return $this;
152 }
153
154 public function needURIs($need_uris) {
155 $this->needURIs = $need_uris;
156 return $this;
157 }
158
159 public function needProfileImage($need) {
160 $this->needProfileImage = $need;
161 return $this;
162 }
163
164 public function getBuiltinOrders() {
165 return array(
166 'committed' => array(
167 'vector' => array('committed', 'id'),
168 'name' => pht('Most Recent Commit'),
169 ),
170 'name' => array(
171 'vector' => array('name', 'id'),
172 'name' => pht('Name'),
173 ),
174 'callsign' => array(
175 'vector' => array('callsign'),
176 'name' => pht('Callsign'),
177 ),
178 'size' => array(
179 'vector' => array('size', 'id'),
180 'name' => pht('Size'),
181 ),
182 ) + parent::getBuiltinOrders();
183 }
184
185 public function getIdentifierMap() {
186 if ($this->identifierMap === null) {
187 throw new PhutilInvalidStateException('execute');
188 }
189 return $this->identifierMap;
190 }
191
192 protected function willExecute() {
193 $this->identifierMap = array();
194 }
195
196 public function newResultObject() {
197 return new PhabricatorRepository();
198 }
199
200 protected function loadPage() {
201 $table = $this->newResultObject();
202 $data = $this->loadStandardPageRows($table);
203 $repositories = $table->loadAllFromArray($data);
204
205 if ($this->needCommitCounts) {
206 $sizes = ipull($data, 'size', 'id');
207 foreach ($repositories as $id => $repository) {
208 $repository->attachCommitCount(nonempty($sizes[$id], 0));
209 }
210 }
211
212 if ($this->needMostRecentCommits) {
213 $commit_ids = ipull($data, 'lastCommitID', 'id');
214 $commit_ids = array_filter($commit_ids);
215 if ($commit_ids) {
216 $commits = id(new DiffusionCommitQuery())
217 ->setViewer($this->getViewer())
218 ->withIDs($commit_ids)
219 ->needCommitData(true)
220 ->needIdentities(true)
221 ->execute();
222 } else {
223 $commits = array();
224 }
225 foreach ($repositories as $id => $repository) {
226 $commit = null;
227 if (idx($commit_ids, $id)) {
228 $commit = idx($commits, $commit_ids[$id]);
229 }
230 $repository->attachMostRecentCommit($commit);
231 }
232 }
233
234 return $repositories;
235 }
236
237 /**
238 * @param array<PhabricatorRepository> $repositories
239 */
240 protected function willFilterPage(array $repositories) {
241 assert_instances_of($repositories, PhabricatorRepository::class);
242
243 // TODO: Denormalize repository status into the PhabricatorRepository
244 // table so we can do this filtering in the database.
245 foreach ($repositories as $key => $repo) {
246 $status = $this->status;
247 switch ($status) {
248 case self::STATUS_OPEN:
249 if (!$repo->isTracked()) {
250 unset($repositories[$key]);
251 }
252 break;
253 case self::STATUS_CLOSED:
254 if ($repo->isTracked()) {
255 unset($repositories[$key]);
256 }
257 break;
258 case self::STATUS_ALL:
259 break;
260 default:
261 throw new Exception("Unknown status '{$status}'!");
262 }
263
264 // TODO: This should also be denormalized.
265 $hosted = $this->hosted;
266 switch ($hosted) {
267 case self::HOSTED_PHABRICATOR:
268 if (!$repo->isHosted()) {
269 unset($repositories[$key]);
270 }
271 break;
272 case self::HOSTED_REMOTE:
273 if ($repo->isHosted()) {
274 unset($repositories[$key]);
275 }
276 break;
277 case self::HOSTED_ALL:
278 break;
279 default:
280 throw new Exception(pht("Unknown hosted failed '%s'!", $hosted));
281 }
282 }
283
284 // Build the identifierMap
285 if ($this->numericIdentifiers) {
286 foreach ($this->numericIdentifiers as $id) {
287 if (isset($repositories[$id])) {
288 $this->identifierMap[$id] = $repositories[$id];
289 }
290 }
291 }
292
293 if ($this->callsignIdentifiers) {
294 $repository_callsigns = mpull($repositories, null, 'getCallsign');
295
296 foreach ($this->callsignIdentifiers as $callsign) {
297 if (isset($repository_callsigns[$callsign])) {
298 $this->identifierMap[$callsign] = $repository_callsigns[$callsign];
299 }
300 }
301 }
302
303 if ($this->phidIdentifiers) {
304 $repository_phids = mpull($repositories, null, 'getPHID');
305
306 foreach ($this->phidIdentifiers as $phid) {
307 if (isset($repository_phids[$phid])) {
308 $this->identifierMap[$phid] = $repository_phids[$phid];
309 }
310 }
311 }
312
313 if ($this->monogramIdentifiers) {
314 $monogram_map = array();
315 foreach ($repositories as $repository) {
316 foreach ($repository->getAllMonograms() as $monogram) {
317 $monogram_map[$monogram] = $repository;
318 }
319 }
320
321 foreach ($this->monogramIdentifiers as $monogram) {
322 if (isset($monogram_map[$monogram])) {
323 $this->identifierMap[$monogram] = $monogram_map[$monogram];
324 }
325 }
326 }
327
328 if ($this->slugIdentifiers) {
329 $slug_map = array();
330 foreach ($repositories as $repository) {
331 $slug = $repository->getRepositorySlug();
332 if ($slug === null) {
333 continue;
334 }
335
336 $normal = phutil_utf8_strtolower($slug);
337 $slug_map[$normal] = $repository;
338 }
339
340 foreach ($this->slugIdentifiers as $slug) {
341 $normal = phutil_utf8_strtolower($slug);
342 if (isset($slug_map[$normal])) {
343 $this->identifierMap[$slug] = $slug_map[$normal];
344 }
345 }
346 }
347
348 return $repositories;
349 }
350
351 protected function didFilterPage(array $repositories) {
352 if ($this->needProjectPHIDs) {
353 $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
354
355 $edge_query = id(new PhabricatorEdgeQuery())
356 ->withSourcePHIDs(mpull($repositories, 'getPHID'))
357 ->withEdgeTypes(array($type_project));
358 $edge_query->execute();
359
360 foreach ($repositories as $repository) {
361 $project_phids = $edge_query->getDestinationPHIDs(
362 array(
363 $repository->getPHID(),
364 ));
365 $repository->attachProjectPHIDs($project_phids);
366 }
367 }
368
369 $viewer = $this->getViewer();
370
371 if ($this->needURIs) {
372 $uris = id(new PhabricatorRepositoryURIQuery())
373 ->setViewer($viewer)
374 ->withRepositories($repositories)
375 ->execute();
376 $uri_groups = mgroup($uris, 'getRepositoryPHID');
377 foreach ($repositories as $repository) {
378 $repository_uris = idx($uri_groups, $repository->getPHID(), array());
379 $repository->attachURIs($repository_uris);
380 }
381 }
382
383 if ($this->needProfileImage) {
384 $default = null;
385
386 $file_phids = mpull($repositories, 'getProfileImagePHID');
387 $file_phids = array_filter($file_phids);
388 if ($file_phids) {
389 $files = id(new PhabricatorFileQuery())
390 ->setParentQuery($this)
391 ->setViewer($this->getViewer())
392 ->withPHIDs($file_phids)
393 ->execute();
394 $files = mpull($files, null, 'getPHID');
395 } else {
396 $files = array();
397 }
398
399 foreach ($repositories as $repository) {
400 $file = idx($files, $repository->getProfileImagePHID());
401 if (!$file) {
402 if (!$default) {
403 $default = PhabricatorFile::loadBuiltin(
404 $this->getViewer(),
405 'repo/code.png');
406 }
407 $file = $default;
408 }
409 $repository->attachProfileImageFile($file);
410 }
411 }
412
413 return $repositories;
414 }
415
416 protected function getPrimaryTableAlias() {
417 return 'r';
418 }
419
420 public function getOrderableColumns() {
421 return parent::getOrderableColumns() + array(
422 'committed' => array(
423 'table' => 's',
424 'column' => 'epoch',
425 'type' => 'int',
426 'null' => 'tail',
427 ),
428 'callsign' => array(
429 'table' => 'r',
430 'column' => 'callsign',
431 'type' => 'string',
432 'unique' => true,
433 'reverse' => true,
434 'null' => 'tail',
435 ),
436 'name' => array(
437 'table' => 'r',
438 'column' => 'name',
439 'type' => 'string',
440 'reverse' => true,
441 ),
442 'size' => array(
443 'table' => 's',
444 'column' => 'size',
445 'type' => 'int',
446 'null' => 'tail',
447 ),
448 );
449 }
450
451 protected function newPagingMapFromCursorObject(
452 PhabricatorQueryCursor $cursor,
453 array $keys) {
454
455 $repository = $cursor->getObject();
456
457 $map = array(
458 'id' => (int)$repository->getID(),
459 'callsign' => $repository->getCallsign(),
460 'name' => $repository->getName(),
461 );
462
463 if (isset($keys['committed'])) {
464 $map['committed'] = $cursor->getRawRowProperty('epoch');
465 }
466
467 if (isset($keys['size'])) {
468 $map['size'] = $cursor->getRawRowProperty('size');
469 }
470
471 return $map;
472 }
473
474 protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
475 $parts = parent::buildSelectClauseParts($conn);
476
477 if ($this->shouldJoinSummaryTable()) {
478 $parts[] = qsprintf($conn, 's.*');
479 }
480
481 return $parts;
482 }
483
484 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
485 $joins = parent::buildJoinClauseParts($conn);
486
487 if ($this->shouldJoinSummaryTable()) {
488 $joins[] = qsprintf(
489 $conn,
490 'LEFT JOIN %T s ON r.id = s.repositoryID',
491 PhabricatorRepository::TABLE_SUMMARY);
492 }
493
494 if ($this->shouldJoinURITable()) {
495 $joins[] = qsprintf(
496 $conn,
497 'LEFT JOIN %R uri ON r.phid = uri.repositoryPHID',
498 new PhabricatorRepositoryURIIndex());
499 }
500
501 return $joins;
502 }
503
504 protected function shouldGroupQueryResultRows() {
505 if ($this->shouldJoinURITable()) {
506 return true;
507 }
508
509 return parent::shouldGroupQueryResultRows();
510 }
511
512 private function shouldJoinURITable() {
513 return ($this->uris !== null);
514 }
515
516 private function shouldJoinSummaryTable() {
517 if ($this->needCommitCounts) {
518 return true;
519 }
520
521 if ($this->needMostRecentCommits) {
522 return true;
523 }
524
525 $vector = $this->getOrderVector();
526 if ($vector->containsKey('committed')) {
527 return true;
528 }
529
530 if ($vector->containsKey('size')) {
531 return true;
532 }
533
534 return false;
535 }
536
537 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
538 $where = parent::buildWhereClauseParts($conn);
539
540 if ($this->ids !== null) {
541 $where[] = qsprintf(
542 $conn,
543 'r.id IN (%Ld)',
544 $this->ids);
545 }
546
547 if ($this->phids !== null) {
548 $where[] = qsprintf(
549 $conn,
550 'r.phid IN (%Ls)',
551 $this->phids);
552 }
553
554 if ($this->callsigns !== null) {
555 $where[] = qsprintf(
556 $conn,
557 'r.callsign IN (%Ls)',
558 $this->callsigns);
559 }
560
561 if ($this->numericIdentifiers ||
562 $this->callsignIdentifiers ||
563 $this->phidIdentifiers ||
564 $this->monogramIdentifiers ||
565 $this->slugIdentifiers) {
566 $identifier_clause = array();
567
568 if ($this->numericIdentifiers) {
569 $identifier_clause[] = qsprintf(
570 $conn,
571 'r.id IN (%Ld)',
572 $this->numericIdentifiers);
573 }
574
575 if ($this->callsignIdentifiers) {
576 $identifier_clause[] = qsprintf(
577 $conn,
578 'r.callsign IN (%Ls)',
579 $this->callsignIdentifiers);
580 }
581
582 if ($this->phidIdentifiers) {
583 $identifier_clause[] = qsprintf(
584 $conn,
585 'r.phid IN (%Ls)',
586 $this->phidIdentifiers);
587 }
588
589 if ($this->monogramIdentifiers) {
590 $monogram_callsigns = array();
591 $monogram_ids = array();
592
593 foreach ($this->monogramIdentifiers as $identifier) {
594 if ($identifier[0] == 'r') {
595 $monogram_callsigns[] = substr($identifier, 1);
596 } else {
597 $monogram_ids[] = substr($identifier, 1);
598 }
599 }
600
601 if ($monogram_ids) {
602 $identifier_clause[] = qsprintf(
603 $conn,
604 'r.id IN (%Ld)',
605 $monogram_ids);
606 }
607
608 if ($monogram_callsigns) {
609 $identifier_clause[] = qsprintf(
610 $conn,
611 'r.callsign IN (%Ls)',
612 $monogram_callsigns);
613 }
614 }
615
616 if ($this->slugIdentifiers) {
617 $identifier_clause[] = qsprintf(
618 $conn,
619 'r.repositorySlug IN (%Ls)',
620 $this->slugIdentifiers);
621 }
622
623 $where[] = qsprintf($conn, '%LO', $identifier_clause);
624 }
625
626 if ($this->types) {
627 $where[] = qsprintf(
628 $conn,
629 'r.versionControlSystem IN (%Ls)',
630 $this->types);
631 }
632
633 if ($this->uuids) {
634 $where[] = qsprintf(
635 $conn,
636 'r.uuid IN (%Ls)',
637 $this->uuids);
638 }
639
640 if ($this->slugs !== null) {
641 $where[] = qsprintf(
642 $conn,
643 'r.repositorySlug IN (%Ls)',
644 $this->slugs);
645 }
646
647 if ($this->uris !== null) {
648 $try_uris = $this->getNormalizedURIs();
649 $try_uris = array_fuse($try_uris);
650
651 $where[] = qsprintf(
652 $conn,
653 'uri.repositoryURI IN (%Ls)',
654 $try_uris);
655 }
656
657 if ($this->almanacServicePHIDs !== null) {
658 $where[] = qsprintf(
659 $conn,
660 'r.almanacServicePHID IN (%Ls)',
661 $this->almanacServicePHIDs);
662 }
663
664 return $where;
665 }
666
667 public function getQueryApplicationClass() {
668 return PhabricatorDiffusionApplication::class;
669 }
670
671 private function getNormalizedURIs() {
672 $normalized_uris = array();
673
674 // Since we don't know which type of repository this URI is in the general
675 // case, just generate all the normalizations. We could refine this in some
676 // cases: if the query specifies VCS types, or the URI is a git-style URI
677 // or an `svn+ssh` URI, we could deduce how to normalize it. However, this
678 // would be more complicated and it's not clear if it matters in practice.
679
680 $domain_map = PhabricatorRepositoryURI::getURINormalizerDomainMap();
681
682 $types = ArcanistRepositoryURINormalizer::getAllURITypes();
683 foreach ($this->uris as $uri) {
684 foreach ($types as $type) {
685 $normalized_uri = new ArcanistRepositoryURINormalizer($type, $uri);
686 $normalized_uri->setDomainMap($domain_map);
687 $normalized_uris[] = $normalized_uri->getNormalizedURI();
688 }
689 }
690
691 return array_unique($normalized_uris);
692 }
693
694}