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

org.netbeans.modules.mercurial.FileStatusCache Maven / Gradle / Ivy

There is a newer version: RELEASE220
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.mercurial;

import java.awt.EventQueue;
import java.beans.PropertyChangeListener;
import java.util.*;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.modules.mercurial.util.HgCommand;
import org.netbeans.modules.mercurial.util.HgUtils;
import org.netbeans.modules.versioning.spi.VCSContext;
import org.netbeans.modules.versioning.spi.VersioningSupport;
import org.netbeans.modules.versioning.util.Utils;
import org.openide.filesystems.FileUtil;
import org.openide.util.RequestProcessor;


/**
 * Central part of status management, deduces and caches statuses of files under version control.
 *
 * @author Ondra Vrabec
 */
public class FileStatusCache {

    /**
     * Indicates that status of a file changed and listeners SHOULD check new status
     * values if they are interested in this file.
     * The New value is a ChangedEvent object (old FileInformation object may be null)
     */
    public static final String PROP_FILE_STATUS_CHANGED = "status.changed"; // NOI18N
    public static final FileStatus REPOSITORY_STATUS_UNKNOWN  = null;
    public static final boolean FULL_REPO_SCAN_ENABLED = "true".equals(System.getProperty("versioning.mercurial.fullRepoScanEnabled", "false")); //NOI18N

    private static final FileInformation FILE_INFORMATION_EXCLUDED = new FileInformation(FileInformation.STATUS_NOTVERSIONED_EXCLUDED, false);
    private static final FileInformation FILE_INFORMATION_UPTODATE = new FileInformation(FileInformation.STATUS_VERSIONED_UPTODATE, false);
    private static final FileInformation FILE_INFORMATION_NOTMANAGED = new FileInformation(FileInformation.STATUS_NOTVERSIONED_NOTMANAGED, false);
    private static final FileInformation FILE_INFORMATION_NOTMANAGED_DIRECTORY = new FileInformation(FileInformation.STATUS_NOTVERSIONED_NOTMANAGED, true);
    private static final FileInformation FILE_INFORMATION_UNKNOWN = new FileInformation(FileInformation.STATUS_UNKNOWN, false);
    private static final FileInformation FILE_INFORMATION_NEWLOCALLY = new FileInformation(FileInformation.STATUS_NOTVERSIONED_NEWLOCALLY, false);
    public static final FileInformation FILE_INFORMATION_CONFLICT = new FileInformation(FileInformation.STATUS_VERSIONED_CONFLICT, false);
    public static final FileInformation FILE_INFORMATION_REMOVEDLOCALLY = new FileInformation(FileInformation.STATUS_VERSIONED_REMOVEDLOCALLY, false);
    private int MAX_COUNT_UPTODATE_FILES = 1024;
    private static final int CACHE_SIZE_WARNING_THRESHOLD = 50000; // log when cache gets too big and steps over this threshold
    private boolean hugeCacheWarningLogged;

    private static final Logger LOG = Logger.getLogger("org.netbeans.modules.mercurial.fileStatusCacheNewGeneration"); //NOI18N
    private static final Logger LOG_UPTODATE_FILES = Logger.getLogger("mercurial.cache.upToDateFiles"); //NOI18N

    private PropertyChangeSupport listenerSupport = new PropertyChangeSupport(this);
    private Mercurial     hg;
    /**
     * Keeps cached statuses for managed files
     */
    private final Map cachedFiles;
    private final LinkedHashSet upToDateFiles = new LinkedHashSet(MAX_COUNT_UPTODATE_FILES);
    private final RequestProcessor rp = new RequestProcessor("Mercurial.cacheNG", 1, true);
    private final HashSet nestedRepositories = new HashSet(2); // mainly for logging

    FileStatusCache (Mercurial hg) {
        this.hg = hg;
        cachedFiles = new HashMap();
    }

