/* * Copyright 2000-2009 JetBrains s.r.o. * * Licensed 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.community.intellij.plugins.communitycase.vfs; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager; import com.intellij.openapi.vfs.*; import com.intellij.util.containers.HashMap; import com.intellij.util.containers.HashSet; import org.community.intellij.plugins.communitycase.Util; import org.community.intellij.plugins.communitycase.Vcs; import org.community.intellij.plugins.communitycase.config.ConfigUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; /** * The tracker for ".ignore" and "./info/exclude" files. If it detects changes * in ignored files in the project it dirties the project state. The following changes * are detected: * <ul> * <li>Addition or removal of the "./info/exclude" or ".ignore" files</li> * <li>The content change of ignore configuration (using modification date change)</li> * <li>VCS root change (the roots are rescanned, but files are not marked dirty)</li> * </ul> * The entire subdirectory is dirtied. The scanner assumes that the repositories * are correctly configured. In the case of incorrect configuration some events could be * missed. */ public class IgnoreTracker { private static final Logger log = Logger.getInstance("#"+IgnoreTracker.class.getName()); /** * The vcs manager that tracks content roots */ private final ProjectLevelVcsManager myVcsManager; /** * The context project */ private final Project myProject; /** * The vcs instance */ private final Vcs myVcs; /** * The local exclude path */ private static final String LOCAL_EXCLUDE = "./info/exclude"; /** * The local exclude path */ private static final String[] LOCAL_EXCLUDE_ARRAY = LOCAL_EXCLUDE.split("/"); /** * The folder */ private static final String _FOLDER = "."; /** * Dirty scope manager */ private final VcsDirtyScopeManager myDirtyScopeManager; /** * The listener for vcs events */ private final RootsListener myVcsListener; /** * The listener for file events */ private final MyFileListener myFileListener; /** * The configuration listener */ private final ConfigListener myConfigListener; /** * The map from roots to paths of exclude files from config */ private final Map<VirtualFile, String> myExcludeFiles = new HashMap<VirtualFile, String>(); /** * The map from roots to paths of exclude files from config */ private final Set<String> myExcludeFilesPaths = new HashSet<String>(); /** * Cygwin absolute path prefix */ private static final String CYGDRIVE_PREFIX = "/cygdrive/"; /** * The constructor for service * * @param project the context project * @param vcs the vcs instance */ public IgnoreTracker(Project project, Vcs vcs) { myProject = project; myVcs = vcs; myVcsManager = ProjectLevelVcsManager.getInstance(project); myDirtyScopeManager = VcsDirtyScopeManager.getInstance(project); myVcsListener = new RootsListener() { public void RootsChanged() { scan(); } }; myConfigListener = new ConfigListener() { public void configChanged(@NotNull VirtualFile Root, @Nullable VirtualFile configFile) { String oldPath; synchronized (myExcludeFiles) { if (!myExcludeFiles.containsKey(Root)) { return; } oldPath = myExcludeFiles.get(Root); } String newPath = getExcludeFile(Root); if (oldPath == null ? newPath == null : oldPath.equals(newPath)) { return; } synchronized (myExcludeFiles) { myExcludeFiles.put(Root, newPath); myExcludeFilesPaths.clear(); myExcludeFilesPaths.addAll(myExcludeFiles.values()); } myDirtyScopeManager.dirDirtyRecursively(Root); } }; myVcs.addRootsListener(myVcsListener); myVcs.addConfigListener(myConfigListener); myFileListener = new MyFileListener(); VirtualFileManager.getInstance().addVirtualFileListener(myFileListener); scan(); } /** * This method is invoked when component is started or when vcs root mapping changes. */ public void scan() { VirtualFile[] contentRoots = myVcsManager.getRootsUnderVcs(myVcs); if (contentRoots == null || contentRoots.length == 0) { return; } HashMap<VirtualFile, String> newRoots = new HashMap<VirtualFile, String>(); for (VirtualFile r : contentRoots) { VirtualFile root = scanParents(r); if (!newRoots.containsKey(root)) { newRoots.put(root, getExcludeFile(root)); } // note that the component relies on root tracker to scan all children including .ignore files. } synchronized (myExcludeFiles) { myExcludeFiles.clear(); myExcludeFiles.putAll(newRoots); myExcludeFilesPaths.clear(); myExcludeFilesPaths.addAll(myExcludeFiles.values()); } } /** * Get normalized path for root and visit config path so it will be noticed by file events * * @param root the root to examine * @return the normalized path */ @Nullable private String getExcludeFile(VirtualFile root) { try { String file = ConfigUtil.getExcludedFiles(myProject, root); file = fixFileName(file); if (file != null && file.trim().length() != 0) { // locate path so it will be tracked VirtualFile fileForPath = LocalFileSystem.getInstance().findFileByPath(file); if (fileForPath != null) { return fileForPath.getPath(); } } } catch (VcsException e) { log.error(e); //just log and return null } return null; } /** * Fix name for the file * * @param file the file name to fix * @return the file name is fixed according to MSYS and cygwin rules */ private String fixFileName(String file) { if (SystemInfo.isWindows && file != null && file.startsWith("/")) { int cp = CYGDRIVE_PREFIX.length(); if (file.startsWith(CYGDRIVE_PREFIX) && file.length() > cp + 3 && Character.isLetter(file.charAt(cp)) && file.charAt(cp + 1) == '/') { // cygwin absolute path syntax is used return String.valueOf(file.charAt(cp)) + ":" + file.substring(cp + 1); } if (file.length() > 3 && Character.isLetter(file.charAt(1)) && file.charAt(2) == '/') { // msys drive syntax is used return String.valueOf(file.charAt(1)) + ":" + file.substring(2); } // otherwise the path is relative to "bin/.exe" File Dir = new File(myVcs.getSettings().getPathToExecutable()).getParentFile(); if (Dir != null) { Dir = Dir.getParentFile(); } if (Dir != null) { return new File(Dir, file.substring(1)).getPath(); } } return file; } /** * Scan this root and parents in the search of .ignore * * @param root the directory to scan * @return VirtualFile ? */ @Nullable private static VirtualFile scanParents(VirtualFile root) { VirtualFile meta = root.findChild(_FOLDER); if (meta != null) { final VirtualFile localExclude = root.findFileByRelativePath(LOCAL_EXCLUDE); if (localExclude != null && localExclude.isValid()) { localExclude.getTimeStamp(); } return root; } else { VirtualFile parent = root.getParent(); if (parent != null) { return scanParents(parent); } } return null; } /** * Dispose the component removing all related listeners */ public void dispose() { myVcs.removeRootsListener(myVcsListener); myVcs.removeConfigListener(myConfigListener); VirtualFileManager.getInstance().removeVirtualFileListener(myFileListener); } /** * The file listener */ class MyFileListener extends VirtualFileAdapter { /** * {@inheritDoc} */ @Override public void fileCreated(VirtualFileEvent event) { checkIgnoreConfigChange(event.getFile()); } /** * {@inheritDoc} */ @Override public void beforeFileDeletion(VirtualFileEvent event) { checkIgnoreConfigChange(event.getFile()); } /** * {@inheritDoc} */ @Override public void beforeFileMovement(VirtualFileMoveEvent event) { if (".ignore".equals(event.getFileName())) { myDirtyScopeManager.dirDirtyRecursively(event.getNewParent()); myDirtyScopeManager.dirDirtyRecursively(event.getOldParent()); } checkExcludeFile(event.getOldParent().findChild(event.getFileName())); } /** * {@inheritDoc} */ @Override public void fileMoved(VirtualFileMoveEvent event) { checkExcludeFile(event.getNewParent().findChild(event.getFileName())); } /** * {@inheritDoc} */ @Override public void fileCopied(VirtualFileCopyEvent event) { checkIgnoreConfigChange(event.getFile()); } /** * {@inheritDoc} */ @Override public void contentsChanged(VirtualFileEvent event) { checkIgnoreConfigChange(event.getFile()); } /** * Check if the event affects dirty scope configuration, and if this the case, notify dirty scope manager. * * @param file the file to check */ private void checkIgnoreConfigChange(VirtualFile file) { if (".ignore".equals(file.getName())) { VirtualFile parent = file.getParent(); if (parent != null) { myDirtyScopeManager.dirDirtyRecursively(parent); } return; } final VirtualFile base = Util.getPossibleBase(file, LOCAL_EXCLUDE_ARRAY); if (base != null) { myDirtyScopeManager.dirDirtyRecursively(base); return; } checkExcludeFile(file); } /** * Check if the file is exclude file (specified in config) and process it * * @param file the file to process */ private void checkExcludeFile(VirtualFile file) { String path = file.getPath(); List<VirtualFile> toDirty = null; synchronized (myExcludeFiles) { if (myExcludeFilesPaths.contains(path)) { toDirty = new ArrayList<VirtualFile>(); for (Map.Entry<VirtualFile, String> entry : myExcludeFiles.entrySet()) { if (path.equals(entry.getValue())) { toDirty.add(entry.getKey()); } } } } if (toDirty != null) { for (VirtualFile f : toDirty) { myDirtyScopeManager.dirDirtyRecursively(f); } } } } }