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 * Close and remove a repository from the cache. 134 * <p> 135 * Removes a repository from the cache, if it is still registered here, and 136 * close it. 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.unregisterAndCloseRepository(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. This 152 * method will not close the repository, only remove it from the cache. See 153 * {@link RepositoryCache#close(Repository)} to remove and close the 154 * repository. 155 * 156 * @param db 157 * repository to unregister. 158 * @since 4.3 159 */ 160 public static void unregister(final Repository db) { 161 if (db.getDirectory() != null) { 162 unregister(FileKey.exact(db.getDirectory(), db.getFS())); 163 } 164 } 165 166 /** 167 * Remove a repository from the cache. 168 * <p> 169 * Removes a repository from the cache, if it is still registered here. This 170 * method will not close the repository, only remove it from the cache. See 171 * {@link RepositoryCache#close(Repository)} to remove and close the 172 * repository. 173 * 174 * @param location 175 * location of the repository to remove. 176 * @since 4.1 177 */ 178 public static void unregister(Key location) { 179 cache.unregisterRepository(location); 180 } 181 182 /** 183 * @return the locations of all repositories registered in the cache. 184 * @since 4.1 185 */ 186 public static Collection<Key> getRegisteredKeys() { 187 return cache.getKeys(); 188 } 189 190 /** Unregister all repositories from the cache. */ 191 public static void clear() { 192 cache.clearAll(); 193 } 194 195 private final ConcurrentHashMap<Key, Reference<Repository>> cacheMap; 196 197 private final Lock[] openLocks; 198 199 private RepositoryCache() { 200 cacheMap = new ConcurrentHashMap<Key, Reference<Repository>>(); 201 openLocks = new Lock[4]; 202 for (int i = 0; i < openLocks.length; i++) 203 openLocks[i] = new Lock(); 204 } 205 206 @SuppressWarnings("resource") 207 private Repository openRepository(final Key location, 208 final boolean mustExist) throws IOException { 209 Reference<Repository> ref = cacheMap.get(location); 210 Repository db = ref != null ? ref.get() : null; 211 if (db == null) { 212 synchronized (lockFor(location)) { 213 ref = cacheMap.get(location); 214 db = ref != null ? ref.get() : null; 215 if (db == null) { 216 db = location.open(mustExist); 217 ref = new SoftReference<Repository>(db); 218 cacheMap.put(location, ref); 219 } else { 220 db.incrementOpen(); 221 } 222 } 223 } else { 224 db.incrementOpen(); 225 } 226 return db; 227 } 228 229 private void registerRepository(final Key location, final Repository db) { 230 SoftReference<Repository> newRef = new SoftReference<Repository>(db); 231 Reference<Repository> oldRef = cacheMap.put(location, newRef); 232 Repository oldDb = oldRef != null ? oldRef.get() : null; 233 if (oldDb != null) 234 oldDb.close(); 235 } 236 237 private Repository unregisterRepository(final Key location) { 238 Reference<Repository> oldRef = cacheMap.remove(location); 239 return oldRef != null ? oldRef.get() : null; 240 } 241 242 private void unregisterAndCloseRepository(final Key location) { 243 Repository oldDb = unregisterRepository(location); 244 if (oldDb != null) { 245 oldDb.close(); 246 } 247 } 248 249 private Collection<Key> getKeys() { 250 return new ArrayList<Key>(cacheMap.keySet()); 251 } 252 253 private void clearAll() { 254 for (int stage = 0; stage < 2; stage++) { 255 for (Iterator<Map.Entry<Key, Reference<Repository>>> i = cacheMap 256 .entrySet().iterator(); i.hasNext();) { 257 final Map.Entry<Key, Reference<Repository>> e = i.next(); 258 final Repository db = e.getValue().get(); 259 if (db != null) 260 db.close(); 261 i.remove(); 262 } 263 } 264 } 265 266 private Lock lockFor(final Key location) { 267 return openLocks[(location.hashCode() >>> 1) % openLocks.length]; 268 } 269 270 private static class Lock { 271 // Used only for its monitor. 272 } 273 274 /** 275 * Abstract hash key for {@link RepositoryCache} entries. 276 * <p> 277 * A Key instance should be lightweight, and implement hashCode() and 278 * equals() such that two Key instances are equal if they represent the same 279 * Repository location. 280 */ 281 public static interface Key { 282 /** 283 * Called by {@link RepositoryCache#open(Key)} if it doesn't exist yet. 284 * <p> 285 * If a repository does not exist yet in the cache, the cache will call 286 * this method to acquire a handle to it. 287 * 288 * @param mustExist 289 * true if the repository must exist in order to be opened; 290 * false if a new non-existent repository is permitted to be 291 * created (the caller is responsible for calling create). 292 * @return the new repository instance. 293 * @throws IOException 294 * the repository could not be read (likely its core.version 295 * property is not supported). 296 * @throws RepositoryNotFoundException 297 * There is no repository at the given location, only thrown 298 * if {@code mustExist} is true. 299 */ 300 Repository open(boolean mustExist) throws IOException, 301 RepositoryNotFoundException; 302 } 303 304 /** Location of a Repository, using the standard java.io.File API. */ 305 public static class FileKey implements Key { 306 /** 307 * Obtain a pointer to an exact location on disk. 308 * <p> 309 * No guessing is performed, the given location is exactly the GIT_DIR 310 * directory of the repository. 311 * 312 * @param directory 313 * location where the repository database is. 314 * @param fs 315 * the file system abstraction which will be necessary to 316 * perform certain file system operations. 317 * @return a key for the given directory. 318 * @see #lenient(File, FS) 319 */ 320 public static FileKey exact(final File directory, FS fs) { 321 return new FileKey(directory, fs); 322 } 323 324 /** 325 * Obtain a pointer to a location on disk. 326 * <p> 327 * The method performs some basic guessing to locate the repository. 328 * Searched paths are: 329 * <ol> 330 * <li>{@code directory} // assume exact match</li> 331 * <li>{@code directory} + "/.git" // assume working directory</li> 332 * <li>{@code directory} + ".git" // assume bare</li> 333 * </ol> 334 * 335 * @param directory 336 * location where the repository database might be. 337 * @param fs 338 * the file system abstraction which will be necessary to 339 * perform certain file system operations. 340 * @return a key for the given directory. 341 * @see #exact(File, FS) 342 */ 343 public static FileKey lenient(final File directory, FS fs) { 344 final File gitdir = resolve(directory, fs); 345 return new FileKey(gitdir != null ? gitdir : directory, fs); 346 } 347 348 private final File path; 349 private final FS fs; 350 351 /** 352 * @param directory 353 * exact location of the repository. 354 * @param fs 355 * the file system abstraction which will be necessary to 356 * perform certain file system operations. 357 */ 358 protected FileKey(final File directory, FS fs) { 359 path = canonical(directory); 360 this.fs = fs; 361 } 362 363 private static File canonical(final File path) { 364 try { 365 return path.getCanonicalFile(); 366 } catch (IOException e) { 367 return path.getAbsoluteFile(); 368 } 369 } 370 371 /** @return location supplied to the constructor. */ 372 public final File getFile() { 373 return path; 374 } 375 376 public Repository open(final boolean mustExist) throws IOException { 377 if (mustExist && !isGitRepository(path, fs)) 378 throw new RepositoryNotFoundException(path); 379 return new FileRepository(path); 380 } 381 382 @Override 383 public int hashCode() { 384 return path.hashCode(); 385 } 386 387 @Override 388 public boolean equals(final Object o) { 389 return o instanceof FileKey && path.equals(((FileKey) o).path); 390 } 391 392 @Override 393 public String toString() { 394 return path.toString(); 395 } 396 397 /** 398 * Guess if a directory contains a Git repository. 399 * <p> 400 * This method guesses by looking for the existence of some key files 401 * and directories. 402 * 403 * @param dir 404 * the location of the directory to examine. 405 * @param fs 406 * the file system abstraction which will be necessary to 407 * perform certain file system operations. 408 * @return true if the directory "looks like" a Git repository; false if 409 * it doesn't look enough like a Git directory to really be a 410 * Git directory. 411 */ 412 public static boolean isGitRepository(final File dir, FS fs) { 413 return fs.resolve(dir, "objects").exists() //$NON-NLS-1$ 414 && fs.resolve(dir, "refs").exists() //$NON-NLS-1$ 415 && isValidHead(new File(dir, Constants.HEAD)); 416 } 417 418 private static boolean isValidHead(final File head) { 419 final String ref = readFirstLine(head); 420 return ref != null 421 && (ref.startsWith("ref: refs/") || ObjectId.isId(ref)); //$NON-NLS-1$ 422 } 423 424 private static String readFirstLine(final File head) { 425 try { 426 final byte[] buf = IO.readFully(head, 4096); 427 int n = buf.length; 428 if (n == 0) 429 return null; 430 if (buf[n - 1] == '\n') 431 n--; 432 return RawParseUtils.decode(buf, 0, n); 433 } catch (IOException e) { 434 return null; 435 } 436 } 437 438 /** 439 * Guess the proper path for a Git repository. 440 * <p> 441 * The method performs some basic guessing to locate the repository. 442 * Searched paths are: 443 * <ol> 444 * <li>{@code directory} // assume exact match</li> 445 * <li>{@code directory} + "/.git" // assume working directory</li> 446 * <li>{@code directory} + ".git" // assume bare</li> 447 * </ol> 448 * 449 * @param directory 450 * location to guess from. Several permutations are tried. 451 * @param fs 452 * the file system abstraction which will be necessary to 453 * perform certain file system operations. 454 * @return the actual directory location if a better match is found; 455 * null if there is no suitable match. 456 */ 457 public static File resolve(final File directory, FS fs) { 458 if (isGitRepository(directory, fs)) 459 return directory; 460 if (isGitRepository(new File(directory, Constants.DOT_GIT), fs)) 461 return new File(directory, Constants.DOT_GIT); 462 463 final String name = directory.getName(); 464 final File parent = directory.getParentFile(); 465 if (isGitRepository(new File(parent, name + Constants.DOT_GIT_EXT), fs)) 466 return new File(parent, name + Constants.DOT_GIT_EXT); 467 return null; 468 } 469 } 470 }