@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 * Update the ref cursors for a repository, which track the positions of
5 * branches, bookmarks, and tags.
6 */
7final class PhabricatorRepositoryRefEngine
8 extends PhabricatorRepositoryEngine {
9
10 private $newPositions = array();
11 private $deadPositions = array();
12 private $permanentCommits = array();
13 private $rebuild;
14
15 public function setRebuild($rebuild) {
16 $this->rebuild = $rebuild;
17 return $this;
18 }
19
20 public function getRebuild() {
21 return $this->rebuild;
22 }
23
24 public function updateRefs() {
25 $this->newPositions = array();
26 $this->deadPositions = array();
27 $this->permanentCommits = array();
28
29 $repository = $this->getRepository();
30 $viewer = $this->getViewer();
31
32 $branches_may_close = false;
33
34 $vcs = $repository->getVersionControlSystem();
35 switch ($vcs) {
36 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
37 // No meaningful refs of any type in Subversion.
38 $maps = array();
39 break;
40 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
41 $branches = $this->loadMercurialBranchPositions($repository);
42 $bookmarks = $this->loadMercurialBookmarkPositions($repository);
43 $maps = array(
44 PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches,
45 PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks,
46 );
47
48 $branches_may_close = true;
49 break;
50 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
51 $maps = $this->loadGitRefPositions($repository);
52 break;
53 default:
54 throw new Exception(pht('Unknown VCS "%s"!', $vcs));
55 }
56
57 // Fill in any missing types with empty lists.
58 $maps = $maps + array(
59 PhabricatorRepositoryRefCursor::TYPE_BRANCH => array(),
60 PhabricatorRepositoryRefCursor::TYPE_TAG => array(),
61 PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => array(),
62 PhabricatorRepositoryRefCursor::TYPE_REF => array(),
63 );
64
65 $all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
66 ->setViewer($viewer)
67 ->withRepositoryPHIDs(array($repository->getPHID()))
68 ->needPositions(true)
69 ->execute();
70 $cursor_groups = mgroup($all_cursors, 'getRefType');
71
72 // Find all the heads of permanent refs.
73 $all_closing_heads = array();
74 foreach ($all_cursors as $cursor) {
75
76 // See T13284. Note that we're considering whether this ref was a
77 // permanent ref or not the last time we updated refs for this
78 // repository. This allows us to handle things properly when a ref
79 // is reconfigured from non-permanent to permanent.
80
81 $was_permanent = $cursor->getIsPermanent();
82 if (!$was_permanent) {
83 continue;
84 }
85
86 foreach ($cursor->getPositionIdentifiers() as $identifier) {
87 $all_closing_heads[] = $identifier;
88 }
89 }
90
91 $all_closing_heads = array_unique($all_closing_heads);
92 $all_closing_heads = $this->removeMissingCommits($all_closing_heads);
93
94 foreach ($maps as $type => $refs) {
95 $cursor_group = idx($cursor_groups, $type, array());
96 $this->updateCursors($cursor_group, $refs, $type, $all_closing_heads);
97 }
98
99 if ($this->permanentCommits) {
100 $this->setPermanentFlagOnCommits($this->permanentCommits);
101 }
102
103 $save_cursors = $this->getCursorsForUpdate($repository, $all_cursors);
104
105 if ($this->newPositions || $this->deadPositions || $save_cursors) {
106 $repository->openTransaction();
107
108 $this->saveNewPositions();
109 $this->deleteDeadPositions();
110
111 foreach ($save_cursors as $cursor) {
112 $cursor->save();
113 }
114
115 $repository->saveTransaction();
116 }
117
118 $branches = $maps[PhabricatorRepositoryRefCursor::TYPE_BRANCH];
119 if ($branches && $branches_may_close) {
120 $this->updateBranchStates($repository, $branches);
121 }
122 }
123
124 /**
125 * @param PhabricatorRepository $repository
126 * @param array<PhabricatorRepositoryRefCursor> $cursors
127 */
128 private function getCursorsForUpdate(
129 PhabricatorRepository $repository,
130 array $cursors) {
131 assert_instances_of($cursors, PhabricatorRepositoryRefCursor::class);
132
133 $publisher = $repository->newPublisher();
134
135 $results = array();
136
137 foreach ($cursors as $cursor) {
138 $diffusion_ref = $cursor->newDiffusionRepositoryRef();
139
140 $is_permanent = $publisher->isPermanentRef($diffusion_ref);
141 if ($is_permanent == $cursor->getIsPermanent()) {
142 continue;
143 }
144
145 $cursor->setIsPermanent((int)$is_permanent);
146 $results[] = $cursor;
147 }
148
149 return $results;
150 }
151
152 /**
153 * @param PhabricatorRepository $repository
154 * @param array<DiffusionRepositoryRef> $branches
155 */
156 private function updateBranchStates(
157 PhabricatorRepository $repository,
158 array $branches) {
159
160 assert_instances_of($branches, DiffusionRepositoryRef::class);
161 $viewer = $this->getViewer();
162
163 $all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
164 ->setViewer($viewer)
165 ->withRepositoryPHIDs(array($repository->getPHID()))
166 ->needPositions(true)
167 ->execute();
168
169 $state_map = array();
170 $type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH;
171 foreach ($all_cursors as $cursor) {
172 if ($cursor->getRefType() !== $type_branch) {
173 continue;
174 }
175 $raw_name = $cursor->getRefNameRaw();
176
177 foreach ($cursor->getPositions() as $position) {
178 $hash = $position->getCommitIdentifier();
179 $state_map[$raw_name][$hash] = $position;
180 }
181 }
182
183 $updates = array();
184 foreach ($branches as $branch) {
185 $position = idx($state_map, $branch->getShortName(), array());
186 $position = idx($position, $branch->getCommitIdentifier());
187 if (!$position) {
188 continue;
189 }
190
191 $fields = $branch->getRawFields();
192
193 $position_state = (bool)$position->getIsClosed();
194 $branch_state = (bool)idx($fields, 'closed');
195
196 if ($position_state != $branch_state) {
197 $updates[$position->getID()] = (int)$branch_state;
198 }
199 }
200
201 if ($updates) {
202 $position_table = new PhabricatorRepositoryRefPosition();
203 $conn = $position_table->establishConnection('w');
204
205 $position_table->openTransaction();
206 foreach ($updates as $position_id => $branch_state) {
207 queryfx(
208 $conn,
209 'UPDATE %T SET isClosed = %d WHERE id = %d',
210 $position_table->getTableName(),
211 $branch_state,
212 $position_id);
213 }
214 $position_table->saveTransaction();
215 }
216 }
217
218 private function markPositionNew(
219 PhabricatorRepositoryRefPosition $position) {
220 $this->newPositions[] = $position;
221 return $this;
222 }
223
224 private function markPositionDead(
225 PhabricatorRepositoryRefPosition $position) {
226 $this->deadPositions[] = $position;
227 return $this;
228 }
229
230 private function markPermanentCommits(array $identifiers) {
231 foreach ($identifiers as $identifier) {
232 $this->permanentCommits[$identifier] = $identifier;
233 }
234 return $this;
235 }
236
237 /**
238 * Remove commits which no longer exist in the repository from a list.
239 *
240 * After a force push and garbage collection, we may have branch cursors which
241 * point at commits which no longer exist. This can make commands issued later
242 * fail. See T5839 for discussion.
243 *
244 * @param list<string> $identifiers List of commit identifiers.
245 * @return list<string> List with nonexistent identifiers removed.
246 */
247 private function removeMissingCommits(array $identifiers) {
248 if (!$identifiers) {
249 return array();
250 }
251
252 $resolved = id(new DiffusionLowLevelResolveRefsQuery())
253 ->setRepository($this->getRepository())
254 ->withRefs($identifiers)
255 ->execute();
256
257 foreach ($identifiers as $key => $identifier) {
258 if (empty($resolved[$identifier])) {
259 unset($identifiers[$key]);
260 }
261 }
262
263 return $identifiers;
264 }
265
266 private function updateCursors(
267 array $cursors,
268 array $new_refs,
269 $ref_type,
270 array $all_closing_heads) {
271 $repository = $this->getRepository();
272 $publisher = $repository->newPublisher();
273
274 // NOTE: Mercurial branches may have multiple branch heads; this logic
275 // is complex primarily to account for that.
276
277 $cursors = mpull($cursors, null, 'getRefNameRaw');
278
279 // Group all the new ref values by their name. As above, these groups may
280 // have multiple members in Mercurial.
281 $ref_groups = mgroup($new_refs, 'getShortName');
282
283 foreach ($ref_groups as $name => $refs) {
284 $new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier');
285
286 $ref_cursor = idx($cursors, $name);
287 if ($ref_cursor) {
288 $old_positions = $ref_cursor->getPositions();
289 } else {
290 $old_positions = array();
291 }
292
293 // We're going to delete all the cursors pointing at commits which are
294 // no longer associated with the refs. This primarily makes the Mercurial
295 // multiple head case easier, and means that when we update a ref we
296 // delete the old one and write a new one.
297 foreach ($old_positions as $old_position) {
298 $hash = $old_position->getCommitIdentifier();
299 if (isset($new_commits[$hash])) {
300 // This ref previously pointed at this commit, and still does.
301 $this->log(
302 pht(
303 'Ref %s "%s" still points at %s.',
304 $ref_type,
305 $name,
306 $hash));
307 continue;
308 }
309
310 // This ref previously pointed at this commit, but no longer does.
311 $this->log(
312 pht(
313 'Ref %s "%s" no longer points at %s.',
314 $ref_type,
315 $name,
316 $hash));
317
318 // Nuke the obsolete cursor.
319 $this->markPositionDead($old_position);
320 }
321
322 // Now, we're going to insert new cursors for all the commits which are
323 // associated with this ref that don't currently have cursors.
324 $old_commits = mpull($old_positions, 'getCommitIdentifier');
325 $old_commits = array_fuse($old_commits);
326
327 $added_commits = array_diff_key($new_commits, $old_commits);
328 foreach ($added_commits as $identifier) {
329 $this->log(
330 pht(
331 'Ref %s "%s" now points at %s.',
332 $ref_type,
333 $name,
334 $identifier));
335
336 if (!$ref_cursor) {
337 // If this is the first time we've seen a particular ref (for
338 // example, a new branch) we need to insert a RefCursor record
339 // for it before we can insert a RefPosition.
340
341 $ref_cursor = $this->newRefCursor(
342 $repository,
343 $ref_type,
344 $name);
345 }
346
347 $new_position = id(new PhabricatorRepositoryRefPosition())
348 ->setCursorID($ref_cursor->getID())
349 ->setCommitIdentifier($identifier)
350 ->setIsClosed(0);
351
352 $this->markPositionNew($new_position);
353 }
354
355 if ($publisher->isPermanentRef(head($refs))) {
356
357 // See T13284. If this cursor was already marked as permanent, we
358 // only need to publish the newly created ref positions. However, if
359 // this cursor was not previously permanent but has become permanent,
360 // we need to publish all the ref positions.
361
362 // This corresponds to users reconfiguring a branch to make it
363 // permanent without pushing any new commits to it.
364
365 $is_rebuild = $this->getRebuild();
366 $was_permanent = $ref_cursor->getIsPermanent();
367
368 if ($is_rebuild || !$was_permanent) {
369 $update_all = true;
370 } else {
371 $update_all = false;
372 }
373
374 if ($update_all) {
375 $update_commits = $new_commits;
376 } else {
377 $update_commits = $added_commits;
378 }
379
380 if ($is_rebuild) {
381 $exclude = array();
382 } else {
383 $exclude = $all_closing_heads;
384 }
385
386 foreach ($update_commits as $identifier) {
387 $new_identifiers = $this->loadNewCommitIdentifiers(
388 $identifier,
389 $exclude);
390
391 $this->markPermanentCommits($new_identifiers);
392 }
393 }
394 }
395
396 // Find any cursors for refs which no longer exist. This happens when a
397 // branch, tag or bookmark is deleted.
398
399 foreach ($cursors as $name => $cursor) {
400 if (!empty($ref_groups[$name])) {
401 // This ref still has some positions, so we don't need to wipe it
402 // out. Try the next one.
403 continue;
404 }
405
406 foreach ($cursor->getPositions() as $position) {
407 $this->log(
408 pht(
409 'Ref %s "%s" no longer exists.',
410 $cursor->getRefType(),
411 $cursor->getRefName()));
412
413 $this->markPositionDead($position);
414 }
415 }
416 }
417
418 /**
419 * Find all ancestors of a new closing branch head which are not ancestors
420 * of any old closing branch head.
421 */
422 private function loadNewCommitIdentifiers(
423 $new_head,
424 array $all_closing_heads) {
425
426 $repository = $this->getRepository();
427 $vcs = $repository->getVersionControlSystem();
428 switch ($vcs) {
429 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
430 if ($all_closing_heads) {
431 $parts = array();
432 foreach ($all_closing_heads as $head) {
433 $parts[] = hgsprintf('%s', $head);
434 }
435
436 // See T5896. Mercurial can not parse an "X or Y or ..." rev list
437 // with more than about 300 items, because it exceeds the maximum
438 // allowed recursion depth. Split all the heads into chunks of
439 // 256, and build a query like this:
440 //
441 // ((1 or 2 or ... or 255) or (256 or 257 or ... 511))
442 //
443 // If we have more than 65535 heads, we'll do that again:
444 //
445 // (((1 or ...) or ...) or ((65536 or ...) or ...))
446
447 $chunk_size = 256;
448 while (count($parts) > $chunk_size) {
449 $chunks = array_chunk($parts, $chunk_size);
450 foreach ($chunks as $key => $chunk) {
451 $chunks[$key] = '('.implode(' or ', $chunk).')';
452 }
453 $parts = array_values($chunks);
454 }
455 $parts = '('.implode(' or ', $parts).')';
456
457 list($stdout) = $this->getRepository()->execxLocalCommand(
458 'log --template %s --rev %s',
459 '{node}\n',
460 hgsprintf('%s', $new_head).' - '.$parts);
461 } else {
462 list($stdout) = $this->getRepository()->execxLocalCommand(
463 'log --template %s --rev %s',
464 '{node}\n',
465 hgsprintf('%s', $new_head));
466 }
467
468 $stdout = trim($stdout);
469 if (!strlen($stdout)) {
470 return array();
471 }
472 return phutil_split_lines($stdout, $retain_newlines = false);
473 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
474 if ($all_closing_heads) {
475
476 // See PHI1474. This length of list may exceed the maximum size of
477 // a command line argument list, so pipe the list in using "--stdin"
478 // instead.
479
480 $ref_list = array();
481 $ref_list[] = $new_head;
482 foreach ($all_closing_heads as $old_head) {
483 $ref_list[] = '^'.$old_head;
484 }
485 $ref_list[] = '--';
486 $ref_list = implode("\n", $ref_list)."\n";
487
488 $future = $this->getRepository()->getLocalCommandFuture(
489 'log %s --stdin --',
490 '--format=%H');
491
492 list($stdout) = $future
493 ->write($ref_list)
494 ->resolvex();
495 } else {
496 list($stdout) = $this->getRepository()->execxLocalCommand(
497 'log %s %s --',
498 '--format=%H',
499 gitsprintf('%s', $new_head));
500 }
501
502 $stdout = trim($stdout);
503 if (!strlen($stdout)) {
504 return array();
505 }
506 return phutil_split_lines($stdout, $retain_newlines = false);
507 default:
508 throw new Exception(pht('Unsupported VCS "%s"!', $vcs));
509 }
510 }
511
512 /**
513 * Mark a list of commits as permanent, and queue workers for those commits
514 * which don't already have the flag.
515 */
516 private function setPermanentFlagOnCommits(array $identifiers) {
517 $repository = $this->getRepository();
518 $commit_table = new PhabricatorRepositoryCommit();
519 $conn = $commit_table->establishConnection('w');
520
521 $identifier_tokens = array();
522 foreach ($identifiers as $identifier) {
523 $identifier_tokens[] = qsprintf(
524 $conn,
525 '%s',
526 $identifier);
527 }
528
529 $all_commits = array();
530 foreach (PhabricatorLiskDAO::chunkSQL($identifier_tokens) as $chunk) {
531 $rows = queryfx_all(
532 $conn,
533 'SELECT id, phid, commitIdentifier, importStatus FROM %T
534 WHERE repositoryID = %d AND commitIdentifier IN (%LQ)',
535 $commit_table->getTableName(),
536 $repository->getID(),
537 $chunk);
538 foreach ($rows as $row) {
539 $all_commits[] = $row;
540 }
541 }
542
543 $commit_refs = array();
544 foreach ($identifiers as $identifier) {
545
546 // See T13591. This construction is a bit ad-hoc, but the priority
547 // function currently only cares about the number of refs we have
548 // discovered, so we'll get the right result even without filling
549 // these records out in detail.
550
551 $commit_refs[] = id(new PhabricatorRepositoryCommitRef())
552 ->setIdentifier($identifier);
553 }
554
555 $task_priority = $this->getImportTaskPriority(
556 $repository,
557 $commit_refs);
558
559 $permanent_flag = PhabricatorRepositoryCommit::IMPORTED_PERMANENT;
560 $published_flag = PhabricatorRepositoryCommit::IMPORTED_PUBLISH;
561
562 $all_commits = ipull($all_commits, null, 'commitIdentifier');
563 foreach ($identifiers as $identifier) {
564 $row = idx($all_commits, $identifier);
565
566 if (!$row) {
567 throw new Exception(
568 pht(
569 'Commit "%s" has not been discovered yet! Run discovery before '.
570 'updating refs.',
571 $identifier));
572 }
573
574 $import_status = $row['importStatus'];
575 if (!($import_status & $permanent_flag)) {
576 // Set the "permanent" flag.
577 $import_status = ($import_status | $permanent_flag);
578
579 // See T13580. Clear the "published" flag, so publishing executes
580 // again. We may have previously performed a no-op "publish" on the
581 // commit to make sure it has all bits in the "IMPORTED_ALL" bitmask.
582 $import_status = ($import_status & ~$published_flag);
583
584 queryfx(
585 $conn,
586 'UPDATE %T SET importStatus = %d WHERE id = %d',
587 $commit_table->getTableName(),
588 $import_status,
589 $row['id']);
590
591 $this->queueCommitImportTask(
592 $repository,
593 $row['phid'],
594 $task_priority,
595 $via = 'ref');
596 }
597 }
598
599 return $this;
600 }
601
602 private function newRefCursor(
603 PhabricatorRepository $repository,
604 $ref_type,
605 $ref_name) {
606
607 $cursor = id(new PhabricatorRepositoryRefCursor())
608 ->setRepositoryPHID($repository->getPHID())
609 ->setRefType($ref_type)
610 ->setRefName($ref_name);
611
612 $publisher = $repository->newPublisher();
613
614 $diffusion_ref = $cursor->newDiffusionRepositoryRef();
615 $is_permanent = $publisher->isPermanentRef($diffusion_ref);
616
617 $cursor->setIsPermanent((int)$is_permanent);
618
619 try {
620 return $cursor->save();
621 } catch (AphrontDuplicateKeyQueryException $ex) {
622 // If we raced another daemon to create this position and lost the race,
623 // load the cursor the other daemon created instead.
624 }
625
626 $viewer = $this->getViewer();
627
628 $cursor = id(new PhabricatorRepositoryRefCursorQuery())
629 ->setViewer($viewer)
630 ->withRepositoryPHIDs(array($repository->getPHID()))
631 ->withRefTypes(array($ref_type))
632 ->withRefNames(array($ref_name))
633 ->needPositions(true)
634 ->executeOne();
635 if (!$cursor) {
636 throw new Exception(
637 pht(
638 'Failed to create a new ref cursor (for "%s", of type "%s", in '.
639 'repository "%s") because it collided with an existing cursor, '.
640 'but then failed to load that cursor.',
641 $ref_name,
642 $ref_type,
643 $repository->getDisplayName()));
644 }
645
646 return $cursor;
647 }
648
649 private function saveNewPositions() {
650 $positions = $this->newPositions;
651
652 foreach ($positions as $position) {
653 try {
654 $position->save();
655 } catch (AphrontDuplicateKeyQueryException $ex) {
656 // We may race another daemon to create this position. If we do, and
657 // we lose the race, that's fine: the other daemon did our work for
658 // us and we can continue.
659 }
660 }
661
662 $this->newPositions = array();
663 }
664
665 private function deleteDeadPositions() {
666 $positions = $this->deadPositions;
667 $repository = $this->getRepository();
668
669 foreach ($positions as $position) {
670 // Shove this ref into the old refs table so the discovery engine
671 // can check if any commits have been rendered unreachable.
672 id(new PhabricatorRepositoryOldRef())
673 ->setRepositoryPHID($repository->getPHID())
674 ->setCommitIdentifier($position->getCommitIdentifier())
675 ->save();
676
677 $position->delete();
678 }
679
680 $this->deadPositions = array();
681 }
682
683
684
685/* -( Updating Git Refs )-------------------------------------------------- */
686
687
688 /**
689 * @task git
690 */
691 private function loadGitRefPositions(PhabricatorRepository $repository) {
692 $refs = id(new DiffusionLowLevelGitRefQuery())
693 ->setRepository($repository)
694 ->execute();
695
696 return mgroup($refs, 'getRefType');
697 }
698
699
700/* -( Updating Mercurial Refs )-------------------------------------------- */
701
702
703 /**
704 * @task hg
705 */
706 private function loadMercurialBranchPositions(
707 PhabricatorRepository $repository) {
708 return id(new DiffusionLowLevelMercurialBranchesQuery())
709 ->setRepository($repository)
710 ->execute();
711 }
712
713
714 /**
715 * @task hg
716 */
717 private function loadMercurialBookmarkPositions(
718 PhabricatorRepository $repository) {
719 // TODO: Implement support for Mercurial bookmarks.
720 return array();
721 }
722
723}