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
.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
;
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
{
55 private final Project myProject
;
57 * The vcs manager that tracks content roots
59 private final ProjectLevelVcsManager myVcsManager
;
63 private final GitVcs myVcs
;
65 * If true, the root configuration has been possibly invalidated
67 private final AtomicBoolean myRootsInvalidated
= new AtomicBoolean(true);
69 * If true, there are some configured git roots, or listener has never been run yet
71 private final AtomicBoolean myHasGitRoots
= new AtomicBoolean(true);
73 * If true, the notification is currently active and has not been dismissed yet.
75 private final AtomicBoolean myNotificationPosted
= new AtomicBoolean(false);
77 * The invalid git roots
79 private static final String GIT_INVALID_ROOTS_ID
= "GIT_INVALID_ROOTS";
81 * The command listener
83 private CommandListener myCommandListener
;
87 private MyFileListener myFileListener
;
89 * Listener for refresh events
91 private VirtualFileManagerAdapter myVirtualFileManagerListener
;
93 * Local file system service
95 private LocalFileSystem myLocalFileSystem
;
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");
108 myVcsManager
= ProjectLevelVcsManager
.getInstance(project
);
109 myVcsManager
.addVcsListener(this);
110 myLocalFileSystem
= LocalFileSystem
.getInstance();
111 myCommandListener
= new CommandAdapter() {
113 public void commandFinished(CommandEvent event
) {
114 if (!myRootsInvalidated
.compareAndSet(true, false)) {
120 CommandProcessor
.getInstance().addCommandListener(myCommandListener
);
121 myFileListener
= new MyFileListener();
122 VirtualFileManagerEx fileManager
= (VirtualFileManagerEx
)VirtualFileManager
.getInstance();
123 fileManager
.addVirtualFileListener(myFileListener
);
124 myVirtualFileManagerListener
= new VirtualFileManagerAdapter() {
126 public void afterRefreshFinish(boolean asynchonous
) {
127 if (!myRootsInvalidated
.compareAndSet(true, false)) {
133 fileManager
.addVirtualFileManagerListener(myVirtualFileManagerListener
);
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
);
151 public void directoryMappingChanged() {
152 if (myProject
.isDisposed()) {
159 * Check roots for changes.
161 * @param rootsChanged
163 private void checkRoots(boolean rootsChanged
) {
164 if (!rootsChanged
&& !myHasGitRoots
.get()) {
167 ApplicationManager
.getApplication().runReadAction(new Runnable() {
169 boolean hasInvalidRoots
= false;
170 HashSet
<String
> rootSet
= new HashSet
<String
>();
171 for (VcsDirectoryMapping m
: myVcsManager
.getDirectoryMappings()) {
172 if (!m
.getVcs().equals(myVcs
.getName())) {
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
);
183 hasInvalidRoots
= true;
187 rootSet
.add(root
.getPath());
190 if (!hasInvalidRoots
&& rootSet
.isEmpty()) {
191 myHasGitRoots
.set(false);
195 myHasGitRoots
.set(true);
197 if (!hasInvalidRoots
) {
198 // check if roots have a problem
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;
207 for (String otherPath
: rootSet
) {
208 if (otherPath
.equals(path
)) {
211 if (otherPath
.startsWith(path
)) {
212 VirtualFile otherFile
= lookupFile(otherPath
);
213 if (otherFile
== null) {
214 hasInvalidRoots
= true;
217 VirtualFile otherRoot
= GitUtil
.gitRootOrNull(otherFile
);
218 if (otherRoot
== null || otherRoot
== root
|| otherFile
!= otherRoot
) {
219 hasInvalidRoots
= true;
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
);
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() {
239 public Continue
perform() {
241 myNotificationPosted
.set(false);
242 return Continue
.REMOVE
;
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()) {
270 if (child
.findChild(".git") != null && !rootSet
.contains(child
.getPath())) {
273 if (hasUnmappedSubroots(child
, rootSet
)) {
283 * @return true if roots now in the correct state
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() {
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())) {
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
310 if (file
== null || GitUtil
.gitRootOrNull(file
) == null) {
314 for (String m
: mapped
) {
315 VirtualFile file
= lookupFile(m
);
319 addSubroots(file
, added
, mapped
);
320 if (removed
.contains(m
)) {
323 VirtualFile root
= GitUtil
.gitRootOrNull(file
);
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
)) {
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
) {
338 else if (otherFile
!= otherRoot
) {
339 added
.add(otherRoot
.getPath());
347 if (added
.isEmpty() && removed
.isEmpty()) {
348 Messages
.showInfoMessage(myProject
, GitBundle
.message("fix.roots.valid.message"), GitBundle
.message("fix.roots.valid.title"));
351 GitFixRootsDialog d
= new GitFixRootsDialog(myProject
, mapped
, added
, removed
);
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()))) {
363 for (String a
: added
) {
364 vcsDirectoryMappings
.add(new VcsDirectoryMapping(a
, myVcs
.getName()));
366 myVcsManager
.setDirectoryMappings(vcsDirectoryMappings
);
367 myVcsManager
.updateActiveVcss();
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
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()) {
394 if (child
.getName().equals(".git") && !mapped
.contains(directory
.getPath())) {
395 toAdd
.add(directory
.getPath());
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()) {
417 if (file
.getName().equals(".git")) {
420 for (VirtualFile child
: file
.getChildren()) {
421 if (hasGitRepositories(child
)) {
429 * Invalidate git root
431 private void invalidate() {
432 myRootsInvalidated
.set(true);
439 public void fileCreated(VirtualFileEvent event
) {
440 if (!myHasGitRoots
.get()) {
443 if (hasGitRepositories(event
.getFile())) {
453 public void beforeFileDeletion(VirtualFileEvent event
) {
454 if (!myHasGitRoots
.get()) {
457 if (hasGitRepositories(event
.getFile())) {
466 public void fileMoved(VirtualFileMoveEvent event
) {
467 if (!myHasGitRoots
.get()) {
470 if (hasGitRepositories(event
.getFile())) {
479 public void fileCopied(VirtualFileCopyEvent event
) {
480 if (!myHasGitRoots
.get()) {
483 if (hasGitRepositories(event
.getFile())) {