[JGIT PATCH 07/11] Introduce RepositoryCache to cache handles of Repository objects
Show changes to 4 files +592 −1
org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryCacheTest.java, org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryTestCase.java, org.spearce.jgit/src/org/spearce/jgit/errors/RepositoryNotFoundException.java, org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryCache.java
diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryCacheTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryCacheTest.java
new file mode 100644
index 0000000..f466e00
--- /dev/null
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryCacheTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2009, Google Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Git Development Community nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.spearce.jgit.lib;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.spearce.jgit.errors.RepositoryNotFoundException;
+import org.spearce.jgit.lib.RepositoryCache.FileKey;
+
+public class RepositoryCacheTest extends RepositoryTestCase {
+ public void testNonBareFileKey() {
+ File gitdir = db.getDirectory();
+ File parent = gitdir.getParentFile();
+ File other = new File(parent, "notagit");
+ assertEquals(gitdir, FileKey.exact(gitdir).getFile());
+ assertEquals(parent, FileKey.exact(parent).getFile());
+ assertEquals(other, FileKey.exact(other).getFile());
+
+ assertEquals(gitdir, FileKey.lenient(gitdir).getFile());
+ assertEquals(gitdir, FileKey.lenient(parent).getFile());
+ assertEquals(other, FileKey.lenient(other).getFile());
+ }
+
+ public void testBareFileKey() throws IOException {
+ Repository bare = createNewEmptyRepo(true);
+ File gitdir = bare.getDirectory();
+ File parent = gitdir.getParentFile();
+ String name = gitdir.getName();
+ assertTrue(name.endsWith(".git"));
+ name = name.substring(0, name.length() - 4);
+
+ assertEquals(gitdir, FileKey.exact(gitdir).getFile());
+
+ assertEquals(gitdir, FileKey.lenient(gitdir).getFile());
+ assertEquals(gitdir, FileKey.lenient(new File(parent, name)).getFile());
+ }
+
+ public void testFileKeyOpenExisting() throws IOException {
+ Repository r;
+
+ r = new FileKey(db.getDirectory()).open(true);
+ assertNotNull(r);
+ assertEquals(db.getDirectory(), r.getDirectory());
+ r.close();
+
+ r = new FileKey(db.getDirectory()).open(false);
+ assertNotNull(r);
+ assertEquals(db.getDirectory(), r.getDirectory());
+ r.close();
+ }
+
+ public void testFileKeyOpenNew() throws IOException {
+ final Repository n = createNewEmptyRepo(true);
+ final File gitdir = n.getDirectory();
+ n.close();
+ recursiveDelete(gitdir);
+ assertFalse(gitdir.exists());
+
+ try {
+ new FileKey(gitdir).open(true);
+ fail("incorrectly opened a non existant repository");
+ } catch (RepositoryNotFoundException e) {
+ assertEquals("repository not found: " + gitdir, e.getMessage());
+ }
+
+ final Repository o = new FileKey(gitdir).open(false);
+ assertNotNull(o);
+ assertEquals(gitdir, o.getDirectory());
+ assertFalse(gitdir.exists());
+ }
+
+ public void testCacheRegisterOpen() throws Exception {
+ final File dir = db.getDirectory();
+ RepositoryCache.register(db);
+ assertSame(db, RepositoryCache.open(FileKey.exact(dir)));
+
+ assertEquals(".git", dir.getName());
+ final File parent = dir.getParentFile();
+ assertSame(db, RepositoryCache.open(FileKey.lenient(parent)));
+ }
+
+ public void testCacheOpen() throws Exception {
+ final FileKey loc = FileKey.exact(db.getDirectory());
+ final Repository d2 = RepositoryCache.open(loc);
+ assertNotSame(db, d2);
+ assertSame(d2, RepositoryCache.open(FileKey.exact(loc.getFile())));
+ d2.close();
+ d2.close();
+ }
+}
diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryTestCase.java b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryTestCase.java
index 3837ea9..2783180 100644
--- a/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryTestCase.java
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryTestCase.java
@@ -307,6 +307,7 @@ public void run() {
}
protected void tearDown() throws Exception {
+ RepositoryCache.clear();
db.close();
for (Repository r : repositoriesToClose)
r.close();
@@ -334,8 +335,21 @@ protected void tearDown() throws Exception {
* @throws IOException
*/
protected Repository createNewEmptyRepo() throws IOException {
+ return createNewEmptyRepo(false);
+ }
+
+ /**
+ * Helper for creating extra empty repos
+ *
+ * @param bare if true, create a bare repository.
+ * @return a new empty git repository for testing purposes
+ *
+ * @throws IOException
+ */
+ protected Repository createNewEmptyRepo(boolean bare) throws IOException {
final File newTestRepo = new File(trashParent, "new"
- + System.currentTimeMillis() + "." + (testcount++) + "/.git");
+ + System.currentTimeMillis() + "." + (testcount++)
+ + (bare ? "" : "/") + ".git");
assertFalse(newTestRepo.exists());
final Repository newRepo = new Repository(newTestRepo);
newRepo.create();
diff --git a/org.spearce.jgit/src/org/spearce/jgit/errors/RepositoryNotFoundException.java b/org.spearce.jgit/src/org/spearce/jgit/errors/RepositoryNotFoundException.java
new file mode 100644
index 0000000..f47f577
--- /dev/null
+++ b/org.spearce.jgit/src/org/spearce/jgit/errors/RepositoryNotFoundException.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2009, Google Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Git Development Community nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.spearce.jgit.errors;
+
+import java.io.File;
+
+/** Indicates a local repository does not exist. */
+public class RepositoryNotFoundException extends TransportException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs an exception indicating a local repository does not exist.
+ *
+ * @param location
+ * description of the repository not found, usually file path.
+ */
+ public RepositoryNotFoundException(final File location) {
+ this(location.getPath());
+ }
+
+ /**
+ * Constructs an exception indicating a local repository does not exist.
+ *
+ * @param location
+ * description of the repository not found, usually file path.
+ */
+ public RepositoryNotFoundException(final String location) {
+ super("repository not found: " + location);
+ }
+}
diff --git a/org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryCache.java b/org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryCache.java
new file mode 100644
index 0000000..50b4330
--- /dev/null
+++ b/org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryCache.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2009, Google Inc.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Git Development Community nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.spearce.jgit.lib;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.spearce.jgit.errors.RepositoryNotFoundException;
+import org.spearce.jgit.util.FS;
+
+/** Cache of active {@link Repository} instances. */
+public class RepositoryCache {
+ 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 Repository#close()} to decrement the repository's usage counter.
+ *
+ * @param location
+ * where the local repository is. Typically a {@link FileKey}.
+ * @return the repository instance requested; caller must close when done.
+ * @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.
+ */
+ public static Repository open(final 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 Repository#close()} to decrement the repository's usage counter.
+ *
+ * @param location
+ * where the local repository is. Typically a {@link 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 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(final Key location, final 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 FileKey} for the
+ * repository's {@link 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(final Repository db) {
+ cache.registerRepository(FileKey.exact(db.getDirectory()), db);
+ }
+
+ /**
+ * Remove a repository from the cache.
+ * <p>
+ * Removes a repository from the cache, if it is still registered here,
+ * permitting it to close.
+ *
+ * @param db
+ * repository to unregister.
+ */
+ public static void close(final Repository db) {
+ cache.unregisterRepository(FileKey.exact(db.getDirectory()));
+ }
+
+ /** Unregister all repositories from the cache. */
+ public static void clear() {
+ cache.clearAll();
+ }
+
+ private final ConcurrentHashMap<Key, Reference<Repository>> cacheMap;
+
+ private final Lock[] openLocks;
+
+ private RepositoryCache() {
+ cacheMap = new ConcurrentHashMap<Key, Reference<Repository>>();
+ openLocks = new Lock[4];
+ for (int i = 0; i < openLocks.length; i++)
+ openLocks[i] = new Lock();
+ }
+
+ private Repository openRepository(final Key location,
+ final boolean mustExist) throws IOException {
+ Reference<Repository> ref = cacheMap.get(location);
+ Repository db = ref != null ? ref.get() : null;
+ if (db == null) {
+ synchronized (lockFor(location)) {
+ ref = cacheMap.get(location);
+ db = ref != null ? ref.get() : null;
+ if (db == null) {
+ db = location.open(mustExist);
+ ref = new WeakReference<Repository>(db);
+ cacheMap.put(location, ref);
+ }
+ }
+ }
+ db.incrementOpen();
+ return db;
+ }
+
+ private void registerRepository(final Key location, final Repository db) {
+ db.incrementOpen();
+ WeakReference<Repository> newRef = new WeakReference<Repository>(db);
+ Reference<Repository> oldRef = cacheMap.put(location, newRef);
+ Repository oldDb = oldRef != null ? oldRef.get() : null;
+ if (oldDb != null)
+ oldDb.close();
+ }
+
+ private void unregisterRepository(final Key location) {
+ Reference<Repository> oldRef = cacheMap.remove(location);
+ Repository oldDb = oldRef != null ? oldRef.get() : null;
+ if (oldDb != null)
+ oldDb.close();
+ }
+
+ private void clearAll() {
+ for (int stage = 0; stage < 2; stage++) {
+ for (Iterator<Map.Entry<Key, Reference<Repository>>> i = cacheMap
+ .entrySet().iterator(); i.hasNext();) {
+ final Map.Entry<Key, Reference<Repository>> e = i.next();
+ final Repository db = e.getValue().get();
+ if (db != null)
+ db.close();
+ i.remove();
+ }
+ }
+ }
+
+ private Lock lockFor(final 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.
+ * @return a key for the given directory.
+ * @see #lenient(File)
+ */
+ public static FileKey exact(final File directory) {
+ return new FileKey(directory);
+ }
+
+ /**
+ * 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.
+ * @return a key for the given directory.
+ * @see #exact(File)
+ */
+ public static FileKey lenient(final File directory) {
+ final File gitdir = resolve(directory);
+ return new FileKey(gitdir != null ? gitdir : directory);
+ }
+
+ private final File path;
+
+ /**
+ * @param directory
+ * exact location of the repository.
+ */
+ protected FileKey(final File directory) {
+ path = canonical(directory);
+ }
+
+ private static File canonical(final File path) {
+ try {
+ return path.getCanonicalFile();
+ } catch (IOException e) {
+ return path.getAbsoluteFile();
+ }
+ }
+
+ /** @return location supplied to the constructor. */
+ public final File getFile() {
+ return path;
+ }
+
+ public Repository open(final boolean mustExist) throws IOException {
+ if (mustExist && !isGitRepository(path))
+ throw new RepositoryNotFoundException(path);
+ return new Repository(path);
+ }
+
+ @Override
+ public int hashCode() {
+ return path.hashCode();
+ }
+
+ @Override
+ public boolean equals(final 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.
+ * @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(final File dir) {
+ return FS.resolve(dir, "objects").exists()
+ && FS.resolve(dir, "refs").exists()
+ && isValidHead(new File(dir, Constants.HEAD));
+ }
+
+ private static boolean isValidHead(final File head) {
+ final String ref = readFirstLine(head);
+ return ref != null
+ && (ref.startsWith("ref: refs/") || ObjectId.isId(ref));
+ }
+
+ private static String readFirstLine(final File head) {
+ try {
+ final BufferedReader br = new BufferedReader(
+ new InputStreamReader(new FileInputStream(head),
+ Constants.CHARSET));
+ try {
+ return br.readLine();
+ } finally {
+ br.close();
+ }
+ } 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.
+ * @return the actual directory location if a better match is found;
+ * null if there is no suitable match.
+ */
+ public static File resolve(final File directory) {
+ if (isGitRepository(directory))
+ return directory;
+ if (isGitRepository(new File(directory, ".git")))
+ return new File(directory, ".git");
+
+ final String name = directory.getName();
+ final File parent = directory.getParentFile();
+ if (isGitRepository(new File(parent, name + ".git")))
+ return new File(parent, name + ".git");
+ return null;
+ }
+ }
+}