GitRootTracker: read action removed
[fedora-idea.git] / plugins / git4idea / src / git4idea / vfs / GitRootTracker.java
blobb65796f03a3153050db508907ffc436e79951cbe
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.ProjectTopics;
20 import com.intellij.notification.Notification;
21 import com.intellij.notification.NotificationListener;
22 import com.intellij.notification.NotificationType;
23 import com.intellij.notification.Notifications;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.command.CommandAdapter;
26 import com.intellij.openapi.command.CommandEvent;
27 import com.intellij.openapi.command.CommandListener;
28 import com.intellij.openapi.command.CommandProcessor;
29 import com.intellij.openapi.progress.ProgressIndicator;
30 import com.intellij.openapi.progress.ProgressManager;
31 import com.intellij.openapi.progress.Task;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.roots.ModuleRootEvent;
34 import com.intellij.openapi.roots.ModuleRootListener;
35 import com.intellij.openapi.roots.ProjectRootManager;
36 import com.intellij.openapi.startup.StartupManager;
37 import com.intellij.openapi.ui.Messages;
38 import com.intellij.openapi.util.Computable;
39 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
40 import com.intellij.openapi.vcs.VcsDirectoryMapping;
41 import com.intellij.openapi.vcs.VcsListener;
42 import com.intellij.openapi.vfs.*;
43 import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
44 import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
45 import com.intellij.util.messages.MessageBusConnection;
46 import git4idea.GitUtil;
47 import git4idea.GitVcs;
48 import git4idea.i18n.GitBundle;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
52 import javax.swing.event.HyperlinkEvent;
53 import java.util.*;
54 import java.util.concurrent.atomic.AtomicBoolean;
56 /**
57 * The component tracks Git roots for the project. If roots are mapped incorrectly it
58 * shows balloon that notifies user about the problem and offers to correct root mapping.
60 public class GitRootTracker implements VcsListener {
61 /**
62 * The context project
64 private final Project myProject;
65 /**
66 * Tracker of roots for project root manager
68 private final ProjectRootManager myProjectRoots;
69 /**
70 * The vcs manager that tracks content roots
72 private final ProjectLevelVcsManager myVcsManager;
73 /**
74 * The vcs instance
76 private final GitVcs myVcs;
77 /**
78 * If true, the tracking is enabled.
80 private final AtomicBoolean myIsEnabled = new AtomicBoolean(false);
81 /**
82 * If true, the root configuration has been possibly invalidated
84 private final AtomicBoolean myRootsInvalidated = new AtomicBoolean(true);
85 /**
86 * If true, there are some configured git roots, or listener has never been run yet
88 private final AtomicBoolean myHasGitRoots = new AtomicBoolean(true);
89 /**
90 * If true, the notification is currently active and has not been dismissed yet.
92 private final AtomicBoolean myNotificationPosted = new AtomicBoolean(false);
94 private final Object myCheckRootsLock = new Object();
96 private Notification myNotification;
98 /**
99 * The invalid git roots
101 private static final String GIT_INVALID_ROOTS_ID = "Git";
103 * The command listener
105 private CommandListener myCommandListener;
107 * The file listener
109 private MyFileListener myFileListener;
111 * Listener for refresh events
113 private VirtualFileManagerAdapter myVirtualFileManagerListener;
115 * Local file system service
117 private LocalFileSystem myLocalFileSystem;
119 * The multicaster for root events
121 private GitRootsListener myMulticaster;
123 private final MessageBusConnection myMessageBusConnection;
126 * The constructor
128 * @param project the project instance
129 * @param multicaster the listeners to notify
131 public GitRootTracker(GitVcs vcs, @NotNull Project project, @NotNull GitRootsListener multicaster) {
132 myMulticaster = multicaster;
133 if (project.isDefault()) {
134 throw new IllegalArgumentException("The project must not be default");
136 myProject = project;
137 myProjectRoots = ProjectRootManager.getInstance(myProject);
138 myVcs = vcs;
139 myVcsManager = ProjectLevelVcsManager.getInstance(project);
140 myVcsManager.addVcsListener(this);
141 myLocalFileSystem = LocalFileSystem.getInstance();
142 myMessageBusConnection = myProject.getMessageBus().connect();
143 myMessageBusConnection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
144 public void beforeRootsChange(ModuleRootEvent event) {
145 // do nothing
148 public void rootsChanged(ModuleRootEvent event) {
149 invalidate();
152 myCommandListener = new CommandAdapter() {
153 @Override
154 public void commandFinished(CommandEvent event) {
155 if (!myRootsInvalidated.compareAndSet(true, false)) {
156 return;
158 scheduleRootsCheck(false);
161 CommandProcessor.getInstance().addCommandListener(myCommandListener);
162 myFileListener = new MyFileListener();
163 VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
164 fileManager.addVirtualFileListener(myFileListener);
165 myVirtualFileManagerListener = new VirtualFileManagerAdapter() {
166 @Override
167 public void afterRefreshFinish(boolean asynchonous) {
168 if (!myRootsInvalidated.compareAndSet(true, false)) {
169 return;
171 scheduleRootsCheck(false);
174 fileManager.addVirtualFileManagerListener(myVirtualFileManagerListener);
175 StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
176 public void run() {
177 myIsEnabled.set(true);
178 scheduleRootsCheck(true);
184 * Dispose the component removing all related listeners
186 public void dispose() {
187 myVcsManager.removeVcsListener(this);
188 myMessageBusConnection.disconnect();
189 CommandProcessor.getInstance().removeCommandListener(myCommandListener);
190 VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
191 fileManager.removeVirtualFileListener(myFileListener);
192 fileManager.removeVirtualFileManagerListener(myVirtualFileManagerListener);
196 * {@inheritDoc}
198 public void directoryMappingChanged() {
199 ApplicationManager.getApplication().invokeLater(new Runnable() {
200 public void run() {
201 scheduleRootsCheck(true);
206 private void scheduleRootsCheck(final boolean rootsChanged) {
207 if (ApplicationManager.getApplication().isUnitTestMode() || ApplicationManager.getApplication().isHeadlessEnvironment()) {
208 doCheckRoots(rootsChanged);
209 return;
211 ProgressManager.getInstance().run(new Task.Backgroundable(myProject, "Checking Git roots...") {
212 public void run(@NotNull ProgressIndicator indicator) {
213 synchronized (myCheckRootsLock) {
214 if (myProject.isDisposed()) return;
215 doCheckRoots(rootsChanged);
222 * Check roots for changes.
224 * @param rootsChanged
226 private void doCheckRoots(boolean rootsChanged) {
227 if (!myIsEnabled.get() || (!rootsChanged && !myHasGitRoots.get())) {
228 return;
231 final HashSet<VirtualFile> rootSet = new HashSet<VirtualFile>();
232 boolean hasInvalidRoots = ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
233 public Boolean compute() {
234 for (VcsDirectoryMapping m : myVcsManager.getDirectoryMappings()) {
235 if (!m.getVcs().equals(myVcs.getName())) {
236 continue;
238 String path = m.getDirectory();
239 if (path.length() == 0) {
240 VirtualFile baseDir = myProject.getBaseDir();
241 assert baseDir != null;
242 path = baseDir.getPath();
244 VirtualFile root = lookupFile(path);
245 if (root == null || rootSet.contains(root)) {
246 return true;
248 rootSet.add(root);
250 return false;
254 if (!hasInvalidRoots && rootSet.isEmpty()) {
255 myHasGitRoots.set(false);
256 return;
258 else {
259 myHasGitRoots.set(true);
262 if (!hasInvalidRoots) {
263 // check if roots have a problem
264 for (final VirtualFile root : rootSet) {
265 hasInvalidRoots = hasUnmappedSubroots(root, rootSet);
266 if (hasInvalidRoots) {
267 break;
272 if (!hasInvalidRoots) {
273 // all roots are correct
274 if (myNotificationPosted.compareAndSet(true, false)) {
275 if (myNotification != null) {
276 if (!myNotification.isExpired()) {
277 myNotification.expire();
280 myNotification = null;
283 return;
286 if (myNotificationPosted.compareAndSet(false, true)) {
287 myNotification = new Notification(GIT_INVALID_ROOTS_ID, GitBundle.getString("root.tracker.message.title"),
288 GitBundle.getString("root.tracker.message"), NotificationType.ERROR,
289 new NotificationListener() {
290 public void hyperlinkUpdate(@NotNull Notification notification,
291 @NotNull HyperlinkEvent event) {
292 if (fixRoots()) {
293 notification.expire();
298 Notifications.Bus.notify(myNotification, myProject);
300 myMulticaster.gitRootsChanged();
305 * Check if there are some unmapped subdirectories under git
307 * @param directory the content root to check
308 * @param rootSet the mapped root set
309 * @return true if there are unmapped subroots
311 private static boolean hasUnmappedSubroots(final VirtualFile directory, final @Nullable HashSet<VirtualFile> rootSet) {
312 VirtualFile[] children = ApplicationManager.getApplication().runReadAction(new Computable<VirtualFile[]>() {
313 public VirtualFile[] compute() {
314 return directory.getChildren();
318 for (final VirtualFile child : children) {
319 if (!child.isDirectory()) {
320 continue;
322 if (child.getName().equals(".git") && (rootSet == null || !rootSet.contains(child.getParent()))) {
323 return true;
325 if (hasUnmappedSubroots(child, rootSet)) {
326 return true;
329 return false;
333 * Fix mapped roots
335 * @return true if roots now in the correct state
337 boolean fixRoots() {
338 final List<VcsDirectoryMapping> vcsDirectoryMappings = new ArrayList<VcsDirectoryMapping>(myVcsManager.getDirectoryMappings());
339 final HashSet<String> mapped = new HashSet<String>();
340 final HashSet<String> removed = new HashSet<String>();
341 final HashSet<String> added = new HashSet<String>();
342 final VirtualFile baseDir = myProject.getBaseDir();
343 assert baseDir != null;
344 ApplicationManager.getApplication().runReadAction(new Runnable() {
345 public void run() {
346 for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
347 VcsDirectoryMapping m = i.next();
348 String vcsName = myVcs.getName();
349 if (!vcsName.equals(m.getVcs())) {
350 continue;
352 String path = m.getDirectory();
353 if (path.length() == 0) {
354 path = baseDir.getPath();
356 VirtualFile file = lookupFile(path);
357 if (file != null && !mapped.add(file.getPath())) {
358 // eliminate duplicates
359 i.remove();
360 continue;
362 if (file == null || GitUtil.gitRootOrNull(file) == null) {
363 removed.add(path);
366 for (String m : mapped) {
367 VirtualFile file = lookupFile(m);
368 if (file == null) {
369 continue;
371 addSubroots(file, added, mapped);
372 if (removed.contains(m)) {
373 continue;
375 VirtualFile root = GitUtil.gitRootOrNull(file);
376 assert root != null;
377 for (String o : mapped) {
378 // the mapped collection is not modified here, so order is being kept
379 if (o.equals(m) || removed.contains(o)) {
380 continue;
382 if (o.startsWith(m)) {
383 VirtualFile otherFile = lookupFile(m);
384 assert otherFile != null;
385 VirtualFile otherRoot = GitUtil.gitRootOrNull(otherFile);
386 assert otherRoot != null;
387 if (otherRoot == root) {
388 removed.add(o);
390 else if (otherFile != otherRoot) {
391 added.add(otherRoot.getPath());
392 removed.add(o);
399 if (added.isEmpty() && removed.isEmpty()) {
400 Messages.showInfoMessage(myProject, GitBundle.message("fix.roots.valid.message"), GitBundle.message("fix.roots.valid.title"));
401 return true;
403 GitFixRootsDialog d = new GitFixRootsDialog(myProject, mapped, added, removed);
404 d.show();
405 if (!d.isOK()) {
406 return false;
408 for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
409 VcsDirectoryMapping m = i.next();
410 String path = m.getDirectory();
411 if (removed.contains(path) || (path.length() == 0 && removed.contains(baseDir.getPath()))) {
412 i.remove();
415 for (String a : added) {
416 vcsDirectoryMappings.add(new VcsDirectoryMapping(a, myVcs.getName()));
418 myVcsManager.setDirectoryMappings(vcsDirectoryMappings);
419 myVcsManager.updateActiveVcss();
420 return true;
424 * Look up file in the file system
426 * @param path the path to lookup
427 * @return the file or null if the file not found
429 @Nullable
430 private VirtualFile lookupFile(String path) {
431 return myLocalFileSystem.findFileByPath(path);
435 * Add subroots for the content root
437 * @param directory the content root to check
438 * @param toAdd collection of roots to be added
439 * @param mapped all mapped git roots
441 private static void addSubroots(VirtualFile directory, HashSet<String> toAdd, HashSet<String> mapped) {
442 for (VirtualFile child : directory.getChildren()) {
443 if (!child.isDirectory()) {
444 continue;
446 if (child.getName().equals(".git") && !mapped.contains(directory.getPath())) {
447 toAdd.add(directory.getPath());
449 else {
450 addSubroots(child, toAdd, mapped);
456 * Invalidate git root
458 private void invalidate() {
459 myRootsInvalidated.set(true);
463 * The listener for git roots
465 private class MyFileListener extends VirtualFileAdapter {
467 * Return true if file has git repositories
469 * @param file the file to check
470 * @return true if file has git repositories
472 private boolean hasGitRepositories(VirtualFile file) {
473 if (!file.isDirectory() || !file.getName().equals(".git")) {
474 return false;
476 VirtualFile baseDir = myProject.getBaseDir();
477 if (baseDir == null) {
478 return false;
480 if (!VfsUtil.isAncestor(baseDir, file, false)) {
481 boolean isUnder = false;
482 for (VirtualFile c : myProjectRoots.getContentRoots()) {
483 if (!VfsUtil.isAncestor(baseDir, c, false) && VfsUtil.isAncestor(c, file, false)) {
484 isUnder = true;
485 break;
488 if (!isUnder) {
489 return false;
492 return true;
497 * {@inheritDoc}
499 @Override
500 public void fileCreated(VirtualFileEvent event) {
501 if (!myHasGitRoots.get()) {
502 return;
504 if (hasGitRepositories(event.getFile())) {
505 invalidate();
510 * {@inheritDoc}
512 @Override
513 public void beforeFileDeletion(VirtualFileEvent event) {
514 if (!myHasGitRoots.get()) {
515 return;
517 if (hasGitRepositories(event.getFile())) {
518 invalidate();
523 * {@inheritDoc}
525 @Override
526 public void fileMoved(VirtualFileMoveEvent event) {
527 if (!myHasGitRoots.get()) {
528 return;
530 if (hasGitRepositories(event.getFile())) {
531 invalidate();
536 * {@inheritDoc}
538 @Override
539 public void fileCopied(VirtualFileCopyEvent event) {
540 if (!myHasGitRoots.get()) {
541 return;
543 if (hasGitRepositories(event.getFile())) {
544 invalidate();