    /**
     * Checks if given files are ignored, also calls a SharebilityQuery. Cached status for ignored files is eventually refreshed.
     * Can be run from AWT, in that case it switches to a background thread.
     * @param files set of files to be ignore-tested.
     */
    private void handleIgnoredFiles(final Set files) {
        Runnable outOfAWT = new Runnable() {
            @Override
            public void run() {
                for (File f : files) {
                    if (HgUtils.isIgnored(f, true)) {
                        // refresh status for this file
                        boolean isDirectory = f.isDirectory();
                        boolean exists = f.exists();
                        if (!exists) {
                            // remove from cache
                            refreshFileStatus(f, FILE_INFORMATION_UNKNOWN);
                        } else {
                            // add to cache as ignored
                            refreshFileStatus(f, isDirectory ? new FileInformation(FileInformation.STATUS_NOTVERSIONED_EXCLUDED, true) : FILE_INFORMATION_EXCLUDED);
                        }
                    }
                }
            }
        };
        // always run outside of AWT, SQ inside isIgnored can last a long time
        if (EventQueue.isDispatchThread()) {
            rp.post(outOfAWT);
        } else {
            outOfAWT.run();
        }
    }

    /**
     * Fast (can be run from AWT) version of {@link #handleIgnoredFiles(Set)}, tests a file if it's ignored, but never runs a SharebilityQuery.
     * If the file is not recognized as ignored, runs {@link #handleIgnoredFiles(Set)}.
     * @param file
     * @return true if the file is recognized as ignored (but not through a SharebilityQuery)
     */
    private FileInformation checkForIgnoredFile (File file) {
        FileInformation fi = null;
        if (HgUtils.isIgnored(file, false)) {
            fi = FILE_INFORMATION_EXCLUDED;
        } else {
            // run the full test with the SQ
            handleIgnoredFiles(Collections.singleton(file));
        }
        return fi;
    }

    /**
     * Returns the cached file information or null if it does not exist in the cache.
     * @param file
     * @return
     */
    private FileInformation getInfo(File file) {
        FileInformation info = null;
        synchronized (cachedFiles) {
            info = cachedFiles.get(file);
            synchronized (upToDateFiles) {
                if (info == null && upToDateFiles.contains(file)) {
                    addUpToDate(file);
                    info = FILE_INFORMATION_UPTODATE;
                }
            }
        }
        return info;
    }

    /**
     * Sets FI for the given files
     * @param file
     * @param info
     */
    private void setInfo (File file, FileInformation info) {
        synchronized (cachedFiles) {
            cachedFiles.put(file, info);
            if (!hugeCacheWarningLogged && cachedFiles.size() > CACHE_SIZE_WARNING_THRESHOLD) {
                LOG.log(Level.WARNING, "Cache contains too many entries: {0}", (Integer) cachedFiles.size()); //NOI18N
                hugeCacheWarningLogged = true;
            }
            removeUpToDate(file);
        }
    }

    /**
     * Removes the cached value for the given file. Call e.g. if the file becomes up-to-date
     * or uninteresting (no longer existing ignored file).
     * @param file
     */
    private void removeInfo (File file) {
        synchronized (cachedFiles) {
            cachedFiles.remove(file);
            removeUpToDate(file);
        }
    }

    /**
     * Adds an up-to-date file to the cache of UTD files.
     * The cache should have a limited size, so if a threshold is reached, the oldest file is automatically removed.
     * @param file file to add
     */
    private void addUpToDate (File file) {
        synchronized (upToDateFiles) {
            upToDateFiles.remove(file);
            upToDateFiles.add(file); // add the file to the end of the linked collection
            if (upToDateFiles.size() >= MAX_COUNT_UPTODATE_FILES) {
                if (LOG_UPTODATE_FILES.isLoggable(Level.FINE)) {
                    // trying to find a reasonable limit for uptodate files in cache
                    LOG_UPTODATE_FILES.log(Level.WARNING, "Cache of uptodate files grows too quickly: {0}", upToDateFiles.size()); //NOI18N
                    MAX_COUNT_UPTODATE_FILES <<= 1;
                    assert false;
                } else {
                    // removing 1/8 eldest entries
                    Iterator it = upToDateFiles.iterator();
                    int toDelete = MAX_COUNT_UPTODATE_FILES >> 3;
                    for (int i = 0; i < toDelete && it.hasNext(); ++i) {
                        it.next();
                        it.remove();
                    }
                }
            }
        }
    }

