You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RepositoryCache.java 15KB

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