All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.git.FilesystemInterceptor Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * 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 filesToRefresh = new HashSet<>();
    private final Map> 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_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 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 remotes = info.getRemotes();
            StringBuilder sb = new StringBuilder();
            for (GitRemoteConfig rc : remotes.values()) {
                List 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 coll = Collections.singleton(file);
                cache.refreshAllRoots(Collections.>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 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 runWithoutExternalEvents(final File repository, String commandName, Callable 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 createdFolders = new LinkedHashMap() {

        @Override
        public Long put (File key, Long value) {
            long t = System.currentTimeMillis();
            for (Iterator> it = entrySet().iterator(); it.hasNext(); ) {
                Map.Entry 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 getCreatedFolders () {
        synchronized (createdFolders) {
            return new HashSet<>(createdFolders.keySet());
        }
    }

    private class CommandUsageLogger {

        private final Map 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 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 {

        @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 checkLockedRepositories (Collection additionalFilesToRefresh, boolean keepCached) {
        List retval = new LinkedList<>();
        // at first sort the files under repositories
        Map> sortedFiles = GitUtils.sortByRepository(additionalFilesToRefresh);
        for (Map.Entry> e : sortedFiles.entrySet()) {
            Set 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>> it = lockedRepositories.entrySet().iterator(); it.hasNext();) {
            Map.Entry> 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.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 filesToRefresh, boolean log) {
        // refresh all at once
        Set filteredFiles = new HashSet<>(filesToRefresh);
        for (Iterator 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> seenRoots = new HashMap<>();
        private final HashMap timestamps = new HashMap<>(5);
        private final HashMap gitToMetadataFolder = new HashMap<>(5);
        private final HashMap metadataToGitFolder = new HashMap<>(5);
        private final HashMap gitFolderRLs = new HashMap<>(5);
        private final HashSet disabledEvents = new HashSet<>(5);

        private final HashSet filesToInitialize = new HashSet<>();
        private final RequestProcessor.Task initializingTask = rp.create(new Runnable() {
            @Override
            public void run() {
                initializeFiles();
            }
        });

        private final HashSet refreshedRepositories = new HashSet<>(5);
        private final RequestProcessor.Task refreshOpenFilesTask = rp.create(new Runnable() {
            @Override
            public void run() {
                Set 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 getSeenRoots (File repositoryRoot) {
            Set retval = new HashSet<>();
            Set seenRootsForRepository = getSeenRootsForRepository(repositoryRoot);
            synchronized (seenRootsForRepository) {
                retval.addAll(seenRootsForRepository);
            }
            return retval;
        }

        private boolean addSeenRoot (File repositoryRoot, File rootToAdd) {
            boolean added = false;
            Set 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 getSeenRootsForRepository (File repositoryRoot) {
            synchronized (seenRoots) {
                 Set 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 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 metadataFoldersToRefresh = new HashSet<>();
        private final RequestProcessor.Task refreshGitRepoTask = rp.create(new RefreshMetadata());
        
        private class RefreshMetadata implements Runnable {
            
            @Override
            public void run () {
                Set 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 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 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;
        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy