/*******************************************************************************
* Copyright (C) 2011, 2016 Jens Baumgart <jens.baumgart@sap.com> and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Thomas Wolf <thomas.wolf@paranor.ch> Bug 483664
*******************************************************************************/
package org.eclipse.egit.core.internal.indexdiff;
import java.io.File;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.IFileBuffer;
import org.eclipse.core.filebuffers.IFileBufferListener;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.JobFamilies;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.IndexDiff;
import org.eclipse.jgit.lib.Repository;
/**
* This class provides access to a cached {@link IndexDiff} for a given
* repository
*/
public class IndexDiffCache {
private Map<File, IndexDiffCacheEntry> entries = new HashMap<File, IndexDiffCacheEntry>();
private Set<IndexDiffChangedListener> listeners = new HashSet<IndexDiffChangedListener>();
private IndexDiffChangedListener globalListener;
private ExternalFileBufferListener bufferListener;
/**
* Listener on buffer changes related to the workspace external files.
*/
class ExternalFileBufferListener implements IFileBufferListener {
private void updateRepoState(IFileBuffer buffer) {
IFile file = getResource(buffer);
if (file != null) {
// this is a workspace file. Changes on those files are
// monitored better (and differently) in IndexDiffCacheEntry.
return;
}
// the file is not known in the workspace: we should check if it
// contained in a Git repository we aware of
Repository repo = getRepository(buffer);
if (repo == null || repo.isBare()) {
return;
}
IPath relativePath = getRelativePath(repo, buffer);
if (relativePath == null || relativePath.isEmpty()) {
return;
}
// manually trigger update of IndexDiffCacheEntry state
IndexDiffCacheEntry diffEntry = getIndexDiffCacheEntry(repo);
if (diffEntry != null) {
// since .gitignore change can affect other files, reload index
if (Constants.DOT_GIT_IGNORE
.equals(relativePath.lastSegment())) {
diffEntry.refresh();
} else {
diffEntry.refreshFiles(
Collections.singleton(relativePath.toString()));
}
}
}
@Nullable
private IPath getRelativePath(Repository repo, IFileBuffer buffer) {
IPath path = getPath(buffer);
if (path == null) {
return null;
}
IPath repositoryRoot = new Path(repo.getWorkTree().getPath());
return path.makeRelativeTo(repositoryRoot);
}
@Nullable
private IFile getResource(IFileBuffer buffer) {
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IPath location = buffer.getLocation();
if (location == null) {
return null;
}
IFile file = root.getFile(location);
if (!file.isAccessible()) {
return null;
}
return file;
}
@Nullable
private Repository getRepository(IFileBuffer buffer) {
IPath location = getPath(buffer);
if (location != null) {
return Activator.getDefault().getRepositoryCache()
.getRepository(location);
}
return null;
}
@Nullable
private IPath getPath(IFileBuffer buffer) {
IPath location = buffer.getLocation();
if (location != null) {
return location;
}
IFileStore store = buffer.getFileStore();
if (store != null) {
URI uri = store.toURI();
if (uri != null) {
try {
File file = new File(uri);
return new Path(file.getAbsolutePath());
} catch (IllegalArgumentException e) {
// ignore
}
}
}
return null;
}
@Override
public void underlyingFileDeleted(IFileBuffer buffer) {
updateRepoState(buffer);
}
@Override
public void dirtyStateChanged(IFileBuffer buffer, boolean isDirty) {
if (!isDirty) {
updateRepoState(buffer);
}
}
@Override
public void underlyingFileMoved(IFileBuffer buffer, IPath path) {
// nop
}
@Override
public void stateValidationChanged(IFileBuffer buffer,
boolean isStateValidated) {
// nop
}
@Override
public void stateChanging(IFileBuffer buffer) {
// nop
}
@Override
public void stateChangeFailed(IFileBuffer buffer) {
// nop
}
@Override
public void bufferDisposed(IFileBuffer buffer) {
// nop
}
@Override
public void bufferCreated(IFileBuffer buffer) {
// nop
}
@Override
public void bufferContentReplaced(IFileBuffer buffer) {
// nop
}
@Override
public void bufferContentAboutToBeReplaced(IFileBuffer buffer) {
// nop
}
}
/**
* constructor
*/
public IndexDiffCache() {
createGlobalListener();
registerBufferListener();
}
private void registerBufferListener() {
bufferListener = new ExternalFileBufferListener();
ITextFileBufferManager bufferManager = FileBuffers
.getTextFileBufferManager();
if (bufferManager != null) {
bufferManager.addFileBufferListener(bufferListener);
}
}
/**
* @param repository
* @return cache entry
*/
@Nullable
public IndexDiffCacheEntry getIndexDiffCacheEntry(@NonNull Repository repository) {
IndexDiffCacheEntry entry;
synchronized (entries) {
File gitDir = new Path(repository.getDirectory().getAbsolutePath())
.toFile();
entry = entries.get(gitDir);
if (entry != null) {
return entry;
}
if (repository.isBare()) {
return null;
}
entry = new IndexDiffCacheEntry(repository, globalListener);
entries.put(gitDir, entry);
}
return entry;
}
/**
* Adds a listener for IndexDiff changes. Note that only caches are
* available for those repositories for which getIndexDiffCacheEntry was
* called.
*
* @param listener
*/
public void addIndexDiffChangedListener(IndexDiffChangedListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
/**
* @param listener
*/
public void removeIndexDiffChangedListener(IndexDiffChangedListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
private void createGlobalListener() {
globalListener = new IndexDiffChangedListener() {
@Override
public void indexDiffChanged(Repository repository,
IndexDiffData indexDiffData) {
notifyListeners(repository, indexDiffData);
}
};
}
private void notifyListeners(Repository repository,
IndexDiffData indexDiffData) {
IndexDiffChangedListener[] tmpListeners;
synchronized (listeners) {
tmpListeners = listeners
.toArray(new IndexDiffChangedListener[listeners.size()]);
}
for (int i = 0; i < tmpListeners.length; i++) {
try {
tmpListeners[i].indexDiffChanged(repository, indexDiffData);
} catch (RuntimeException e) {
Activator.logError(
"Exception occured in an IndexDiffChangedListener", e); //$NON-NLS-1$
}
}
}
/**
* Used by {@link Activator}
*/
public void dispose() {
if (bufferListener != null) {
ITextFileBufferManager bufferManager = FileBuffers
.getTextFileBufferManager();
if (bufferManager != null) {
bufferManager.removeFileBufferListener(bufferListener);
bufferListener = null;
}
}
for (IndexDiffCacheEntry entry : entries.values()) {
entry.dispose();
}
Job.getJobManager().cancel(JobFamilies.INDEX_DIFF_CACHE_UPDATE);
try {
Job.getJobManager().join(JobFamilies.INDEX_DIFF_CACHE_UPDATE, null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Removes the {@link IndexDiffCacheEntry} for the given repository.
*
* @param gitDir
* of the {@link Repository} to remove the cache entry of
*/
public void remove(@NonNull File gitDir) {
synchronized (entries) {
IndexDiffCacheEntry cachedEntry = entries.remove(gitDir);
if (cachedEntry != null) {
cachedEntry.dispose();
}
}
}
/**
* Retrieves the set of git directories of repositories for which there are
* currently entries in the cache; primarily intended for use in tests.
*
* @return the set of git directories of repositories for which the cache
* currently has entries
*/
@NonNull
public Set<File> currentCacheEntries() {
Set<File> result = null;
synchronized (entries) {
result = new HashSet<>(entries.keySet());
}
return result;
}
}