blob: 2ccec482b4414e1ca292432bb2f8d40640528336 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.git;
import java.awt.EventQueue;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.libs.git.GitBranch;
import org.netbeans.libs.git.GitException;
import org.netbeans.libs.git.GitRemoteConfig;
import org.netbeans.libs.git.GitURI;
import org.netbeans.libs.git.progress.ProgressMonitor;
import org.netbeans.modules.git.FileInformation.Status;
import org.netbeans.modules.git.client.GitClient;
import org.netbeans.modules.git.ui.history.SearchHistoryAction;
import org.netbeans.modules.git.ui.repository.RepositoryInfo;
import org.netbeans.modules.git.utils.GitUtils;
import org.netbeans.modules.versioning.spi.VCSInterceptor;
import org.netbeans.modules.versioning.spi.VersioningSupport;
import org.netbeans.modules.versioning.util.DelayScanRegistry;
import org.netbeans.modules.versioning.util.FileUtils;
import org.netbeans.modules.versioning.util.SearchHistorySupport;
import org.netbeans.modules.versioning.util.Utils;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileUtil;
import org.openide.modules.OnStop;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
/**
* @author ondra
*/
class FilesystemInterceptor extends VCSInterceptor {
private final FileStatusCache cache;
private final Set<File> filesToRefresh = new HashSet<>();
private final Map<File, Set<File>> lockedRepositories = new HashMap<>(5);
private final RequestProcessor.Task refreshTask, lockedRepositoryRefreshTask;
private final RequestProcessor.Task refreshOwnersTask;
private static final RequestProcessor rp = new RequestProcessor("GitRefresh", 1, true);
private final GitFolderEventsHandler gitFolderEventsHandler;
private final CommandUsageLogger commandLogger;
// not final due to tests
private static boolean AUTOMATIC_REFRESH_ENABLED = !"true".equals(System.getProperty("versioning.git.autoRefreshDisabled", "false")); //NOI18N
private static final String INDEX_FILE_NAME = "index"; //NOI18N
private static final String HEAD_FILE_NAME = "HEAD"; //NOI18N
private static final String REFS_FILE_NAME = "refs"; //NOI18N
private static final Logger LOG = Logger.getLogger(FilesystemInterceptor.class.getName());
private static final EnumSet<Status> STATUS_VCS_MODIFIED_ATTRIBUTE = EnumSet.of(
Status.NEW_HEAD_WORKING_TREE,
Status.IN_CONFLICT,
Status.MODIFIED_HEAD_INDEX,
Status.MODIFIED_HEAD_WORKING_TREE,
Status.MODIFIED_INDEX_WORKING_TREE
);
public FilesystemInterceptor () {
cache = Git.getInstance().getFileStatusCache();
refreshTask = rp.create(new RefreshTask(), true);
lockedRepositoryRefreshTask = rp.create(new LockedRepositoryRefreshTask());
gitFolderEventsHandler = new GitFolderEventsHandler();
commandLogger = new CommandUsageLogger();
refreshOwnersTask = rp.create(new Runnable() {
@Override
public void run() {
Git git = Git.getInstance();
git.versionedFilesChanged();
VersioningSupport.versionedRootsChanged();
}
});
}
@Override
public long refreshRecursively (File dir, long lastTimeStamp, List<? super File> children) {
long retval = -1;
if (GitUtils.DOT_GIT.equals(dir.getName()) || gitFolderEventsHandler.isMetadataFolder(dir)) {
Git.STATUS_LOG.log(Level.FINER, "Interceptor.refreshRecursively: {0}", dir.getAbsolutePath()); //NOI18N
children.clear();
retval = gitFolderEventsHandler.refreshAdminFolder(dir);
File[] ch = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept (File dir, String name) {
return REFS_FILE_NAME.equals(name);
}
});
if (ch != null) {
children.addAll(Arrays.asList(ch));
}
} else if (GitUtils.isPartOfGitMetadata(dir)) {
// the condition above is to limit number of following code invocations
// changes done in metadata not present under .git folder are not recognized - there's still the manual refresh
File metadataFolder = gitFolderEventsHandler.getMetadataForReferences(dir);
if (metadataFolder != null) {
gitFolderEventsHandler.refreshReferences(metadataFolder, dir);
}
}
return retval;
}
@Override
public boolean beforeCreate (final File file, boolean isDirectory) {
LOG.log(Level.FINE, "beforeCreate {0} - {1}", new Object[] { file, isDirectory }); //NOI18N
if (GitUtils.isPartOfGitMetadata(file)) return false;
if (!isDirectory && !file.exists()) {
Git git = Git.getInstance();
final File root = git.getRepositoryRoot(file);
if (root == null) return false;
GitClient client = null;
try {
client = git.getClient(root);
client.reset(new File[] { file }, GitUtils.HEAD, true, GitUtils.NULL_PROGRESS_MONITOR);
} catch (GitException.MissingObjectException ex) {
if (!GitUtils.HEAD.equals(ex.getObjectName())) {
// log only if we already have a commit. Just initialized repository does not allow us to reset
LOG.log(Level.INFO, "beforeCreate(): File: {0} {1}", new Object[] { file.getAbsolutePath(), ex.toString()}); //NOI18N
}
} catch (GitException ex) {
LOG.log(Level.INFO, "beforeCreate(): File: {0} {1}", new Object[] { file.getAbsolutePath(), ex.toString()}); //NOI18N
} finally {
if (client != null) {
client.release();
}
}
LOG.log(Level.FINER, "beforeCreate(): finished: {0}", file); // NOI18N
}
return false;
}
@Override
public void afterCreate (final File file) {
LOG.log(Level.FINE, "afterCreate {0}", file); //NOI18N
if (GitUtils.isPartOfGitMetadata(file) && GitUtils.INDEX_LOCK.equals(file.getName())) {
commandLogger.locked(file);
}
if (GitUtils.isAdministrative(file)) {
// new metadata created, we should refresh owners
refreshOwnersTask.schedule(0);
}
// There is no point in refreshing the cache for ignored files.
addToCreated(file);
if (!cache.getStatus(file).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
reScheduleRefresh(800, Collections.singleton(file), true);
}
}
@Override
public boolean beforeDelete (File file) {
LOG.log(Level.FINE, "beforeDelete {0}", file); //NOI18N
if (file == null) return false;
if (GitUtils.isPartOfGitMetadata(file)) return false;
// do not handle delete for ignored files
return !cache.getStatus(file).containsStatus(Status.NOTVERSIONED_EXCLUDED);
}
@Override
public void doDelete (File file) throws IOException {
LOG.log(Level.FINE, "doDelete {0}", file); //NOI18N
if (file == null) return;
Git git = Git.getInstance();
File root = git.getRepositoryRoot(file);
GitClient client = null;
try {
if (GitUtils.getGitFolderForRoot(root).exists()) {
client = git.getClient(root);
client.remove(new File[] { file }, false, GitUtils.NULL_PROGRESS_MONITOR);
} else if (file.exists()) {
Utils.deleteRecursively(file);
if (file.exists()) {
IOException ex = new IOException();
Exceptions.attachLocalizedMessage(ex, NbBundle.getMessage(FilesystemInterceptor.class, "MSG_DeleteFailed", new Object[] { file, "" })); //NOI18N
throw ex;
}
}
if (file.equals(root)) {
// the whole repository was deleted -> release references to the repository folder
gitFolderEventsHandler.refreshIndexFileTimestamp(root);
}
} catch (GitException e) {
IOException ex = new IOException();
Exceptions.attachLocalizedMessage(e, NbBundle.getMessage(FilesystemInterceptor.class, "MSG_DeleteFailed", new Object[] { file, e.getLocalizedMessage() })); //NOI18N
ex.initCause(e);
throw ex;
} finally {
if (client != null) {
client.release();
}
}
}
@Override
public void afterDelete(final File file) {
LOG.log(Level.FINE, "afterDelete {0}", file); //NOI18N
if (file == null) return;
if (GitUtils.isPartOfGitMetadata(file) && GitUtils.INDEX_LOCK.equals(file.getName())) {
commandLogger.unlocked(file);
}
if (GitUtils.DOT_GIT.equals(file.getName())) {
// new metadata created, we should refresh owners
refreshOwnersTask.schedule(3000);
}
// we don't care about ignored files
if (!cache.getStatus(file).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
reScheduleRefresh(800, Collections.singleton(file), true);
}
}
@Override
public boolean beforeMove(File from, File to) {
LOG.log(Level.FINE, "beforeMove {0} -> {1}", new Object[] { from, to }); //NOI18N
if (from == null || to == null || to.exists()) return true;
Git hg = Git.getInstance();
return hg.isManaged(from) && hg.isManaged(to);
}
@Override
public void doMove(final File from, final File to) throws IOException {
LOG.log(Level.FINE, "doMove {0} -> {1}", new Object[] { from, to }); //NOI18N
if (from == null || to == null || to.exists() && !equalPathsIgnoreCase(from, to)) return;
Git git = Git.getInstance();
File root = git.getRepositoryRoot(from);
File dstRoot = git.getRepositoryRoot(to);
GitClient client = null;
try {
if (root != null && root.equals(dstRoot) && !cache.getStatus(to).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
// target does not lie under ignored folder and is in the same repo as src
client = git.getClient(root);
if (equalPathsIgnoreCase(from, to)) {
// must do rename --after because the files/paths equal on Win or Mac
if (!from.renameTo(to)) {
throw new IOException(NbBundle.getMessage(FilesystemInterceptor.class, "MSG_MoveFailed", new Object[] { from, to, "" })); //NOI18N
}
client.rename(from, to, true, GitUtils.NULL_PROGRESS_MONITOR);
} else {
client.rename(from, to, false, GitUtils.NULL_PROGRESS_MONITOR);
}
} else {
boolean result = from.renameTo(to);
if (!result) {
throw new IOException(NbBundle.getMessage(FilesystemInterceptor.class, "MSG_MoveFailed", new Object[] { from, to, "" })); //NOI18N
}
if (root != null) {
client = git.getClient(root);
client.remove(new File[] { from }, true, GitUtils.NULL_PROGRESS_MONITOR);
}
}
} catch (GitException e) {
IOException ex = new IOException();
Exceptions.attachLocalizedMessage(e, NbBundle.getMessage(FilesystemInterceptor.class, "MSG_MoveFailed", new Object[] { from, to, e.getLocalizedMessage() })); //NOI18N
ex.initCause(e);
throw ex;
} finally {
if (client != null) {
client.release();
}
}
}
private boolean equalPathsIgnoreCase (final File from, final File to) {
return Utilities.isWindows() && from.equals(to) || Utilities.isMac() && from.getPath().equalsIgnoreCase(to.getPath());
}
@Override
public void afterMove(final File from, final File to) {
LOG.log(Level.FINE, "afterMove {0} -> {1}", new Object[] { from, to }); //NOI18N
if (from == null || to == null || !to.exists()) return;
if (from.equals(Git.getInstance().getRepositoryRoot(from))
|| to.equals(Git.getInstance().getRepositoryRoot(to))) {
// whole repository was renamed/moved, need to refresh versioning roots
refreshOwnersTask.schedule(0);
}
// There is no point in refreshing the cache for ignored files.
if (!cache.getStatus(from).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
reScheduleRefresh(800, Collections.singleton(from), true);
}
addToCreated(to);
// There is no point in refreshing the cache for ignored files.
if (!cache.getStatus(to).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
reScheduleRefresh(800, Collections.singleton(to), true);
}
}
@Override
public boolean beforeCopy (File from, File to) {
LOG.log(Level.FINE, "beforeCopy {0}->{1}", new Object[] { from, to }); //NOI18N
if (from == null || to == null || to.exists()) return true;
Git git = Git.getInstance();
return git.isManaged(from) && git.isManaged(to);
}
@Override
public void doCopy (final File from, final File to) throws IOException {
LOG.log(Level.FINE, "doCopy {0}->{1}", new Object[] { from, to }); //NOI18N
if (from == null || to == null || to.exists()) return;
Git git = Git.getInstance();
File root = git.getRepositoryRoot(from);
File dstRoot = git.getRepositoryRoot(to);
if (from.isDirectory()) {
FileUtils.copyDirFiles(from, to);
} else {
FileUtils.copyFile(from, to);
}
if (root == null || cache.getStatus(to).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
// target lies under ignored folder, do not add it
return;
}
GitClient client = null;
try {
if (root.equals(dstRoot)) {
client = git.getClient(root);
client.copyAfter(from, to, GitUtils.NULL_PROGRESS_MONITOR);
}
} catch (GitException e) {
IOException ex = new IOException();
Exceptions.attachLocalizedMessage(e, NbBundle.getMessage(FilesystemInterceptor.class, "MSG_CopyFailed", new Object[] { from, to, e.getLocalizedMessage() })); //NOI18N
ex.initCause(e);
throw ex;
} finally {
if (client != null) {
client.release();
}
}
}
@Override
public void afterCopy (final File from, final File to) {
LOG.log(Level.FINE, "afterCopy {0}->{1}", new Object[] { from, to }); //NOI18N
if (to == null) return;
addToCreated(to);
// There is no point in refreshing the cache for ignored files.
if (!cache.getStatus(to).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
reScheduleRefresh(800, Collections.singleton(to), true);
}
}
@Override
public void afterChange (final File file) {
if (file.isDirectory()) return;
LOG.log(Level.FINE, "afterChange {0}", new Object[] { file }); //NOI18N
// There is no point in refreshing the cache for ignored files.
if (!cache.getStatus(file).containsStatus(Status.NOTVERSIONED_EXCLUDED)) {
reScheduleRefresh(800, Collections.singleton(file), true);
}
}
@Override
public boolean isMutable(File file) {
return GitUtils.isPartOfGitMetadata(file) || super.isMutable(file);
}
@Override
public Object getAttribute(File file, String attrName) {
if (SearchHistorySupport.PROVIDED_EXTENSIONS_SEARCH_HISTORY.equals(attrName)){
return new GitSearchHistorySupport(file);
} else if("ProvidedExtensions.RemoteLocation".equals(attrName)) { //NOI18N
File repoRoot = Git.getInstance().getRepositoryRoot(file);
RepositoryInfo info = RepositoryInfo.getInstance(repoRoot);
Map<String, GitRemoteConfig> remotes = info.getRemotes();
StringBuilder sb = new StringBuilder();
for (GitRemoteConfig rc : remotes.values()) {
List<String> uris = rc.getUris();
for (int i = 0; i < uris.size(); i++) {
try {
GitURI u = new GitURI(uris.get(i));
u = u.setUser(null).setPass(null);
sb.append(u.toString()).append(';');
} catch (URISyntaxException ex) {
LOG.log(Level.FINE, null, ex);
}
}
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
} else if ("ProvidedExtensions.VCSIsModified".equals(attrName)) {
File repoRoot = Git.getInstance().getRepositoryRoot(file);
Boolean modified = null;
if (repoRoot != null) {
Set<File> coll = Collections.singleton(file);
cache.refreshAllRoots(Collections.<File, Collection<File>>singletonMap(repoRoot, coll));
modified = cache.containsFiles(coll, STATUS_VCS_MODIFIED_ATTRIBUTE, true);
}
return modified;
} else {
return super.getAttribute(file, attrName);
}
}
/**
* Checks if administrative folder for a repository with the file is registered.
* @param file
*/
void pingRepositoryRootFor(final File file) {
if (!AUTOMATIC_REFRESH_ENABLED) {
return;
}
gitFolderEventsHandler.initializeFor(file);
}
/**
* Returns a set of known repository roots (those visible or open in IDE)
* @param repositoryRoot
* @return
*/
Set<File> getSeenRoots (File repositoryRoot) {
return gitFolderEventsHandler.getSeenRoots(repositoryRoot);
}
/**
* Runs a given callable and disable listening for external repository events for the time the callable is running.
* Refreshes cached modification timestamp of metadata for the given git repository after.
* @param callable code to run
* @param repository
* @param commandName name of the git command if available
*/
<T> T runWithoutExternalEvents(final File repository, String commandName, Callable<T> callable) throws Exception {
assert repository != null;
try {
if (repository != null) {
gitFolderEventsHandler.enableEvents(repository, false);
commandLogger.lockedInternally(repository, commandName);
}
return callable.call();
} finally {
if (repository != null) {
LOG.log(Level.FINER, "Refreshing index timestamp after: {0} on {1}", new Object[] { commandName, repository.getAbsolutePath() }); //NOI18N
if (EventQueue.isDispatchThread()) {
Git.getInstance().getRequestProcessor().post(new Runnable() {
@Override
public void run () {
gitFolderEventsHandler.refreshIndexFileTimestamp(repository);
}
});
} else {
gitFolderEventsHandler.refreshIndexFileTimestamp(repository);
}
commandLogger.unlockedInternally(repository);
gitFolderEventsHandler.enableEvents(repository, true);
}
}
}
private final Map<File, Long> createdFolders = new LinkedHashMap<File, Long>() {
@Override
public Long put (File key, Long value) {
long t = System.currentTimeMillis();
for (Iterator<Map.Entry<File, Long>> it = entrySet().iterator(); it.hasNext(); ) {
Map.Entry<File, Long> e = it.next();
if (e.getValue() < t - 600000) { // keep for 10 minutes
it.remove();
}
}
return super.put(key, value);
}
};
private void addToCreated (File createdFile) {
if (!GitModuleConfig.getDefault().getAutoIgnoreFiles() || !createdFile.isDirectory()) {
// no need to keep files and no need to keep anything if auto-ignore-files is disabled
return;
}
synchronized (createdFolders) {
for (File f : createdFolders.keySet()) {
if (Utils.isAncestorOrEqual(f, createdFile)) {
// just keep created roots, no children
return;
}
}
createdFolders.put(createdFile, createdFile.lastModified());
}
}
Collection<File> getCreatedFolders () {
synchronized (createdFolders) {
return new HashSet<>(createdFolders.keySet());
}
}
private class CommandUsageLogger {
private final Map<File, Events> events = new HashMap<>();
private void locked (File file) {
File gitFolder = getGitFolderFor(file);
// it is a lock file, lock file still exists
if (gitFolder != null && file.exists()) {
long time = System.currentTimeMillis();
synchronized (events) {
Events ev = events.get(gitFolder);
if (ev == null || ev.isExternal() || ev.timeFinished > 0
&& ev.timeFinished < time - 10000) {
// is new lock or is an old unfinished stale event
// and is not part of any internal command that could leave
// pending events to be delivered with 10s delay
ev = new Events();
ev.timeStarted = time;
events.put(gitFolder, ev);
}
}
}
}
/**
* Command run internally from the IDE
*/
private void lockedInternally (File repository, String commandName) {
File gitFolder = GitUtils.getGitFolderForRoot(repository);
Events ev = new Events();
ev.timeStarted = System.currentTimeMillis();
ev.commandName = commandName;
synchronized (events) {
events.put(gitFolder, ev);
}
}
private void unlocked (File file) {
File gitFolder = getGitFolderFor(file);
if (gitFolder != null) {
Events ev;
synchronized (events) {
ev = events.remove(gitFolder);
if (ev != null && !ev.isExternal()) {
// this does not log internal commands
events.put(gitFolder, ev);
return;
}
}
if (ev != null) {
long time = System.currentTimeMillis() - ev.timeStarted;
Utils.logVCSCommandUsageEvent("GIT", time, ev.modifications, ev.commandName, ev.isExternal());
}
}
}
/**
* Internal command finish
*/
private void unlockedInternally (File repository) {
File gitFolder = GitUtils.getGitFolderForRoot(repository);
Events ev;
synchronized (events) {
ev = events.get(gitFolder);
if (ev == null) {
return;
} else if (ev.isExternal()) {
events.remove(gitFolder);
}
}
ev.timeFinished = System.currentTimeMillis();
long time = ev.timeFinished - ev.timeStarted;
Utils.logVCSCommandUsageEvent("GIT", time, ev.modifications, ev.commandName, ev.isExternal());
}
/**
*
* @param wlockFile
* @return parent git folder for wlock file or null if the file is not
* a write lock repository file
*/
private File getGitFolderFor (File wlockFile) {
File repository = Git.getInstance().getRepositoryRoot(wlockFile);
File gitFolder = GitUtils.getGitFolderForRoot(repository);
return gitFolder.equals(wlockFile.getParentFile())
? gitFolder
: null;
}
private void logModification (File file) {
if (GitUtils.isPartOfGitMetadata(file)) {
return;
}
File repository = Git.getInstance().getRepositoryRoot(file);
File gitFolder = GitUtils.getGitFolderForRoot(repository);
if (gitFolder != null) {
synchronized (events) {
Events ev = events.get(gitFolder);
if (ev != null) {
++ev.modifications;
}
}
}
}
}
private static class Events {
long timeStarted;
long timeFinished;
long modifications;
String commandName;
private boolean isExternal () {
return commandName == null;
}
}
final ProgressMonitor.DefaultProgressMonitor shutdownMonitor = new ProgressMonitor.DefaultProgressMonitor();
private class RefreshTask implements Runnable {
@Override
public void run() {
Thread.interrupted();
if (DelayScanRegistry.getInstance().isDelayed(refreshTask, Git.STATUS_LOG, "GitInterceptor.refreshTask")) { //NOI18N
return;
}
Collection<File> files;
synchronized (filesToRefresh) {
files = new HashSet<>(filesToRefresh);
filesToRefresh.clear();
}
if (shutdownMonitor.isCanceled()) {
return;
}
if (!"false".equals(System.getProperty("versioning.git.delayStatusForLockedRepositories"))) {
files = checkLockedRepositories(files, false);
}
if (!files.isEmpty()) {
cache.refreshAllRoots(files, shutdownMonitor);
}
if (!lockedRepositories.isEmpty()) {
lockedRepositoryRefreshTask.schedule(5000);
}
}
}
@OnStop
public static class ShutdownCallable implements Callable<Boolean> {
@Override
public Boolean call () throws Exception {
LOG.log(Level.FINE, "Canceling the auto refresh progress monitor");
Git.getInstance().getVCSInterceptor().shutdownMonitor.cancel();
try {
Git.getInstance().getVCSInterceptor().refreshTask.waitFinished(3000);
} catch (InterruptedException ex) {}
return true;
}
}
private Collection<File> checkLockedRepositories (Collection<File> additionalFilesToRefresh, boolean keepCached) {
List<File> retval = new LinkedList<>();
// at first sort the files under repositories
Map<File, Set<File>> sortedFiles = GitUtils.sortByRepository(additionalFilesToRefresh);
for (Map.Entry<File, Set<File>> e : sortedFiles.entrySet()) {
Set<File> alreadyPlanned = lockedRepositories.get(e.getKey());
if (alreadyPlanned == null) {
alreadyPlanned = new HashSet<>();
lockedRepositories.put(e.getKey(), alreadyPlanned);
}
alreadyPlanned.addAll(e.getValue());
}
// return all files that do not belong to a locked repository
for (Iterator<Map.Entry<File, Set<File>>> it = lockedRepositories.entrySet().iterator(); it.hasNext();) {
Map.Entry<File, Set<File>> entry = it.next();
File repository = entry.getKey();
if (!repository.exists()) {
// repository does not exist, no need to keep it
it.remove();
} else if (GitUtils.isRepositoryLocked(repository)) {
Git.STATUS_LOG.log(Level.FINE, "checkLockedRepositories(): Repository {0} locked, status refresh delayed", repository); //NOI18N
} else {
// repo not locked, add all files into the returned collection
retval.addAll(entry.getValue());
if (!keepCached) {
it.remove();
}
}
}
return retval;
}
private class LockedRepositoryRefreshTask implements Runnable {
@Override
public void run() {
if (!checkLockedRepositories(Collections.<File>emptySet(), true).isEmpty()) {
// there are some newly unlocked repositories to refresh
refreshTask.schedule(0);
} else if (!lockedRepositories.isEmpty()) {
lockedRepositoryRefreshTask.schedule(5000);
}
}
}
private void reScheduleRefresh (int delayMillis, Set<File> filesToRefresh, boolean log) {
// refresh all at once
Set<File> filteredFiles = new HashSet<>(filesToRefresh);
for (Iterator<File> it = filteredFiles.iterator(); it.hasNext(); ) {
if (GitUtils.isPartOfGitMetadata(it.next())) {
it.remove();
}
}
boolean changed;
synchronized (this.filesToRefresh) {
changed = this.filesToRefresh.addAll(filteredFiles);
}
if (changed) {
Git.STATUS_LOG.log(Level.FINE, "reScheduleRefresh: adding {0}", filteredFiles);
if (log) {
for (File file : filteredFiles) {
commandLogger.logModification(file);
}
}
refreshTask.schedule(delayMillis);
}
}
private static class GitFolderTimestamps {
private final File indexFile;
private final long indexFileTS;
private final File headFile;
private final long headFileTS;
private final File refFile;
private final long refFileTS;
private final File gitFolder;
private final File metadataFolder;
private long referencesFolderTS;
public GitFolderTimestamps (File indexFile, File headFile, File refFile, File gitFolder, File metadataFolder) {
this.indexFile = indexFile;
this.indexFileTS = indexFile.lastModified();
this.headFile = headFile;
this.headFileTS = headFile.lastModified();
this.refFile = refFile;
this.refFileTS = refFile.lastModified();
this.gitFolder = gitFolder;
this.metadataFolder = metadataFolder;
referencesFolderTS = System.currentTimeMillis();
}
private File getIndexFile () {
return indexFile;
}
private boolean isNewer (GitFolderTimestamps other) {
boolean newer = true;
if (other != null) {
newer = indexFileTS > other.indexFileTS || headFileTS > other.headFileTS
|| refFileTS > other.refFileTS;
}
return newer;
}
private File getGitFolder () {
return gitFolder;
}
private File getMetadataFolder () {
return metadataFolder;
}
private boolean repositoryExists () {
return indexFileTS > 0 || gitFolder.exists();
}
private boolean isOutdated () {
// first check the index
boolean upToDate = indexFileTS >= indexFile.lastModified();
// then check the current head
if (upToDate) {
upToDate = headFileTS >= headFile.lastModified();
}
// if pointer to branch did not change, there could still be a commit to the same branch - in that case refs/heads/... file changed
if (upToDate) {
upToDate = refFileTS >= refFile.lastModified();
}
return !upToDate;
}
private boolean updateReferences (File triggerFolder) {
boolean updated = false;
long ts = triggerFolder.lastModified();
if (ts > referencesFolderTS) {
updated = true;
referencesFolderTS = System.currentTimeMillis();
}
return updated;
}
}
private static class MetadataMapping {
private final File metadataFolder;
private final long ts;
public MetadataMapping (File metadataFolder, long ts) {
this.metadataFolder = metadataFolder;
this.ts = ts;
}
}
private class GitFolderEventsHandler {
private final HashMap<File, Set<File>> seenRoots = new HashMap<>();
private final HashMap<File, GitFolderTimestamps> timestamps = new HashMap<>(5);
private final HashMap<File, MetadataMapping> gitToMetadataFolder = new HashMap<>(5);
private final HashMap<File, File> metadataToGitFolder = new HashMap<>(5);
private final HashMap<File, FileChangeListener> gitFolderRLs = new HashMap<>(5);
private final HashSet<File> disabledEvents = new HashSet<>(5);
private final HashSet<File> filesToInitialize = new HashSet<>();
private final RequestProcessor.Task initializingTask = rp.create(new Runnable() {
@Override
public void run() {
initializeFiles();
}
});
private final HashSet<File> refreshedRepositories = new HashSet<>(5);
private final RequestProcessor.Task refreshOpenFilesTask = rp.create(new Runnable() {
@Override
public void run() {
Set<File> repositories;
synchronized (refreshedRepositories) {
repositories = new HashSet<>(refreshedRepositories);
refreshedRepositories.clear();
}
GitUtils.headChanged(repositories.toArray(new File[repositories.size()]));
}
});
private final GitRepositories gitRepositories = GitRepositories.getInstance();
public void initializeFor (File file) {
if (addFileToInitialize(file)) {
initializingTask.schedule(500);
}
}
private Set<File> getSeenRoots (File repositoryRoot) {
Set<File> retval = new HashSet<>();
Set<File> seenRootsForRepository = getSeenRootsForRepository(repositoryRoot);
synchronized (seenRootsForRepository) {
retval.addAll(seenRootsForRepository);
}
return retval;
}
private boolean addSeenRoot (File repositoryRoot, File rootToAdd) {
boolean added = false;
Set<File> seenRootsForRepository = getSeenRootsForRepository(repositoryRoot);
synchronized (seenRootsForRepository) {
if (!seenRootsForRepository.contains(repositoryRoot)) {
// try to add the file only when the repository root is not yet registered
rootToAdd = FileUtil.normalizeFile(rootToAdd);
added = !GitUtils.prepareRootFiles(repositoryRoot, seenRootsForRepository, rootToAdd);
}
}
return added;
}
private Set<File> getSeenRootsForRepository (File repositoryRoot) {
synchronized (seenRoots) {
Set<File> seenRootsForRepository = seenRoots.get(repositoryRoot);
if (seenRootsForRepository == null) {
seenRoots.put(repositoryRoot, seenRootsForRepository = new HashSet<>());
}
return seenRootsForRepository;
}
}
private boolean addFileToInitialize(File file) {
synchronized (filesToInitialize) {
return filesToInitialize.add(file);
}
}
private File getFileToInitialize () {
File nextFile = null;
synchronized (filesToInitialize) {
Iterator<File> iterator = filesToInitialize.iterator();
if (iterator.hasNext()) {
nextFile = iterator.next();
iterator.remove();
}
}
return nextFile;
}
private GitFolderTimestamps scanGitFolderTimestamps (File gitFolder) {
File metadataFolder = translateToMetadataFolder(gitFolder);
File indexFile = new File(metadataFolder, INDEX_FILE_NAME);
File headFile = new File(metadataFolder, HEAD_FILE_NAME);
GitBranch activeBranch = null;
RepositoryInfo info = RepositoryInfo.getInstance(gitFolder.getParentFile());
if (info != null) {
info.refresh();
activeBranch = info.getActiveBranch();
}
File refFile = headFile;
if (activeBranch != null && !GitBranch.NO_BRANCH.equals(activeBranch.getName())) {
refFile = new File(metadataFolder, (GitUtils.PREFIX_R_HEADS + activeBranch.getName()).replace("/", File.separator)); //NOI18N
}
return new GitFolderTimestamps(indexFile, headFile, refFile, gitFolder, metadataFolder);
}
public void refreshIndexFileTimestamp (File repository) {
refreshIndexFileTimestamp(scanGitFolderTimestamps(GitUtils.getGitFolderForRoot(repository)));
}
private void refreshIndexFileTimestamp (GitFolderTimestamps newTimestamps) {
if (Utils.isAncestorOrEqual(new File(System.getProperty("java.io.tmpdir")), newTimestamps.getIndexFile())) { //NOI18N
// skip repositories in temp folder
return;
}
File gitFolder = newTimestamps.getGitFolder(); // this can sadly be a link file gitrdir: PATH_TO_FOLDER
final File metadataFolder = newTimestamps.getMetadataFolder();
boolean exists = newTimestamps.repositoryExists();
synchronized (timestamps) {
if (exists && !newTimestamps.isNewer(timestamps.get(gitFolder))) {
// do not enter the filesystem module unless really need to
return;
}
}
boolean add = false;
boolean remove = false;
synchronized (timestamps) {
timestamps.remove(gitFolder);
FileChangeListener list = gitFolderRLs.remove(gitFolder);
if (exists) {
timestamps.put(gitFolder, newTimestamps);
if (list == null) {
final FileChangeListener fList = list = new FileChangeAdapter();
// has to run in a different thread, otherwise we may get a deadlock
rp.post(new Runnable () {
@Override
public void run() {
FileUtil.addRecursiveListener(fList, metadataFolder);
}
});
}
gitFolderRLs.put(gitFolder, list);
add = true;
} else {
if (list != null) {
final FileChangeListener fList = list;
// has to run in a different thread, otherwise we may get a deadlock
rp.post(new Runnable () {
@Override
public void run() {
FileUtil.removeRecursiveListener(fList, metadataFolder);
// repository was deleted, we should refresh versioned parents
Git.getInstance().versionedFilesChanged();
}
});
}
Git.STATUS_LOG.log(Level.FINE, "refreshAdminFolderTimestamp: {0} no longer exists", gitFolder.getAbsolutePath()); //NOI18N
remove = true;
}
if (remove) {
gitRepositories.remove(gitFolder.getParentFile(), false);
gitToMetadataFolder.remove(gitFolder);
metadataToGitFolder.remove(metadataFolder);
} else if (add) {
File repository = gitFolder.getParentFile();
if (!repository.equals(Git.getInstance().getRepositoryRoot(repository))) {
// guess this is needed, versionedFilesChanged might not have been called yet (see InitAction)
Git.getInstance().versionedFilesChanged();
}
gitRepositories.add(repository, false);
}
}
}
private void initializeFiles() {
File file;
while ((file = getFileToInitialize()) != null) {
Git.STATUS_LOG.log(Level.FINEST, "GitFolderEventsHandler.initializeFiles: {0}", file.getAbsolutePath()); //NOI18N
// select repository root for the file and finds it's .git folder
File repositoryRoot = Git.getInstance().getRepositoryRoot(file);
if (repositoryRoot != null) {
if (addSeenRoot(repositoryRoot, file)) {
// this means the repository has not yet been scanned, so scan it
Git.STATUS_LOG.log(Level.FINE, "initializeFiles: planning a scan for {0} - {1}", new Object[]{repositoryRoot.getAbsolutePath(), file.getAbsolutePath()}); //NOI18N
reScheduleRefresh(4000, Collections.singleton(file), false);
File gitFolder = GitUtils.getGitFolderForRoot(repositoryRoot);
boolean refreshNeeded = false;
synchronized (timestamps) {
if (!timestamps.containsKey(gitFolder)) {
File metadataFolder = translateToMetadataFolder(gitFolder);
if (new File(metadataFolder, INDEX_FILE_NAME).canRead()) {
timestamps.put(gitFolder, null);
refreshNeeded = true;
}
}
}
if (refreshNeeded) {
refreshIndexFileTimestamp(scanGitFolderTimestamps(gitFolder));
}
}
}
}
Git.STATUS_LOG.log(Level.FINEST, "GitFolderEventsHandler.initializeFiles: finished"); //NOI18N
}
private long refreshAdminFolder (File metadataFolder) {
long lastModified = 0;
if (AUTOMATIC_REFRESH_ENABLED && !"false".equals(System.getProperty("versioning.git.handleExternalEvents", "true"))) { //NOI18N
metadataFolder = FileUtil.normalizeFile(metadataFolder);
Git.STATUS_LOG.log(Level.FINER, "refreshAdminFolder: special FS event handling for {0}", metadataFolder.getAbsolutePath()); //NOI18N
GitFolderTimestamps cached;
File gitFolder = translateToGitFolder(metadataFolder);
if (isEnabled(gitFolder)) {
synchronized (timestamps) {
cached = timestamps.get(gitFolder);
}
if (cached == null || !cached.repositoryExists() || cached.isOutdated()) {
synchronized (metadataFoldersToRefresh) {
if (metadataFoldersToRefresh.add(gitFolder)) {
refreshGitRepoTask.schedule(1000);
}
}
}
}
}
return lastModified;
}
private final Set<File> metadataFoldersToRefresh = new HashSet<>();
private final RequestProcessor.Task refreshGitRepoTask = rp.create(new RefreshMetadata());
private class RefreshMetadata implements Runnable {
@Override
public void run () {
Set<File> stillLockedRepos = new HashSet<>();
for (File gitFolder = getNextRepository(); gitFolder != null; gitFolder = getNextRepository()) {
if (GitUtils.isRepositoryLocked(gitFolder.getParentFile())) {
Git.STATUS_LOG.log(Level.FINE, "refreshAdminFolder: replanning repository scan for locked {0}", gitFolder); //NOI18N
stillLockedRepos.add(gitFolder);
} else {
refreshIndexFileTimestamp(scanGitFolderTimestamps(gitFolder));
File repository = gitFolder.getParentFile();
RepositoryInfo.refreshAsync(repository);
Git.STATUS_LOG.log(Level.FINE, "refreshAdminFolder: planning repository scan for {0}", repository.getAbsolutePath()); //NOI18N
reScheduleRefresh(3000, getSeenRoots(repository), false); // scan repository root
refreshOpenFiles(repository);
}
}
synchronized (metadataFoldersToRefresh) {
if (metadataFoldersToRefresh.addAll(stillLockedRepos)) {
refreshGitRepoTask.schedule(2000);
}
}
}
private File getNextRepository () {
File gitFolder = null;
synchronized (metadataFoldersToRefresh) {
if (!metadataFoldersToRefresh.isEmpty()) {
Iterator<File> it = metadataFoldersToRefresh.iterator();
gitFolder = it.next();
it.remove();
}
}
return gitFolder;
}
}
private void refreshReferences (File metadataFolder, File triggerFolder) {
if (AUTOMATIC_REFRESH_ENABLED && !"false".equals(System.getProperty("versioning.git.handleExternalEvents", "true"))) { //NOI18N
metadataFolder = FileUtil.normalizeFile(metadataFolder);
Git.STATUS_LOG.log(Level.FINER, "refreshReferences: special FS event handling for {0}", triggerFolder.getAbsolutePath()); //NOI18N
boolean refreshNeeded = false;
GitFolderTimestamps cached;
File gitFolder = translateToGitFolder(metadataFolder);
if (isEnabled(gitFolder)) {
synchronized (timestamps) {
cached = timestamps.get(gitFolder);
}
if (cached != null && cached.updateReferences(triggerFolder)) {
refreshNeeded = true;
}
if (refreshNeeded) {
File repository = gitFolder.getParentFile();
RepositoryInfo.refreshAsync(repository);
}
}
}
}
private void refreshOpenFiles (File repository) {
boolean refreshPlanned;
synchronized (refreshedRepositories) {
refreshPlanned = !refreshedRepositories.add(repository);
}
if (!refreshPlanned) {
refreshOpenFilesTask.schedule(3000);
}
}
private void enableEvents (File repository, boolean enabled) {
File gitFolder = FileUtil.normalizeFile(GitUtils.getGitFolderForRoot(repository));
synchronized (disabledEvents) {
if (enabled) {
disabledEvents.remove(gitFolder);
} else {
disabledEvents.add(gitFolder);
}
}
}
private boolean isEnabled (File gitFolder) {
synchronized (disabledEvents) {
return !disabledEvents.contains(gitFolder);
}
}
private File translateToMetadataFolder (File gitFolder) {
MetadataMapping mapping;
synchronized(timestamps) {
mapping = gitToMetadataFolder.get(gitFolder);
}
File metadataFolder;
long ts;
if (mapping == null) {
metadataFolder = gitFolder;
ts = System.currentTimeMillis();
} else {
metadataFolder = mapping.metadataFolder;
ts = mapping.ts;
}
if (gitFolder.isFile()) {
ts = gitFolder.lastModified();
if (mapping == null || mapping.ts < ts) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(gitFolder));
for (String line = br.readLine(); line != null; line = br.readLine()) {
line = line.trim();
if (line.startsWith("gitdir:")) { //NOI18N
line = line.substring(7).trim();
File tmp = new File(line);
if (!tmp.isAbsolute()) {
tmp = new File(gitFolder, line).getCanonicalFile();
}
metadataFolder = tmp;
break;
}
}
} catch (IOException ex) {
//
} finally {
if (br != null) {
try {
br.close();
} catch (IOException ex) {
}
}
}
}
}
synchronized (timestamps) {
gitToMetadataFolder.put(gitFolder, new MetadataMapping(metadataFolder, ts));
metadataToGitFolder.put(metadataFolder, gitFolder);
}
return FileUtil.normalizeFile(metadataFolder);
}
private File translateToGitFolder (File metadataFolder) {
File gitFolder;
synchronized (timestamps) {
gitFolder = metadataToGitFolder.get(metadataFolder);
}
if (gitFolder == null) {
gitFolder = metadataFolder;
}
return gitFolder;
}
private boolean isMetadataFolder (File dir) {
synchronized (timestamps) {
return metadataToGitFolder.containsKey(dir);
}
}
private File getMetadataForReferences (File file) {
List<File> metadataFolders;
synchronized (timestamps) {
metadataFolders = new ArrayList<>(metadataToGitFolder.keySet());
}
File candidate = null;
for (File metadataFolder : metadataFolders) {
String refsPath = new File(metadataFolder.getAbsolutePath(), REFS_FILE_NAME).getAbsolutePath();
if (file.getAbsolutePath().startsWith(refsPath)) {
if (candidate == null || candidate.getAbsolutePath().length() < metadataFolder.getAbsolutePath().length()) {
candidate = metadataFolder;
}
}
}
return candidate;
}
}
public class GitSearchHistorySupport extends SearchHistorySupport {
public GitSearchHistorySupport(File file) {
super(file);
}
@Override
protected boolean searchHistoryImpl(final int line) throws IOException {
File file = getFile();
SearchHistoryAction.openSearch(Git.getInstance().getRepositoryRoot(file), file, file.getName(), line);
return true;
}
}
}