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

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