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.
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
;
54 import java
.util
.concurrent
.atomic
.AtomicBoolean
;
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
{
64 private final Project myProject
;
66 * Tracker of roots for project root manager
68 private final ProjectRootManager myProjectRoots
;
70 * The vcs manager that tracks content roots
72 private final ProjectLevelVcsManager myVcsManager
;
76 private final GitVcs myVcs
;
78 * If true, the tracking is enabled.
80 private final AtomicBoolean myIsEnabled
= new AtomicBoolean(false);
82 * If true, the root configuration has been possibly invalidated
84 private final AtomicBoolean myRootsInvalidated
= new AtomicBoolean(true);
86 * If true, there are some configured git roots, or listener has never been run yet
88 private final AtomicBoolean myHasGitRoots
= new AtomicBoolean(true);
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
;
99 * The invalid git roots
101 private static final String GIT_INVALID_ROOTS_ID
= "Git";
103 * The command listener
105 private CommandListener myCommandListener
;
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
;
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");
137 myProjectRoots
= ProjectRootManager
.getInstance(myProject
);
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
) {
148 public void rootsChanged(ModuleRootEvent event
) {
152 myCommandListener
= new CommandAdapter() {
154 public void commandFinished(CommandEvent event
) {
155 if (!myRootsInvalidated
.compareAndSet(true, false)) {
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() {
167 public void afterRefreshFinish(boolean asynchonous
) {
168 if (!myRootsInvalidated
.compareAndSet(true, false)) {
171 scheduleRootsCheck(false);
174 fileManager
.addVirtualFileManagerListener(myVirtualFileManagerListener
);
175 StartupManager
.getInstance(myProject
).runWhenProjectIsInitialized(new Runnable() {
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
);
198 public void directoryMappingChanged() {
199 ApplicationManager
.getApplication().invokeLater(new Runnable() {
201 scheduleRootsCheck(true);
206 private void scheduleRootsCheck(final boolean rootsChanged
) {
207 if (ApplicationManager
.getApplication().isUnitTestMode() || ApplicationManager
.getApplication().isHeadlessEnvironment()) {
208 doCheckRoots(rootsChanged
);
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())) {
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())) {
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
)) {
254 if (!hasInvalidRoots
&& rootSet
.isEmpty()) {
255 myHasGitRoots
.set(false);
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
) {
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;
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
) {
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()) {
322 if (child
.getName().equals(".git") && (rootSet
== null || !rootSet
.contains(child
.getParent()))) {
325 if (hasUnmappedSubroots(child
, rootSet
)) {
335 * @return true if roots now in the correct state
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() {
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())) {
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
362 if (file
== null || GitUtil
.gitRootOrNull(file
) == null) {
366 for (String m
: mapped
) {
367 VirtualFile file
= lookupFile(m
);
371 addSubroots(file
, added
, mapped
);
372 if (removed
.contains(m
)) {
375 VirtualFile root
= GitUtil
.gitRootOrNull(file
);
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
)) {
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
) {
390 else if (otherFile
!= otherRoot
) {
391 added
.add(otherRoot
.getPath());
399 if (added
.isEmpty() && removed
.isEmpty()) {
400 Messages
.showInfoMessage(myProject
, GitBundle
.message("fix.roots.valid.message"), GitBundle
.message("fix.roots.valid.title"));
403 GitFixRootsDialog d
= new GitFixRootsDialog(myProject
, mapped
, added
, removed
);
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()))) {
415 for (String a
: added
) {
416 vcsDirectoryMappings
.add(new VcsDirectoryMapping(a
, myVcs
.getName()));
418 myVcsManager
.setDirectoryMappings(vcsDirectoryMappings
);
419 myVcsManager
.updateActiveVcss();
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
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()) {
446 if (child
.getName().equals(".git") && !mapped
.contains(directory
.getPath())) {
447 toAdd
.add(directory
.getPath());
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")) {
476 VirtualFile baseDir
= myProject
.getBaseDir();
477 if (baseDir
== null) {
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)) {
500 public void fileCreated(VirtualFileEvent event
) {
501 if (!myHasGitRoots
.get()) {
504 if (hasGitRepositories(event
.getFile())) {
513 public void beforeFileDeletion(VirtualFileEvent event
) {
514 if (!myHasGitRoots
.get()) {
517 if (hasGitRepositories(event
.getFile())) {
526 public void fileMoved(VirtualFileMoveEvent event
) {
527 if (!myHasGitRoots
.get()) {
530 if (hasGitRepositories(event
.getFile())) {
539 public void fileCopied(VirtualFileCopyEvent event
) {
540 if (!myHasGitRoots
.get()) {
543 if (hasGitRepositories(event
.getFile())) {