@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

Move Git discovery into DiscoveryEngine

Summary:
Ref T4327. Consolidates and simplifies infrastructure:

- Moves Git discovery into DiscoveryEngine.
- Collapses a bunch of the Git and Mercurial code related to stream discovery.
- Removes all cach code from PullLocal daemon (it's no longer called).
- Adds basic unit tests for Git and Mercurial discovery.

Various cleanup:

- Makes GitStream and MercurialStream extend a common base.
- Improves performance of MercurialStream in some cases, by requiring fewer commits be output and parsed.
- Makes mirroring exceptions easier to debug.
- Fixes discovery of Mercurial repositories with multiple branch heads.
- Adds some missing `pht()`.

Test Plan:
I tested this fairly throughly because I think this phase is complete:

- Made new repositories in multiple VCSes and did full imports.
- Particularly, I reimported Arcanist to make sure that TODO was resolved. I think it was related to the toposort stuff.
- Pushed commits to multiple VCSes.
- Pushed commits to a non-close branch, then pushed a merge commit. Observed commits import initially as non-close, then get flagged for close.
- Started full daemons and resolved various minor issues that showed up in the daemon log until everything ran cleanly.
- Basically spent about 30 minutes banging on this in every way I could think of to try to break it. I found and fixed some minor stuff, but it seems solid.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4327

Differential Revision: https://secure.phabricator.com/D7987

+284 -359
+4
src/__phutil_library_map__.php
··· 1847 1847 'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php', 1848 1848 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php', 1849 1849 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php', 1850 + 'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php', 1850 1851 'PhabricatorRepositoryListController' => 'applications/repository/controller/PhabricatorRepositoryListController.php', 1851 1852 'PhabricatorRepositoryManagementDeleteWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php', 1852 1853 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', ··· 4154 4155 'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions', 4155 4156 'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon', 4156 4157 'PhabricatorGestureExample' => 'PhabricatorUIExample', 4158 + 'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream', 4157 4159 'PhabricatorGlobalLock' => 'PhutilLock', 4158 4160 'PhabricatorGlobalUploadTargetView' => 'AphrontView', 4159 4161 'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', ··· 4241 4243 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', 4242 4244 'PhabricatorMarkupOneOff' => 'PhabricatorMarkupInterface', 4243 4245 'PhabricatorMarkupPreviewController' => 'PhabricatorController', 4246 + 'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream', 4244 4247 'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery', 4245 4248 'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions', 4246 4249 'PhabricatorMetaMTAController' => 'PhabricatorController', ··· 4516 4519 'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor', 4517 4520 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 4518 4521 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 4522 + 'PhabricatorRepositoryGraphStream' => 'Phobject', 4519 4523 'PhabricatorRepositoryListController' => 'PhabricatorRepositoryController', 4520 4524 'PhabricatorRepositoryManagementDeleteWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 4521 4525 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
+2 -1
src/applications/repository/daemon/PhabricatorGitGraphStream.php
··· 1 1 <?php 2 2 3 - final class PhabricatorGitGraphStream { 3 + final class PhabricatorGitGraphStream 4 + extends PhabricatorRepositoryGraphStream { 4 5 5 6 private $repository; 6 7 private $iterator;
+6 -3
src/applications/repository/daemon/PhabricatorMercurialGraphStream.php
··· 5 5 * the Mercurial commit graph with one nonblocking invocation of "hg". See 6 6 * @{class:PhabricatorRepositoryPullLocalDaemon}. 7 7 */ 8 - final class PhabricatorMercurialGraphStream { 8 + final class PhabricatorMercurialGraphStream 9 + extends PhabricatorRepositoryGraphStream { 9 10 10 11 private $repository; 11 12 private $iterator; ··· 15 16 private $local = array(); 16 17 private $localParents = array(); 17 18 18 - public function __construct(PhabricatorRepository $repository) { 19 + public function __construct(PhabricatorRepository $repository, $commit) { 19 20 $this->repository = $repository; 20 21 21 22 $future = $repository->getLocalCommandFuture( 22 - "log --template '{rev}\1{node}\1{date}\1{parents}\2'"); 23 + 'log --template %s --rev %s', 24 + '{rev}\1{node}\1{date}\1{parents}\2', 25 + hgsprintf('reverse(ancestors(%s))', $commit)); 23 26 24 27 $this->iterator = new LinesOfALargeExecFuture($future); 25 28 $this->iterator->setDelimiter("\2");
+8
src/applications/repository/daemon/PhabricatorRepositoryGraphStream.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorRepositoryGraphStream extends Phobject { 4 + 5 + abstract public function getParents($commit); 6 + abstract public function getCommitDate($commit); 7 + 8 + }
+15 -323
src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
··· 29 29 final class PhabricatorRepositoryPullLocalDaemon 30 30 extends PhabricatorDaemon { 31 31 32 - private $commitCache = array(); 33 32 private $repair; 34 33 private $discoveryEngines = array(); 35 34 ··· 223 222 } 224 223 225 224 public function discoverRepository(PhabricatorRepository $repository) { 226 - $vcs = $repository->getVersionControlSystem(); 225 + $refs = $this->getDiscoveryEngine($repository) 226 + ->discoverCommits(); 227 227 228 - $result = null; 229 - $refs = null; 230 - switch ($vcs) { 231 - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 232 - $result = $this->executeGitDiscover($repository); 233 - break; 234 - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 235 - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 236 - $refs = $this->getDiscoveryEngine($repository) 237 - ->discoverCommits(); 238 - break; 239 - default: 240 - throw new Exception("Unknown VCS '{$vcs}'!"); 241 - } 242 - 243 - if ($refs !== null) { 244 - foreach ($refs as $ref) { 245 - $this->recordCommit( 246 - $repository, 247 - $ref->getIdentifier(), 248 - $ref->getEpoch(), 249 - $close_immediately = true); 250 - } 228 + foreach ($refs as $ref) { 229 + $this->recordCommit( 230 + $repository, 231 + $ref->getIdentifier(), 232 + $ref->getEpoch(), 233 + $ref->getCanCloseImmediately()); 251 234 } 252 235 253 236 $this->checkIfRepositoryIsFullyImported($repository); ··· 258 241 // TODO: We should report these into the UI properly, but for 259 242 // now just complain. These errors are much less severe than 260 243 // pull errors. 261 - phlog($ex); 244 + $proxy = new PhutilProxyException( 245 + pht( 246 + 'Error while pushing "%s" repository to a mirror.', 247 + $repository->getCallsign()), 248 + $ex); 249 + phlog($proxy); 262 250 } 263 251 264 - if ($refs !== null) { 265 - return (bool)count($refs); 266 - } else { 267 - return $result; 268 - } 252 + return (bool)count($refs); 269 253 } 270 254 271 255 private function updateRepositoryRefs(PhabricatorRepository $repository) { ··· 287 271 return $this->discoveryEngines[$id]; 288 272 } 289 273 290 - private function isKnownCommit( 291 - PhabricatorRepository $repository, 292 - $target) { 293 - 294 - if ($this->getCache($repository, $target)) { 295 - return true; 296 - } 297 - 298 - if ($this->repair) { 299 - // In repair mode, rediscover the entire repository, ignoring the 300 - // database state. We can hit the local cache above, but if we miss it 301 - // stop the script from going to the database cache. 302 - return false; 303 - } 304 - 305 - $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 306 - 'repositoryID = %d AND commitIdentifier = %s', 307 - $repository->getID(), 308 - $target); 309 - 310 - if (!$commit) { 311 - return false; 312 - } 313 - 314 - $this->setCache($repository, $target); 315 - while (count($this->commitCache) > 2048) { 316 - array_shift($this->commitCache); 317 - } 318 - 319 - return true; 320 - } 321 - 322 274 private function recordCommit( 323 275 PhabricatorRepository $repository, 324 276 $commit_identifier, ··· 363 315 // reach this in repair mode, we've actually performed a repair. 364 316 $this->log("Repaired commit '{$commit_identifier}'."); 365 317 } 366 - 367 - $this->setCache($repository, $commit_identifier); 368 318 369 319 PhutilEventEngine::dispatchEvent( 370 320 new PhabricatorEvent( ··· 380 330 // more than once when looking at history, or because of races or 381 331 // data inconsistency or cosmic radiation; in any case, we're still 382 332 // in a good state if we ignore the failure. 383 - $this->setCache($repository, $commit_identifier); 384 333 } 385 334 } 386 335 ··· 409 358 PhabricatorWorker::scheduleTask($class, $data); 410 359 } 411 360 412 - 413 - private function setCache( 414 - PhabricatorRepository $repository, 415 - $commit_identifier) { 416 - 417 - $key = $this->getCacheKey($repository, $commit_identifier); 418 - $this->commitCache[$key] = true; 419 - } 420 - 421 - private function getCache( 422 - PhabricatorRepository $repository, 423 - $commit_identifier) { 424 - 425 - $key = $this->getCacheKey($repository, $commit_identifier); 426 - return idx($this->commitCache, $key, false); 427 - } 428 - 429 - private function getCacheKey( 430 - PhabricatorRepository $repository, 431 - $commit_identifier) { 432 - 433 - return $repository->getID().':'.$commit_identifier; 434 - } 435 - 436 361 private function checkIfRepositoryIsFullyImported( 437 362 PhabricatorRepository $repository) { 438 363 ··· 468 393 $repository->saveTransaction(); 469 394 } 470 395 471 - /* -( Git Implementation )------------------------------------------------- */ 472 - 473 - 474 - /** 475 - * @task git 476 - */ 477 - private function executeGitDiscover( 478 - PhabricatorRepository $repository) { 479 - 480 - if (!$repository->isHosted()) { 481 - $this->verifyOrigin($repository); 482 - } 483 - 484 - $refs = id(new DiffusionLowLevelGitRefQuery()) 485 - ->setRepository($repository) 486 - ->withIsOriginBranch(true) 487 - ->execute(); 488 - 489 - $branches = mpull($refs, 'getCommitIdentifier', 'getShortName'); 490 - 491 - if (!$branches) { 492 - // This repository has no branches at all, so we don't need to do 493 - // anything. Generally, this means the repository is empty. 494 - return; 495 - } 496 - 497 - $branches = $this->sortBranches($repository, $branches); 498 - 499 - $callsign = $repository->getCallsign(); 500 - 501 - $tracked_something = false; 502 - 503 - $this->log("Discovering commits in repository '{$callsign}'..."); 504 - foreach ($branches as $name => $commit) { 505 - $this->log("Examining branch '{$name}', at {$commit}."); 506 - if (!$repository->shouldTrackBranch($name)) { 507 - $this->log("Skipping, branch is untracked."); 508 - continue; 509 - } 510 - 511 - $tracked_something = true; 512 - 513 - if ($this->isKnownCommit($repository, $commit)) { 514 - $this->log("Skipping, HEAD is known."); 515 - continue; 516 - } 517 - 518 - $this->log("Looking for new commits."); 519 - $this->executeGitDiscoverCommit( 520 - $repository, 521 - $commit, 522 - $repository->shouldAutocloseBranch($name)); 523 - } 524 - 525 - if (!$tracked_something) { 526 - $repo_name = $repository->getName(); 527 - $repo_callsign = $repository->getCallsign(); 528 - throw new Exception( 529 - "Repository r{$repo_callsign} '{$repo_name}' has no tracked branches! ". 530 - "Verify that your branch filtering settings are correct."); 531 - } 532 - } 533 - 534 - /** 535 - * Sort branches so we process closeable branches first. This makes the 536 - * whole import process a little cheaper, since we can close these commits 537 - * the first time through rather than catching them in the refs step. 538 - */ 539 - private function sortBranches( 540 - PhabricatorRepository $repository, 541 - array $branches) { 542 - 543 - $head_branches = array(); 544 - $tail_branches = array(); 545 - foreach ($branches as $name => $commit) { 546 - if ($repository->shouldAutocloseBranch($name)) { 547 - $head_branches[$name] = $commit; 548 - } else { 549 - $tail_branches[$name] = $commit; 550 - } 551 - } 552 - 553 - return $head_branches + $tail_branches; 554 - } 555 - 556 - 557 - /** 558 - * @task git 559 - */ 560 - private function executeGitDiscoverCommit( 561 - PhabricatorRepository $repository, 562 - $commit, 563 - $close_immediately) { 564 - 565 - $discover = array($commit); 566 - $insert = array($commit); 567 - 568 - $seen_parent = array(); 569 - 570 - $stream = new PhabricatorGitGraphStream($repository, $commit); 571 - 572 - while (true) { 573 - $target = array_pop($discover); 574 - $parents = $stream->getParents($target); 575 - foreach ($parents as $parent) { 576 - if (isset($seen_parent[$parent])) { 577 - // We end up in a loop here somehow when we parse Arcanist if we 578 - // don't do this. TODO: Figure out why and draw a pretty diagram 579 - // since it's not evident how parsing a DAG with this causes the 580 - // loop to stop terminating. 581 - continue; 582 - } 583 - $seen_parent[$parent] = true; 584 - $known = $this->isKnownCommit($repository, $parent); 585 - if (!$known) { 586 - $this->log(pht('Discovered commit "%s".', $parent)); 587 - $discover[] = $parent; 588 - $insert[] = $parent; 589 - } 590 - } 591 - if (empty($discover)) { 592 - break; 593 - } 594 - } 595 - 596 - $n = count($insert); 597 - $this->log(pht('Found %d new commits.', new PhutilNumber($n))); 598 - 599 - while (true) { 600 - $target = array_pop($insert); 601 - $epoch = $stream->getCommitDate($target); 602 - $epoch = trim($epoch); 603 - 604 - $this->recordCommit($repository, $target, $epoch, $close_immediately); 605 - 606 - if (empty($insert)) { 607 - break; 608 - } 609 - } 610 - } 611 - 612 - 613 - /** 614 - * Verify that the "origin" remote exists, and points at the correct URI. 615 - * 616 - * This catches or corrects some types of misconfiguration, and also repairs 617 - * an issue where Git 1.7.1 does not create an "origin" for `--bare` clones. 618 - * See T4041. 619 - * 620 - * @param PhabricatorRepository Repository to verify. 621 - * @return void 622 - */ 623 - private function verifyOrigin(PhabricatorRepository $repository) { 624 - list($remotes) = $repository->execxLocalCommand( 625 - 'remote show -n origin'); 626 - 627 - $matches = null; 628 - if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { 629 - throw new Exception( 630 - "Expected 'Fetch URL' in 'git remote show -n origin'."); 631 - } 632 - 633 - $remote_uri = $matches[1]; 634 - $expect_remote = $repository->getRemoteURI(); 635 - 636 - if ($remote_uri == "origin") { 637 - // If a remote does not exist, git pretends it does and prints out a 638 - // made up remote where the URI is the same as the remote name. This is 639 - // definitely not correct. 640 - 641 - // Possibly, we should use `git remote --verbose` instead, which does not 642 - // suffer from this problem (but is a little more complicated to parse). 643 - $valid = false; 644 - $exists = false; 645 - } else { 646 - $normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; 647 - 648 - $remote_normal = id(new PhabricatorRepositoryURINormalizer( 649 - $normal_type_git, 650 - $remote_uri))->getNormalizedPath(); 651 - 652 - $expect_normal = id(new PhabricatorRepositoryURINormalizer( 653 - $normal_type_git, 654 - $expect_remote))->getNormalizedPath(); 655 - 656 - $valid = ($remote_normal == $expect_normal); 657 - $exists = true; 658 - } 659 - 660 - if (!$valid) { 661 - if (!$exists) { 662 - // If there's no "origin" remote, just create it regardless of how 663 - // strongly we own the working copy. There is almost no conceivable 664 - // scenario in which this could do damage. 665 - $this->log( 666 - pht( 667 - 'Remote "origin" does not exist. Creating "origin", with '. 668 - 'URI "%s".', 669 - $expect_remote)); 670 - $repository->execxLocalCommand( 671 - 'remote add origin %P', 672 - $repository->getRemoteURIEnvelope()); 673 - 674 - // NOTE: This doesn't fetch the origin (it just creates it), so we won't 675 - // know about origin branches until the next "pull" happens. That's fine 676 - // for our purposes, but might impact things in the future. 677 - } else { 678 - if ($repository->canDestroyWorkingCopy()) { 679 - // Bad remote, but we can try to repair it. 680 - $this->log( 681 - pht( 682 - 'Remote "origin" exists, but is pointed at the wrong URI, "%s". '. 683 - 'Resetting origin URI to "%s.', 684 - $remote_uri, 685 - $expect_remote)); 686 - $repository->execxLocalCommand( 687 - 'remote set-url origin %P', 688 - $repository->getRemoteURIEnvelope()); 689 - } else { 690 - // Bad remote and we aren't comfortable repairing it. 691 - $message = pht( 692 - 'Working copy at "%s" has a mismatched origin URI, "%s". '. 693 - 'The expected origin URI is "%s". Fix your configuration, or '. 694 - 'set the remote URI correctly. To avoid breaking anything, '. 695 - 'Phabricator will not automatically fix this.', 696 - $repository->getLocalPath(), 697 - $remote_uri, 698 - $expect_remote); 699 - throw new Exception($message); 700 - } 701 - } 702 - } 703 - } 704 396 705 397 private function pushToMirrors(PhabricatorRepository $repository) { 706 398 if (!$repository->canMirror()) {
+10
src/applications/repository/engine/PhabricatorRepositoryCommitRef.php
··· 5 5 private $identifier; 6 6 private $epoch; 7 7 private $branch; 8 + private $canCloseImmediately; 8 9 9 10 public function setIdentifier($identifier) { 10 11 $this->identifier = $identifier; ··· 31 32 32 33 public function getBranch() { 33 34 return $this->branch; 35 + } 36 + 37 + public function setCanCloseImmediately($can_close_immediately) { 38 + $this->canCloseImmediately = $can_close_immediately; 39 + return $this; 40 + } 41 + 42 + public function getCanCloseImmediately() { 43 + return $this->canCloseImmediately; 34 44 } 35 45 36 46 }
+206 -24
src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php
··· 43 43 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 44 44 $refs = $this->discoverMercurialCommits(); 45 45 break; 46 - /* 47 - TODO: Implement this! 48 - 49 46 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 50 47 $refs = $this->discoverGitCommits(); 51 48 break; 52 - */ 53 49 default: 54 50 throw new Exception("Unknown VCS '{$vcs}'!"); 55 51 } ··· 63 59 } 64 60 65 61 62 + /* -( Discovering Git Repositories )--------------------------------------- */ 63 + 64 + 65 + /** 66 + * @task git 67 + */ 68 + private function discoverGitCommits() { 69 + $repository = $this->getRepository(); 70 + 71 + if (!$repository->isHosted()) { 72 + $this->verifyGitOrigin($repository); 73 + } 74 + 75 + $branches = id(new DiffusionLowLevelGitRefQuery()) 76 + ->setRepository($repository) 77 + ->withIsOriginBranch(true) 78 + ->execute(); 79 + 80 + if (!$branches) { 81 + // This repository has no branches at all, so we don't need to do 82 + // anything. Generally, this means the repository is empty. 83 + return array(); 84 + } 85 + 86 + $branches = $this->sortBranches($branches); 87 + $branches = mpull($branches, 'getCommitIdentifier', 'getShortName'); 88 + 89 + $this->log( 90 + pht( 91 + 'Discovering commits in repository %s.', 92 + $repository->getCallsign())); 93 + 94 + $refs = array(); 95 + foreach ($branches as $name => $commit) { 96 + $this->log(pht('Examining branch "%s", at "%s".', $name, $commit)); 97 + 98 + if (!$repository->shouldTrackBranch($name)) { 99 + $this->log(pht("Skipping, branch is untracked.")); 100 + continue; 101 + } 102 + 103 + if ($this->isKnownCommit($commit)) { 104 + $this->log(pht("Skipping, HEAD is known.")); 105 + continue; 106 + } 107 + 108 + $this->log(pht("Looking for new commits.")); 109 + 110 + $refs[] = $this->discoverStreamAncestry( 111 + new PhabricatorGitGraphStream($repository, $commit), 112 + $commit, 113 + $repository->shouldAutocloseBranch($name)); 114 + } 115 + 116 + return array_mergev($refs); 117 + } 118 + 119 + 120 + /** 121 + * Verify that the "origin" remote exists, and points at the correct URI. 122 + * 123 + * This catches or corrects some types of misconfiguration, and also repairs 124 + * an issue where Git 1.7.1 does not create an "origin" for `--bare` clones. 125 + * See T4041. 126 + * 127 + * @param PhabricatorRepository Repository to verify. 128 + * @return void 129 + */ 130 + private function verifyGitOrigin(PhabricatorRepository $repository) { 131 + list($remotes) = $repository->execxLocalCommand( 132 + 'remote show -n origin'); 133 + 134 + $matches = null; 135 + if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { 136 + throw new Exception( 137 + "Expected 'Fetch URL' in 'git remote show -n origin'."); 138 + } 139 + 140 + $remote_uri = $matches[1]; 141 + $expect_remote = $repository->getRemoteURI(); 142 + 143 + if ($remote_uri == "origin") { 144 + // If a remote does not exist, git pretends it does and prints out a 145 + // made up remote where the URI is the same as the remote name. This is 146 + // definitely not correct. 147 + 148 + // Possibly, we should use `git remote --verbose` instead, which does not 149 + // suffer from this problem (but is a little more complicated to parse). 150 + $valid = false; 151 + $exists = false; 152 + } else { 153 + $normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; 154 + 155 + $remote_normal = id(new PhabricatorRepositoryURINormalizer( 156 + $normal_type_git, 157 + $remote_uri))->getNormalizedPath(); 158 + 159 + $expect_normal = id(new PhabricatorRepositoryURINormalizer( 160 + $normal_type_git, 161 + $expect_remote))->getNormalizedPath(); 162 + 163 + $valid = ($remote_normal == $expect_normal); 164 + $exists = true; 165 + } 166 + 167 + if (!$valid) { 168 + if (!$exists) { 169 + // If there's no "origin" remote, just create it regardless of how 170 + // strongly we own the working copy. There is almost no conceivable 171 + // scenario in which this could do damage. 172 + $this->log( 173 + pht( 174 + 'Remote "origin" does not exist. Creating "origin", with '. 175 + 'URI "%s".', 176 + $expect_remote)); 177 + $repository->execxLocalCommand( 178 + 'remote add origin %P', 179 + $repository->getRemoteURIEnvelope()); 180 + 181 + // NOTE: This doesn't fetch the origin (it just creates it), so we won't 182 + // know about origin branches until the next "pull" happens. That's fine 183 + // for our purposes, but might impact things in the future. 184 + } else { 185 + if ($repository->canDestroyWorkingCopy()) { 186 + // Bad remote, but we can try to repair it. 187 + $this->log( 188 + pht( 189 + 'Remote "origin" exists, but is pointed at the wrong URI, "%s". '. 190 + 'Resetting origin URI to "%s.', 191 + $remote_uri, 192 + $expect_remote)); 193 + $repository->execxLocalCommand( 194 + 'remote set-url origin %P', 195 + $repository->getRemoteURIEnvelope()); 196 + } else { 197 + // Bad remote and we aren't comfortable repairing it. 198 + $message = pht( 199 + 'Working copy at "%s" has a mismatched origin URI, "%s". '. 200 + 'The expected origin URI is "%s". Fix your configuration, or '. 201 + 'set the remote URI correctly. To avoid breaking anything, '. 202 + 'Phabricator will not automatically fix this.', 203 + $repository->getLocalPath(), 204 + $remote_uri, 205 + $expect_remote); 206 + throw new Exception($message); 207 + } 208 + } 209 + } 210 + } 211 + 212 + 66 213 /* -( Discovering Subversion Repositories )-------------------------------- */ 67 214 68 215 ··· 108 255 $epoch = (int)strtotime((string)$entry->date[0]); 109 256 $refs[$identifier] = id(new PhabricatorRepositoryCommitRef()) 110 257 ->setIdentifier($identifier) 111 - ->setEpoch($epoch); 258 + ->setEpoch($epoch) 259 + ->setCanCloseImmediately(true); 112 260 113 261 if ($upper_bound === null) { 114 262 $upper_bound = $identifier; ··· 147 295 $branches = id(new DiffusionLowLevelMercurialBranchesQuery()) 148 296 ->setRepository($repository) 149 297 ->execute(); 150 - $branches = mpull($branches, 'getHeadCommitIdentifier', 'getName'); 151 298 152 299 $refs = array(); 153 - foreach ($branches as $name => $commit) { 154 - $this->log("Examining branch '{$name}', at {$commit}'."); 300 + foreach ($branches as $branch) { 301 + // NOTE: Mercurial branches may have multiple heads, so the names may 302 + // not be unique. 303 + $name = $branch->getShortName(); 304 + $commit = $branch->getCommitIdentifier(); 305 + 306 + $this->log(pht('Examining branch "%s" head "%s".', $name, $commit)); 155 307 if (!$repository->shouldTrackBranch($name)) { 156 - $this->log("Skipping, branch is untracked."); 308 + $this->log(pht("Skipping, branch is untracked.")); 157 309 continue; 158 310 } 159 311 160 312 if ($this->isKnownCommit($commit)) { 161 - $this->log("Skipping, tip is a known commit."); 313 + $this->log(pht("Skipping, this head is a known commit.")); 162 314 continue; 163 315 } 164 316 165 - $this->log("Looking for new commits."); 166 - $refs[] = $this->discoverMercurialAncestry($repository, $commit); 317 + $this->log(pht("Looking for new commits.")); 318 + 319 + $refs[] = $this->discoverStreamAncestry( 320 + new PhabricatorMercurialGraphStream($repository, $commit), 321 + $commit, 322 + $close_immediately = true); 167 323 } 168 324 169 325 return array_mergev($refs); 170 326 } 171 327 172 328 173 - /** 174 - * @task hg 175 - */ 176 - private function discoverMercurialAncestry( 177 - PhabricatorRepository $repository, 178 - $commit) { 329 + /* -( Internals )---------------------------------------------------------- */ 330 + 331 + 332 + private function discoverStreamAncestry( 333 + PhabricatorRepositoryGraphStream $stream, 334 + $commit, 335 + $close_immediately) { 179 336 180 337 $discover = array($commit); 181 338 $graph = array(); 182 339 $seen = array(); 183 - 184 - $stream = new PhabricatorMercurialGraphStream($repository); 185 340 186 341 // Find all the reachable, undiscovered commits. Build a graph of the 187 342 // edges. ··· 214 369 foreach ($commits as $commit) { 215 370 $refs[] = id(new PhabricatorRepositoryCommitRef()) 216 371 ->setIdentifier($commit) 217 - ->setEpoch($stream->getCommitDate($commit)); 372 + ->setEpoch($stream->getCommitDate($commit)) 373 + ->setCanCloseImmediately($close_immediately); 218 374 } 219 375 220 376 return $refs; 221 377 } 222 - 223 - 224 - /* -( Internals )---------------------------------------------------------- */ 225 378 226 379 227 380 private function reduceGraph(array $edges) { ··· 269 422 } 270 423 271 424 return true; 425 + } 426 + 427 + 428 + /** 429 + * Sort branches so we process closeable branches first. This makes the 430 + * whole import process a little cheaper, since we can close these commits 431 + * the first time through rather than catching them in the refs step. 432 + * 433 + * @task internal 434 + * 435 + * @param list<DiffusionRepositoryRef> List of branch heads. 436 + * @return list<DiffusionRepositoryRef> Sorted list of branch heads. 437 + */ 438 + private function sortBranches(array $branches) { 439 + $repository = $this->getRepository(); 440 + 441 + $head_branches = array(); 442 + $tail_branches = array(); 443 + foreach ($branches as $branch) { 444 + $name = $branch->getShortName(); 445 + 446 + if ($repository->shouldAutocloseBranch($name)) { 447 + $head_branches[] = $branch; 448 + } else { 449 + $tail_branches[] = $branch; 450 + } 451 + } 452 + 453 + return array_merge($head_branches, $tail_branches); 272 454 } 273 455 274 456 }
+33 -8
src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php
··· 4 4 extends PhabricatorWorkingCopyTestCase { 5 5 6 6 public function testSubversionCommitDiscovery() { 7 - $repo = $this->buildPulledRepository('ST'); 8 - 9 - $engine = id(new PhabricatorRepositoryDiscoveryEngine()) 10 - ->setRepository($repo); 11 - 12 - $refs = $engine->discoverCommits($repo); 7 + $refs = $this->discoverRefs('ST'); 13 8 $this->assertEqual( 14 9 array( 15 10 1368319433, ··· 17 12 ), 18 13 mpull($refs, 'getEpoch'), 19 14 'Commit Epochs'); 15 + } 20 16 21 - // The next time through, these should be cached as already discovered. 17 + public function testMercurialCommitDiscovery() { 18 + $refs = $this->discoverRefs('HT'); 19 + $this->assertEqual( 20 + array( 21 + '4a110ae879f473f2e82ffd032475caedd6cdba91', 22 + ), 23 + mpull($refs, 'getIdentifier')); 24 + } 25 + 26 + public function testGitCommitDiscovery() { 27 + $refs = $this->discoverRefs('GT'); 28 + $this->assertEqual( 29 + array( 30 + '763d4ab372445551c95fb5cccd1a7a223f5b2ac8', 31 + ), 32 + mpull($refs, 'getIdentifier')); 33 + } 34 + 35 + private function discoverRefs($callsign) { 36 + $repo = $this->buildPulledRepository($callsign); 37 + 38 + $engine = id(new PhabricatorRepositoryDiscoveryEngine()) 39 + ->setRepository($repo); 22 40 23 41 $refs = $engine->discoverCommits($repo); 24 - $this->assertEqual(array(), $refs); 42 + 43 + // The next time through, these should be cached as already discovered. 44 + 45 + $new_refs = $engine->discoverCommits($repo); 46 + $this->assertEqual(array(), $new_refs); 47 + 48 + return $refs; 25 49 } 50 + 26 51 27 52 }