View Javadoc
1   /*
2    * Copyright (C) 2017, 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    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.internal.storage.reftable;
12  
13  import java.io.IOException;
14  import java.util.ArrayList;
15  import java.util.Collections;
16  import java.util.HashSet;
17  import java.util.Iterator;
18  import java.util.List;
19  import java.util.Set;
20  import java.util.TreeSet;
21  import java.util.concurrent.locks.ReentrantLock;
22  import java.util.stream.Collectors;
23  
24  import org.eclipse.jgit.annotations.Nullable;
25  import org.eclipse.jgit.lib.ObjectId;
26  import org.eclipse.jgit.lib.Ref;
27  import org.eclipse.jgit.lib.RefDatabase;
28  import org.eclipse.jgit.lib.ReflogReader;
29  import org.eclipse.jgit.transport.ReceiveCommand;
30  
31  /**
32   * Operations on {@link MergedReftable} that is common to various reftable-using
33   * subclasses of {@link RefDatabase}. See
34   * {@link org.eclipse.jgit.internal.storage.dfs.DfsReftableDatabase} for an
35   * example.
36   */
37  public abstract class ReftableDatabase {
38  	// Protects mergedTables.
39  	private final ReentrantLock lock = new ReentrantLock(true);
40  
41  	private Reftable mergedTables;
42  
43  	/**
44  	 * ReftableDatabase lazily initializes its merged reftable on the first read after
45  	 * construction or clearCache() call. This function should always instantiate a new
46  	 * MergedReftable based on the list of reftables specified by the underlying storage.
47  	 *
48  	 * @return the ReftableStack for this instance
49  	 * @throws IOException
50  	 *             on I/O problems.
51  	 */
52  	protected abstract MergedReftable openMergedReftable() throws IOException;
53  
54  	/**
55  	 * @return the next available logical timestamp for an additional reftable
56  	 *         in the stack.
57  	 * @throws java.io.IOException
58  	 *             on I/O problems.
59  	 */
60  	public long nextUpdateIndex() throws IOException {
61  		lock.lock();
62  		try {
63  			return reader().maxUpdateIndex() + 1;
64  		} finally {
65  			lock.unlock();
66  		}
67  	}
68  
69  	/**
70  	 * @return a ReflogReader for the given ref
71  	 * @param refname
72  	 *            the name of the ref.
73  	 * @throws IOException
74  	 *             on I/O problems
75  	 */
76  	public ReflogReader getReflogReader(String refname) throws IOException {
77  		lock.lock();
78  		try {
79  			return new ReftableReflogReader(lock, reader(), refname);
80  		} finally {
81  			lock.unlock();
82  		}
83  	}
84  
85  	/**
86  	 * @return a ReceiveCommand for the change from oldRef to newRef
87  	 * @param oldRef
88  	 *            a ref
89  	 * @param newRef
90  	 *            a ref
91  	 */
92  	public static ReceiveCommand toCommand(Ref oldRef, Ref newRef) {
93  		ObjectId oldId = toId(oldRef);
94  		ObjectId newId = toId(newRef);
95  		String name = oldRef != null ? oldRef.getName() : newRef.getName();
96  
97  		if (oldRef != null && oldRef.isSymbolic()) {
98  			if (newRef != null) {
99  				if (newRef.isSymbolic()) {
100 					return ReceiveCommand.link(oldRef.getTarget().getName(),
101 							newRef.getTarget().getName(), name);
102 				}
103 				// This should pass in oldId for compat with
104 				// RefDirectoryUpdate
105 				return ReceiveCommand.unlink(oldRef.getTarget().getName(),
106 						newId, name);
107 			}
108 			return ReceiveCommand.unlink(oldRef.getTarget().getName(),
109 					ObjectId.zeroId(), name);
110 		}
111 
112 		if (newRef != null && newRef.isSymbolic()) {
113 			if (oldRef != null) {
114 				if (oldRef.isSymbolic()) {
115 					return ReceiveCommand.link(oldRef.getTarget().getName(),
116 							newRef.getTarget().getName(), name);
117 				}
118 				return ReceiveCommand.link(oldId,
119 						newRef.getTarget().getName(), name);
120 			}
121 			return ReceiveCommand.link(ObjectId.zeroId(),
122 					newRef.getTarget().getName(), name);
123 		}
124 
125 		return new ReceiveCommand(oldId, newId, name);
126 	}
127 
128 	private static ObjectId toId(Ref ref) {
129 		if (ref != null) {
130 			ObjectId id = ref.getObjectId();
131 			if (id != null) {
132 				return id;
133 			}
134 		}
135 		return ObjectId.zeroId();
136 	}
137 
138 	/**
139 	 * @return the lock protecting underlying ReftableReaders against concurrent
140 	 *         reads.
141 	 */
142 	public ReentrantLock getLock() {
143 		return lock;
144 	}
145 
146 	/**
147 	 * @return the merged reftable that is implemented by the stack of
148 	 *         reftables. Return value must be accessed under lock.
149 	 * @throws IOException
150 	 *             on I/O problems
151 	 */
152 	private Reftable reader() throws IOException {
153 		if (!lock.isLocked()) {
154 			throw new IllegalStateException(
155 					"must hold lock to access merged table"); //$NON-NLS-1$
156 		}
157 		if (mergedTables == null) {
158 			mergedTables = openMergedReftable();
159 		}
160 		return mergedTables;
161 	}
162 
163 	/**
164 	 * @return whether the given refName would be illegal in a repository that
165 	 *         uses loose refs.
166 	 * @param refName
167 	 *            the name to check
168 	 * @param added
169 	 *            a sorted set of refs we pretend have been added to the
170 	 *            database.
171 	 * @param deleted
172 	 *            a set of refs we pretend have been removed from the database.
173 	 * @throws IOException
174 	 *             on I/O problems
175 	 */
176 	public boolean isNameConflicting(String refName, TreeSet<String> added,
177 			Set<String> deleted) throws IOException {
178 		lock.lock();
179 		try {
180 			Reftable table = reader();
181 
182 			// Cannot be nested within an existing reference.
183 			int lastSlash = refName.lastIndexOf('/');
184 			while (0 < lastSlash) {
185 				String prefix = refName.substring(0, lastSlash);
186 				if (!deleted.contains(prefix)
187 						&& (table.hasRef(prefix) || added.contains(prefix))) {
188 					return true;
189 				}
190 				lastSlash = refName.lastIndexOf('/', lastSlash - 1);
191 			}
192 
193 			// Cannot be the container of an existing reference.
194 			String prefix = refName + '/';
195 			RefCursor c = table.seekRefsWithPrefix(prefix);
196 			while (c.next()) {
197 				if (!deleted.contains(c.getRef().getName())) {
198 					return true;
199 				}
200 			}
201 
202 			String it = added.ceiling(refName + '/');
203 			if (it != null && it.startsWith(prefix)) {
204 				return true;
205 			}
206 			return false;
207 		} finally {
208 			lock.unlock();
209 		}
210 	}
211 
212 	/**
213 	 * Read a single reference.
214 	 * <p>
215 	 * This method expects an unshortened reference name and does not search
216 	 * using the standard search path.
217 	 *
218 	 * @param name
219 	 *            the unabbreviated name of the reference.
220 	 * @return the reference (if it exists); else {@code null}.
221 	 * @throws java.io.IOException
222 	 *             the reference space cannot be accessed.
223 	 */
224 	@Nullable
225 	public Ref exactRef(String name) throws IOException {
226 		lock.lock();
227 		try {
228 			Reftable table = reader();
229 			Ref ref = table.exactRef(name);
230 			if (ref != null && ref.isSymbolic()) {
231 				return table.resolve(ref);
232 			}
233 			return ref;
234 		} finally {
235 			lock.unlock();
236 		}
237 	}
238 
239 	/**
240 	 * Returns refs whose names start with a given prefix.
241 	 *
242 	 * @param prefix
243 	 *            string that names of refs should start with; may be empty (to
244 	 *            return all refs).
245 	 * @return immutable list of refs whose names start with {@code prefix}.
246 	 * @throws java.io.IOException
247 	 *             the reference space cannot be accessed.
248 	 */
249 	public List<Ref> getRefsByPrefix(String prefix) throws IOException {
250 		List<Ref> all = new ArrayList<>();
251 		lock.lock();
252 		try {
253 			Reftable table = reader();
254 			try (RefCursor rc = RefDatabase.ALL.equals(prefix) ? table.allRefs()
255 					: table.seekRefsWithPrefix(prefix)) {
256 				while (rc.next()) {
257 					Ref ref = table.resolve(rc.getRef());
258 					if (ref != null && ref.getObjectId() != null) {
259 						all.add(ref);
260 					}
261 				}
262 			}
263 		} finally {
264 			lock.unlock();
265 		}
266 
267 		return Collections.unmodifiableList(all);
268 	}
269 
270 	/**
271 	 * Returns refs whose names start with a given prefix excluding all refs that
272 	 * start with one of the given prefixes.
273 	 *
274 	 * @param include string that names of refs should start with; may be empty.
275 	 * @param excludes strings that names of refs can't start with; may be empty.
276 	 * @return immutable list of refs whose names start with {@code include} and
277 	 *         none of the strings in {@code exclude}.
278 	 * @throws java.io.IOException the reference space cannot be accessed.
279 	 */
280 	public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes) throws IOException {
281 		if (excludes.isEmpty()) {
282 			return getRefsByPrefix(include);
283 		}
284 		List<Ref> results = new ArrayList<>();
285 		lock.lock();
286 		try {
287 			Reftable table = reader();
288 			Iterator<String> excludeIterator =
289 					excludes.stream().sorted().collect(Collectors.toList()).iterator();
290 			String currentExclusion = excludeIterator.hasNext() ? excludeIterator.next() : null;
291 			try (RefCursor rc = RefDatabase.ALL.equals(include) ? table.allRefs() : table.seekRefsWithPrefix(include)) {
292 				while (rc.next()) {
293 					Ref ref = table.resolve(rc.getRef());
294 					if (ref == null || ref.getObjectId() == null) {
295 						continue;
296 					}
297 					// Skip prefixes that will never see since we are already further than those
298 					// prefixes lexicographically.
299 					while (excludeIterator.hasNext() && !ref.getName().startsWith(currentExclusion)
300 							&& ref.getName().compareTo(currentExclusion) > 0) {
301 						currentExclusion = excludeIterator.next();
302 					}
303 
304 					if (currentExclusion != null && ref.getName().startsWith(currentExclusion)) {
305 						rc.seekPastPrefix(currentExclusion);
306 						continue;
307 					}
308 					results.add(ref);
309 				}
310 			}
311 		} finally {
312 			lock.unlock();
313 		}
314 
315 		return Collections.unmodifiableList(results);
316 	}
317 
318 	/**
319 	 * @return whether there is a fast SHA1 to ref map.
320 	 * @throws IOException in case of I/O problems.
321 	 */
322 	public boolean hasFastTipsWithSha1() throws IOException {
323 		lock.lock();
324 		try {
325 			return reader().hasObjectMap();
326 		} finally {
327 			lock.unlock();
328 		}
329 	}
330 
331 	/**
332 	 * Returns all refs that resolve directly to the given {@link ObjectId}.
333 	 * Includes peeled {@link ObjectId}s.
334 	 *
335 	 * @param id
336 	 *            {@link ObjectId} to resolve
337 	 * @return a {@link Set} of {@link Ref}s whose tips point to the provided
338 	 *         id.
339 	 * @throws java.io.IOException
340 	 *             on I/O errors.
341 	 */
342 	public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
343 		lock.lock();
344 		try {
345 			RefCursor cursor = reader().byObjectId(id);
346 			Set<Ref> refs = new HashSet<>();
347 			while (cursor.next()) {
348 				refs.add(cursor.getRef());
349 			}
350 			return refs;
351 		} finally {
352 			lock.unlock();
353 		}
354 	}
355 
356 	/**
357 	 * Drops all data that might be cached in memory.
358 	 */
359 	public void clearCache() {
360 		lock.lock();
361 		try {
362 			mergedTables = null;
363 		} finally {
364 			lock.unlock();
365 		}
366 	}
367 }