IterativeConnectivityChecker.java

  1. /*
  2.  * Copyright (c) 2019, Google LLC  and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * http://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.internal.transport.connectivity;

  11. import static java.util.stream.Collectors.toList;

  12. import java.io.IOException;
  13. import java.util.ArrayDeque;
  14. import java.util.Arrays;
  15. import java.util.Collections;
  16. import java.util.HashSet;
  17. import java.util.List;
  18. import java.util.Queue;
  19. import java.util.Set;
  20. import java.util.stream.Stream;

  21. import org.eclipse.jgit.errors.MissingObjectException;
  22. import org.eclipse.jgit.lib.ObjectId;
  23. import org.eclipse.jgit.lib.ProgressMonitor;
  24. import org.eclipse.jgit.revwalk.RevCommit;
  25. import org.eclipse.jgit.revwalk.RevObject;
  26. import org.eclipse.jgit.revwalk.RevWalk;
  27. import org.eclipse.jgit.transport.ConnectivityChecker;
  28. import org.eclipse.jgit.transport.ReceiveCommand;

  29. /**
  30.  * Implementation of connectivity checker which tries to do check with smaller
  31.  * set of references first and if it fails will fall back to check against all
  32.  * advertised references.
  33.  *
  34.  * This is useful for big repos with enormous number of references.
  35.  */
  36. public class IterativeConnectivityChecker implements ConnectivityChecker {
  37.     private static final int MAXIMUM_PARENTS_TO_CHECK = 128;

  38.     private final ConnectivityChecker delegate;

  39.     private Set<ObjectId> forcedHaves = Collections.emptySet();

  40.     /**
  41.      * @param delegate
  42.      *            Delegate checker which will be called for actual checks.
  43.      */
  44.     public IterativeConnectivityChecker(ConnectivityChecker delegate) {
  45.         this.delegate = delegate;
  46.     }

  47.     @Override
  48.     public void checkConnectivity(ConnectivityCheckInfo connectivityCheckInfo,
  49.             Set<ObjectId> advertisedHaves, ProgressMonitor pm)
  50.             throws MissingObjectException, IOException {
  51.         try {
  52.             Set<ObjectId> newRefs = new HashSet<>();
  53.             Set<ObjectId> expectedParents = new HashSet<>();

  54.             getAllObjectIds(connectivityCheckInfo.getCommands())
  55.                     .forEach(oid -> {
  56.                         if (advertisedHaves.contains(oid)) {
  57.                             expectedParents.add(oid);
  58.                         } else {
  59.                             newRefs.add(oid);
  60.                         }
  61.                     });
  62.             if (!newRefs.isEmpty()) {
  63.                 expectedParents.addAll(extractAdvertisedParentCommits(newRefs,
  64.                         advertisedHaves, connectivityCheckInfo.getWalk()));
  65.             }

  66.             expectedParents.addAll(forcedHaves);

  67.             if (!expectedParents.isEmpty()) {
  68.                 delegate.checkConnectivity(connectivityCheckInfo,
  69.                         expectedParents, pm);
  70.                 return;
  71.             }
  72.         } catch (MissingObjectException e) {
  73.             // This is fine, retry with all haves.
  74.         }
  75.         delegate.checkConnectivity(connectivityCheckInfo, advertisedHaves, pm);
  76.     }

  77.     private static Stream<ObjectId> getAllObjectIds(
  78.             List<ReceiveCommand> commands) {
  79.         return commands.stream().flatMap(cmd -> {
  80.             if (cmd.getType() == ReceiveCommand.Type.UPDATE || cmd
  81.                     .getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) {
  82.                 return Stream.of(cmd.getOldId(), cmd.getNewId());
  83.             } else if (cmd.getType() == ReceiveCommand.Type.CREATE) {
  84.                 return Stream.of(cmd.getNewId());
  85.             }
  86.             return Stream.of();
  87.         });
  88.     }

  89.     /**
  90.      * Sets additional haves that client can depend on (e.g. gerrit changes).
  91.      *
  92.      * @param forcedHaves
  93.      *            Haves server expects client to depend on.
  94.      */
  95.     public void setForcedHaves(Set<ObjectId> forcedHaves) {
  96.         this.forcedHaves = Collections.unmodifiableSet(forcedHaves);
  97.     }

  98.     private static Set<ObjectId> extractAdvertisedParentCommits(
  99.             Set<ObjectId> newRefs, Set<ObjectId> advertisedHaves, RevWalk rw)
  100.             throws MissingObjectException, IOException {
  101.         Set<ObjectId> advertisedParents = new HashSet<>();
  102.         for (ObjectId newRef : newRefs) {
  103.             RevObject object = rw.parseAny(newRef);
  104.             if (object instanceof RevCommit) {
  105.                 int numberOfParentsToCheck = 0;
  106.                 Queue<RevCommit> parents = new ArrayDeque<>(
  107.                         MAXIMUM_PARENTS_TO_CHECK);
  108.                 parents.addAll(
  109.                         parseParents(((RevCommit) object).getParents(), rw));
  110.                 // Looking through a chain of ancestors handles the case where a
  111.                 // series of commits is sent in a single push for a new branch.
  112.                 while (!parents.isEmpty()) {
  113.                     RevCommit parentCommit = parents.poll();
  114.                     if (advertisedHaves.contains(parentCommit.getId())) {
  115.                         advertisedParents.add(parentCommit.getId());
  116.                     } else if (numberOfParentsToCheck < MAXIMUM_PARENTS_TO_CHECK) {
  117.                         RevCommit[] grandParents = parentCommit.getParents();
  118.                         numberOfParentsToCheck += grandParents.length;
  119.                         parents.addAll(parseParents(grandParents, rw));
  120.                     }
  121.                 }
  122.             }
  123.         }
  124.         return advertisedParents;
  125.     }

  126.     private static List<RevCommit> parseParents(RevCommit[] parents,
  127.             RevWalk rw) {
  128.         return Arrays.stream(parents).map((commit) -> {
  129.             try {
  130.                 return rw.parseCommit(commit);
  131.             } catch (Exception e) {
  132.                 throw new RuntimeException(e);
  133.             }
  134.         }).collect(toList());
  135.     }
  136. }