git4idea: IDEADEV-34938: Added tracker for .gitignore and .git/info/exclude files...
authorConstantine Plotnikov <Constantine.Plotnikov@jetbrains.com>
Tue, 2 Jun 2009 12:14:06 +0000 (16:14 +0400)
committerConstantine Plotnikov <Constantine.Plotnikov@jetbrains.com>
Tue, 2 Jun 2009 12:14:06 +0000 (16:14 +0400)
plugins/git4idea/src/git4idea/GitVcs.java
plugins/git4idea/src/git4idea/vfs/GitIgnoreTracker.java [new file with mode: 0644]
plugins/git4idea/src/git4idea/vfs/GitRootTracker.java

index 7a0760a5bc8329e0ce2da707fcb53eff7c9ea547..6b59ba262cbb0f49fad3744bf78472ff317da4e3 100644 (file)
@@ -51,6 +51,7 @@ import git4idea.i18n.GitBundle;
 import git4idea.merge.GitMergeProvider;
 import git4idea.rollback.GitRollbackEnvironment;
 import git4idea.update.GitUpdateEnvironment;
+import git4idea.vfs.GitIgnoreTracker;
 import git4idea.vfs.GitRootTracker;
 import git4idea.vfs.GitVFSListener;
 import org.jetbrains.annotations.NonNls;
