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