    private boolean removeUpToDate (File file) {
        synchronized (upToDateFiles) {
            return upToDateFiles.remove(file);
        }
    }

    /**
     * Do not call from AWT.
     * Can result in a status call. Returns a cached status and runs a status command for not cached files (e.g. up to date files)
     * @param file
     * @return
     */
    public FileInformation getStatus(File file) {
        // at first get cached info
        FileInformation fi = getInfo(file);
        if (fi == null) { // no info cached
            boolean isAdministrative = HgUtils.isAdministrative(file); // is the file a .hg folder?
            boolean isDirectory = isAdministrative || file.isDirectory(); // is the file a directory? .hg folder is also a directory
            if (isDirectory && (isAdministrative || HgUtils.isIgnored(file))) { // is ignored?
                return new FileInformation(FileInformation.STATUS_NOTVERSIONED_EXCLUDED, true);
            }
            if (!isDirectory && !exists(file)) { // does not exist - if isDirectory, then it SHOULD exist
                fi = FILE_INFORMATION_UNKNOWN;
            } else if (!isDirectory && HgUtils.isIgnored(file)) { // ignored file
                fi = FILE_INFORMATION_EXCLUDED;
            } else if (isDirectory) { // is a dir and not in cache - do refresh in here
                fi = refresh(file);
            } else { // exists, is a file and is not ignored and is not in cache - so is probably up to date
                fi = FILE_INFORMATION_UPTODATE;
            }
        }
        return fi;
    }

    int upToDateAccess = 0;
    private static final int UTD_NOTIFY_NUMBER = 100;

    /**
     * Fast version of {@link #getStatus(java.io.File)}.
     * @param file
     * @return always returns a not null value
     */
    public FileInformation getCachedStatus(final File file) {
        return getCachedStatus(file, false);
    }

    /**
     * Marks the file seen in UI. The file is added to seen roots and from now
     * on all external changes will trigger status refresh on this file.
     *
     * @param file
     * @return file's current cached status
     */
    FileInformation markAsSeenInUI (File file) {
        return getCachedStatus(file, true);
    }

    /**
     * Fast version of {@link #getStatus(java.io.File)}.
     * @param file
     * @param seenInUI false value means the file/folder is not visible in UI and thus cannot trigger initial hg status scan
     * @return always returns a not null value
     */
    private FileInformation getCachedStatus(final File file, boolean seenInUI) {
        FileInformation info = getInfo(file); // cached value
        LOG.log(Level.FINER, "getCachedStatus for file {0}: {1}", new Object[] {file, info}); //NOI18N
        boolean triggerHgScan = false;
        if (info == null) {
            if (hg.isManaged(file)) {
                // ping repository scan, this means it has not yet been scanned
                // but scan only files/folders visible in IDE
                triggerHgScan = seenInUI;
                // fast ignore-test
                info = checkForIgnoredFile(file);
                if (file.isDirectory()) {
                    info = createFolderFileInformation(file, info == null ? null : new FileInformation(FileInformation.STATUS_NOTVERSIONED_EXCLUDED, true));
                } else {
                    if (info == null) {
                        info = FILE_INFORMATION_UPTODATE;
                        addUpToDate(file);
                        // XXX delete later
                        if (++upToDateAccess > UTD_NOTIFY_NUMBER) {
                            upToDateAccess = 0;
                            if (LOG_UPTODATE_FILES.isLoggable(Level.FINE)) {
                                synchronized (upToDateFiles) {
                                    LOG_UPTODATE_FILES.log(Level.FINE, "Another {0} U2D files added: {1}", new Object[] {Integer.valueOf(UTD_NOTIFY_NUMBER), upToDateFiles});
                                }
                            }
                        }
                    } else {
                        // add ignored file to cache
                        rp.post(new Runnable() {
                            @Override
                            public void run() {
                                refreshFileStatus(file, FILE_INFORMATION_EXCLUDED);
                            }
                        });
                    }
                }
            } else {
                // unmanaged files
                info = file.isDirectory() ? FILE_INFORMATION_NOTMANAGED_DIRECTORY : FILE_INFORMATION_NOTMANAGED;
            }
            LOG.log(Level.FINER, "getCachedStatus: default for file {0}: {1}", new Object[] {file, info}); //NOI18N
        } else {
            triggerHgScan = seenInUI && !info.wasSeenInUi();
        }
        if (triggerHgScan) {
            info.setSeenInUI(true); // next time this file/folder will not trigger the hg scan
            hg.getMercurialInterceptor().pingRepositoryRootFor(file);
        }
        return info;
    }

