blob: b44c4ae5cb5606dcec928056cae3a6073e0b9b52 [file] [log] [blame]
/*
* 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<ObjectId> 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<ObjectId> advertisedHaves, ProgressMonitor pm)
throws MissingObjectException, IOException {
try {
Set<ObjectId> newRefs = new HashSet<>();
Set<ObjectId> 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<ObjectId> getAllObjectIds(
List<ReceiveCommand> 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<ObjectId> forcedHaves) {
this.forcedHaves = Collections.unmodifiableSet(forcedHaves);
}
private static Set<ObjectId> extractAdvertisedParentCommits(
Set<ObjectId> newRefs, Set<ObjectId> advertisedHaves, RevWalk rw)
throws MissingObjectException, IOException {
Set<ObjectId> advertisedParents = new HashSet<>();
for (ObjectId newRef : newRefs) {
RevObject object = rw.parseAny(newRef);
if (object instanceof RevCommit) {
int numberOfParentsToCheck = 0;
Queue<RevCommit> 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<RevCommit> 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());
}
}