Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / repository / engine / PhabricatorRepositoryDiscoveryEngine.php
blob8f5b346ecd030ea790dc3ddc30bf8cd2ec24a981
1 <?php
3 /**
4 * @task discover Discovering Repositories
5 * @task svn Discovering Subversion Repositories
6 * @task git Discovering Git Repositories
7 * @task hg Discovering Mercurial Repositories
8 * @task internal Internals
9 */
10 final class PhabricatorRepositoryDiscoveryEngine
11 extends PhabricatorRepositoryEngine {
13 private $repairMode;
14 private $commitCache = array();
15 private $workingSet = array();
17 const MAX_COMMIT_CACHE_SIZE = 65535;
20 /* -( Discovering Repositories )------------------------------------------- */
23 public function setRepairMode($repair_mode) {
24 $this->repairMode = $repair_mode;
25 return $this;
29 public function getRepairMode() {
30 return $this->repairMode;
34 /**
35 * @task discovery
37 public function discoverCommits() {
38 $repository = $this->getRepository();
40 $lock = $this->newRepositoryLock($repository, 'repo.look', false);
42 try {
43 $lock->lock();
44 } catch (PhutilLockException $ex) {
45 throw new DiffusionDaemonLockException(
46 pht(
47 'Another process is currently discovering repository "%s", '.
48 'skipping discovery.',
49 $repository->getDisplayName()));
52 try {
53 $result = $this->discoverCommitsWithLock();
54 } catch (Exception $ex) {
55 $lock->unlock();
56 throw $ex;
59 $lock->unlock();
61 return $result;
64 private function discoverCommitsWithLock() {
65 $repository = $this->getRepository();
66 $viewer = $this->getViewer();
68 $vcs = $repository->getVersionControlSystem();
69 switch ($vcs) {
70 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
71 $refs = $this->discoverSubversionCommits();
72 break;
73 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
74 $refs = $this->discoverMercurialCommits();
75 break;
76 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
77 $refs = $this->discoverGitCommits();
78 break;
79 default:
80 throw new Exception(pht("Unknown VCS '%s'!", $vcs));
83 if ($this->isInitialImport($refs)) {
84 $this->log(
85 pht(
86 'Discovered more than %s commit(s) in an empty repository, '.
87 'marking repository as importing.',
88 new PhutilNumber(PhabricatorRepository::IMPORT_THRESHOLD)));
90 $repository->markImporting();
93 // Clear the working set cache.
94 $this->workingSet = array();
96 $task_priority = $this->getImportTaskPriority($repository, $refs);
98 // Record discovered commits and mark them in the cache.
99 foreach ($refs as $ref) {
100 $this->recordCommit(
101 $repository,
102 $ref->getIdentifier(),
103 $ref->getEpoch(),
104 $ref->getIsPermanent(),
105 $ref->getParents(),
106 $task_priority);
108 $this->commitCache[$ref->getIdentifier()] = true;
111 $this->markUnreachableCommits($repository);
113 $version = $this->getObservedVersion($repository);
114 if ($version !== null) {
115 id(new DiffusionRepositoryClusterEngine())
116 ->setViewer($viewer)
117 ->setRepository($repository)
118 ->synchronizeWorkingCopyAfterDiscovery($version);
121 return $refs;
125 /* -( Discovering Git Repositories )--------------------------------------- */
129 * @task git
131 private function discoverGitCommits() {
132 $repository = $this->getRepository();
133 $publisher = $repository->newPublisher();
135 $heads = id(new DiffusionLowLevelGitRefQuery())
136 ->setRepository($repository)
137 ->execute();
139 if (!$heads) {
140 // This repository has no heads at all, so we don't need to do
141 // anything. Generally, this means the repository is empty.
142 return array();
145 $this->log(
146 pht(
147 'Discovering commits in repository "%s".',
148 $repository->getDisplayName()));
150 $ref_lists = array();
152 $head_groups = $this->getRefGroupsForDiscovery($heads);
153 foreach ($head_groups as $head_group) {
155 $group_identifiers = mpull($head_group, 'getCommitIdentifier');
156 $group_identifiers = array_fuse($group_identifiers);
157 $this->fillCommitCache($group_identifiers);
159 foreach ($head_group as $ref) {
160 $name = $ref->getShortName();
161 $commit = $ref->getCommitIdentifier();
163 $this->log(
164 pht(
165 'Examining "%s" (%s) at "%s".',
166 $name,
167 $ref->getRefType(),
168 $commit));
170 if (!$repository->shouldTrackRef($ref)) {
171 $this->log(pht('Skipping, ref is untracked.'));
172 continue;
175 if ($this->isKnownCommit($commit)) {
176 $this->log(pht('Skipping, HEAD is known.'));
177 continue;
180 // In Git, it's possible to tag anything. We just skip tags that don't
181 // point to a commit. See T11301.
182 $fields = $ref->getRawFields();
183 $ref_type = idx($fields, 'objecttype');
184 $tag_type = idx($fields, '*objecttype');
185 if ($ref_type != 'commit' && $tag_type != 'commit') {
186 $this->log(pht('Skipping, this is not a commit.'));
187 continue;
190 $this->log(pht('Looking for new commits.'));
192 $head_refs = $this->discoverStreamAncestry(
193 new PhabricatorGitGraphStream($repository, $commit),
194 $commit,
195 $publisher->isPermanentRef($ref));
197 $this->didDiscoverRefs($head_refs);
199 $ref_lists[] = $head_refs;
203 $refs = array_mergev($ref_lists);
205 return $refs;
209 * @task git
211 private function getRefGroupsForDiscovery(array $heads) {
212 $heads = $this->sortRefs($heads);
214 // See T13593. We hold a commit cache with a fixed maximum size. Split the
215 // refs into chunks no larger than the cache size, so we don't overflow the
216 // cache when testing them.
218 $array_iterator = new ArrayIterator($heads);
220 $chunk_iterator = new PhutilChunkedIterator(
221 $array_iterator,
222 self::MAX_COMMIT_CACHE_SIZE);
224 return $chunk_iterator;
228 /* -( Discovering Subversion Repositories )-------------------------------- */
232 * @task svn
234 private function discoverSubversionCommits() {
235 $repository = $this->getRepository();
237 if (!$repository->isHosted()) {
238 $this->verifySubversionRoot($repository);
241 $upper_bound = null;
242 $limit = 1;
243 $refs = array();
244 do {
245 // Find all the unknown commits on this path. Note that we permit
246 // importing an SVN subdirectory rather than the entire repository, so
247 // commits may be nonsequential.
249 if ($upper_bound === null) {
250 $at_rev = 'HEAD';
251 } else {
252 $at_rev = ($upper_bound - 1);
255 try {
256 list($xml, $stderr) = $repository->execxRemoteCommand(
257 'log --xml --quiet --limit %d %s',
258 $limit,
259 $repository->getSubversionBaseURI($at_rev));
260 } catch (CommandException $ex) {
261 $stderr = $ex->getStderr();
262 if (preg_match('/(path|File) not found/', $stderr)) {
263 // We've gone all the way back through history and this path was not
264 // affected by earlier commits.
265 break;
267 throw $ex;
270 $xml = phutil_utf8ize($xml);
271 $log = new SimpleXMLElement($xml);
272 foreach ($log->logentry as $entry) {
273 $identifier = (int)$entry['revision'];
274 $epoch = (int)strtotime((string)$entry->date[0]);
275 $refs[$identifier] = id(new PhabricatorRepositoryCommitRef())
276 ->setIdentifier($identifier)
277 ->setEpoch($epoch)
278 ->setIsPermanent(true);
280 if ($upper_bound === null) {
281 $upper_bound = $identifier;
282 } else {
283 $upper_bound = min($upper_bound, $identifier);
287 // Discover 2, 4, 8, ... 256 logs at a time. This allows us to initially
288 // import large repositories fairly quickly, while pulling only as much
289 // data as we need in the common case (when we've already imported the
290 // repository and are just grabbing one commit at a time).
291 $limit = min($limit * 2, 256);
293 } while ($upper_bound > 1 && !$this->isKnownCommit($upper_bound));
295 krsort($refs);
296 while ($refs && $this->isKnownCommit(last($refs)->getIdentifier())) {
297 array_pop($refs);
299 $refs = array_reverse($refs);
301 $this->didDiscoverRefs($refs);
303 return $refs;
307 private function verifySubversionRoot(PhabricatorRepository $repository) {
308 list($xml) = $repository->execxRemoteCommand(
309 'info --xml %s',
310 $repository->getSubversionPathURI());
312 $xml = phutil_utf8ize($xml);
313 $xml = new SimpleXMLElement($xml);
315 $remote_root = (string)($xml->entry[0]->repository[0]->root[0]);
316 $expect_root = $repository->getSubversionPathURI();
318 $normal_type_svn = ArcanistRepositoryURINormalizer::TYPE_SVN;
320 $remote_normal = id(new ArcanistRepositoryURINormalizer(
321 $normal_type_svn,
322 $remote_root))->getNormalizedPath();
324 $expect_normal = id(new ArcanistRepositoryURINormalizer(
325 $normal_type_svn,
326 $expect_root))->getNormalizedPath();
328 if ($remote_normal != $expect_normal) {
329 throw new Exception(
330 pht(
331 'Repository "%s" does not have a correctly configured remote URI. '.
332 'The remote URI for a Subversion repository MUST point at the '.
333 'repository root. The root for this repository is "%s", but the '.
334 'configured URI is "%s". To resolve this error, set the remote URI '.
335 'to point at the repository root. If you want to import only part '.
336 'of a Subversion repository, use the "Import Only" option.',
337 $repository->getDisplayName(),
338 $remote_root,
339 $expect_root));
344 /* -( Discovering Mercurial Repositories )--------------------------------- */
348 * @task hg
350 private function discoverMercurialCommits() {
351 $repository = $this->getRepository();
353 $branches = id(new DiffusionLowLevelMercurialBranchesQuery())
354 ->setRepository($repository)
355 ->execute();
357 $this->fillCommitCache(mpull($branches, 'getCommitIdentifier'));
359 $refs = array();
360 foreach ($branches as $branch) {
361 // NOTE: Mercurial branches may have multiple heads, so the names may
362 // not be unique.
363 $name = $branch->getShortName();
364 $commit = $branch->getCommitIdentifier();
366 $this->log(pht('Examining branch "%s" head "%s".', $name, $commit));
367 if (!$repository->shouldTrackBranch($name)) {
368 $this->log(pht('Skipping, branch is untracked.'));
369 continue;
372 if ($this->isKnownCommit($commit)) {
373 $this->log(pht('Skipping, this head is a known commit.'));
374 continue;
377 $this->log(pht('Looking for new commits.'));
379 $branch_refs = $this->discoverStreamAncestry(
380 new PhabricatorMercurialGraphStream($repository, $commit),
381 $commit,
382 $is_permanent = true);
384 $this->didDiscoverRefs($branch_refs);
386 $refs[] = $branch_refs;
389 return array_mergev($refs);
393 /* -( Internals )---------------------------------------------------------- */
396 private function discoverStreamAncestry(
397 PhabricatorRepositoryGraphStream $stream,
398 $commit,
399 $is_permanent) {
401 $discover = array($commit);
402 $graph = array();
403 $seen = array();
405 // Find all the reachable, undiscovered commits. Build a graph of the
406 // edges.
407 while ($discover) {
408 $target = array_pop($discover);
410 if (empty($graph[$target])) {
411 $graph[$target] = array();
414 $parents = $stream->getParents($target);
415 foreach ($parents as $parent) {
416 if ($this->isKnownCommit($parent)) {
417 continue;
420 $graph[$target][$parent] = true;
422 if (empty($seen[$parent])) {
423 $seen[$parent] = true;
424 $discover[] = $parent;
429 // Now, sort them topologically.
430 $commits = $this->reduceGraph($graph);
432 $refs = array();
433 foreach ($commits as $commit) {
434 $epoch = $stream->getCommitDate($commit);
436 // If the epoch doesn't fit into a uint32, treat it as though it stores
437 // the current time. For discussion, see T11537.
438 if ($epoch > 0xFFFFFFFF) {
439 $epoch = PhabricatorTime::getNow();
442 // If the epoch is not present at all, treat it as though it stores the
443 // value "0". For discussion, see T12062. This behavior is consistent
444 // with the behavior of "git show".
445 if (!strlen($epoch)) {
446 $epoch = 0;
449 $refs[] = id(new PhabricatorRepositoryCommitRef())
450 ->setIdentifier($commit)
451 ->setEpoch($epoch)
452 ->setIsPermanent($is_permanent)
453 ->setParents($stream->getParents($commit));
456 return $refs;
460 private function reduceGraph(array $edges) {
461 foreach ($edges as $commit => $parents) {
462 $edges[$commit] = array_keys($parents);
465 $graph = new PhutilDirectedScalarGraph();
466 $graph->addNodes($edges);
468 $commits = $graph->getNodesInTopologicalOrder();
470 // NOTE: We want the most ancestral nodes first, so we need to reverse the
471 // list we get out of AbstractDirectedGraph.
472 $commits = array_reverse($commits);
474 return $commits;
478 private function isKnownCommit($identifier) {
479 if (isset($this->commitCache[$identifier])) {
480 return true;
483 if (isset($this->workingSet[$identifier])) {
484 return true;
487 $this->fillCommitCache(array($identifier));
489 return isset($this->commitCache[$identifier]);
492 private function fillCommitCache(array $identifiers) {
493 if (!$identifiers) {
494 return;
497 if ($this->repairMode) {
498 // In repair mode, rediscover the entire repository, ignoring the
499 // database state. The engine still maintains a local cache (the
500 // "Working Set") but we just give up before looking in the database.
501 return;
504 $max_size = self::MAX_COMMIT_CACHE_SIZE;
506 // If we're filling more identifiers than would fit in the cache, ignore
507 // the ones that don't fit. Because the cache is FIFO, overfilling it can
508 // cause the entire cache to miss. See T12296.
509 if (count($identifiers) > $max_size) {
510 $identifiers = array_slice($identifiers, 0, $max_size);
513 // When filling the cache we ignore commits which have been marked as
514 // unreachable, treating them as though they do not exist. When recording
515 // commits later we'll revive commits that exist but are unreachable.
517 $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
518 'repositoryID = %d AND commitIdentifier IN (%Ls)
519 AND (importStatus & %d) != %d',
520 $this->getRepository()->getID(),
521 $identifiers,
522 PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE,
523 PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
525 foreach ($commits as $commit) {
526 $this->commitCache[$commit->getCommitIdentifier()] = true;
529 while (count($this->commitCache) > $max_size) {
530 array_shift($this->commitCache);
535 * Sort refs so we process permanent refs first. This makes the whole import
536 * process a little cheaper, since we can publish these commits the first
537 * time through rather than catching them in the refs step.
539 * @task internal
541 * @param list<DiffusionRepositoryRef> List of refs.
542 * @return list<DiffusionRepositoryRef> Sorted list of refs.
544 private function sortRefs(array $refs) {
545 $repository = $this->getRepository();
546 $publisher = $repository->newPublisher();
548 $head_refs = array();
549 $tail_refs = array();
550 foreach ($refs as $ref) {
551 if ($publisher->isPermanentRef($ref)) {
552 $head_refs[] = $ref;
553 } else {
554 $tail_refs[] = $ref;
558 return array_merge($head_refs, $tail_refs);
562 private function recordCommit(
563 PhabricatorRepository $repository,
564 $commit_identifier,
565 $epoch,
566 $is_permanent,
567 array $parents,
568 $task_priority) {
570 $commit = new PhabricatorRepositoryCommit();
571 $conn_w = $repository->establishConnection('w');
573 // First, try to revive an existing unreachable commit (if one exists) by
574 // removing the "unreachable" flag. If we succeed, we don't need to do
575 // anything else: we already discovered this commit some time ago.
576 queryfx(
577 $conn_w,
578 'UPDATE %T SET importStatus = (importStatus & ~%d)
579 WHERE repositoryID = %d AND commitIdentifier = %s',
580 $commit->getTableName(),
581 PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE,
582 $repository->getID(),
583 $commit_identifier);
584 if ($conn_w->getAffectedRows()) {
585 $commit = $commit->loadOneWhere(
586 'repositoryID = %d AND commitIdentifier = %s',
587 $repository->getID(),
588 $commit_identifier);
590 // After reviving a commit, schedule new daemons for it.
591 $this->didDiscoverCommit($repository, $commit, $epoch, $task_priority);
592 return;
595 $commit->setRepositoryID($repository->getID());
596 $commit->setCommitIdentifier($commit_identifier);
597 $commit->setEpoch($epoch);
598 if ($is_permanent) {
599 $commit->setImportStatus(PhabricatorRepositoryCommit::IMPORTED_PERMANENT);
602 $data = new PhabricatorRepositoryCommitData();
604 try {
605 // If this commit has parents, look up their IDs. The parent commits
606 // should always exist already.
608 $parent_ids = array();
609 if ($parents) {
610 $parent_rows = queryfx_all(
611 $conn_w,
612 'SELECT id, commitIdentifier FROM %T
613 WHERE commitIdentifier IN (%Ls) AND repositoryID = %d',
614 $commit->getTableName(),
615 $parents,
616 $repository->getID());
618 $parent_map = ipull($parent_rows, 'id', 'commitIdentifier');
620 foreach ($parents as $parent) {
621 if (empty($parent_map[$parent])) {
622 throw new Exception(
623 pht('Unable to identify parent "%s"!', $parent));
625 $parent_ids[] = $parent_map[$parent];
627 } else {
628 // Write an explicit 0 so we can distinguish between "really no
629 // parents" and "data not available".
630 if (!$repository->isSVN()) {
631 $parent_ids = array(0);
635 $commit->openTransaction();
636 $commit->save();
638 $data->setCommitID($commit->getID());
639 $data->save();
641 foreach ($parent_ids as $parent_id) {
642 queryfx(
643 $conn_w,
644 'INSERT IGNORE INTO %T (childCommitID, parentCommitID)
645 VALUES (%d, %d)',
646 PhabricatorRepository::TABLE_PARENTS,
647 $commit->getID(),
648 $parent_id);
650 $commit->saveTransaction();
652 $this->didDiscoverCommit($repository, $commit, $epoch, $task_priority);
654 if ($this->repairMode) {
655 // Normally, the query should throw a duplicate key exception. If we
656 // reach this in repair mode, we've actually performed a repair.
657 $this->log(pht('Repaired commit "%s".', $commit_identifier));
660 PhutilEventEngine::dispatchEvent(
661 new PhabricatorEvent(
662 PhabricatorEventType::TYPE_DIFFUSION_DIDDISCOVERCOMMIT,
663 array(
664 'repository' => $repository,
665 'commit' => $commit,
666 )));
668 } catch (AphrontDuplicateKeyQueryException $ex) {
669 $commit->killTransaction();
670 // Ignore. This can happen because we discover the same new commit
671 // more than once when looking at history, or because of races or
672 // data inconsistency or cosmic radiation; in any case, we're still
673 // in a good state if we ignore the failure.
677 private function didDiscoverCommit(
678 PhabricatorRepository $repository,
679 PhabricatorRepositoryCommit $commit,
680 $epoch,
681 $task_priority) {
683 $this->queueCommitImportTask(
684 $repository,
685 $commit->getPHID(),
686 $task_priority,
687 $via = 'discovery');
689 // Update the repository summary table.
690 queryfx(
691 $commit->establishConnection('w'),
692 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch)
693 VALUES (%d, 1, %d, %d)
694 ON DUPLICATE KEY UPDATE
695 size = size + 1,
696 lastCommitID =
697 IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID),
698 epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)',
699 PhabricatorRepository::TABLE_SUMMARY,
700 $repository->getID(),
701 $commit->getID(),
702 $epoch);
705 private function didDiscoverRefs(array $refs) {
706 foreach ($refs as $ref) {
707 $this->workingSet[$ref->getIdentifier()] = true;
711 private function isInitialImport(array $refs) {
712 $commit_count = count($refs);
714 if ($commit_count <= PhabricatorRepository::IMPORT_THRESHOLD) {
715 // If we fetched a small number of commits, assume it's an initial
716 // commit or a stack of a few initial commits.
717 return false;
720 $viewer = $this->getViewer();
721 $repository = $this->getRepository();
723 $any_commits = id(new DiffusionCommitQuery())
724 ->setViewer($viewer)
725 ->withRepository($repository)
726 ->setLimit(1)
727 ->execute();
729 if ($any_commits) {
730 // If the repository already has commits, this isn't an import.
731 return false;
734 return true;
738 private function getObservedVersion(PhabricatorRepository $repository) {
739 if ($repository->isHosted()) {
740 return null;
743 if ($repository->isGit()) {
744 return $this->getGitObservedVersion($repository);
747 return null;
750 private function getGitObservedVersion(PhabricatorRepository $repository) {
751 $refs = id(new DiffusionLowLevelGitRefQuery())
752 ->setRepository($repository)
753 ->execute();
754 if (!$refs) {
755 return null;
758 // In Git, the observed version is the most recently discovered commit
759 // at any repository HEAD. It's possible for this to regress temporarily
760 // if a branch is pushed and then deleted. This is acceptable because it
761 // doesn't do anything meaningfully bad and will fix itself on the next
762 // push.
764 $ref_identifiers = mpull($refs, 'getCommitIdentifier');
765 $ref_identifiers = array_fuse($ref_identifiers);
767 $version = queryfx_one(
768 $repository->establishConnection('w'),
769 'SELECT MAX(id) version FROM %T WHERE repositoryID = %d
770 AND commitIdentifier IN (%Ls)',
771 id(new PhabricatorRepositoryCommit())->getTableName(),
772 $repository->getID(),
773 $ref_identifiers);
775 if (!$version) {
776 return null;
779 return (int)$version['version'];
782 private function markUnreachableCommits(PhabricatorRepository $repository) {
783 if (!$repository->isGit() && !$repository->isHg()) {
784 return;
787 // Find older versions of refs which we haven't processed yet. We're going
788 // to make sure their commits are still reachable.
789 $old_refs = id(new PhabricatorRepositoryOldRef())->loadAllWhere(
790 'repositoryPHID = %s',
791 $repository->getPHID());
793 // If we don't have any refs to update, bail out before building a graph
794 // stream. In particular, this improves behavior in empty repositories,
795 // where `git log` exits with an error.
796 if (!$old_refs) {
797 return;
800 // We can share a single graph stream across all the checks we need to do.
801 if ($repository->isGit()) {
802 $stream = new PhabricatorGitGraphStream($repository);
803 } else if ($repository->isHg()) {
804 $stream = new PhabricatorMercurialGraphStream($repository);
807 foreach ($old_refs as $old_ref) {
808 $identifier = $old_ref->getCommitIdentifier();
809 $this->markUnreachableFrom($repository, $stream, $identifier);
811 // If nothing threw an exception, we're all done with this ref.
812 $old_ref->delete();
816 private function markUnreachableFrom(
817 PhabricatorRepository $repository,
818 PhabricatorRepositoryGraphStream $stream,
819 $identifier) {
821 $unreachable = array();
823 $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
824 'repositoryID = %s AND commitIdentifier = %s',
825 $repository->getID(),
826 $identifier);
827 if (!$commit) {
828 return;
831 $look = array($commit);
832 $seen = array();
833 while ($look) {
834 $target = array_pop($look);
836 // If we've already checked this commit (for example, because history
837 // branches and then merges) we don't need to check it again.
838 $target_identifier = $target->getCommitIdentifier();
839 if (isset($seen[$target_identifier])) {
840 continue;
843 $seen[$target_identifier] = true;
845 // See PHI1688. If this commit is already marked as unreachable, we don't
846 // need to consider its ancestors. This may skip a lot of work if many
847 // branches with a lot of shared ancestry are deleted at the same time.
848 if ($target->isUnreachable()) {
849 continue;
852 try {
853 $stream->getCommitDate($target_identifier);
854 $reachable = true;
855 } catch (Exception $ex) {
856 $reachable = false;
859 if ($reachable) {
860 // This commit is reachable, so we don't need to go any further
861 // down this road.
862 continue;
865 $unreachable[] = $target;
867 // Find the commit's parents and check them for reachability, too. We
868 // have to look in the database since we no may longer have the commit
869 // in the repository.
870 $rows = queryfx_all(
871 $commit->establishConnection('w'),
872 'SELECT commit.* FROM %T commit
873 JOIN %T parents ON commit.id = parents.parentCommitID
874 WHERE parents.childCommitID = %d',
875 $commit->getTableName(),
876 PhabricatorRepository::TABLE_PARENTS,
877 $target->getID());
878 if (!$rows) {
879 continue;
882 $parents = id(new PhabricatorRepositoryCommit())
883 ->loadAllFromArray($rows);
884 foreach ($parents as $parent) {
885 $look[] = $parent;
889 $unreachable = array_reverse($unreachable);
891 $flag = PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE;
892 foreach ($unreachable as $unreachable_commit) {
893 $unreachable_commit->writeImportStatusFlag($flag);
896 // If anything was unreachable, just rebuild the whole summary table.
897 // We can't really update it incrementally when a commit becomes
898 // unreachable.
899 if ($unreachable) {
900 $this->rebuildSummaryTable($repository);
904 private function rebuildSummaryTable(PhabricatorRepository $repository) {
905 $conn_w = $repository->establishConnection('w');
907 $data = queryfx_one(
908 $conn_w,
909 'SELECT COUNT(*) N, MAX(id) id, MAX(epoch) epoch
910 FROM %T WHERE repositoryID = %d AND (importStatus & %d) != %d',
911 id(new PhabricatorRepositoryCommit())->getTableName(),
912 $repository->getID(),
913 PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE,
914 PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
916 queryfx(
917 $conn_w,
918 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch)
919 VALUES (%d, %d, %d, %d)
920 ON DUPLICATE KEY UPDATE
921 size = VALUES(size),
922 lastCommitID = VALUES(lastCommitID),
923 epoch = VALUES(epoch)',
924 PhabricatorRepository::TABLE_SUMMARY,
925 $repository->getID(),
926 $data['N'],
927 $data['id'],
928 $data['epoch']);