    /**
     * Puts folder's information into the cache.
     * @param folder
     * @param fi null means an up-to-date folder.
     * @return
     */
    private FileInformation createFolderFileInformation (File folder, FileInformation fi) {
        FileInformation info;
        // must lock, so possibly elsewhere-created information is not overwritten
        synchronized (cachedFiles) {
            info = getInfo(folder);
            if (info == null || !info.isDirectory()) { // not yet in cache or is stored as a file
                // create an uptodate directory
                info = fi == null ? new FileInformation(FileInformation.STATUS_VERSIONED_UPTODATE, true) : fi;
                setInfo(folder, info);
            }
        }
        return info;
    }

    private Map getModifiedFiles (File root, int includeStatus) {
        boolean check = false;
        assert check = true;
        Map modifiedFiles = new HashMap();
        FileInformation info = getCachedStatus(root);
        if ((info.getStatus() & includeStatus) != 0) {
            modifiedFiles.put(root, info);
        }
        for (File child : info.getModifiedChildren(false)) {
            if (check) {
                checkIsParentOf(root, child);
            }
            modifiedFiles.putAll(getModifiedFiles(child, includeStatus));
        }
        return modifiedFiles;
    }

    /**
     * Refreshes all files under given roots in the cache.
     * @param rootFiles root files to scan sorted under their repository roots
     */
    public void refreshAllRoots (Map> rootFiles) {
        for (Map.Entry> refreshEntry : rootFiles.entrySet()) {
            File repository = refreshEntry.getKey();
            if (repository == null) {
                continue;
            }
            if (LOG.isLoggable(Level.FINE)) {
                LOG.log(Level.FINE, "refreshAllRoots() roots: {0}, repositoryRoot: {1} ", new Object[] {refreshEntry.getValue(), repository.getAbsolutePath()}); // NOI18N
            }
            Map interestingFiles;
            try {
                // find all files with not up-to-date or ignored status
                interestingFiles = HgCommand.getStatus(repository, new LinkedList(refreshEntry.getValue()), null, null);
                for (Map.Entry interestingEntry : interestingFiles.entrySet()) {
                    // put the file's FI into the cache
                    File file = interestingEntry.getKey();
                    FileInformation fi = interestingEntry.getValue();
                    LOG.log(Level.FINE, "refreshAllRoots() file: {0} {1} ", new Object[] {file.getAbsolutePath(), fi}); // NOI18N
                    refreshFileStatus(file, fi);
                }
                for (File root : refreshEntry.getValue()) {
                // clean all files originally in the cache but now being up-to-date or obsolete (as ignored && deleted)
                for (Map.Entry entry : getModifiedFiles(root, ~FileInformation.STATUS_VERSIONED_UPTODATE).entrySet()) {
                    File file = entry.getKey();
                    FileInformation fi = entry.getValue();
                    boolean exists = file.exists();
                    File filesOwner = null;
                    boolean correctRepository = true;
                    if (!interestingFiles.containsKey(file) // file no longer has an interesting status
                            && ((fi.getStatus() & FileInformation.STATUS_NOTVERSIONED_EXCLUDED) != 0 && !exists || // file was ignored and is now deleted
                            (fi.getStatus() & FileInformation.STATUS_NOTVERSIONED_EXCLUDED) == 0 && (!exists || file.isFile())) // file is now up-to-date or also ignored by .hgignore
                            && (correctRepository = repository.equals(filesOwner = hg.getRepositoryRoot(file)))) { // do not remove info for nested repositories
                        LOG.log(Level.FINE, "refreshAllRoots() uninteresting file: {0} {1}", new Object[]{file, fi}); // NOI18N
                        // TODO better way to detect conflicts
                        if (HgCommand.existsConflictFile(file.getAbsolutePath())) {
                            refreshFileStatus(file, FILE_INFORMATION_CONFLICT); // set the files status to 'IN CONFLICT'
                        } else {
                            refreshFileStatus(file, FILE_INFORMATION_UNKNOWN); // remove the file from cache
                        }
                    }
                    if (!correctRepository) {
                        if (nestedRepositories.add(filesOwner)) {
                            LOG.log(Level.INFO, "refreshAllRoots: nested repository found: {0} contains {1}", new File[] {repository, filesOwner}); //NOI18N
                        }
                    }
                }
                }
            } catch (HgException ex) {
                LOG.log(Level.INFO, "refreshAll() file: {0} {1} {2} ", new Object[] {repository.getAbsolutePath(), refreshEntry.getValue(), ex.toString()}); //NOI18N
            }
        }
    }

