@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<PhabricatorOwnersPackage>
5 */
6final class PhabricatorOwnersPackageQuery
7 extends PhabricatorCursorPagedPolicyAwareQuery {
8
9 private $ids;
10 private $phids;
11 private $ownerPHIDs;
12 private $authorityPHIDs;
13 private $repositoryPHIDs;
14 private $paths;
15 private $statuses;
16 private $authorityModes;
17
18 private $controlMap = array();
19 private $controlResults;
20
21 private $needPaths;
22
23
24 /**
25 * Query owner PHIDs exactly. This does not expand authorities, so a user
26 * PHID will not match projects the user is a member of.
27 */
28 public function withOwnerPHIDs(array $phids) {
29 $this->ownerPHIDs = $phids;
30 return $this;
31 }
32
33 /**
34 * Query owner authority. This will expand authorities, so a user PHID will
35 * match both packages they own directly and packages owned by a project they
36 * are a member of.
37 */
38 public function withAuthorityPHIDs(array $phids) {
39 $this->authorityPHIDs = $phids;
40 return $this;
41 }
42
43 public function withPHIDs(array $phids) {
44 $this->phids = $phids;
45 return $this;
46 }
47
48 public function withIDs(array $ids) {
49 $this->ids = $ids;
50 return $this;
51 }
52
53 public function withRepositoryPHIDs(array $phids) {
54 $this->repositoryPHIDs = $phids;
55 return $this;
56 }
57
58 public function withPaths(array $paths) {
59 $this->paths = $paths;
60 return $this;
61 }
62
63 public function withStatuses(array $statuses) {
64 $this->statuses = $statuses;
65 return $this;
66 }
67
68 public function withControl($repository_phid, array $paths) {
69 if (empty($this->controlMap[$repository_phid])) {
70 $this->controlMap[$repository_phid] = array();
71 }
72
73 foreach ($paths as $path) {
74 $path = (string)$path;
75 $this->controlMap[$repository_phid][$path] = $path;
76 }
77
78 // We need to load paths to execute control queries.
79 $this->needPaths = true;
80
81 return $this;
82 }
83
84 public function withAuthorityModes(array $modes) {
85 $this->authorityModes = $modes;
86 return $this;
87 }
88
89 public function withNameNgrams($ngrams) {
90 return $this->withNgramsConstraint(
91 new PhabricatorOwnersPackageNameNgrams(),
92 $ngrams);
93 }
94
95 public function needPaths($need_paths) {
96 $this->needPaths = $need_paths;
97 return $this;
98 }
99
100 public function newResultObject() {
101 return new PhabricatorOwnersPackage();
102 }
103
104 protected function willExecute() {
105 $this->controlResults = array();
106 }
107
108 protected function willFilterPage(array $packages) {
109 $package_ids = mpull($packages, 'getID');
110
111 $owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
112 'packageID IN (%Ld)',
113 $package_ids);
114 $owners = mgroup($owners, 'getPackageID');
115 foreach ($packages as $package) {
116 $package->attachOwners(idx($owners, $package->getID(), array()));
117 }
118
119 return $packages;
120 }
121
122 protected function didFilterPage(array $packages) {
123 $package_ids = mpull($packages, 'getID');
124
125 if ($this->needPaths) {
126 $paths = id(new PhabricatorOwnersPath())->loadAllWhere(
127 'packageID IN (%Ld)',
128 $package_ids);
129 $paths = mgroup($paths, 'getPackageID');
130
131 foreach ($packages as $package) {
132 $package->attachPaths(idx($paths, $package->getID(), array()));
133 }
134 }
135
136 if ($this->controlMap) {
137 foreach ($packages as $package) {
138 // If this package is archived, it's no longer a controlling package
139 // for any path. In particular, it can not force active packages with
140 // weak dominion to give up control.
141 if ($package->isArchived()) {
142 continue;
143 }
144
145 $this->controlResults[$package->getID()] = $package;
146 }
147 }
148
149 return $packages;
150 }
151
152 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
153 $joins = parent::buildJoinClauseParts($conn);
154
155 if ($this->shouldJoinOwnersTable()) {
156 $joins[] = qsprintf(
157 $conn,
158 'JOIN %T o ON o.packageID = p.id',
159 id(new PhabricatorOwnersOwner())->getTableName());
160 }
161
162 if ($this->shouldJoinPathTable()) {
163 $joins[] = qsprintf(
164 $conn,
165 'JOIN %T rpath ON rpath.packageID = p.id',
166 id(new PhabricatorOwnersPath())->getTableName());
167 }
168
169 return $joins;
170 }
171
172 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
173 $where = parent::buildWhereClauseParts($conn);
174
175 if ($this->phids !== null) {
176 $where[] = qsprintf(
177 $conn,
178 'p.phid IN (%Ls)',
179 $this->phids);
180 }
181
182 if ($this->ids !== null) {
183 $where[] = qsprintf(
184 $conn,
185 'p.id IN (%Ld)',
186 $this->ids);
187 }
188
189 if ($this->repositoryPHIDs !== null) {
190 $where[] = qsprintf(
191 $conn,
192 'rpath.repositoryPHID IN (%Ls)',
193 $this->repositoryPHIDs);
194 }
195
196 if ($this->authorityPHIDs !== null) {
197 $authority_phids = $this->expandAuthority($this->authorityPHIDs);
198 $where[] = qsprintf(
199 $conn,
200 'o.userPHID IN (%Ls)',
201 $authority_phids);
202 }
203
204 if ($this->ownerPHIDs !== null) {
205 $where[] = qsprintf(
206 $conn,
207 'o.userPHID IN (%Ls)',
208 $this->ownerPHIDs);
209 }
210
211 if ($this->paths !== null) {
212 $where[] = qsprintf(
213 $conn,
214 'rpath.pathIndex IN (%Ls)',
215 $this->getFragmentIndexesForPaths($this->paths));
216 }
217
218 if ($this->statuses !== null) {
219 $where[] = qsprintf(
220 $conn,
221 'p.status IN (%Ls)',
222 $this->statuses);
223 }
224
225 if ($this->controlMap) {
226 $clauses = array();
227 foreach ($this->controlMap as $repository_phid => $paths) {
228 $indexes = $this->getFragmentIndexesForPaths($paths);
229
230 $clauses[] = qsprintf(
231 $conn,
232 '(rpath.repositoryPHID = %s AND rpath.pathIndex IN (%Ls))',
233 $repository_phid,
234 $indexes);
235 }
236 $where[] = qsprintf($conn, '%LO', $clauses);
237 }
238
239 if ($this->authorityModes !== null) {
240 $where[] = qsprintf(
241 $conn,
242 'authorityMode IN (%Ls)',
243 $this->authorityModes);
244 }
245
246 return $where;
247 }
248
249 protected function shouldGroupQueryResultRows() {
250 if ($this->shouldJoinOwnersTable()) {
251 return true;
252 }
253
254 if ($this->shouldJoinPathTable()) {
255 return true;
256 }
257
258 return parent::shouldGroupQueryResultRows();
259 }
260
261 public function getBuiltinOrders() {
262 return array(
263 'name' => array(
264 'vector' => array('name'),
265 'name' => pht('Name'),
266 ),
267 ) + parent::getBuiltinOrders();
268 }
269
270 public function getOrderableColumns() {
271 return parent::getOrderableColumns() + array(
272 'name' => array(
273 'table' => $this->getPrimaryTableAlias(),
274 'column' => 'name',
275 'type' => 'string',
276 'unique' => true,
277 'reverse' => true,
278 ),
279 );
280 }
281
282 protected function newPagingMapFromPartialObject($object) {
283 return array(
284 'id' => (int)$object->getID(),
285 'name' => $object->getName(),
286 );
287 }
288
289 public function getQueryApplicationClass() {
290 return PhabricatorOwnersApplication::class;
291 }
292
293 protected function getPrimaryTableAlias() {
294 return 'p';
295 }
296
297 private function shouldJoinOwnersTable() {
298 if ($this->ownerPHIDs !== null) {
299 return true;
300 }
301
302 if ($this->authorityPHIDs !== null) {
303 return true;
304 }
305
306 return false;
307 }
308
309 private function shouldJoinPathTable() {
310 if ($this->repositoryPHIDs !== null) {
311 return true;
312 }
313
314 if ($this->paths !== null) {
315 return true;
316 }
317
318 if ($this->controlMap) {
319 return true;
320 }
321
322 return false;
323 }
324
325 private function expandAuthority(array $phids) {
326 $projects = id(new PhabricatorProjectQuery())
327 ->setViewer($this->getViewer())
328 ->withMemberPHIDs($phids)
329 ->execute();
330 $project_phids = mpull($projects, 'getPHID');
331
332 return array_fuse($phids) + array_fuse($project_phids);
333 }
334
335 private function getFragmentsForPaths(array $paths) {
336 $fragments = array();
337
338 foreach ($paths as $path) {
339 foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) {
340 $fragments[$fragment] = $fragment;
341 }
342 }
343
344 return $fragments;
345 }
346
347 private function getFragmentIndexesForPaths(array $paths) {
348 $indexes = array();
349
350 foreach ($this->getFragmentsForPaths($paths) as $fragment) {
351 $indexes[] = PhabricatorHash::digestForIndex($fragment);
352 }
353
354 return $indexes;
355 }
356
357
358/* -( Path Control )------------------------------------------------------- */
359
360
361 /**
362 * Get a list of all packages which control a path or its parent directories,
363 * ordered from weakest to strongest.
364 *
365 * The first package has the most specific claim on the path; the last
366 * package has the most general claim. Multiple packages may have claims of
367 * equal strength, so this ordering is primarily one of usability and
368 * convenience.
369 *
370 * @return list<PhabricatorOwnersPackage> List of controlling packages.
371 */
372 public function getControllingPackagesForPath(
373 $repository_phid,
374 $path,
375 $ignore_dominion = false) {
376 $path = (string)$path;
377
378 if (!isset($this->controlMap[$repository_phid][$path])) {
379 throw new PhutilInvalidStateException('withControl');
380 }
381
382 if ($this->controlResults === null) {
383 throw new PhutilInvalidStateException('execute');
384 }
385
386 $packages = $this->controlResults;
387 $weak_dominion = PhabricatorOwnersPackage::DOMINION_WEAK;
388
389 $path_fragments = PhabricatorOwnersPackage::splitPath($path);
390 $fragment_count = count($path_fragments);
391
392 $matches = array();
393 foreach ($packages as $package_id => $package) {
394 $best_match = null;
395 $include = false;
396
397 $repository_paths = $package->getPathsForRepository($repository_phid);
398 foreach ($repository_paths as $package_path) {
399 $strength = $package_path->getPathMatchStrength(
400 $path_fragments,
401 $fragment_count);
402 if ($strength > $best_match) {
403 $best_match = $strength;
404 $include = !$package_path->getExcluded();
405 }
406 }
407
408 if ($best_match && $include) {
409 if ($ignore_dominion) {
410 $is_weak = false;
411 } else {
412 $is_weak = ($package->getDominion() == $weak_dominion);
413 }
414 $matches[$package_id] = array(
415 'strength' => $best_match,
416 'weak' => $is_weak,
417 'package' => $package,
418 );
419 }
420 }
421
422 // At each strength level, drop weak packages if there are also strong
423 // packages of the same strength.
424 $strength_map = igroup($matches, 'strength');
425 foreach ($strength_map as $strength => $package_list) {
426 $any_strong = false;
427 foreach ($package_list as $package_id => $package) {
428 if (!$package['weak']) {
429 $any_strong = true;
430 break;
431 }
432 }
433 if ($any_strong) {
434 foreach ($package_list as $package_id => $package) {
435 if ($package['weak']) {
436 unset($matches[$package_id]);
437 }
438 }
439 }
440 }
441
442 $matches = isort($matches, 'strength');
443 $matches = array_reverse($matches);
444
445 $strongest = null;
446 foreach ($matches as $package_id => $match) {
447 if ($strongest === null) {
448 $strongest = $match['strength'];
449 }
450
451 if ($match['strength'] === $strongest) {
452 continue;
453 }
454
455 if ($match['weak']) {
456 unset($matches[$package_id]);
457 }
458 }
459
460 return array_values(ipull($matches, 'package'));
461 }
462
463}