/* * Copyright (c) 2019, Google LLC and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.transport.connectivity; import static java.util.stream.Collectors.toList; import java.io.IOException; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.Set; import java.util.stream.Stream; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ConnectivityChecker; import org.eclipse.jgit.transport.ReceiveCommand; /** * Implementation of connectivity checker which tries to do check with smaller * set of references first and if it fails will fall back to check against all * advertised references. * * This is useful for big repos with enormous number of references. */ public class IterativeConnectivityChecker implements ConnectivityChecker { private static final int MAXIMUM_PARENTS_TO_CHECK = 128; private final ConnectivityChecker delegate; private Set forcedHaves = Collections.emptySet(); /** * @param delegate * Delegate checker which will be called for actual checks. */ public IterativeConnectivityChecker(ConnectivityChecker delegate) { this.delegate = delegate; } @Override public void checkConnectivity(ConnectivityCheckInfo connectivityCheckInfo, Set advertisedHaves, ProgressMonitor pm) throws MissingObjectException, IOException { try { Set newRefs = new HashSet<>(); Set expectedParents = new HashSet<>(); getAllObjectIds(connectivityCheckInfo.getCommands()) .forEach(oid -> { if (advertisedHaves.contains(oid)) { expectedParents.add(oid); } else { newRefs.add(oid); } }); if (!newRefs.isEmpty()) { expectedParents.addAll(extractAdvertisedParentCommits(newRefs, advertisedHaves, connectivityCheckInfo.getWalk())); } expectedParents.addAll(forcedHaves); if (!expectedParents.isEmpty()) { delegate.checkConnectivity(connectivityCheckInfo, expectedParents, pm); return; } } catch (MissingObjectException e) { // This is fine, retry with all haves. } delegate.checkConnectivity(connectivityCheckInfo, advertisedHaves, pm); } private static Stream getAllObjectIds( List commands) { return commands.stream().flatMap(cmd -> { if (cmd.getType() == ReceiveCommand.Type.UPDATE || cmd .getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) { return Stream.of(cmd.getOldId(), cmd.getNewId()); } else if (cmd.getType() == ReceiveCommand.Type.CREATE) { return Stream.of(cmd.getNewId()); } return Stream.of(); }); } /** * Sets additional haves that client can depend on (e.g. gerrit changes). * * @param forcedHaves * Haves server expects client to depend on. */ public void setForcedHaves(Set forcedHaves) { this.forcedHaves = Collections.unmodifiableSet(forcedHaves); } private static Set extractAdvertisedParentCommits( Set newRefs, Set advertisedHaves, RevWalk rw) throws MissingObjectException, IOException { Set advertisedParents = new HashSet<>(); for (ObjectId newRef : newRefs) { RevObject object = rw.parseAny(newRef); if (object instanceof RevCommit) { int numberOfParentsToCheck = 0; Queue parents = new ArrayDeque<>( MAXIMUM_PARENTS_TO_CHECK); parents.addAll( parseParents(((RevCommit) object).getParents(), rw)); // Looking through a chain of ancestors handles the case where a // series of commits is sent in a single push for a new branch. while (!parents.isEmpty()) { RevCommit parentCommit = parents.poll(); if (advertisedHaves.contains(parentCommit.getId())) { advertisedParents.add(parentCommit.getId()); } else if (numberOfParentsToCheck < MAXIMUM_PARENTS_TO_CHECK) { RevCommit[] grandParents = parentCommit.getParents(); numberOfParentsToCheck += grandParents.length; parents.addAll(parseParents(grandParents, rw)); } } } } return advertisedParents; } private static List parseParents(RevCommit[] parents, RevWalk rw) { return Arrays.stream(parents).map((commit) -> { try { return rw.parseCommit(commit); } catch (Exception e) { throw new RuntimeException(e); } }).collect(toList()); } }