    /**
     * Prepares refresh candidates, sorts them under their repository roots and eventually calls the cache refresh
     * @param files roots to refresh
     */
    public void refreshAllRoots (final Collection files) {
        long startTime = 0;
        if (Mercurial.STATUS_LOG.isLoggable(Level.FINE)) {
            startTime = System.currentTimeMillis();
            Mercurial.STATUS_LOG.fine("refreshAll: starting for " + files.size() + " files."); //NOI18N
        }
        if (files.isEmpty()) {
            return;
        }
        HashMap> rootFiles = new HashMap>(5);

        for (File file : files) {
            // go through all files and sort them under repository roots
            file = FileUtil.normalizeFile(file);
            File repository = Mercurial.getInstance().getRepositoryRoot(file);
            if (repository == null) {
                // we have an unversioned root, maybe the whole subtree should be removed from cache (VCS owners might have changed)
                continue;
            }
            Set filesUnderRoot = rootFiles.get(repository);
            if (filesUnderRoot == null) {
                filesUnderRoot = new HashSet();
                rootFiles.put(repository, filesUnderRoot);
            }
            HgUtils.prepareRootFiles(repository, filesUnderRoot, file);
        }
        if (Mercurial.STATUS_LOG.isLoggable(Level.FINE)) {
            Mercurial.STATUS_LOG.fine("refreshAll: starting status scan for " + rootFiles.values() + " after " + (System.currentTimeMillis() - startTime)); //NOI18N
            startTime = System.currentTimeMillis();
        }
        if (!rootFiles.isEmpty()) {
            refreshAllRoots(rootFiles);
        }
        if (Mercurial.STATUS_LOG.isLoggable(Level.FINE)) {
            Mercurial.STATUS_LOG.fine("refreshAll: finishes status scan after " + (System.currentTimeMillis() - startTime)); //NOI18N
        }
    }

