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.lang.ref.Reference; 49 import java.lang.ref.SoftReference; 50 import java.util.ArrayList; 51 import java.util.Collection; 52 import java.util.Iterator; 53 import java.util.Map; 54 import java.util.concurrent.ConcurrentHashMap; 55 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 62 /** Cache of active {@link Repository} instances. */ 63 public class RepositoryCache { 64 private static final RepositoryCache cache = new RepositoryCache(); 65 66 /** 67 * Open an existing repository, reusing a cached instance if possible. 68 * <p> 69 * When done with the repository, the caller must call 70 * {@link Repository#close()} to decrement the repository's usage counter. 71 * 72 * @param location 73 * where the local repository is. Typically a {@link FileKey}. 74 * @return the repository instance requested; caller must close when done. 75 * @throws IOException 76 * the repository could not be read (likely its core.version 77 * property is not supported). 78 * @throws RepositoryNotFoundException 79 * there is no repository at the given location. 80 */ 81 public static Repository open(final Key location) throws IOException, 82 RepositoryNotFoundException { 83 return open(location, true); 84 } 85 86 /** 87 * Open a repository, reusing a cached instance if possible. 88 * <p> 89 * When done with the repository, the caller must call 90 * {@link Repository#close()} to decrement the repository's usage counter. 91 * 92 * @param location 93 * where the local repository is. Typically a {@link FileKey}. 94 * @param mustExist 95 * If true, and the repository is not found, throws {@code 96 * RepositoryNotFoundException}. If false, a repository instance 97 * is created and registered anyway. 98 * @return the repository instance requested; caller must close when done. 99 * @throws IOException 100 * the repository could not be read (likely its core.version 101 * property is not supported). 102 * @throws RepositoryNotFoundException 103 * There is no repository at the given location, only thrown if 104 * {@code mustExist} is true. 105 */ 106 public static Repository open(final Key location, final boolean mustExist) 107 throws IOException { 108 return cache.openRepository(location, mustExist); 109 } 110 111 /** 112 * Register one repository into the cache. 113 * <p> 114 * During registration the cache automatically increments the usage counter, 115 * permitting it to retain the reference. A {@link FileKey} for the 116 * repository's {@link Repository#getDirectory()} is used to index the 117 * repository in the cache. 118 * <p> 119 * If another repository already is registered in the cache at this 120 * location, the other instance is closed. 121 * 122 * @param db 123 * repository to register. 124 */ 125 public static void register(final Repository db) { 126 if (db.getDirectory() != null) { 127 FileKey key = FileKey.exact(db.getDirectory(), db.getFS()); 128 cache.registerRepository(key, db); 129 } 130 } 131 132 /** 133 * Remove a repository from the cache. 134 * <p> 135 * Removes a repository from the cache, if it is still registered here, 136 * permitting it to close. 137 * 138 * @param db 139 * repository to unregister. 140 */ 141 public static void close(final Repository db) { 142 if (db.getDirectory() != null) { 143 FileKey key = FileKey.exact(db.getDirectory(), db.getFS()); 144 cache.unregisterRepository(key); 145 } 146 } 147 148 /** 149 * Remove a repository from the cache. 150 * <p> 151 * Removes a repository from the cache, if it is still registered here, 152 * permitting it to close. 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 * @return the locations of all repositories registered in the cache. 164 * @since 4.1 165 */ 166 public static Collection<Key> getRegisteredKeys() { 167 return cache.getKeys(); 168 } 169 170 /** Unregister all repositories from the cache. */ 171 public static void clear() { 172 cache.clearAll(); 173 } 174 175 private final ConcurrentHashMap<Key, Reference<Repository>> cacheMap; 176 177 private final Lock[] openLocks; 178 179 private RepositoryCache() { 180 cacheMap = new ConcurrentHashMap<Key, Reference<Repository>>(); 181 openLocks = new Lock[4]; 182 for (int i = 0; i < openLocks.length; i++) 183 openLocks[i] = new Lock(); 184 } 185 186 @SuppressWarnings("resource") 187 private Repository openRepository(final Key location, 188 final boolean mustExist) throws IOException { 189 Reference<Repository> ref = cacheMap.get(location); 190 Repository db = ref != null ? ref.get() : null; 191 if (db == null) { 192 synchronized (lockFor(location)) { 193 ref = cacheMap.get(location); 194 db = ref != null ? ref.get() : null; 195 if (db == null) { 196 db = location.open(mustExist); 197 ref = new SoftReference<Repository>(db); 198 cacheMap.put(location, ref); 199 } 200 } 201 } 202 db.incrementOpen(); 203 return db; 204 } 205 206 private void registerRepository(final Key location, final Repository db) { 207 db.incrementOpen(); 208 SoftReference<Repository> newRef = new SoftReference<Repository>(db); 209 Reference<Repository> oldRef = cacheMap.put(location, newRef); 210 Repository oldDb = oldRef != null ? oldRef.get() : null; 211 if (oldDb != null) 212 oldDb.close(); 213 } 214 215 private void unregisterRepository(final Key location) { 216 Reference<Repository> oldRef = cacheMap.remove(location); 217 Repository oldDb = oldRef != null ? oldRef.get() : null; 218 if (oldDb != null) 219 oldDb.close(); 220 } 221 222 private Collection<Key> getKeys() { 223 return new ArrayList<Key>(cacheMap.keySet()); 224 } 225 226 private void clearAll() { 227 for (int stage = 0; stage < 2; stage++) { 228 for (Iterator<Map.Entry<Key, Reference<Repository>>> i = cacheMap 229 .entrySet().iterator(); i.hasNext();) { 230 final Map.Entry<Key, Reference<Repository>> e = i.next(); 231 final Repository db = e.getValue().get(); 232 if (db != null) 233 db.close(); 234 i.remove(); 235 } 236 } 237 } 238 239 private Lock lockFor(final Key location) { 240 return openLocks[(location.hashCode() >>> 1) % openLocks.length]; 241 } 242 243 private static class Lock { 244 // Used only for its monitor. 245 } 246 247 /** 248 * Abstract hash key for {@link RepositoryCache} entries. 249 * <p> 250 * A Key instance should be lightweight, and implement hashCode() and 251 * equals() such that two Key instances are equal if they represent the same 252 * Repository location. 253 */ 254 public static interface Key { 255 /** 256 * Called by {@link RepositoryCache#open(Key)} if it doesn't exist yet. 257 * <p> 258 * If a repository does not exist yet in the cache, the cache will call 259 * this method to acquire a handle to it. 260 * 261 * @param mustExist 262 * true if the repository must exist in order to be opened; 263 * false if a new non-existent repository is permitted to be 264 * created (the caller is responsible for calling create). 265 * @return the new repository instance. 266 * @throws IOException 267 * the repository could not be read (likely its core.version 268 * property is not supported). 269 * @throws RepositoryNotFoundException 270 * There is no repository at the given location, only thrown 271 * if {@code mustExist} is true. 272 */ 273 Repository open(boolean mustExist) throws IOException, 274 RepositoryNotFoundException; 275 } 276 277 /** Location of a Repository, using the standard java.io.File API. */ 278 public static class FileKey implements Key { 279 /** 280 * Obtain a pointer to an exact location on disk. 281 * <p> 282 * No guessing is performed, the given location is exactly the GIT_DIR 283 * directory of the repository. 284 * 285 * @param directory 286 * location where the repository database is. 287 * @param fs 288 * the file system abstraction which will be necessary to 289 * perform certain file system operations. 290 * @return a key for the given directory. 291 * @see #lenient(File, FS) 292 */ 293 public static FileKey exact(final File directory, FS fs) { 294 return new FileKey(directory, fs); 295 } 296 297 /** 298 * Obtain a pointer to a location on disk. 299 * <p> 300 * The method performs some basic guessing to locate the repository. 301 * Searched paths are: 302 * <ol> 303 * <li>{@code directory} // assume exact match</li> 304 * <li>{@code directory} + "/.git" // assume working directory</li> 305 * <li>{@code directory} + ".git" // assume bare</li> 306 * </ol> 307 * 308 * @param directory 309 * location where the repository database might be. 310 * @param fs 311 * the file system abstraction which will be necessary to 312 * perform certain file system operations. 313 * @return a key for the given directory. 314 * @see #exact(File, FS) 315 */ 316 public static FileKey lenient(final File directory, FS fs) { 317 final File gitdir = resolve(directory, fs); 318 return new FileKey(gitdir != null ? gitdir : directory, fs); 319 } 320 321 private final File path; 322 private final FS fs; 323 324 /** 325 * @param directory 326 * exact location of the repository. 327 * @param fs 328 * the file system abstraction which will be necessary to 329 * perform certain file system operations. 330 */ 331 protected FileKey(final File directory, FS fs) { 332 path = canonical(directory); 333 this.fs = fs; 334 } 335 336 private static File canonical(final File path) { 337 try { 338 return path.getCanonicalFile(); 339 } catch (IOException e) { 340 return path.getAbsoluteFile(); 341 } 342 } 343 344 /** @return location supplied to the constructor. */ 345 public final File getFile() { 346 return path; 347 } 348 349 public Repository open(final boolean mustExist) throws IOException { 350 if (mustExist && !isGitRepository(path, fs)) 351 throw new RepositoryNotFoundException(path); 352 return new FileRepository(path); 353 } 354 355 @Override 356 public int hashCode() { 357 return path.hashCode(); 358 } 359 360 @Override 361 public boolean equals(final Object o) { 362 return o instanceof FileKey && path.equals(((FileKey) o).path); 363 } 364 365 @Override 366 public String toString() { 367 return path.toString(); 368 } 369 370 /** 371 * Guess if a directory contains a Git repository. 372 * <p> 373 * This method guesses by looking for the existence of some key files 374 * and directories. 375 * 376 * @param dir 377 * the location of the directory to examine. 378 * @param fs 379 * the file system abstraction which will be necessary to 380 * perform certain file system operations. 381 * @return true if the directory "looks like" a Git repository; false if 382 * it doesn't look enough like a Git directory to really be a 383 * Git directory. 384 */ 385 public static boolean isGitRepository(final File dir, FS fs) { 386 return fs.resolve(dir, "objects").exists() //$NON-NLS-1$ 387 && fs.resolve(dir, "refs").exists() //$NON-NLS-1$ 388 && isValidHead(new File(dir, Constants.HEAD)); 389 } 390 391 private static boolean isValidHead(final File head) { 392 final String ref = readFirstLine(head); 393 return ref != null 394 && (ref.startsWith("ref: refs/") || ObjectId.isId(ref)); //$NON-NLS-1$ 395 } 396 397 private static String readFirstLine(final File head) { 398 try { 399 final byte[] buf = IO.readFully(head, 4096); 400 int n = buf.length; 401 if (n == 0) 402 return null; 403 if (buf[n - 1] == '\n') 404 n--; 405 return RawParseUtils.decode(buf, 0, n); 406 } catch (IOException e) { 407 return null; 408 } 409 } 410 411 /** 412 * Guess the proper path for a Git repository. 413 * <p> 414 * The method performs some basic guessing to locate the repository. 415 * Searched paths are: 416 * <ol> 417 * <li>{@code directory} // assume exact match</li> 418 * <li>{@code directory} + "/.git" // assume working directory</li> 419 * <li>{@code directory} + ".git" // assume bare</li> 420 * </ol> 421 * 422 * @param directory 423 * location to guess from. Several permutations are tried. 424 * @param fs 425 * the file system abstraction which will be necessary to 426 * perform certain file system operations. 427 * @return the actual directory location if a better match is found; 428 * null if there is no suitable match. 429 */ 430 public static File resolve(final File directory, FS fs) { 431 if (isGitRepository(directory, fs)) 432 return directory; 433 if (isGitRepository(new File(directory, Constants.DOT_GIT), fs)) 434 return new File(directory, Constants.DOT_GIT); 435 436 final String name = directory.getName(); 437 final File parent = directory.getParentFile(); 438 if (isGitRepository(new File(parent, name + Constants.DOT_GIT_EXT), fs)) 439 return new File(parent, name + Constants.DOT_GIT_EXT); 440 return null; 441 } 442 } 443 }