@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
at recaptime-dev/main 723 lines 22 kB view raw
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}