    /**
     * Refreshes the status of the root and all files under the root
     * @param root
     * @return status of the root itself
     */
    public FileInformation refresh (File root) {
        File repositoryRoot = hg.getRepositoryRoot(root);
        FileInformation fi;
        if (repositoryRoot == null) {
            if (root.isDirectory()) {
                fi = FILE_INFORMATION_NOTMANAGED_DIRECTORY;
            } else {
                fi = FILE_INFORMATION_NOTMANAGED;
            }
        } else {
            // start the recursive refresh
            refreshAllRoots(Collections.singletonMap(repositoryRoot, Collections.singleton(root)));
            // and return scanned value
            fi = getCachedStatus(root);
        }
        return fi;
    }

    /**
     * Updates cache with scanned information for the given file
     * @param file
     * @param fi
     * @param interestingFiles
     * @param alwaysFireEvent
     */
    private void refreshFileStatus(File file, FileInformation fi) {
        if(file == null || fi == null) return;

        FileInformation current;
        synchronized (this) {
            // XXX the question here is: do we want to keep ignored files in the cache (i mean those ignored by hg, not by SQ)?
            // if yes, add equivalent(FILE_INFORMATION_UNKNOWN, fi) into the following test
            if ((equivalent(FILE_INFORMATION_NEWLOCALLY, fi)) && (HgUtils.isIgnored(file)
                    || (getCachedStatus(file.getParentFile()).getStatus() & FileInformation.STATUS_NOTVERSIONED_EXCLUDED) != 0)) {
                // Sharebility query recognized this file as ignored
                LOG.log(Level.FINE, "refreshFileStatus() file: {0} was LocallyNew but is NotSharable", file.getAbsolutePath()); // NOI18N
                fi = file.isDirectory() ? new FileInformation(FileInformation.STATUS_NOTVERSIONED_EXCLUDED, true) : FILE_INFORMATION_EXCLUDED;
            }
            file = FileUtil.normalizeFile(file);
            current = getInfo(file);
            if (equivalent(fi, current)) {
                // no need to fire an event
                return;
            }
            if (fi.getStatus() == FileInformation.STATUS_UNKNOWN) {
                removeInfo(file);
            } else if (fi.getStatus() == FileInformation.STATUS_VERSIONED_UPTODATE && file.isFile()) {
                removeInfo(file);
                addUpToDate(file);
            } else {
                setInfo(file, fi);
            }
            updateParentInformation(file, current, fi);
        }
        fireFileStatusChanged(file, current, fi);
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        listenerSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        listenerSupport.removePropertyChangeListener(listener);
    }

    /**
     * Updates parent information
     * @param file
     * @param oldInfo
     * @param newInfo
     */
    private void updateParentInformation (File file, FileInformation oldInfo, FileInformation newInfo) {
        boolean check = false;
        assert check = true;
        File parent = file;
        FileInformation info;
        FileInformation childInfo = newInfo;
        // update all managed parents
        File child = file;
        while ((parent = parent.getParentFile()) != null && (info = getCachedStatus(parent, false)) != null && (info.getStatus() & FileInformation.STATUS_MANAGED) != 0) {
            if (!info.isDirectory()) {
                info = createFolderFileInformation(parent, null);
            }
            if (LOG.isLoggable(Level.FINE)) {
                LOG.log(Level.FINE, "updateParentInformation: updating {0} with {1} triggered by {2}", new Object[]{parent, newInfo, file});
            }
            if (check) {
                checkIsParentOf(parent, child);
                if (info == FILE_INFORMATION_EXCLUDED || info == FILE_INFORMATION_UPTODATE || info == FILE_INFORMATION_NOTMANAGED
                        || info == FILE_INFORMATION_NOTMANAGED_DIRECTORY || info == FILE_INFORMATION_UNKNOWN || info == FILE_INFORMATION_NEWLOCALLY) {
                    throw new IllegalStateException("Wrong info, expected an own instance for " + parent + ", " + info.getStatusText() + " - " + info.getStatus()); //NOI18N
                    }
            }
            if (!info.setModifiedChild(child, childInfo)) {
                // do not notify parent
                break;
            }
            if (!info.getModifiedChildren(true).isEmpty() && (newInfo.getStatus() & FileInformation.STATUS_VERSIONED_CONFLICT) == 0) {
                childInfo = new FileInformation(FileInformation.STATUS_VERSIONED_CONFLICT, true);
            }
            child = parent;
        }
    }