@@ -142,7 +143,14 @@ public class GitVcs extends AbstractVcs {
    * The changelist provider
    */
   private GitCommittedChangeListProvider myCommittedChangeListProvider;
+  /**
+   * The tracker that checks validity of git roots
+   */
   private GitRootTracker myRootTracker;
+  /**
+   * Tracker for ignored files
+   */
+  private GitIgnoreTracker myGitIgnoreTracker;
 
   public static GitVcs getInstance(@NotNull Project project) {
     return (GitVcs)ProjectLevelVcsManager.getInstance(project).findVcsByName(NAME);
@@ -158,7 +166,6 @@ public class GitVcs extends AbstractVcs {
                 @NotNull final GitRollbackEnvironment gitRollbackEnvironment,
                 @NotNull final GitVcsSettings gitSettings) {
     super(project);
-
     myVcsManager = gitVcsManager;
     mySettings = gitSettings;
     myChangeProvider = gitChangeProvider;
@@ -172,9 +179,6 @@ public class GitVcs extends AbstractVcs {
     myUpdateEnvironment = new GitUpdateEnvironment(myProject);
     myMergeProvider = new GitMergeProvider(myProject);
     myCommittedChangeListProvider = new GitCommittedChangeListProvider(myProject);
-    if (!myProject.isDefault()) {
-      myRootTracker = new GitRootTracker(this, myProject);
-    }
   }
 
   /**
@@ -329,6 +333,29 @@ public class GitVcs extends AbstractVcs {
     return dir.isDirectory() && GitUtil.gitRootOrNull(dir) != null;
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void start() throws VcsException {
+    super.start();
+    if (!myProject.isDefault() && myRootTracker == null) {
+      myRootTracker = new GitRootTracker(this, myProject);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void shutdown() throws VcsException {
+    if (myRootTracker != null) {
+      myRootTracker.dispose();
+      myRootTracker = null;
+    }
+    super.shutdown();
+  }
+
   /**
    * {@inheritDoc}
    */
@@ -338,6 +365,9 @@ public class GitVcs extends AbstractVcs {
     if (myVFSListener == null) {
       myVFSListener = new GitVFSListener(myProject, this);
     }
+    if (myGitIgnoreTracker == null) {
+      myGitIgnoreTracker = new GitIgnoreTracker(myProject, this);
+    }
   }
 
   /**
@@ -349,9 +379,9 @@ public class GitVcs extends AbstractVcs {
       myVFSListener.dispose();
       myVFSListener = null;
     }
-    if (myRootTracker != null) {
-      myRootTracker.dispose();
-      myRootTracker = null;
+    if (myGitIgnoreTracker != null) {
+      myGitIgnoreTracker.dispose();
+      myGitIgnoreTracker = null;
     }
     super.deactivate();
   }
diff --git a/plugins/git4idea/src/git4idea/vfs/GitIgnoreTracker.java b/plugins/git4idea/src/git4idea/vfs/GitIgnoreTracker.java
new file mode 100644 (file)
index 0000000..532d566
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+ * 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 git4idea.vfs;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vcs.ProjectLevelVcsManager;
+import com.intellij.openapi.vcs.VcsListener;
+import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
+import com.intellij.openapi.vfs.*;
+import git4idea.GitVcs;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The tracker for ".gitignore" and ".git/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 ".git/info/exclude" or ".gitignore" 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 git repositories
+ * are correctly configured. In the case of incorrect configuration some events could be
+ * missed.
+ */
+public class GitIgnoreTracker {
+  /**
+   * The vcs manager that tracks content roots
+   */
+  private final ProjectLevelVcsManager myVcsManager;
+  /**
+   * The vcs instance
+   */
+  private final GitVcs myVcs;
+  /**
+   * The local exclude path
+   */
+  private static final String LOCAL_EXCLUDE = ".git/info/exclude";
+  /**
+   * The local exclude path
+   */
+  private static final String[] LOCAL_EXCLUDE_ARRAY = LOCAL_EXCLUDE.split("/");
+  /**
+   * The git folder
+   */
+  private static final String GIT_FOLDER = ".git";
+  /**
+   * Dirty scope manager
+   */
+  private final VcsDirtyScopeManager myDirtyScopeManager;
+  /**
+   * The listener for vcs events
+   */
+  private final VcsListener myVcsListener;
+  /**
+   * The listener for file events
+   */
+  private final MyFileListener myFileListener;
+
+  /**
+   * The constructor for service
+   *
+   * @param project the context project
+   * @param vcs     the git vcs instance
+   */
+  public GitIgnoreTracker(Project project, GitVcs vcs) {
+    myVcs = vcs;
+    myVcsManager = ProjectLevelVcsManager.getInstance(project);
+    myDirtyScopeManager = VcsDirtyScopeManager.getInstance(project);
+    myVcsListener = new VcsListener() {
+      public void directoryMappingChanged() {
+        scan();
+      }
+    };
+    myVcsManager.addVcsListener(myVcsListener);
+    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;
+    }
+    for (VirtualFile root : contentRoots) {
+      scanParents(root);
+      // note that the component relies on root tracker to scan all children including .gitignore files.
+    }
+  }
+
+  /**
+   * Scan this root and parents in the search of .gitignore
+   *
+   * @param root the directory to scan
+   */
+  private static void scanParents(VirtualFile root) {
+    VirtualFile meta = root.findChild(GIT_FOLDER);
+    if (meta != null) {
+      root.findFileByRelativePath(LOCAL_EXCLUDE);
+    }
+    else {
+      VirtualFile parent = root.getParent();
+      if (parent != null) {
+        scanParents(parent);
+      }
+    }
+  }
+
+  /**
+   * Dispose the component removing all related listeners
+   */
+  public void dispose() {
+    myVcsManager.removeVcsListener(myVcsListener);
+    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 (".gitignore".equals(event.getFileName())) {
+        myDirtyScopeManager.dirDirtyRecursively(event.getNewParent());
+        myDirtyScopeManager.dirDirtyRecursively(event.getOldParent());
+      }
+    }
+
+    /**
+     * {@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 (".gitignore".equals(file.getName())) {
+        VirtualFile parent = file.getParent();
+        if (parent != null) {
+          myDirtyScopeManager.dirDirtyRecursively(parent);
+        }
+        return;
+      }
+      VirtualFile base = getBase(file, LOCAL_EXCLUDE_ARRAY);
+      if (base != null) {
+        myDirtyScopeManager.dirDirtyRecursively(base);
+      }
+    }
+
+    /**
+     * The get the possible base for the path. It tries to find the parent for the provided path, if it fails, it looks for the path without last member.
+     *
+     * @param file the file to get base for
+     * @param path the path to to check
+     * @return the file base
+     */
+    @Nullable
+    private VirtualFile getBase(VirtualFile file, String... path) {
+      return getBase(file, path.length, path);
+    }
+
+    /**
+     * The get the possible base for the path. It tries to find the parent for the provided path, if it fails, it looks for the path without last member.
+     *
+     * @param file the file to get base for
+     * @param n    the length of the path to check
+     * @param path the path to to check
+     * @return the file base
+     */
+    @Nullable
+    private VirtualFile getBase(VirtualFile file, int n, String... path) {
+      if (file == null || n <= 0 || n > path.length) {
+        return null;
+      }
+      int i = 1;
+      VirtualFile c = file;
+      for (; c != null && i < n; i++, c = c.getParent()) {
+        if (!path[n - i].equals(c.getName())) {
+          break;
+        }
+      }
+      if (i == n && c != null) {
+        // all components matched
+        return c.getParent();
+      }
+      // try shorter paths paths
+      return getBase(file, n - 1, path);
+    }
+  }
+}
index 951d7f5146114b016fd27b7c80ae52cc958b8199..764a82b219483a43eee061551ca1ff4f80b6b122 100644 (file)
@@ -19,7 +19,6 @@ package git4idea.vfs;
 import com.intellij.notification.NotificationListener;
 import com.intellij.notification.NotificationType;
 import com.intellij.notification.Notifications;
-import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.command.CommandAdapter;
 import com.intellij.openapi.command.CommandEvent;
@@ -49,7 +48,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
  * The component tracks Git roots for the project. If roots are mapped incorrectly it
  * shows balloon that notifies user about the problem and offers to correct root mapping.
  */
-public class GitRootTracker implements Disposable, VcsListener {
+public class GitRootTracker implements VcsListener {
   /**
    * The context project
    */
@@ -132,7 +131,7 @@ public class GitRootTracker implements Disposable, VcsListener {
   }
 
   /**
-   * {@inheritDoc}
+   * Dispose the component removing all related listeners
    */
   public void dispose() {
     myVcsManager.removeVcsListener(this);