View Javadoc
1   /*
2    * Copyright (C) 2009, Google Inc. 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.lib;
12  
13  import java.io.File;
14  import java.io.IOException;
15  import java.util.ArrayList;
16  import java.util.Collection;
17  import java.util.concurrent.ConcurrentHashMap;
18  import java.util.concurrent.ScheduledFuture;
19  import java.util.concurrent.ScheduledThreadPoolExecutor;
20  import java.util.concurrent.TimeUnit;
21  
22  import org.eclipse.jgit.annotations.NonNull;
23  import org.eclipse.jgit.errors.RepositoryNotFoundException;
24  import org.eclipse.jgit.internal.storage.file.FileRepository;
25  import org.eclipse.jgit.lib.internal.WorkQueue;
26  import org.eclipse.jgit.util.FS;
27  import org.eclipse.jgit.util.IO;
28  import org.eclipse.jgit.util.RawParseUtils;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  /**
33   * Cache of active {@link org.eclipse.jgit.lib.Repository} instances.
34   */
35  public class RepositoryCache {
36  	private static final Logger LOG = LoggerFactory
37  			.getLogger(RepositoryCache.class);
38  
39  	private static final RepositoryCache.html#RepositoryCache">RepositoryCache cache = new RepositoryCache();
40  
41  	/**
42  	 * Open an existing repository, reusing a cached instance if possible.
43  	 * <p>
44  	 * When done with the repository, the caller must call
45  	 * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the
46  	 * repository's usage counter.
47  	 *
48  	 * @param location
49  	 *            where the local repository is. Typically a
50  	 *            {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}.
51  	 * @return the repository instance requested; caller must close when done.
52  	 * @throws java.io.IOException
53  	 *             the repository could not be read (likely its core.version
54  	 *             property is not supported).
55  	 * @throws org.eclipse.jgit.errors.RepositoryNotFoundException
56  	 *             there is no repository at the given location.
57  	 */
58  	public static Repository open(Key location) throws IOException,
59  			RepositoryNotFoundException {
60  		return open(location, true);
61  	}
62  
63  	/**
64  	 * Open a repository, reusing a cached instance if possible.
65  	 * <p>
66  	 * When done with the repository, the caller must call
67  	 * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the
68  	 * repository's usage counter.
69  	 *
70  	 * @param location
71  	 *            where the local repository is. Typically a
72  	 *            {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}.
73  	 * @param mustExist
74  	 *            If true, and the repository is not found, throws {@code
75  	 *            RepositoryNotFoundException}. If false, a repository instance
76  	 *            is created and registered anyway.
77  	 * @return the repository instance requested; caller must close when done.
78  	 * @throws java.io.IOException
79  	 *             the repository could not be read (likely its core.version
80  	 *             property is not supported).
81  	 * @throws RepositoryNotFoundException
82  	 *             There is no repository at the given location, only thrown if
83  	 *             {@code mustExist} is true.
84  	 */
85  	public static Repository open(Key location, boolean mustExist)
86  			throws IOException {
87  		return cache.openRepository(location, mustExist);
88  	}
89  
90  	/**
91  	 * Register one repository into the cache.
92  	 * <p>
93  	 * During registration the cache automatically increments the usage counter,
94  	 * permitting it to retain the reference. A
95  	 * {@link org.eclipse.jgit.lib.RepositoryCache.FileKey} for the repository's
96  	 * {@link org.eclipse.jgit.lib.Repository#getDirectory()} is used to index
97  	 * the repository in the cache.
98  	 * <p>
99  	 * If another repository already is registered in the cache at this
100 	 * location, the other instance is closed.
101 	 *
102 	 * @param db
103 	 *            repository to register.
104 	 */
105 	public static void register(Repository db) {
106 		if (db.getDirectory() != null) {
107 			FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
108 			cache.registerRepository(key, db);
109 		}
110 	}
111 
112 	/**
113 	 * Close and remove a repository from the cache.
114 	 * <p>
115 	 * Removes a repository from the cache, if it is still registered here, and
116 	 * close it.
117 	 *
118 	 * @param db
119 	 *            repository to unregister.
120 	 */
121 	public static void close(@NonNull Repository db) {
122 		if (db.getDirectory() != null) {
123 			FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
124 			cache.unregisterAndCloseRepository(key);
125 		}
126 	}
127 
128 	/**
129 	 * Remove a repository from the cache.
130 	 * <p>
131 	 * Removes a repository from the cache, if it is still registered here. This
132 	 * method will not close the repository, only remove it from the cache. See
133 	 * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove
134 	 * and close the repository.
135 	 *
136 	 * @param db
137 	 *            repository to unregister.
138 	 * @since 4.3
139 	 */
140 	public static void unregister(Repository db) {
141 		if (db.getDirectory() != null) {
142 			unregister(FileKey.exact(db.getDirectory(), db.getFS()));
143 		}
144 	}
145 
146 	/**
147 	 * Remove a repository from the cache.
148 	 * <p>
149 	 * Removes a repository from the cache, if it is still registered here. This
150 	 * method will not close the repository, only remove it from the cache. See
151 	 * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove
152 	 * and close the repository.
153 	 *
154 	 * @param location
155 	 *            location of the repository to remove.
156 	 * @since 4.1
157 	 */
158 	public static void unregister(Key location) {
159 		cache.unregisterRepository(location);
160 	}
161 
162 	/**
163 	 * Get the locations of all repositories registered in the cache.
164 	 *
165 	 * @return the locations of all repositories registered in the cache.
166 	 * @since 4.1
167 	 */
168 	public static Collection<Key> getRegisteredKeys() {
169 		return cache.getKeys();
170 	}
171 
172 	static boolean isCached(@NonNull Repository repo) {
173 		File gitDir = repo.getDirectory();
174 		if (gitDir == null) {
175 			return false;
176 		}
177 		FileKey key = new FileKey(gitDir, repo.getFS());
178 		return cache.cacheMap.get(key) == repo;
179 	}
180 
181 	/**
182 	 * Unregister all repositories from the cache.
183 	 */
184 	public static void clear() {
185 		cache.clearAll();
186 	}
187 
188 	static void clearExpired() {
189 		cache.clearAllExpired();
190 	}
191 
192 	static void reconfigure(RepositoryCacheConfig repositoryCacheConfig) {
193 		cache.configureEviction(repositoryCacheConfig);
194 	}
195 
196 	private final ConcurrentHashMap<Key, Repository> cacheMap;
197 
198 	private final Lock[] openLocks;
199 
200 	private ScheduledFuture<?> cleanupTask;
201 
202 	private volatile long expireAfter;
203 
204 	private RepositoryCache() {
205 		cacheMap = new ConcurrentHashMap<>();
206 		openLocks = new Lock[4];
207 		for (int i = 0; i < openLocks.length; i++) {
208 			openLocks[i] = new Lock();
209 		}
210 		configureEviction(new RepositoryCacheConfig());
211 	}
212 
213 	private void configureEviction(
214 			RepositoryCacheConfig repositoryCacheConfig) {
215 		expireAfter = repositoryCacheConfig.getExpireAfter();
216 		ScheduledThreadPoolExecutor scheduler = WorkQueue.getExecutor();
217 		synchronized (scheduler) {
218 			if (cleanupTask != null) {
219 				cleanupTask.cancel(false);
220 			}
221 			long delay = repositoryCacheConfig.getCleanupDelay();
222 			if (delay == RepositoryCacheConfig.NO_CLEANUP) {
223 				return;
224 			}
225 			cleanupTask = scheduler.scheduleWithFixedDelay(() -> {
226 				try {
227 					cache.clearAllExpired();
228 				} catch (Throwable e) {
229 					LOG.error(e.getMessage(), e);
230 				}
231 			}, delay, delay, TimeUnit.MILLISECONDS);
232 		}
233 	}
234 
235 	private Repository openRepository(final Key location,
236 			final boolean mustExist) throws IOException {
237 		Repository db = cacheMap.get(location);
238 		if (db == null) {
239 			synchronized (lockFor(location)) {
240 				db = cacheMap.get(location);
241 				if (db == null) {
242 					db = location.open(mustExist);
243 					cacheMap.put(location, db);
244 				} else {
245 					db.incrementOpen();
246 				}
247 			}
248 		} else {
249 			db.incrementOpen();
250 		}
251 		return db;
252 	}
253 
254 	private void registerRepository(Key location, Repository db) {
255 		try (Repository oldDb = cacheMap.put(location, db)) {
256 			// oldDb is auto-closed
257 		}
258 	}
259 
260 	private Repository unregisterRepository(Key location) {
261 		return cacheMap.remove(location);
262 	}
263 
264 	private boolean isExpired(Repository db) {
265 		return db != null && db.useCnt.get() <= 0
266 			&& (System.currentTimeMillis() - db.closedAt.get() > expireAfter);
267 	}
268 
269 	private void unregisterAndCloseRepository(Key location) {
270 		synchronized (lockFor(location)) {
271 			Repository oldDb = unregisterRepository(location);
272 			if (oldDb != null) {
273 				oldDb.doClose();
274 			}
275 		}
276 	}
277 
278 	private Collection<Key> getKeys() {
279 		return new ArrayList<>(cacheMap.keySet());
280 	}
281 
282 	private void clearAllExpired() {
283 		for (Repository db : cacheMap.values()) {
284 			if (isExpired(db)) {
285 				RepositoryCache.close(db);
286 			}
287 		}
288 	}
289 
290 	private void clearAll() {
291 		for (Key k : cacheMap.keySet()) {
292 			unregisterAndCloseRepository(k);
293 		}
294 	}
295 
296 	private Lock lockFor(Key location) {
297 		return openLocks[(location.hashCode() >>> 1) % openLocks.length];
298 	}
299 
300 	private static class Lock {
301 		// Used only for its monitor.
302 	}
303 
304 	/**
305 	 * Abstract hash key for {@link RepositoryCache} entries.
306 	 * <p>
307 	 * A Key instance should be lightweight, and implement hashCode() and
308 	 * equals() such that two Key instances are equal if they represent the same
309 	 * Repository location.
310 	 */
311 	public static interface Key {
312 		/**
313 		 * Called by {@link RepositoryCache#open(Key)} if it doesn't exist yet.
314 		 * <p>
315 		 * If a repository does not exist yet in the cache, the cache will call
316 		 * this method to acquire a handle to it.
317 		 *
318 		 * @param mustExist
319 		 *            true if the repository must exist in order to be opened;
320 		 *            false if a new non-existent repository is permitted to be
321 		 *            created (the caller is responsible for calling create).
322 		 * @return the new repository instance.
323 		 * @throws IOException
324 		 *             the repository could not be read (likely its core.version
325 		 *             property is not supported).
326 		 * @throws RepositoryNotFoundException
327 		 *             There is no repository at the given location, only thrown
328 		 *             if {@code mustExist} is true.
329 		 */
330 		Repository open(boolean mustExist) throws IOException,
331 				RepositoryNotFoundException;
332 	}
333 
334 	/** Location of a Repository, using the standard java.io.File API. */
335 	public static class FileKey implements Key {
336 		/**
337 		 * Obtain a pointer to an exact location on disk.
338 		 * <p>
339 		 * No guessing is performed, the given location is exactly the GIT_DIR
340 		 * directory of the repository.
341 		 *
342 		 * @param directory
343 		 *            location where the repository database is.
344 		 * @param fs
345 		 *            the file system abstraction which will be necessary to
346 		 *            perform certain file system operations.
347 		 * @return a key for the given directory.
348 		 * @see #lenient(File, FS)
349 		 */
350 		public static FileKey exact(File directory, FS fs) {
351 			return new FileKey(directory, fs);
352 		}
353 
354 		/**
355 		 * Obtain a pointer to a location on disk.
356 		 * <p>
357 		 * The method performs some basic guessing to locate the repository.
358 		 * Searched paths are:
359 		 * <ol>
360 		 * <li>{@code directory} // assume exact match</li>
361 		 * <li>{@code directory} + "/.git" // assume working directory</li>
362 		 * <li>{@code directory} + ".git" // assume bare</li>
363 		 * </ol>
364 		 *
365 		 * @param directory
366 		 *            location where the repository database might be.
367 		 * @param fs
368 		 *            the file system abstraction which will be necessary to
369 		 *            perform certain file system operations.
370 		 * @return a key for the given directory.
371 		 * @see #exact(File, FS)
372 		 */
373 		public static FileKey lenient(File directory, FS fs) {
374 			final File gitdir = resolve(directory, fs);
375 			return new FileKey(gitdir != null ? gitdir : directory, fs);
376 		}
377 
378 		private final File path;
379 		private final FS fs;
380 
381 		/**
382 		 * @param directory
383 		 *            exact location of the repository.
384 		 * @param fs
385 		 *            the file system abstraction which will be necessary to
386 		 *            perform certain file system operations.
387 		 */
388 		protected FileKey(File directory, FS fs) {
389 			path = canonical(directory);
390 			this.fs = fs;
391 		}
392 
393 		private static File canonical(File path) {
394 			try {
395 				return path.getCanonicalFile();
396 			} catch (IOException e) {
397 				return path.getAbsoluteFile();
398 			}
399 		}
400 
401 		/** @return location supplied to the constructor. */
402 		public final File getFile() {
403 			return path;
404 		}
405 
406 		@Override
407 		public Repository open(boolean mustExist) throws IOException {
408 			if (mustExist && !isGitRepository(path, fs))
409 				throw new RepositoryNotFoundException(path);
410 			return new FileRepository(path);
411 		}
412 
413 		@Override
414 		public int hashCode() {
415 			return path.hashCode();
416 		}
417 
418 		@Override
419 		public boolean equals(Object o) {
420 			return o instanceof FileKey && path.equals(((FileKey) o).path);
421 		}
422 
423 		@Override
424 		public String toString() {
425 			return path.toString();
426 		}
427 
428 		/**
429 		 * Guess if a directory contains a Git repository.
430 		 * <p>
431 		 * This method guesses by looking for the existence of some key files
432 		 * and directories.
433 		 *
434 		 * @param dir
435 		 *            the location of the directory to examine.
436 		 * @param fs
437 		 *            the file system abstraction which will be necessary to
438 		 *            perform certain file system operations.
439 		 * @return true if the directory "looks like" a Git repository; false if
440 		 *         it doesn't look enough like a Git directory to really be a
441 		 *         Git directory.
442 		 */
443 		public static boolean isGitRepository(File dir, FS fs) {
444 			return fs.resolve(dir, Constants.OBJECTS).exists()
445 					&& fs.resolve(dir, "refs").exists() //$NON-NLS-1$
446 					&& (fs.resolve(dir, Constants.REFTABLE).exists()
447 							|| isValidHead(new File(dir, Constants.HEAD)));
448 		}
449 
450 		private static boolean isValidHead(File head) {
451 			final String ref = readFirstLine(head);
452 			return ref != null
453 					&& (ref.startsWith("ref: refs/") || ObjectId.isId(ref)); //$NON-NLS-1$
454 		}
455 
456 		private static String readFirstLine(File head) {
457 			try {
458 				final byte[] buf = IO.readFully(head, 4096);
459 				int n = buf.length;
460 				if (n == 0)
461 					return null;
462 				if (buf[n - 1] == '\n')
463 					n--;
464 				return RawParseUtils.decode(buf, 0, n);
465 			} catch (IOException e) {
466 				return null;
467 			}
468 		}
469 
470 		/**
471 		 * Guess the proper path for a Git repository.
472 		 * <p>
473 		 * The method performs some basic guessing to locate the repository.
474 		 * Searched paths are:
475 		 * <ol>
476 		 * <li>{@code directory} // assume exact match</li>
477 		 * <li>{@code directory} + "/.git" // assume working directory</li>
478 		 * <li>{@code directory} + ".git" // assume bare</li>
479 		 * </ol>
480 		 *
481 		 * @param directory
482 		 *            location to guess from. Several permutations are tried.
483 		 * @param fs
484 		 *            the file system abstraction which will be necessary to
485 		 *            perform certain file system operations.
486 		 * @return the actual directory location if a better match is found;
487 		 *         null if there is no suitable match.
488 		 */
489 		public static File resolve(File directory, FS fs) {
490 			if (isGitRepository(directory, fs))
491 				return directory;
492 			if (isGitRepository(new File(directory, Constants.DOT_GIT), fs))
493 				return new File(directory, Constants.DOT_GIT);
494 
495 			final String name = directory.getName();
496 			final File parent = directory.getParentFile();
497 			if (isGitRepository(new File(parent, name + Constants.DOT_GIT_EXT), fs))
498 				return new File(parent, name + Constants.DOT_GIT_EXT);
499 			return null;
500 		}
501 	}
502 }