    /**
     * Fires an event into IDE
     * @param file
     * @param oldInfo
     * @param newInfo
     */
    private void fireFileStatusChanged(File file, FileInformation oldInfo, FileInformation newInfo) {
        listenerSupport.firePropertyChange(PROP_FILE_STATUS_CHANGED, null, new ChangedEvent(file, oldInfo, newInfo));
    }

    private boolean exists(File file) {
        if (!file.exists()) return false;
        return file.getAbsolutePath().equals(FileUtil.normalizeFile(file).getAbsolutePath());
    }

    /**
     * Two FileInformation objects are equivalent if their status contants are equal AND they both reperesent a file (or
     * both represent a directory) AND Entries they cache, if they can be compared, are equal.
     *
     * @param other object to compare to
     * @return true if status constants of both object are equal, false otherwise
     */
    private static boolean equivalent(FileInformation main, FileInformation other) {
        if (other == null || main.getStatus() != other.getStatus() || main.isDirectory() != other.isDirectory()) return false;

        FileStatus e1 = main.getStatus(null);
        FileStatus e2 = other.getStatus(null);
        return e1 == e2 || e1 == null || e2 == null || equal(e1, e2);
    }

    /**
     * Replacement for missing Entry.equals(). It is implemented as a separate method to maintain compatibility.
     *
     * @param e1 first entry to compare
     * @param e2 second Entry to compare
     * @return true if supplied entries contain equivalent information
     */
    private static boolean equal(FileStatus e1, FileStatus e2) {
        // TODO: use your own logic here
        return true;
    }

    /**
     * Lists modified files that are known to be inside
     * this folder. There are locally modified files present
     * plus any files that exist in the folder in the remote repository.
     * Not recursive.
     *
     * @param dir folder to list
     * @return
     */
    public File [] listFiles (File dir) {
        Set set = getStatus(dir).getModifiedChildren(false);
        return set.toArray(new File[0]);
    }


    /**
     * Check if this context has at least one file with the passed in status
     * @param context context to examine
     * @param includeStatus file status to check for
     * @param checkCommitExclusions if set to true then files excluded from commit will not be tested
     * @param cached if set to true, only cached values will be checked otherwise it may call I/O operations
     * @return boolean true if this context contains at least one file with the includeStatus, false otherwise
     */
    public boolean containsFileOfStatus(VCSContext context, int includeStatus, boolean checkCommitExclusions){
        return containsFileOfStatus(context.getRootFiles(), includeStatus, checkCommitExclusions);
    }

    /**
     * Check if any file from the roots has at least one file with the passed in status
     * @param roots root files to recursively check 
     * @param includeStatus file status to check for
     * @param checkCommitExclusions if set to true then files excluded from commit will not be tested
     * @param cached if set to true, only cached values will be checked otherwise it may call I/O operations
     * @return boolean true if this context contains at least one file with the includeStatus, false otherwise
     */
    public boolean containsFileOfStatus(Collection roots, int includeStatus, boolean checkCommitExclusions) {
        for (File root : roots) {
            if (hasStatus(root, includeStatus, checkCommitExclusions)
                    || containsFileOfStatus(root, includeStatus, checkCommitExclusions, !VersioningSupport.isFlat(root))) {
                return true;
            }
        }
        return false;
    }

    private boolean containsFileOfStatus(File root, int includeStatus, boolean checkExclusions, boolean recursive) {
        boolean check = false;
        assert check = true;
        FileInformation info = getCachedStatus(root);
        for (File child : info.getModifiedChildren(includeStatus == FileInformation.STATUS_VERSIONED_CONFLICT)) {
            if (check) {
                checkIsParentOf(root, child);
            }
            if (hasStatus(child, includeStatus, checkExclusions)) {
                return true;
            } else if (recursive && containsFileOfStatus(child, includeStatus, checkExclusions, recursive)) {
                return true;
            }
        }
        return false;
    }

