| /******************************************************************************* |
| * Copyright (C) 2010, Jens Baumgart <jens.baumgart@sap.com> |
| * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> |
| * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> |
| * Copyright (C) 2008, Google Inc. |
| * Copyright (C) 2016, 2021 Thomas Wolf <thomas.wolf@paranor.ch> |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *******************************************************************************/ |
| package org.eclipse.egit.core; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.ref.Reference; |
| import java.lang.ref.ReferenceQueue; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.events.ConfigChangedEvent; |
| import org.eclipse.jgit.events.IndexChangedEvent; |
| import org.eclipse.jgit.events.ListenerList; |
| import org.eclipse.jgit.events.RefsChangedEvent; |
| import org.eclipse.jgit.events.WorkingTreeModifiedEvent; |
| import org.eclipse.jgit.lib.BaseRepositoryBuilder; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** |
| * Central cache for Repository instances. |
| */ |
| public enum RepositoryCache { |
| |
| /** |
| * The singleton {@link RepositoryCache}. |
| */ |
| INSTANCE; |
| |
| // EGit uses a weak-reference cache. In Eclipse, EGit can never be sure that |
| // a repo instance isn't still used somewhere, and thus it never explicitly |
| // closes repository instances. Instead, this cache wraps any repository |
| // in a {@link RepositoryHandle} and returns that, and closes the wrapped |
| // repository once that handle is garbage collected. |
| |
| private final ReferenceQueue<RepositoryHandle> queue = new ReferenceQueue<>(); |
| |
| private final Map<File, RepositoryReference> repositoryCache = new HashMap<>(); |
| |
| private final ListenerList globalListeners = new ListenerList(); |
| |
| private RepositoryCache() { |
| new Closer(queue).start(); |
| // Set up listeners on the JGit global listener list to be able to |
| // re-fire events with the correct repository. |
| ListenerList global = Repository.getGlobalListenerList(); |
| global.addConfigChangedListener(event -> { |
| Repository repo = getRepository( |
| event.getRepository().getDirectory()); |
| if (repo == null || repo == event.getRepository()) { |
| // Re-dispatch the original event |
| globalListeners.dispatch(event); |
| } else { |
| ConfigChangedEvent newEvent = new ConfigChangedEvent(); |
| newEvent.setRepository(repo); |
| globalListeners.dispatch(newEvent); |
| } |
| }); |
| global.addIndexChangedListener(event -> { |
| Repository repo = getRepository( |
| event.getRepository().getDirectory()); |
| if (repo == null || repo == event.getRepository()) { |
| globalListeners.dispatch(event); |
| } else { |
| IndexChangedEvent newEvent = new IndexChangedEvent( |
| event.isInternal()); |
| newEvent.setRepository(repo); |
| globalListeners.dispatch(newEvent); |
| } |
| }); |
| global.addRefsChangedListener(event -> { |
| Repository repo = getRepository( |
| event.getRepository().getDirectory()); |
| if (repo == null || repo == event.getRepository()) { |
| globalListeners.dispatch(event); |
| } else { |
| RefsChangedEvent newEvent = new RefsChangedEvent(); |
| newEvent.setRepository(repo); |
| globalListeners.dispatch(newEvent); |
| } |
| }); |
| global.addWorkingTreeModifiedListener(event -> { |
| Repository repo = getRepository( |
| event.getRepository().getDirectory()); |
| if (repo == null || repo == event.getRepository()) { |
| globalListeners.dispatch(event); |
| } else { |
| WorkingTreeModifiedEvent newEvent = new WorkingTreeModifiedEvent( |
| event.getModified(), event.getDeleted()); |
| newEvent.setRepository(repo); |
| globalListeners.dispatch(newEvent); |
| } |
| }); |
| } |
| |
| /** |
| * Gets a global listener list that fires events from any repository. EGit |
| * uses this instead of {@link Repository#getGlobalListenerList()} so that |
| * we can be sure to only ever pass around {@link RepositoryHandle}s instead |
| * of the underlying real repositories. |
| * |
| * @return the global listener list |
| */ |
| public ListenerList getGlobalListenerList() { |
| return globalListeners; |
| } |
| |
| /** |
| * Looks in the cache for a {@link Repository} matching the given git |
| * directory. If there is no such Repository instance in the cache, one is |
| * created. |
| * |
| * @param gitDir |
| * @return an existing instance of Repository for <code>gitDir</code> or a |
| * new one if no Repository instance for <code>gitDir</code> exists |
| * in the cache. |
| * @throws IOException |
| */ |
| public Repository lookupRepository(final File gitDir) throws IOException { |
| // Make sure we have a normalized path without .. segments here. |
| File normalizedGitDir = new Path(gitDir.getAbsolutePath()).toFile(); |
| synchronized (repositoryCache) { |
| RepositoryReference r = repositoryCache.get(normalizedGitDir); |
| if (r == null) { |
| Repository inner = new Builder().setGitDir(normalizedGitDir) |
| .readEnvironment().setup().createRepository(); |
| RepositoryHandle result = new RepositoryHandle(inner); |
| repositoryCache.put(normalizedGitDir, |
| new RepositoryReference(result, inner, queue)); |
| return result; |
| } else { |
| Repository result = r.get(); |
| if (result != null && result.getDirectory().exists()) { |
| return result; |
| } else { |
| Closer.closeReference( |
| repositoryCache.remove(normalizedGitDir)); |
| } |
| } |
| } |
| // If we get here, we found a stale repository. We must remove |
| // a possibly still existing IndexDiffCache outside the synchronized |
| // block, otherwise we may run into a deadlock due to lock inversion |
| // between our repositoryCache and IndexDiffCache.entries. |
| IndexDiffCache.INSTANCE.remove(normalizedGitDir); |
| return lookupRepository(gitDir); |
| } |
| |
| /** |
| * A {@link WeakReference} that keeps the link to the underlying real |
| * repository of a {@link RepositoryHandle}. This is necessary because the |
| * referent (the handle) is already nulled out by the time the weak |
| * reference is returned from the {@link ReferenceQueue}. |
| */ |
| private static class RepositoryReference |
| extends WeakReference<RepositoryHandle> { |
| |
| private Repository inner; |
| |
| public RepositoryReference(RepositoryHandle handle, Repository delegate, |
| ReferenceQueue<RepositoryHandle> queue) { |
| super(handle, queue); |
| inner = delegate; |
| } |
| |
| public Repository getRepository() { |
| return inner; |
| } |
| |
| public void clearRepository() { |
| inner = null; |
| } |
| } |
| |
| /** |
| * A specialized {@link BaseRepositoryBuilder} that returns already existing |
| * instances from this cache instead of creating new ones. If a repository |
| * doesn't exist yet in the cache, it creates a {@link CachingRepository} |
| * and adds a {@link RepositoryHandle} for it to the cache, then returns |
| * that handle. |
| */ |
| private class Builder |
| extends BaseRepositoryBuilder<Builder, RepositoryHandle> { |
| |
| @Override |
| public Builder setGitDir(File gitDir) { |
| File normalizedGitDir = new Path(gitDir.getAbsolutePath()).toFile(); |
| return super.setGitDir(normalizedGitDir); |
| } |
| |
| public CachingRepository createRepository() throws IOException { |
| CachingRepository repo = new CachingRepository(this); |
| if (isMustExist()) { |
| if (!((Repository) repo).getObjectDatabase().exists()) { |
| throw new RepositoryNotFoundException(getGitDir()); |
| } |
| } |
| return repo; |
| } |
| |
| @Override |
| public RepositoryHandle build() throws IOException { |
| setup(); |
| File gitDir = getGitDir(); |
| RepositoryHandle result = null; |
| boolean removeCache = false; |
| try { |
| synchronized (repositoryCache) { |
| Reference<RepositoryHandle> r = repositoryCache.get(gitDir); |
| if (r != null) { |
| RepositoryHandle cached = r.get(); |
| if (cached != null && cached.getDirectory().exists()) { |
| return cached; |
| } else { |
| Closer.closeReference( |
| repositoryCache.remove(gitDir)); |
| removeCache = true; |
| } |
| } |
| CachingRepository inner = createRepository(); |
| result = new RepositoryHandle(inner); |
| repositoryCache.put(gitDir, |
| new RepositoryReference(result, inner, queue)); |
| } |
| } finally { |
| if (removeCache) { |
| IndexDiffCache.INSTANCE.remove(gitDir); |
| } |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * Get a repository builder that can be used to build our cached |
| * repositories. It automatically caches the result, and ensures its git |
| * directory is normalized. If a cached repository already exists, it |
| * returns the cached instance instead of creating a new one. |
| * |
| * @param preventClose |
| * whether to ensure that the next {@link Repository#close()} |
| * call on the repository is a no-op |
| * @param cache |
| * whether the repository config should be cached for all |
| * operations until the next call to {@link Repository#close()}. |
| * |
| * @return A repository builder that caches the repository or returns a |
| * possibly already cached instance. |
| */ |
| public BaseRepositoryBuilder<? extends BaseRepositoryBuilder, ? extends Repository> getBuilder( |
| boolean preventClose, boolean cache) { |
| return new Builder() { |
| |
| @Override |
| public RepositoryHandle build() throws IOException { |
| RepositoryHandle result = super.build(); |
| if (preventClose) { |
| result.incrementOpen(); |
| } |
| if (cache) { |
| Repository inner = result.getDelegate(); |
| if (inner instanceof CachingRepository) { |
| ((CachingRepository) inner).cacheConfig(true); |
| } |
| } |
| return result; |
| } |
| }; |
| } |
| |
| /** |
| * Looks in the cache for a {@link Repository} matching the given git |
| * directory. |
| * |
| * @param gitDir |
| * @return the cached repository, if any, or {@code null} if node found in |
| * the cache. |
| */ |
| public Repository getRepository(final File gitDir) { |
| if (gitDir == null) { |
| return null; |
| } |
| File normalizedGitDir = new Path(gitDir.getAbsolutePath()).toFile(); |
| synchronized (repositoryCache) { |
| RepositoryReference r = repositoryCache.get(normalizedGitDir); |
| if (r == null) { |
| return null; |
| } |
| Repository result = r.get(); |
| if (result != null && result.getDirectory().exists()) { |
| return result; |
| } |
| Closer.closeReference(repositoryCache.remove(normalizedGitDir)); |
| } |
| IndexDiffCache.INSTANCE.remove(normalizedGitDir); |
| return null; |
| } |
| |
| /** |
| * @return all Repository instances contained in the cache |
| */ |
| public Repository[] getAllRepositories() { |
| List<Repository> repositories = new ArrayList<>(); |
| List<File> toRemove = new ArrayList<>(); |
| synchronized (repositoryCache) { |
| for (Iterator<Map.Entry<File, RepositoryReference>> i = repositoryCache |
| .entrySet().iterator(); i.hasNext();) { |
| Map.Entry<File, RepositoryReference> entry = i.next(); |
| Repository repository = entry.getValue().get(); |
| if (repository == null || !repository.getDirectory().exists()) { |
| i.remove(); |
| Closer.closeReference(entry.getValue()); |
| toRemove.add(entry.getKey()); |
| } else { |
| repositories.add(repository); |
| } |
| } |
| } |
| removeIndexDiffCaches(toRemove); |
| return repositories.toArray(new Repository[0]); |
| } |
| |
| /** |
| * Lookup the closest git repository with a working tree containing the |
| * given resource. If there are repositories nested above in the file system |
| * hierarchy we select the closest one above the given resource. |
| * |
| * @param resource |
| * the resource to find the repository for |
| * @return the git repository which has the given resource in its working |
| * tree, or null if none found |
| * @since 3.2 |
| */ |
| public Repository getRepository(final IResource resource) { |
| IPath location = resource.getLocation(); |
| return location == null ? null : getRepository(location); |
| } |
| |
| /** |
| * Lookup the closest git repository with a working tree containing the |
| * given file location. If there are repositories nested above in the file |
| * system hierarchy we select the closest one above the given location. |
| * |
| * @param location |
| * the file location to find the repository for |
| * @return the git repository which has the given location in its working |
| * tree, or null if none found |
| * @since 3.2 |
| */ |
| public Repository getRepository(final IPath location) { |
| if (location == null) { |
| return null; |
| } |
| Repository repository = null; |
| int largestSegmentCount = 0; |
| List<File> toRemove = new ArrayList<>(); |
| synchronized (repositoryCache) { |
| for (Iterator<Map.Entry<File, RepositoryReference>> i = repositoryCache |
| .entrySet().iterator(); i.hasNext();) { |
| Map.Entry<File, RepositoryReference> entry = i.next(); |
| Repository repo = entry.getValue().get(); |
| if (repo == null) { |
| i.remove(); |
| Closer.closeReference(entry.getValue()); |
| toRemove.add(entry.getKey()); |
| continue; |
| } |
| if (repo.isBare()) { |
| continue; |
| } |
| IPath repoPath = new Path(repo.getWorkTree().getAbsolutePath()); |
| if (repoPath.isPrefixOf(location)) { |
| if (repository == null |
| || repoPath.segmentCount() > largestSegmentCount) { |
| if (!repo.getDirectory().exists()) { |
| i.remove(); |
| Closer.closeReference(entry.getValue()); |
| toRemove.add(entry.getKey()); |
| continue; |
| } |
| repository = repo; |
| largestSegmentCount = repoPath.segmentCount(); |
| } |
| } |
| } |
| } |
| removeIndexDiffCaches(toRemove); |
| return repository; |
| } |
| |
| /** |
| * Removes all cached repositories and their IndexDiffCache entries. |
| */ |
| public void clear() { |
| List<File> gitDirs; |
| List<RepositoryReference> references; |
| synchronized (repositoryCache) { |
| gitDirs = new ArrayList<>(repositoryCache.keySet()); |
| references = new ArrayList<>(repositoryCache.values()); |
| repositoryCache.clear(); |
| } |
| removeIndexDiffCaches(gitDirs); |
| references.forEach(Closer::closeReference); |
| } |
| |
| private void removeIndexDiffCaches(List<File> gitDirs) { |
| if (!gitDirs.isEmpty()) { |
| for (File f : gitDirs) { |
| IndexDiffCache.INSTANCE.remove(f); |
| } |
| } |
| } |
| |
| /** |
| * Closes the real repository behind the {@link RepositoryHandle}s handed |
| * out by this cache when the handle is garbage collected. |
| */ |
| private static class Closer extends Thread { |
| |
| private final ReferenceQueue<RepositoryHandle> queue; |
| |
| public Closer(ReferenceQueue<RepositoryHandle> queue) { |
| this.queue = queue; |
| setDaemon(true); |
| setName("Git Repository Closer"); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void run() { |
| try { |
| for (;;) { |
| Reference<?> stale = queue.remove(); |
| if (stale instanceof RepositoryReference) { |
| closeReference((RepositoryReference) stale); |
| } |
| } |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| } |
| |
| public static void closeReference(RepositoryReference stale) { |
| Repository repository = stale.getRepository(); |
| if (repository != null) { |
| repository.close(); |
| } |
| stale.clearRepository(); |
| } |
| } |
| } |