git4idea: optimized root tracker to preform fs checks only if there are configured...
[fedora-idea.git] / plugins / git4idea / src / git4idea / vfs / GitRootTracker.java
blob47ef5aab688235a9fac09557e1e522cda3943f17
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package git4idea.vfs;
19 import com.intellij.notification.NotificationListener;
20 import com.intellij.notification.NotificationType;
21 import com.intellij.notification.Notifications;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.command.CommandAdapter;
24 import com.intellij.openapi.command.CommandEvent;
25 import com.intellij.openapi.command.CommandListener;
26 import com.intellij.openapi.command.CommandProcessor;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.ui.Messages;
29 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
30 import com.intellij.openapi.vcs.VcsDirectoryMapping;
31 import com.intellij.openapi.vcs.VcsListener;
32 import com.intellij.openapi.vfs.*;
33 import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
34 import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
35 import git4idea.GitUtil;
36 import git4idea.GitVcs;
37 import git4idea.i18n.GitBundle;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
41 import java.util.ArrayList;
42 import java.util.HashSet;
43 import java.util.Iterator;
44 import java.util.List;
45 import java.util.concurrent.atomic.AtomicBoolean;
47 /**
48 * The component tracks Git roots for the project. If roots are mapped incorrectly it
49 * shows balloon that notifies user about the problem and offers to correct root mapping.
51 public class GitRootTracker implements VcsListener {
52 /**
53 * The context project
55 private final Project myProject;
56 /**
57 * The vcs manager that tracks content roots
59 private final ProjectLevelVcsManager myVcsManager;
60 /**
61 * The vcs instance
63 private final GitVcs myVcs;
64 /**
65 * If true, the root configuration has been possibly invalidated
67 private final AtomicBoolean myRootsInvalidated = new AtomicBoolean(true);
68 /**
69 * If true, there are some configured git roots, or listener has never been run yet
71 private final AtomicBoolean myHasGitRoots = new AtomicBoolean(true);
72 /**
73 * If true, the notification is currently active and has not been dismissed yet.
75 private final AtomicBoolean myNotificationPosted = new AtomicBoolean(false);
76 /**
77 * The invalid git roots
79 private static final String GIT_INVALID_ROOTS_ID = "GIT_INVALID_ROOTS";
80 /**
81 * The command listener
83 private CommandListener myCommandListener;
84 /**
85 * The file listener
87 private MyFileListener myFileListener;
88 /**
89 * Listener for refresh events
91 private VirtualFileManagerAdapter myVirtualFileManagerListener;
92 /**
93 * Local file system service
95 private LocalFileSystem myLocalFileSystem;
97 /**
98 * The constructor
100 * @param project the project instance
102 public GitRootTracker(GitVcs vcs, @NotNull Project project) {
103 if (project.isDefault()) {
104 throw new IllegalArgumentException("The project must not be default");
106 myProject = project;
107 myVcs = vcs;
108 myVcsManager = ProjectLevelVcsManager.getInstance(project);
109 myVcsManager.addVcsListener(this);
110 myLocalFileSystem = LocalFileSystem.getInstance();
111 myCommandListener = new CommandAdapter() {
112 @Override
113 public void commandFinished(CommandEvent event) {
114 if (!myRootsInvalidated.compareAndSet(true, false)) {
115 return;
117 checkRoots(false);
120 CommandProcessor.getInstance().addCommandListener(myCommandListener);
121 myFileListener = new MyFileListener();
122 VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
123 fileManager.addVirtualFileListener(myFileListener);
124 myVirtualFileManagerListener = new VirtualFileManagerAdapter() {
125 @Override
126 public void afterRefreshFinish(boolean asynchonous) {
127 if (!myRootsInvalidated.compareAndSet(true, false)) {
128 return;
130 checkRoots(false);
133 fileManager.addVirtualFileManagerListener(myVirtualFileManagerListener);
134 checkRoots(true);
138 * Dispose the component removing all related listeners
140 public void dispose() {
141 myVcsManager.removeVcsListener(this);
142 CommandProcessor.getInstance().removeCommandListener(myCommandListener);
143 VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
144 fileManager.removeVirtualFileListener(myFileListener);
145 fileManager.removeVirtualFileManagerListener(myVirtualFileManagerListener);
149 * {@inheritDoc}
151 public void directoryMappingChanged() {
152 if (myProject.isDisposed()) {
153 return;
155 checkRoots(true);
159 * Check roots for changes.
161 * @param rootsChanged
163 private void checkRoots(boolean rootsChanged) {
164 if (!rootsChanged && !myHasGitRoots.get()) {
165 return;
167 ApplicationManager.getApplication().runReadAction(new Runnable() {
168 public void run() {
169 boolean hasInvalidRoots = false;
170 HashSet<String> rootSet = new HashSet<String>();
171 for (VcsDirectoryMapping m : myVcsManager.getDirectoryMappings()) {
172 if (!m.getVcs().equals(myVcs.getName())) {
173 continue;
175 String path = m.getDirectory();
176 if (path.length() == 0) {
177 VirtualFile baseDir = myProject.getBaseDir();
178 assert baseDir != null;
179 path = baseDir.getPath();
181 VirtualFile root = lookupFile(path);
182 if (root == null) {
183 hasInvalidRoots = true;
184 break;
186 else {
187 rootSet.add(root.getPath());
190 if (!hasInvalidRoots && rootSet.isEmpty()) {
191 myHasGitRoots.set(false);
192 return;
194 else {
195 myHasGitRoots.set(true);
197 if (!hasInvalidRoots) {
198 // check if roots have a problem
199 loop:
200 for (String path : rootSet) {
201 VirtualFile root = lookupFile(path);
202 VirtualFile gitRoot = GitUtil.gitRootOrNull(root);
203 if (gitRoot == null || hasUnmappedSubroots(root, rootSet)) {
204 hasInvalidRoots = true;
205 break;
207 for (String otherPath : rootSet) {
208 if (otherPath.equals(path)) {
209 continue;
211 if (otherPath.startsWith(path)) {
212 VirtualFile otherFile = lookupFile(otherPath);
213 if (otherFile == null) {
214 hasInvalidRoots = true;
215 break loop;
217 VirtualFile otherRoot = GitUtil.gitRootOrNull(otherFile);
218 if (otherRoot == null || otherRoot == root || otherFile != otherRoot) {
219 hasInvalidRoots = true;
220 break loop;
226 if (!hasInvalidRoots) {
227 // all roots are correct
228 if (myNotificationPosted.compareAndSet(true, false)) {
229 final Notifications notifications = myProject.getMessageBus().syncPublisher(Notifications.TOPIC);
230 notifications.invalidateAll(GIT_INVALID_ROOTS_ID);
232 return;
234 if (myNotificationPosted.compareAndSet(false, true)) {
235 String title = GitBundle.message("root.tracker.message");
236 final Notifications notifications = myProject.getMessageBus().syncPublisher(Notifications.TOPIC);
237 notifications.notify(GIT_INVALID_ROOTS_ID, title, title, NotificationType.ERROR, new NotificationListener() {
238 @NotNull
239 public Continue perform() {
240 if (fixRoots()) {
241 myNotificationPosted.set(false);
242 return Continue.REMOVE;
244 else {
245 return Continue.LEAVE;
249 public Continue onRemove() {
250 return Continue.LEAVE;
259 * Check if there are some unmapped subdirectories under git
261 * @param directory the content root to check
262 * @param rootSet the mapped root set
263 * @return true if there are unmapped subroots
265 private static boolean hasUnmappedSubroots(VirtualFile directory, HashSet<String> rootSet) {
266 for (VirtualFile child : directory.getChildren()) {
267 if (child.getName().equals(".git") || !child.isDirectory()) {
268 continue;
270 if (child.findChild(".git") != null && !rootSet.contains(child.getPath())) {
271 return true;
273 if (hasUnmappedSubroots(child, rootSet)) {
274 return true;
277 return false;
281 * Fix mapped roots
283 * @return true if roots now in the correct state
285 boolean fixRoots() {
286 final List<VcsDirectoryMapping> vcsDirectoryMappings = new ArrayList<VcsDirectoryMapping>(myVcsManager.getDirectoryMappings());
287 final HashSet<String> mapped = new HashSet<String>();
288 final HashSet<String> removed = new HashSet<String>();
289 final HashSet<String> added = new HashSet<String>();
290 final VirtualFile baseDir = myProject.getBaseDir();
291 assert baseDir != null;
292 ApplicationManager.getApplication().runReadAction(new Runnable() {
293 public void run() {
294 for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
295 VcsDirectoryMapping m = i.next();
296 String vcsName = myVcs.getName();
297 if (!vcsName.equals(m.getVcs())) {
298 continue;
300 String path = m.getDirectory();
301 if (path.length() == 0) {
302 path = baseDir.getPath();
304 VirtualFile file = lookupFile(path);
305 if (file != null && !mapped.add(file.getPath())) {
306 // eliminate duplicates
307 i.remove();
308 continue;
310 if (file == null || GitUtil.gitRootOrNull(file) == null) {
311 removed.add(path);
314 for (String m : mapped) {
315 VirtualFile file = lookupFile(m);
316 if (file == null) {
317 continue;
319 addSubroots(file, added, mapped);
320 if (removed.contains(m)) {
321 continue;
323 VirtualFile root = GitUtil.gitRootOrNull(file);
324 assert root != null;
325 for (String o : mapped) {
326 // the mapped collection is not modified here, so order is being kept
327 if (o.equals(m) || removed.contains(o)) {
328 continue;
330 if (o.startsWith(m)) {
331 VirtualFile otherFile = lookupFile(m);
332 assert otherFile != null;
333 VirtualFile otherRoot = GitUtil.gitRootOrNull(otherFile);
334 assert otherRoot != null;
335 if (otherRoot == root) {
336 removed.add(o);
338 else if (otherFile != otherRoot) {
339 added.add(otherRoot.getPath());
340 removed.add(o);
347 if (added.isEmpty() && removed.isEmpty()) {
348 Messages.showInfoMessage(myProject, GitBundle.message("fix.roots.valid.message"), GitBundle.message("fix.roots.valid.title"));
349 return true;
351 GitFixRootsDialog d = new GitFixRootsDialog(myProject, mapped, added, removed);
352 d.show();
353 if (!d.isOK()) {
354 return false;
356 for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
357 VcsDirectoryMapping m = i.next();
358 String path = m.getDirectory();
359 if (removed.contains(path) || (path.length() == 0 && removed.contains(baseDir.getPath()))) {
360 i.remove();
363 for (String a : added) {
364 vcsDirectoryMappings.add(new VcsDirectoryMapping(a, myVcs.getName()));
366 myVcsManager.setDirectoryMappings(vcsDirectoryMappings);
367 myVcsManager.updateActiveVcss();
368 return true;
372 * Look up file in the file system
374 * @param path the path to lookup
375 * @return the file or null if the file not found
377 @Nullable
378 private VirtualFile lookupFile(String path) {
379 return myLocalFileSystem.findFileByPath(path);
383 * Add subroots for the content root
385 * @param directory the content root to check
386 * @param toAdd collection of roots to be added
387 * @param mapped all mapped git roots
389 private static void addSubroots(VirtualFile directory, HashSet<String> toAdd, HashSet<String> mapped) {
390 for (VirtualFile child : directory.getChildren()) {
391 if (!child.isDirectory()) {
392 continue;
394 if (child.getName().equals(".git") && !mapped.contains(directory.getPath())) {
395 toAdd.add(directory.getPath());
397 else {
398 addSubroots(child, toAdd, mapped);
404 * The listener for git roots
406 private class MyFileListener extends VirtualFileAdapter {
408 * Return true if file has git repositories
410 * @param file the file to check
411 * @return true if file has git repositories
413 private boolean hasGitRepositories(VirtualFile file) {
414 if (!file.isDirectory()) {
415 return false;
417 if (file.getName().equals(".git")) {
418 return true;
420 for (VirtualFile child : file.getChildren()) {
421 if (hasGitRepositories(child)) {
422 return true;
425 return false;
429 * Invalidate git root
431 private void invalidate() {
432 myRootsInvalidated.set(true);
436 * {@inheritDoc}
438 @Override
439 public void fileCreated(VirtualFileEvent event) {
440 if (!myHasGitRoots.get()) {
441 return;
443 if (hasGitRepositories(event.getFile())) {
444 invalidate();
450 * {@inheritDoc}
452 @Override
453 public void beforeFileDeletion(VirtualFileEvent event) {
454 if (!myHasGitRoots.get()) {
455 return;
457 if (hasGitRepositories(event.getFile())) {
458 invalidate();
463 * {@inheritDoc}
465 @Override
466 public void fileMoved(VirtualFileMoveEvent event) {
467 if (!myHasGitRoots.get()) {
468 return;
470 if (hasGitRepositories(event.getFile())) {
471 invalidate();
476 * {@inheritDoc}
478 @Override
479 public void fileCopied(VirtualFileCopyEvent event) {
480 if (!myHasGitRoots.get()) {
481 return;
483 if (hasGitRepositories(event.getFile())) {
484 invalidate();