View Javadoc
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  
11  package org.eclipse.jgit.internal.transport.connectivity;
12  
13  import static java.util.stream.Collectors.toList;
14  
15  import java.io.IOException;
16  import java.util.ArrayDeque;
17  import java.util.Arrays;
18  import java.util.Collections;
19  import java.util.HashSet;
20  import java.util.List;
21  import java.util.Queue;
22  import java.util.Set;
23  import java.util.stream.Stream;
24  
25  import org.eclipse.jgit.errors.MissingObjectException;
26  import org.eclipse.jgit.lib.ObjectId;
27  import org.eclipse.jgit.lib.ProgressMonitor;
28  import org.eclipse.jgit.revwalk.RevCommit;
29  import org.eclipse.jgit.revwalk.RevObject;
30  import org.eclipse.jgit.revwalk.RevWalk;
31  import org.eclipse.jgit.transport.ConnectivityChecker;
32  import org.eclipse.jgit.transport.ReceiveCommand;
33  
34  /**
35   * Implementation of connectivity checker which tries to do check with smaller
36   * set of references first and if it fails will fall back to check against all
37   * advertised references.
38   *
39   * This is useful for big repos with enormous number of references.
40   */
41  public class IterativeConnectivityChecker implements ConnectivityChecker {
42  	private static final int MAXIMUM_PARENTS_TO_CHECK = 128;
43  
44  	private final ConnectivityChecker delegate;
45  
46  	private Set<ObjectId> forcedHaves = Collections.emptySet();
47  
48  	/**
49  	 * @param delegate
50  	 *            Delegate checker which will be called for actual checks.
51  	 */
52  	public IterativeConnectivityChecker(ConnectivityChecker delegate) {
53  		this.delegate = delegate;
54  	}
55  
56  	@Override
57  	public void checkConnectivity(ConnectivityCheckInfo connectivityCheckInfo,
58  			Set<ObjectId> advertisedHaves, ProgressMonitor pm)
59  			throws MissingObjectException, IOException {
60  		try {
61  			Set<ObjectId> newRefs = new HashSet<>();
62  			Set<ObjectId> expectedParents = new HashSet<>();
63  
64  			getAllObjectIds(connectivityCheckInfo.getCommands())
65  					.forEach(oid -> {
66  						if (advertisedHaves.contains(oid)) {
67  							expectedParents.add(oid);
68  						} else {
69  							newRefs.add(oid);
70  						}
71  					});
72  			if (!newRefs.isEmpty()) {
73  				expectedParents.addAll(extractAdvertisedParentCommits(newRefs,
74  						advertisedHaves, connectivityCheckInfo.getWalk()));
75  			}
76  
77  			expectedParents.addAll(forcedHaves);
78  
79  			if (!expectedParents.isEmpty()) {
80  				delegate.checkConnectivity(connectivityCheckInfo,
81  						expectedParents, pm);
82  				return;
83  			}
84  		} catch (MissingObjectException e) {
85  			// This is fine, retry with all haves.
86  		}
87  		delegate.checkConnectivity(connectivityCheckInfo, advertisedHaves, pm);
88  	}
89  
90  	private static Stream<ObjectId> getAllObjectIds(
91  			List<ReceiveCommand> commands) {
92  		return commands.stream().flatMap(cmd -> {
93  			if (cmd.getType() == ReceiveCommand.Type.UPDATE || cmd
94  					.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) {
95  				return Stream.of(cmd.getOldId(), cmd.getNewId());
96  			} else if (cmd.getType() == ReceiveCommand.Type.CREATE) {
97  				return Stream.of(cmd.getNewId());
98  			}
99  			return Stream.of();
100 		});
101 	}
102 
103 	/**
104 	 * Sets additional haves that client can depend on (e.g. gerrit changes).
105 	 *
106 	 * @param forcedHaves
107 	 *            Haves server expects client to depend on.
108 	 */
109 	public void setForcedHaves(Set<ObjectId> forcedHaves) {
110 		this.forcedHaves = Collections.unmodifiableSet(forcedHaves);
111 	}
112 
113 	private static Set<ObjectId> extractAdvertisedParentCommits(
114 			Set<ObjectId> newRefs, Set<ObjectId> advertisedHaves, RevWalk rw)
115 			throws MissingObjectException, IOException {
116 		Set<ObjectId> advertisedParents = new HashSet<>();
117 		for (ObjectId newRef : newRefs) {
118 			RevObject object = rw.parseAny(newRef);
119 			if (object instanceof RevCommit) {
120 				int numberOfParentsToCheck = 0;
121 				Queue<RevCommit> parents = new ArrayDeque<>(
122 						MAXIMUM_PARENTS_TO_CHECK);
123 				parents.addAll(
124 						parseParents(((RevCommit) object).getParents(), rw));
125 				// Looking through a chain of ancestors handles the case where a
126 				// series of commits is sent in a single push for a new branch.
127 				while (!parents.isEmpty()) {
128 					RevCommit parentCommit = parents.poll();
129 					if (advertisedHaves.contains(parentCommit.getId())) {
130 						advertisedParents.add(parentCommit.getId());
131 					} else if (numberOfParentsToCheck < MAXIMUM_PARENTS_TO_CHECK) {
132 						RevCommit[] grandParents = parentCommit.getParents();
133 						numberOfParentsToCheck += grandParents.length;
134 						parents.addAll(parseParents(grandParents, rw));
135 					}
136 				}
137 			}
138 		}
139 		return advertisedParents;
140 	}
141 
142 	private static List<RevCommit> parseParents(RevCommit[] parents,
143 			RevWalk rw) {
144 		return Arrays.stream(parents).map((commit) -> {
145 			try {
146 				return rw.parseCommit(commit);
147 			} catch (Exception e) {
148 				throw new RuntimeException(e);
149 			}
150 		}).collect(toList());
151 	}
152 }