| /* | |
| * Copyright (C) 2009, Google Inc. and others | |
| * | |
| * This program and the accompanying materials are made available under the | |
| * terms of the Eclipse Distribution License v. 1.0 which is available at | |
| * https://www.eclipse.org/org/documents/edl-v10.php. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| package org.eclipse.jgit.lib; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.Collection; | |
| import java.util.Map; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ScheduledFuture; | |
| import java.util.concurrent.ScheduledThreadPoolExecutor; | |
| import java.util.concurrent.TimeUnit; | |
| import org.eclipse.jgit.annotations.NonNull; | |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; | |
| import org.eclipse.jgit.internal.storage.file.FileRepository; | |
| import org.eclipse.jgit.lib.internal.WorkQueue; | |
| import org.eclipse.jgit.util.FS; | |
| import org.eclipse.jgit.util.IO; | |
| import org.eclipse.jgit.util.RawParseUtils; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| /** | |
| * Cache of active {@link org.eclipse.jgit.lib.Repository} instances. | |
| */ | |
| public class RepositoryCache { | |
| private static final Logger LOG = LoggerFactory | |
| .getLogger(RepositoryCache.class); | |
| private static final RepositoryCache cache = new RepositoryCache(); | |
| /** | |
| * Open an existing repository, reusing a cached instance if possible. | |
| * <p> | |
| * When done with the repository, the caller must call | |
| * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the | |
| * repository's usage counter. | |
| * | |
| * @param location | |
| * where the local repository is. Typically a | |
| * {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}. | |
| * @return the repository instance requested; caller must close when done. | |
| * @throws java.io.IOException | |
| * the repository could not be read (likely its core.version | |
| * property is not supported). | |
| * @throws org.eclipse.jgit.errors.RepositoryNotFoundException | |
| * there is no repository at the given location. | |
| */ | |
| public static Repository open(Key location) throws IOException, | |
| RepositoryNotFoundException { | |
| return open(location, true); | |
| } | |
| /** | |
| * Open a repository, reusing a cached instance if possible. | |
| * <p> | |
| * When done with the repository, the caller must call | |
| * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the | |
| * repository's usage counter. | |
| * | |
| * @param location | |
| * where the local repository is. Typically a | |
| * {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}. | |
| * @param mustExist | |
| * If true, and the repository is not found, throws {@code | |
| * RepositoryNotFoundException}. If false, a repository instance | |
| * is created and registered anyway. | |
| * @return the repository instance requested; caller must close when done. | |
| * @throws java.io.IOException | |
| * the repository could not be read (likely its core.version | |
| * property is not supported). | |
| * @throws RepositoryNotFoundException | |
| * There is no repository at the given location, only thrown if | |
| * {@code mustExist} is true. | |
| */ | |
| public static Repository open(Key location, boolean mustExist) | |
| throws IOException { | |
| return cache.openRepository(location, mustExist); | |
| } | |
| /** | |
| * Register one repository into the cache. | |
| * <p> | |
| * During registration the cache automatically increments the usage counter, | |
| * permitting it to retain the reference. A | |
| * {@link org.eclipse.jgit.lib.RepositoryCache.FileKey} for the repository's | |
| * {@link org.eclipse.jgit.lib.Repository#getDirectory()} is used to index | |
| * the repository in the cache. | |
| * <p> | |
| * If another repository already is registered in the cache at this | |
| * location, the other instance is closed. | |
| * | |
| * @param db | |
| * repository to register. | |
| */ | |
| public static void register(Repository db) { | |
| if (db.getDirectory() != null) { | |
| FileKey key = FileKey.exact(db.getDirectory(), db.getFS()); | |
| cache.registerRepository(key, db); | |
| } | |
| } | |
| /** | |
| * Close and remove a repository from the cache. | |
| * <p> | |
| * Removes a repository from the cache, if it is still registered here, and | |
| * close it. | |
| * | |
| * @param db | |
| * repository to unregister. | |
| */ | |
| public static void close(@NonNull Repository db) { | |
| if (db.getDirectory() != null) { | |
| FileKey key = FileKey.exact(db.getDirectory(), db.getFS()); | |
| cache.unregisterAndCloseRepository(key); | |
| } | |
| } | |
| /** | |
| * Remove a repository from the cache. | |
| * <p> | |
| * Removes a repository from the cache, if it is still registered here. This | |
| * method will not close the repository, only remove it from the cache. See | |
| * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove | |
| * and close the repository. | |
| * | |
| * @param db | |
| * repository to unregister. | |
| * @since 4.3 | |
| */ | |
| public static void unregister(Repository db) { | |
| if (db.getDirectory() != null) { | |
| unregister(FileKey.exact(db.getDirectory(), db.getFS())); | |
| } | |
| } | |
| /** | |
| * Remove a repository from the cache. | |
| * <p> | |
| * Removes a repository from the cache, if it is still registered here. This | |
| * method will not close the repository, only remove it from the cache. See | |
| * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove | |
| * and close the repository. | |
| * | |
| * @param location | |
| * location of the repository to remove. | |
| * @since 4.1 | |
| */ | |
| public static void unregister(Key location) { | |
| cache.unregisterRepository(location); | |
| } | |
| /** | |
| * Get the locations of all repositories registered in the cache. | |
| * | |
| * @return the locations of all repositories registered in the cache. | |
| * @since 4.1 | |
| */ | |
| public static Collection<Key> getRegisteredKeys() { | |
| return cache.getKeys(); | |
| } | |
| static boolean isCached(@NonNull Repository repo) { | |
| File gitDir = repo.getDirectory(); | |
| if (gitDir == null) { | |
| return false; | |
| } | |
| FileKey key = new FileKey(gitDir, repo.getFS()); | |
| return cache.cacheMap.get(key) == repo; | |
| } | |
| /** | |
| * Unregister all repositories from the cache. | |
| */ | |
| public static void clear() { | |
| cache.clearAll(); | |
| } | |
| static void clearExpired() { | |
| cache.clearAllExpired(); | |
| } | |
| static void reconfigure(RepositoryCacheConfig repositoryCacheConfig) { | |
| cache.configureEviction(repositoryCacheConfig); | |
| } | |
| private final Map<Key, Repository> cacheMap; | |
| private final Lock[] openLocks; | |
| private ScheduledFuture<?> cleanupTask; | |
| private volatile long expireAfter; | |
| private final Object schedulerLock = new Lock(); | |
| private RepositoryCache() { | |
| cacheMap = new ConcurrentHashMap<>(); | |
| openLocks = new Lock[4]; | |
| for (int i = 0; i < openLocks.length; i++) { | |
| openLocks[i] = new Lock(); | |
| } | |
| configureEviction(new RepositoryCacheConfig()); | |
| } | |
| private void configureEviction( | |
| RepositoryCacheConfig repositoryCacheConfig) { | |
| expireAfter = repositoryCacheConfig.getExpireAfter(); | |
| ScheduledThreadPoolExecutor scheduler = WorkQueue.getExecutor(); | |
| synchronized (schedulerLock) { | |
| if (cleanupTask != null) { | |
| cleanupTask.cancel(false); | |
| } | |
| long delay = repositoryCacheConfig.getCleanupDelay(); | |
| if (delay == RepositoryCacheConfig.NO_CLEANUP) { | |
| return; | |
| } | |
| cleanupTask = scheduler.scheduleWithFixedDelay(() -> { | |
| try { | |
| cache.clearAllExpired(); | |
| } catch (Throwable e) { | |
| LOG.error(e.getMessage(), e); | |
| } | |
| }, delay, delay, TimeUnit.MILLISECONDS); | |
| } | |
| } | |
| private Repository openRepository(final Key location, | |
| final boolean mustExist) throws IOException { | |
| Repository db = cacheMap.get(location); | |
| if (db == null) { | |
| synchronized (lockFor(location)) { | |
| db = cacheMap.get(location); | |
| if (db == null) { | |
| db = location.open(mustExist); | |
| cacheMap.put(location, db); | |
| } else { | |
| db.incrementOpen(); | |
| } | |
| } | |
| } else { | |
| db.incrementOpen(); | |
| } | |
| return db; | |
| } | |
| private void registerRepository(Key location, Repository db) { | |
| try (Repository oldDb = cacheMap.put(location, db)) { | |
| // oldDb is auto-closed | |
| } | |
| } | |
| private Repository unregisterRepository(Key location) { | |
| return cacheMap.remove(location); | |
| } | |
| private boolean isExpired(Repository db) { | |
| return db != null && db.useCnt.get() <= 0 | |
| && (System.currentTimeMillis() - db.closedAt.get() > expireAfter); | |
| } | |
| private void unregisterAndCloseRepository(Key location) { | |
| synchronized (lockFor(location)) { | |
| Repository oldDb = unregisterRepository(location); | |
| if (oldDb != null) { | |
| oldDb.doClose(); | |
| } | |
| } | |
| } | |
| private Collection<Key> getKeys() { | |
| return new ArrayList<>(cacheMap.keySet()); | |
| } | |
| private void clearAllExpired() { | |
| for (Repository db : cacheMap.values()) { | |
| if (isExpired(db)) { | |
| RepositoryCache.close(db); | |
| } | |
| } | |
| } | |
| private void clearAll() { | |
| for (Key k : cacheMap.keySet()) { | |
| unregisterAndCloseRepository(k); | |
| } | |
| } | |
| private Lock lockFor(Key location) { | |
| return openLocks[(location.hashCode() >>> 1) % openLocks.length]; | |
| } | |
| private static class Lock { | |
| // Used only for its monitor. | |
| } | |
| /** | |
| * Abstract hash key for {@link RepositoryCache} entries. | |
| * <p> | |
| * A Key instance should be lightweight, and implement hashCode() and | |
| * equals() such that two Key instances are equal if they represent the same | |
| * Repository location. | |
| */ | |
| public static interface Key { | |
| /** | |
| * Called by {@link RepositoryCache#open(Key)} if it doesn't exist yet. | |
| * <p> | |
| * If a repository does not exist yet in the cache, the cache will call | |
| * this method to acquire a handle to it. | |
| * | |
| * @param mustExist | |
| * true if the repository must exist in order to be opened; | |
| * false if a new non-existent repository is permitted to be | |
| * created (the caller is responsible for calling create). | |
| * @return the new repository instance. | |
| * @throws IOException | |
| * the repository could not be read (likely its core.version | |
| * property is not supported). | |
| * @throws RepositoryNotFoundException | |
| * There is no repository at the given location, only thrown | |
| * if {@code mustExist} is true. | |
| */ | |
| Repository open(boolean mustExist) throws IOException, | |
| RepositoryNotFoundException; | |
| } | |
| /** Location of a Repository, using the standard java.io.File API. */ | |
| public static class FileKey implements Key { | |
| /** | |
| * Obtain a pointer to an exact location on disk. | |
| * <p> | |
| * No guessing is performed, the given location is exactly the GIT_DIR | |
| * directory of the repository. | |
| * | |
| * @param directory | |
| * location where the repository database is. | |
| * @param fs | |
| * the file system abstraction which will be necessary to | |
| * perform certain file system operations. | |
| * @return a key for the given directory. | |
| * @see #lenient(File, FS) | |
| */ | |
| public static FileKey exact(File directory, FS fs) { | |
| return new FileKey(directory, fs); | |
| } | |
| /** | |
| * Obtain a pointer to a location on disk. | |
| * <p> | |
| * The method performs some basic guessing to locate the repository. | |
| * Searched paths are: | |
| * <ol> | |
| * <li>{@code directory} // assume exact match</li> | |
| * <li>{@code directory} + "/.git" // assume working directory</li> | |
| * <li>{@code directory} + ".git" // assume bare</li> | |
| * </ol> | |
| * | |
| * @param directory | |
| * location where the repository database might be. | |
| * @param fs | |
| * the file system abstraction which will be necessary to | |
| * perform certain file system operations. | |
| * @return a key for the given directory. | |
| * @see #exact(File, FS) | |
| */ | |
| public static FileKey lenient(File directory, FS fs) { | |
| final File gitdir = resolve(directory, fs); | |
| return new FileKey(gitdir != null ? gitdir : directory, fs); | |
| } | |
| private final File path; | |
| private final FS fs; | |
| /** | |
| * Create a pointer to a location on disk. | |
| * | |
| * @param directory | |
| * exact location of the repository. | |
| * @param fs | |
| * the file system abstraction which will be necessary to | |
| * perform certain file system operations. | |
| */ | |
| protected FileKey(File directory, FS fs) { | |
| path = canonical(directory); | |
| this.fs = fs; | |
| } | |
| private static File canonical(File path) { | |
| try { | |
| return path.getCanonicalFile(); | |
| } catch (IOException e) { | |
| return path.getAbsoluteFile(); | |
| } | |
| } | |
| /** | |
| * Get directory containing the repository database | |
| * | |
| * @return directory containing the repository database. | |
| */ | |
| public final File getFile() { | |
| return path; | |
| } | |
| @Override | |
| public Repository open(boolean mustExist) throws IOException { | |
| if (mustExist && !isGitRepository(path, fs)) | |
| throw new RepositoryNotFoundException(path); | |
| return new FileRepository(path); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return path.hashCode(); | |
| } | |
| @Override | |
| public boolean equals(Object o) { | |
| return o instanceof FileKey && path.equals(((FileKey) o).path); | |
| } | |
| @Override | |
| public String toString() { | |
| return path.toString(); | |
| } | |
| /** | |
| * Guess if a directory contains a Git repository. | |
| * <p> | |
| * This method guesses by looking for the existence of some key files | |
| * and directories. | |
| * | |
| * @param dir | |
| * the location of the directory to examine. | |
| * @param fs | |
| * the file system abstraction which will be necessary to | |
| * perform certain file system operations. | |
| * @return true if the directory "looks like" a Git repository; false if | |
| * it doesn't look enough like a Git directory to really be a | |
| * Git directory. | |
| */ | |
| public static boolean isGitRepository(File dir, FS fs) { | |
| // check if common-dir available or fallback to git-dir | |
| File commonDir; | |
| try { | |
| commonDir = fs.getCommonDir(dir); | |
| } catch (IOException e) { | |
| commonDir = null; | |
| } | |
| if (commonDir == null) { | |
| commonDir = dir; | |
| } | |
| return fs.resolve(commonDir, Constants.OBJECTS).exists() | |
| && fs.resolve(commonDir, "refs").exists() //$NON-NLS-1$ | |
| && (fs.resolve(commonDir, Constants.REFTABLE).exists() | |
| || isValidHead( | |
| new File(commonDir, Constants.HEAD))); | |
| } | |
| private static boolean isValidHead(File head) { | |
| final String ref = readFirstLine(head); | |
| return ref != null | |
| && (ref.startsWith("ref: refs/") || ObjectId.isId(ref)); //$NON-NLS-1$ | |
| } | |
| private static String readFirstLine(File head) { | |
| try { | |
| final byte[] buf = IO.readFully(head, 4096); | |
| int n = buf.length; | |
| if (n == 0) | |
| return null; | |
| if (buf[n - 1] == '\n') | |
| n--; | |
| return RawParseUtils.decode(buf, 0, n); | |
| } catch (IOException e) { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Guess the proper path for a Git repository. | |
| * <p> | |
| * The method performs some basic guessing to locate the repository. | |
| * Searched paths are: | |
| * <ol> | |
| * <li>{@code directory} // assume exact match</li> | |
| * <li>{@code directory} + "/.git" // assume working directory</li> | |
| * <li>{@code directory} + ".git" // assume bare</li> | |
| * </ol> | |
| * | |
| * @param directory | |
| * location to guess from. Several permutations are tried. | |
| * @param fs | |
| * the file system abstraction which will be necessary to | |
| * perform certain file system operations. | |
| * @return the actual directory location if a better match is found; | |
| * null if there is no suitable match. | |
| */ | |
| public static File resolve(File directory, FS fs) { | |
| // the folder itself | |
| if (isGitRepository(directory, fs)) { | |
| return directory; | |
| } | |
| // the .git subfolder or file (reference) | |
| File dotDir = new File(directory, Constants.DOT_GIT); | |
| if (dotDir.isFile()) { | |
| try { | |
| File refDir = BaseRepositoryBuilder.getSymRef(directory, | |
| dotDir, fs); | |
| if (refDir != null && isGitRepository(refDir, fs)) { | |
| return refDir; | |
| } | |
| } catch (IOException ignored) { | |
| // Continue searching if gitdir ref isn't found | |
| } | |
| } else if (isGitRepository(dotDir, fs)) { | |
| return dotDir; | |
| } | |
| // the folder extended with .git (bare) | |
| File bareDir = new File(directory.getParentFile(), | |
| directory.getName() + Constants.DOT_GIT_EXT); | |
| if (isGitRepository(bareDir, fs)) { | |
| return bareDir; | |
| } | |
| return null; | |
| } | |
| } | |
| } |