    private boolean hasStatus (File file, int includeStatus, boolean checkExclusions) {
        FileInformation info = getCachedStatus(file);
        return (info.getStatus() & includeStatus) != 0
                && (!checkExclusions || !HgModuleConfig.getDefault().isExcludedFromCommit(file.getAbsolutePath()));
    }

    /**
     * Lists interesting files that are known to be inside given folders.
     * These are locally and remotely modified and ignored files.
     *
     * @param context context to examine
     * @param includeStatus limit returned files to those having one of supplied statuses
     * @return File [] array of interesting files
     */
    public File [] listFiles(VCSContext context, int includeStatus) {
        Set roots = context.getRootFiles();
        Set set = listFilesIntern(roots.toArray(new File[0]), includeStatus);
        for (File excluded : context.getExclusions()) {
            for (Iterator j = set.iterator(); j.hasNext();) {
                File file = (File) j.next();
                if (Utils.isAncestorOrEqual(excluded, file)) {
                    j.remove();
                }
            }
        }
        return set.toArray(new File[0]);
    }

    /**
     * Lists interesting files that are known to be inside given folders.
     * These are locally modified and ignored files.
     *
     * Is not recursive for flat roots
     * 
     * @param roots context to examine
     * @param includeStatus limit returned files to those having one of supplied statuses
     * @return File [] array of interesting files
     */
    public File [] listFiles(File[] roots, int includeStatus) {
        Set listedFiles = listFilesIntern(roots, includeStatus);
        return listedFiles.toArray(new File[0]);
    }

    private Set listFilesIntern(File[] roots, int includeStatus) {
        Set listedFiles = new HashSet();
        for (File root : roots) {
            if (VersioningSupport.isFlat(root)) {
                for (File listed : listFiles(root)) {
                    if ((getCachedStatus(listed).getStatus() & includeStatus) != 0) {
                        listedFiles.add(listed);
                    }
                }
            } else {
                Map modified = getModifiedFiles(root, includeStatus);
                for (File listed : modified.keySet()) {
                    listedFiles.add(listed);
                }
            }
        }
        return listedFiles;
    }

    public void notifyFileChanged(File file) {
        fireFileStatusChanged(file, null, FILE_INFORMATION_UPTODATE);
    }

    /**
     * Refreshes cached information about file and all its descendants.
     * Experimental method mainly for Ignore Action, SHOULD NOT be called elsewhere.
     * We cannot use pure refresh because hg does not track folders and folder info is permanently kept in cache.
     * @param file
     */
    public void refreshIgnores (File file) {
        Map files = getModifiedFiles(file, FileInformation.STATUS_ALL);
        synchronized (this) {
            for (File f : files.keySet()) {
                refreshFileStatus(f, FILE_INFORMATION_UNKNOWN);
            }
        }
        refresh(file);
        LOG.log(Level.FINER, "refreshIgnores: File {0} refreshed", file); //NOI18N
    }

    public static class ChangedEvent {

        private File file;
        private FileInformation oldInfo;
        private FileInformation newInfo;

        public ChangedEvent(File file, FileInformation oldInfo, FileInformation newInfo) {
            this.file = file;
            this.oldInfo = oldInfo;
            this.newInfo = newInfo;
        }

        public File getFile() {
            return file;
        }

        public FileInformation getOldInfo() {
            return oldInfo;
        }

        public FileInformation getNewInfo() {
            return newInfo;
        }
    }

    private void checkIsParentOf(File parent, File child) {
        if (!parent.equals(child.getParentFile())) {
            throw new IllegalStateException(parent.getAbsolutePath() + " is not parent of " + child.getAbsolutePath());
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy