IDEA-80512 Watch for refs/remotes and refs/heads dirs recursively; update branches...
[idea/community.git] / plugins / git4idea / src / git4idea / repo / GitRepositoryUpdater.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package git4idea.repo;
17
18 import com.intellij.openapi.Disposable;
19 import com.intellij.openapi.vfs.LocalFileSystem;
20 import com.intellij.openapi.vfs.VfsUtil;
21 import com.intellij.openapi.vfs.VirtualFile;
22 import com.intellij.openapi.vfs.VirtualFileManager;
23 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
24 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
25 import com.intellij.util.Consumer;
26 import com.intellij.util.Processor;
27 import com.intellij.util.concurrency.QueueProcessor;
28 import com.intellij.util.messages.MessageBusConnection;
29 import com.intellij.vcsUtil.VcsUtil;
30 import git4idea.util.GitFileUtils;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33
34 import java.util.List;
35
36 /**
37  * Listens to .git service files changes and updates {@link GitRepository} when needed.
38  * @author Kirill Likhodedov
39  */
40 final class GitRepositoryUpdater implements Disposable, BulkFileListener {
41
42   private final GitRepository myRepository;
43   private final GitRepositoryFiles myRepositoryFiles;
44   private final MessageBusConnection myMessageBusConnection;
45   private final QueueProcessor<GitRepository.TrackedTopic> myUpdateQueue;
46   private final VirtualFile myRemotesDir;
47   private final VirtualFile myHeadsDir;
48
49   GitRepositoryUpdater(GitRepository repository) {
50     myRepository = repository;
51     VirtualFile root = repository.getRoot();
52
53     VirtualFile gitDir = root.findChild(".git");
54     assert gitDir != null;
55     LocalFileSystem.getInstance().addRootToWatch(gitDir.getPath(), true);
56     
57     myRepositoryFiles = GitRepositoryFiles.getInstance(root);
58     visitGitDirVfs(gitDir);
59     myHeadsDir = VcsUtil.getVirtualFile(myRepositoryFiles.getRefsHeadsPath());
60     myRemotesDir = VcsUtil.getVirtualFile(myRepositoryFiles.getRefsRemotesPath());
61
62     myUpdateQueue = new QueueProcessor<GitRepository.TrackedTopic>(new Updater(myRepository), myRepository.getProject().getDisposed());
63     myMessageBusConnection = repository.getProject().getMessageBus().connect();
64     myMessageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, this);
65   }
66
67   private static void visitGitDirVfs(@NotNull VirtualFile gitDir) {
68     gitDir.getChildren();
69     for (String subdir : GitRepositoryFiles.getSubDirRelativePaths()) {
70       VirtualFile dir = gitDir.findFileByRelativePath(subdir);
71       // process recursively, because we need to visit all branches under refs/heads and refs/remotes
72       visitAllChildrenRecursively(dir);
73     }
74   }
75
76   private static void visitAllChildrenRecursively(@Nullable VirtualFile dir) {
77     if (dir == null) {
78       return;
79     }
80     VfsUtil.processFilesRecursively(dir, new Processor<VirtualFile>() {
81       @Override
82       public boolean process(VirtualFile virtualFile) {
83         return true;
84       }
85     });
86   }
87
88   @Override
89   public void dispose() {
90     myMessageBusConnection.disconnect();
91   }
92
93   @Override
94   public void before(List<? extends VFileEvent> events) {
95     // everything is handled in #after()
96   }
97
98   @Override
99   public void after(List<? extends VFileEvent> events) {
100     // which files in .git were changed
101     boolean configChanged = false;
102     boolean headChanged = false;
103     boolean branchFileChanged = false;
104     boolean packedRefsChanged = false;
105     boolean rebaseFileChanged = false;
106     boolean mergeFileChanged = false;
107     for (VFileEvent event : events) {
108       final VirtualFile file = event.getFile();
109       if (file == null) {
110         continue;
111       }
112       String filePath = GitFileUtils.stripFileProtocolPrefix(file.getPath());
113       if (myRepositoryFiles.isConfigFile(filePath)) {
114         configChanged = true;
115       } else if (myRepositoryFiles.isHeadFile(filePath)) {
116         headChanged = true;
117       } else if (myRepositoryFiles.isBranchFile(filePath)) {
118         // it is also possible, that a local branch with complex name ("myfolder/mybranch") was created => the folder also to be watched.
119         branchFileChanged = true;   
120         visitAllChildrenRecursively(myHeadsDir);
121       } else if (myRepositoryFiles.isRemoteBranchFile(filePath)) {
122         // it is possible, that a branch from a new remote was fetch => we need to add new remote folder to the VFS
123         branchFileChanged = true;
124         visitAllChildrenRecursively(myRemotesDir);
125       } else if (myRepositoryFiles.isPackedRefs(filePath)) {
126         packedRefsChanged = true;
127       } else if (myRepositoryFiles.isRebaseFile(filePath)) {
128         rebaseFileChanged = true;
129       } else if (myRepositoryFiles.isMergeFile(filePath)) {
130         mergeFileChanged = true;
131       }
132     }
133
134     // what should be updated in GitRepository
135     boolean updateCurrentBranch = false;
136     boolean updateCurrentRevision = false;
137     boolean updateState = false;
138     boolean updateBranches = false;
139     if (headChanged) {
140       updateCurrentBranch = true;
141       updateCurrentRevision = true;
142       updateState = true;
143     }
144     if (branchFileChanged) {
145       updateCurrentRevision = true;
146       updateBranches = true;
147     }
148     if (rebaseFileChanged || mergeFileChanged) {
149       updateState = true;
150     }
151     if (packedRefsChanged) {
152       updateCurrentBranch = true;
153       updateBranches = true;
154     }
155
156     // update GitRepository on pooled thread, because it requires reading from disk and parsing data.
157     if (updateCurrentBranch) {
158       myUpdateQueue.add(GitRepository.TrackedTopic.CURRENT_BRANCH);
159     }
160     if (updateCurrentRevision) {
161       myUpdateQueue.add(GitRepository.TrackedTopic.CURRENT_REVISION);
162     }
163     if (updateState) {
164       myUpdateQueue.add(GitRepository.TrackedTopic.STATE);
165     }
166     if (updateBranches) {
167       myUpdateQueue.add(GitRepository.TrackedTopic.BRANCHES);
168     }
169     if (configChanged) {
170       myUpdateQueue.add(GitRepository.TrackedTopic.CONFIG);
171     }
172   }
173
174   private static class Updater implements Consumer<GitRepository.TrackedTopic> {
175     private final GitRepository myRepository;
176
177     public Updater(GitRepository repository) {
178       myRepository = repository;
179     }
180
181     @Override
182     public void consume(GitRepository.TrackedTopic trackedTopic) {
183       myRepository.update(trackedTopic);
184     }
185   }
186 }