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