git4idea:IDEADEV-34882: now user is notified when invalid roots are configured
authorConstantine Plotnikov <Constantine.Plotnikov@jetbrains.com>
Fri, 29 May 2009 13:14:24 +0000 (17:14 +0400)
committerConstantine Plotnikov <Constantine.Plotnikov@jetbrains.com>
Fri, 29 May 2009 13:14:24 +0000 (17:14 +0400)
plugins/git4idea/src/git4idea/GitVcs.java
plugins/git4idea/src/git4idea/i18n/GitBundle.properties
plugins/git4idea/src/git4idea/vfs/GitFixRootsDialog.form [new file with mode: 0644]
plugins/git4idea/src/git4idea/vfs/GitFixRootsDialog.java [new file with mode: 0644]
plugins/git4idea/src/git4idea/vfs/GitRootTracker.java [new file with mode: 0644]

index b065787575d83832d9ebb2e0a5a7f0f5f83632c7..7a0760a5bc8329e0ce2da707fcb53eff7c9ea547 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.GitRootTracker;
 import git4idea.vfs.GitVFSListener;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
@@ -141,7 +142,7 @@ public class GitVcs extends AbstractVcs {
    * The changelist provider
    */
   private GitCommittedChangeListProvider myCommittedChangeListProvider;
-
+  private GitRootTracker myRootTracker;
 
   public static GitVcs getInstance(@NotNull Project project) {
     return (GitVcs)ProjectLevelVcsManager.getInstance(project).findVcsByName(NAME);
@@ -171,6 +172,9 @@ 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);
+    }
   }
 
   /**
@@ -345,6 +349,10 @@ public class GitVcs extends AbstractVcs {
       myVFSListener.dispose();
       myVFSListener = null;
     }
+    if (myRootTracker != null) {
+      myRootTracker.dispose();
+      myRootTracker = null;
+    }
     super.deactivate();
   }
 
index da61fe252bf6462aaf4fdfed5c33473121e1b25b..2d13943c4ad339958219d107c7ff2c6b1443e401 100644 (file)
@@ -105,6 +105,12 @@ find.git.error.title=Error Running git
 find.git.success.title=Git Executed Successfully
 find.git.title=Git Configuration
 find.git.unsupported.message=<html><tt>{0}</tt><br>This version is unsupported, and some plugin functionality could fail to work.<br>The minimal supported version is <em>{1}</em>.</html>
+fix.roots.button=Accept
+fix.roots.list.tooltip=The suggested list of Git VCS roots, new roots are marked as bold, removed roots are marked as overstriked.
+fix.roots.message=The following Git vcs roots will be used instead of the current Git VCS roots.
+fix.roots.title=Fix Git VCS Roots
+fix.roots.valid.message=The invalid Git roots have been fixed already.
+fix.roots.valid.title=All Git Roots Are Valid
 getting.history=Getting history for {0}
 git.default.commit.message=\n# Brief commit description here\n\n# Full commit description here (comment lines starting with '#' will not be included)\n\n
 git.error.exit=The git process exited with the code {0}
@@ -283,6 +289,7 @@ reset.validate=&Validate
 resetting.title=Resetting HEAD...
 revert.action.name=Revert
 revision.graph=RevisionGraph
+root.tracker.message=Some configured Git VCS roots are not under Git or have Git repsoitories in subdirectories without configured VCS root.
 select.branch.to.checkout=Select branch to checkout
 show.all.paths.affected.action.name=Show All Affected Paths
 ssh.ask.passphrase.title=SSH Key Passphrase
diff --git a/plugins/git4idea/src/git4idea/vfs/GitFixRootsDialog.form b/plugins/git4idea/src/git4idea/vfs/GitFixRootsDialog.form
new file mode 100644 (file)
index 0000000..abba0a0
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="git4idea.vfs.GitFixRootsDialog">
+  <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+    <margin top="0" left="0" bottom="0" right="0"/>
+    <constraints>
+      <xy x="20" y="20" width="500" height="400"/>
+    </constraints>
+    <properties/>
+    <border type="none"/>
+    <children>
+      <component id="b9e52" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <labelFor value="70a36"/>
+          <text resource-bundle="git4idea/i18n/GitBundle" key="fix.roots.message"/>
+        </properties>
+      </component>
+      <scrollpane id="70a36">
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+        <border type="none"/>
+        <children>
+          <component id="704d2" class="javax.swing.JList" binding="myGitRoots">
+            <constraints/>
+            <properties>
+              <toolTipText resource-bundle="git4idea/i18n/GitBundle" key="fix.roots.list.tooltip"/>
+            </properties>
+          </component>
+        </children>
+      </scrollpane>
+    </children>
+  </grid>
+</form>
diff --git a/plugins/git4idea/src/git4idea/vfs/GitFixRootsDialog.java b/plugins/git4idea/src/git4idea/vfs/GitFixRootsDialog.java
new file mode 100644 (file)
index 0000000..45a0752
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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.ui.DialogWrapper;
+import com.intellij.openapi.vcs.FileStatus;
+import git4idea.i18n.GitBundle;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.util.HashSet;
+import java.util.TreeSet;
+
+/**
+ * This dialog shows a new git root set
+ */
+class GitFixRootsDialog extends DialogWrapper {
+  /**
+   * The list of roots
+   */
+  private JList myGitRoots;
+  /**
+   * The root panel
+   */
+  private JPanel myPanel;
+
+  /**
+   * The constructor
+   *
+   * @param project the context project
+   */
+  protected GitFixRootsDialog(Project project, HashSet<String> current, HashSet<String> added, HashSet<String> removed) {
+    super(project, true);
+    setTitle(GitBundle.getString("fix.roots.title"));
+    setOKButtonText(GitBundle.getString("fix.roots.button"));
+    TreeSet<Item> items = new TreeSet<Item>();
+    for (String f : added) {
+      items.add(new Item(f, FileStatus.ADDED));
+    }
+    for (String f : current) {
+      items.add(new Item(f, removed.contains(f) ? FileStatus.DELETED : FileStatus.NOT_CHANGED));
+    }
+    DefaultListModel listModel = new DefaultListModel();
+    for (Item i : items) {
+      listModel.addElement(i);
+    }
+    myGitRoots.setModel(listModel);
+    init();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  protected JComponent createCenterPanel() {
+    return myPanel;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected String getDimensionServiceKey() {
+    return getClass().getName();
+  }
+
+  /**
+   * The item in the list
+   */
+  private class Item implements Comparable<Item> {
+    /**
+     * The status of the file
+     */
+    @NotNull final FileStatus status;
+    /**
+     * The file name
+     */
+    @NotNull final String fileName;
+
+    /**
+     * The constructor
+     *
+     * @param fileName the root path
+     * @param status   the root status
+     */
+    public Item(@NotNull String fileName, @NotNull FileStatus status) {
+      this.fileName = fileName;
+      this.status = status;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int compareTo(Item o) {
+      return fileName.compareTo(o.fileName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+      if (status == FileStatus.ADDED) {
+        return "<html><b>" + fileName + "</b></html>";
+      }
+      else if (status == FileStatus.DELETED) {
+        return "<html><strike>" + fileName + "</strike></html>";
+      }
+      else {
+        return fileName;
+      }
+    }
+  }
+}
diff --git a/plugins/git4idea/src/git4idea/vfs/GitRootTracker.java b/plugins/git4idea/src/git4idea/vfs/GitRootTracker.java
new file mode 100644 (file)
index 0000000..951d7f5
--- /dev/null
@@ -0,0 +1,457 @@
+/*
+ * 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.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;
+import com.intellij.openapi.command.CommandListener;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.vcs.ProjectLevelVcsManager;
+import com.intellij.openapi.vcs.VcsDirectoryMapping;
+import com.intellij.openapi.vcs.VcsListener;
+import com.intellij.openapi.vfs.*;
+import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
+import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
+import git4idea.GitUtil;
+import git4idea.GitVcs;
+import git4idea.i18n.GitBundle;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+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 {
+  /**
+   * The context project
+   */
+  private final Project myProject;
+  /**
+   * The vcs manager that tracks content roots
+   */
+  private final ProjectLevelVcsManager myVcsManager;
+  /**
+   * The vcs instance
+   */
+  private final GitVcs myVcs;
+  /**
+   * If true, the root configuration has been possibly invalidated
+   */
+  private final AtomicBoolean myRootsInvalidated = new AtomicBoolean(true);
+  /**
+   * If true, the notification is currently active and has not been dismissed yet.
+   */
+  private final AtomicBoolean myNotificationPosted = new AtomicBoolean(false);
+  /**
+   * The invalid git roots
+   */
+  private static final String GIT_INVALID_ROOTS_ID = "GIT_INVALID_ROOTS";
+  /**
+   * The command listener
+   */
+  private CommandListener myCommandListener;
+  /**
+   * The file listener
+   */
+  private MyFileListener myFileListener;
+  /**
+   * Listener for refresh events
+   */
+  private VirtualFileManagerAdapter myVirtualFileManagerListener;
+  /**
+   * Local file system service
+   */
+  private LocalFileSystem myLocalFileSystem;
+
+  /**
+   * The constructor
+   *
+   * @param project the project instance
+   */
+  public GitRootTracker(GitVcs vcs, @NotNull Project project) {
+    if (project.isDefault()) {
+      throw new IllegalArgumentException("The project must not be default");
+    }
+    myProject = project;
+    myVcs = vcs;
+    myVcsManager = ProjectLevelVcsManager.getInstance(project);
+    myVcsManager.addVcsListener(this);
+    myLocalFileSystem = LocalFileSystem.getInstance();
+    myCommandListener = new CommandAdapter() {
+      @Override
+      public void commandFinished(CommandEvent event) {
+        if (!myRootsInvalidated.compareAndSet(true, false)) {
+          return;
+        }
+        directoryMappingChanged();
+      }
+    };
+    CommandProcessor.getInstance().addCommandListener(myCommandListener);
+    myFileListener = new MyFileListener();
+    VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
+    fileManager.addVirtualFileListener(myFileListener);
+    myVirtualFileManagerListener = new VirtualFileManagerAdapter() {
+      @Override
+      public void afterRefreshFinish(boolean asynchonous) {
+        if (!myRootsInvalidated.compareAndSet(true, false)) {
+          return;
+        }
+        directoryMappingChanged();
+      }
+    };
+    fileManager.addVirtualFileManagerListener(myVirtualFileManagerListener);
+    directoryMappingChanged();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public void dispose() {
+    myVcsManager.removeVcsListener(this);
+    CommandProcessor.getInstance().removeCommandListener(myCommandListener);
+    VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
+    fileManager.removeVirtualFileListener(myFileListener);
+    fileManager.removeVirtualFileManagerListener(myVirtualFileManagerListener);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public void directoryMappingChanged() {
+    if (myProject.isDisposed()) {
+      return;
+    }
+    ApplicationManager.getApplication().runReadAction(new Runnable() {
+      public void run() {
+        boolean hasInvalidRoots = false;
+        HashSet<String> rootSet = new HashSet<String>();
+        for (VcsDirectoryMapping m : myVcsManager.getDirectoryMappings()) {
+          if (!m.getVcs().equals(myVcs.getName())) {
+            continue;
+          }
+          String path = m.getDirectory();
+          if (path.length() == 0) {
+            VirtualFile baseDir = myProject.getBaseDir();
+            assert baseDir != null;
+            path = baseDir.getPath();
+          }
+          VirtualFile root = lookupFile(path);
+          if (root == null) {
+            hasInvalidRoots = true;
+            break;
+          }
+          else {
+            rootSet.add(root.getPath());
+          }
+        }
+        if (!hasInvalidRoots && rootSet.isEmpty()) {
+          return;
+        }
+        if (!hasInvalidRoots) {
+          // check if roots have a problem
+          loop:
+          for (String path : rootSet) {
+            VirtualFile root = lookupFile(path);
+            VirtualFile gitRoot = GitUtil.gitRootOrNull(root);
+            if (gitRoot == null || hasUnmappedSubroots(root, rootSet)) {
+              hasInvalidRoots = true;
+              break;
+            }
+            for (String otherPath : rootSet) {
+              if (otherPath.equals(path)) {
+                continue;
+              }
+              if (otherPath.startsWith(path)) {
+                VirtualFile otherFile = lookupFile(otherPath);
+                if (otherFile == null) {
+                  hasInvalidRoots = true;
+                  break loop;
+                }
+                VirtualFile otherRoot = GitUtil.gitRootOrNull(otherFile);
+                if (otherRoot == null || otherRoot == root || otherFile != otherRoot) {
+                  hasInvalidRoots = true;
+                  break loop;
+                }
+              }
+            }
+          }
+        }
+        if (!hasInvalidRoots) {
+          // all roots are correct
+          if (myNotificationPosted.compareAndSet(true, false)) {
+            final Notifications notifications = myProject.getMessageBus().syncPublisher(Notifications.TOPIC);
+            notifications.invalidateAll(GIT_INVALID_ROOTS_ID);
+          }
+          return;
+        }
+        if (myNotificationPosted.compareAndSet(false, true)) {
+          String title = GitBundle.message("root.tracker.message");
+          final Notifications notifications = myProject.getMessageBus().syncPublisher(Notifications.TOPIC);
+          notifications.notify(GIT_INVALID_ROOTS_ID, title, title, NotificationType.ERROR, new NotificationListener() {
+            @NotNull
+            public Continue perform() {
+              if (fixRoots()) {
+                myNotificationPosted.set(false);
+                return Continue.REMOVE;
+              }
+              else {
+                return Continue.LEAVE;
+              }
+            }
+
+            public Continue onRemove() {
+              return Continue.LEAVE;
+            }
+          });
+        }
+      }
+    });
+  }
+
+  /**
+   * Check if there are some unmapped subdirectories under git
+   *
+   * @param directory the content root to check
+   * @param rootSet   the mapped root set
+   * @return true if there are unmapped subroots
+   */
+  private static boolean hasUnmappedSubroots(VirtualFile directory, HashSet<String> rootSet) {
+    for (VirtualFile child : directory.getChildren()) {
+      if (child.getName().equals(".git") || !child.isDirectory()) {
+        continue;
+      }
+      if (child.findChild(".git") != null && !rootSet.contains(child.getPath())) {
+        return true;
+      }
+      if (hasUnmappedSubroots(child, rootSet)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Fix mapped roots
+   *
+   * @return true if roots now in the correct state
+   */
+  boolean fixRoots() {
+    final List<VcsDirectoryMapping> vcsDirectoryMappings = new ArrayList<VcsDirectoryMapping>(myVcsManager.getDirectoryMappings());
+    final HashSet<String> mapped = new HashSet<String>();
+    final HashSet<String> removed = new HashSet<String>();
+    final HashSet<String> added = new HashSet<String>();
+    final VirtualFile baseDir = myProject.getBaseDir();
+    assert baseDir != null;
+    ApplicationManager.getApplication().runReadAction(new Runnable() {
+      public void run() {
+        for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
+          VcsDirectoryMapping m = i.next();
+          String vcsName = myVcs.getName();
+          if (!vcsName.equals(m.getVcs())) {
+            continue;
+          }
+          String path = m.getDirectory();
+          if (path.length() == 0) {
+            path = baseDir.getPath();
+          }
+          VirtualFile file = lookupFile(path);
+          if (file != null && !mapped.add(file.getPath())) {
+            // eliminate duplicates
+            i.remove();
+            continue;
+          }
+          if (file == null || GitUtil.gitRootOrNull(file) == null) {
+            removed.add(path);
+          }
+        }
+        for (String m : mapped) {
+          VirtualFile file = lookupFile(m);
+          if (file == null) {
+            continue;
+          }
+          addSubroots(file, added, mapped);
+          if (removed.contains(m)) {
+            continue;
+          }
+          VirtualFile root = GitUtil.gitRootOrNull(file);
+          assert root != null;
+          for (String o : mapped) {
+            // the mapped collection is not modified here, so order is being kept
+            if (o.equals(m) || removed.contains(o)) {
+              continue;
+            }
+            if (o.startsWith(m)) {
+              VirtualFile otherFile = lookupFile(m);
+              assert otherFile != null;
+              VirtualFile otherRoot = GitUtil.gitRootOrNull(otherFile);
+              assert otherRoot != null;
+              if (otherRoot == root) {
+                removed.add(o);
+              }
+              else if (otherFile != otherRoot) {
+                added.add(otherRoot.getPath());
+                removed.add(o);
+              }
+            }
+          }
+        }
+      }
+    });
+    if (added.isEmpty() && removed.isEmpty()) {
+      Messages.showInfoMessage(myProject, GitBundle.message("fix.roots.valid.message"), GitBundle.message("fix.roots.valid.title"));
+      return true;
+    }
+    GitFixRootsDialog d = new GitFixRootsDialog(myProject, mapped, added, removed);
+    d.show();
+    if (!d.isOK()) {
+      return false;
+    }
+    for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
+      VcsDirectoryMapping m = i.next();
+      String path = m.getDirectory();
+      if (removed.contains(path) || (path.length() == 0 && removed.contains(baseDir.getPath()))) {
+        i.remove();
+      }
+    }
+    for (String a : added) {
+      vcsDirectoryMappings.add(new VcsDirectoryMapping(a, myVcs.getName()));
+    }
+    myVcsManager.setDirectoryMappings(vcsDirectoryMappings);
+    myVcsManager.updateActiveVcss();
+    return true;
+  }
+
+  /**
+   * Look up file in the file system
+   *
+   * @param path the path to lookup
+   * @return the file or null if the file not found
+   */
+  @Nullable
+  private VirtualFile lookupFile(String path) {
+    return myLocalFileSystem.findFileByPath(path);
+  }
+
+  /**
+   * Add subroots for the content root
+   *
+   * @param directory the content root to check
+   * @param toAdd     collection of roots to be added
+   * @param mapped    all mapped git roots
+   */
+  private static void addSubroots(VirtualFile directory, HashSet<String> toAdd, HashSet<String> mapped) {
+    for (VirtualFile child : directory.getChildren()) {
+      if (!child.isDirectory()) {
+        continue;
+      }
+      if (child.getName().equals(".git") && !mapped.contains(directory.getPath())) {
+        toAdd.add(directory.getPath());
+      }
+      else {
+        addSubroots(child, toAdd, mapped);
+      }
+    }
+  }
+
+  /**
+   * The listener for git roots
+   */
+  private class MyFileListener extends VirtualFileAdapter {
+    /**
+     * Return true if file has git repositories
+     *
+     * @param file the file to check
+     * @return true if file has git repositories
+     */
+    private boolean hasGitRepositories(VirtualFile file) {
+      if (!file.isDirectory()) {
+        return false;
+      }
+      if (file.getName().equals(".git")) {
+        return true;
+      }
+      for (VirtualFile child : file.getChildren()) {
+        if (hasGitRepositories(child)) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    /**
+     * Invalidate git root
+     */
+    private void invalidate() {
+      myRootsInvalidated.set(true);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void fileCreated(VirtualFileEvent event) {
+      if (hasGitRepositories(event.getFile())) {
+        invalidate();
+      }
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void beforeFileDeletion(VirtualFileEvent event) {
+      if (hasGitRepositories(event.getFile())) {
+        invalidate();
+      }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void fileMoved(VirtualFileMoveEvent event) {
+      if (hasGitRepositories(event.getFile())) {
+        invalidate();
+      }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void fileCopied(VirtualFileCopyEvent event) {
+      if (hasGitRepositories(event.getFile())) {
+        invalidate();
+      }
+    }
+  }
+}