replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / repo / GitUntrackedFilesHolder.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.diagnostic.Logger;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
22 import com.intellij.openapi.vcs.VcsException;
23 import com.intellij.openapi.vcs.changes.ChangeListManager;
24 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
25 import com.intellij.openapi.vfs.VirtualFile;
26 import com.intellij.openapi.vfs.VirtualFileManager;
27 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
28 import com.intellij.openapi.vfs.newvfs.events.*;
29 import com.intellij.util.messages.MessageBusConnection;
30 import git4idea.GitLocalBranch;
31 import git4idea.GitUtil;
32 import git4idea.commands.Git;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35
36 import java.util.*;
37
38 /**
39  * <p>
40  *   Stores files which are untracked by the Git repository.
41  *   Should be updated by calling {@link #add(VirtualFile)} and {@link #remove(Collection)}
42  *   whenever the list of unversioned files changes.
43  *   Able to get the list of unversioned files from Git.
44  * </p>
45  *
46  * <p>
47  *   This class is used by {@link git4idea.status.GitNewChangesCollector}.
48  *   By keeping track of unversioned files in the Git repository we may invoke
49  *   {@code 'git status --porcelain --untracked-files=no'} which gives a significant speed boost: the command gets more than twice
50  *   faster, because it doesn't need to seek for untracked files.
51  * </p>
52  *
53  * <p>
54  *   "Keeping track" means the following:
55  *   <ul>
56  *     <li>
57  *       Once a file is created, it is added to untracked (by this class).
58  *       Once a file is deleted, it is removed from untracked.
59  *     </li>
60  *     <li>
61  *       Once a file is added to the index, it is removed from untracked.
62  *       Once it is removed from the index, it is added to untracked.
63  *     </li>
64  *   </ul>
65  * </p>
66  * <p>
67  *   In some cases (file creation/deletion) the file is not silently added/removed from the list - instead the file is marked as
68  *   "possibly untracked" and Git is asked for the exact status of this file.
69  *   It is needed, since the file may be created and added to the index independently, and events may race.
70  * </p>
71  * <p>
72  *   Also, if .git/index changes, then a full refresh is initiated. The reason is not only untracked files tracking, but also handling
73  *   committing outside IDEA, etc.
74  * </p>
75  * <p>
76  *   Synchronization policy used in this class:<br/>
77  *   myDefinitelyUntrackedFiles is accessed under the myDefinitelyUntrackedFiles lock.<br/>
78  *   myPossiblyUntrackedFiles and myReady is accessed under the LOCK lock.<br/>
79  *   This is done so, because the latter two variables are accessed from the AWT in after() and we don't want to lock the AWT long,
80  *   while myDefinitelyUntrackedFiles is modified along with native request to Git.
81  * </p>
82  *
83  * @author Kirill Likhodedov
84  */
85 public class GitUntrackedFilesHolder implements Disposable, BulkFileListener {
86
87   private static final Logger LOG = Logger.getInstance(GitUntrackedFilesHolder.class);
88
89   private final Project myProject;
90   private final VirtualFile myRoot;
91   private final GitRepository myRepository;
92   private final ChangeListManager myChangeListManager;
93   private final VcsDirtyScopeManager myDirtyScopeManager;
94   private final ProjectLevelVcsManager myVcsManager;
95   private final GitRepositoryFiles myRepositoryFiles;
96   private final Git myGit;
97
98   private final Set<VirtualFile> myDefinitelyUntrackedFiles = new HashSet<>();
99   private final Set<VirtualFile> myPossiblyUntrackedFiles = new HashSet<>();
100   private boolean myReady;   // if false, total refresh is needed
101   private final Object LOCK = new Object();
102   private final GitRepositoryManager myRepositoryManager;
103
104   GitUntrackedFilesHolder(@NotNull GitRepository repository, @NotNull GitRepositoryFiles gitFiles) {
105     myProject = repository.getProject();
106     myRepository = repository;
107     myRoot = repository.getRoot();
108     myChangeListManager = ChangeListManager.getInstance(myProject);
109     myDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject);
110     myGit = Git.getInstance();
111     myVcsManager = ProjectLevelVcsManager.getInstance(myProject);
112
113     myRepositoryManager = GitUtil.getRepositoryManager(myProject);
114     myRepositoryFiles = gitFiles;
115   }
116
117   void setupVfsListener(@NotNull Project project) {
118     if (!project.isDisposed()) {
119       MessageBusConnection connection = project.getMessageBus().connect(this);
120       connection.subscribe(VirtualFileManager.VFS_CHANGES, this);
121     }
122   }
123
124   @Override
125   public void dispose() {
126     synchronized (myDefinitelyUntrackedFiles) {
127       myDefinitelyUntrackedFiles.clear();
128     }
129     synchronized (LOCK) {
130       myPossiblyUntrackedFiles.clear();
131     }
132   }
133
134   /**
135    * Adds the file to the list of untracked.
136    */
137   public void add(@NotNull VirtualFile file) {
138     synchronized (myDefinitelyUntrackedFiles) {
139       myDefinitelyUntrackedFiles.add(file);
140     }
141   }
142
143   /**
144    * Adds several files to the list of untracked.
145    */
146   public void add(@NotNull Collection<VirtualFile> files) {
147     synchronized (myDefinitelyUntrackedFiles) {
148       myDefinitelyUntrackedFiles.addAll(files);
149     }
150   }
151
152   /**
153    * Removes several files from untracked.
154    */
155   public void remove(@NotNull Collection<VirtualFile> files) {
156     synchronized (myDefinitelyUntrackedFiles) {
157       myDefinitelyUntrackedFiles.removeAll(files);
158     }
159   }
160
161   /**
162    * Returns the list of unversioned files.
163    * This method may be slow, if the full-refresh of untracked files is needed.
164    * @return untracked files.
165    * @throws VcsException if there is an unexpected error during Git execution.
166    */
167   @NotNull
168   public Collection<VirtualFile> retrieveUntrackedFiles() throws VcsException {
169     if (isReady()) {
170       verifyPossiblyUntrackedFiles();
171     } else {
172       rescanAll();
173     }
174     synchronized (myDefinitelyUntrackedFiles) {
175       return new ArrayList<>(myDefinitelyUntrackedFiles);
176     }
177   }
178
179   public void invalidate() {
180     synchronized (LOCK) {
181       myReady = false;
182     }
183   }
184
185   /**
186    * Resets the list of untracked files after retrieving the full list of them from Git.
187    */
188   private void rescanAll() throws VcsException {
189     Set<VirtualFile> untrackedFiles = myGit.untrackedFiles(myProject, myRoot, null);
190     synchronized (myDefinitelyUntrackedFiles) {
191       myDefinitelyUntrackedFiles.clear();
192       myDefinitelyUntrackedFiles.addAll(untrackedFiles);
193     }
194     synchronized (LOCK) {
195       myPossiblyUntrackedFiles.clear();
196       myReady = true;
197     }
198   }
199
200   /**
201    * @return {@code true} if untracked files list is initialized and being kept up-to-date, {@code false} if full refresh is needed.
202    */
203   private boolean isReady() {
204     synchronized (LOCK) {
205       return myReady;
206     }
207   }
208
209   /**
210    * Queries Git to check the status of {@code myPossiblyUntrackedFiles} and moves them to {@code myDefinitelyUntrackedFiles}.
211    */
212   private void verifyPossiblyUntrackedFiles() throws VcsException {
213     Set<VirtualFile> suspiciousFiles = new HashSet<>();
214     synchronized (LOCK) {
215       suspiciousFiles.addAll(myPossiblyUntrackedFiles);
216       myPossiblyUntrackedFiles.clear();
217     }
218
219     synchronized (myDefinitelyUntrackedFiles) {
220       Set<VirtualFile> untrackedFiles = myGit.untrackedFiles(myProject, myRoot, suspiciousFiles);
221       suspiciousFiles.removeAll(untrackedFiles);
222       // files that were suspicious (and thus passed to 'git ls-files'), but are not untracked, are definitely tracked.
223       @SuppressWarnings("UnnecessaryLocalVariable")
224       Set<VirtualFile> trackedFiles  = suspiciousFiles;
225
226       myDefinitelyUntrackedFiles.addAll(untrackedFiles);
227       myDefinitelyUntrackedFiles.removeAll(trackedFiles);
228     }
229   }
230
231   @Override
232   public void before(@NotNull List<? extends VFileEvent> events) {
233   }
234
235   @Override
236   public void after(@NotNull List<? extends VFileEvent> events) {
237     boolean allChanged = false;
238     Set<VirtualFile> filesToRefresh = new HashSet<>();
239
240     for (VFileEvent event : events) {
241       if (allChanged) {
242         break;
243       }
244       String path = event.getPath();
245       if (totalRefreshNeeded(path)) {
246         allChanged = true;
247       }
248       else {
249         VirtualFile affectedFile = getAffectedFile(event);
250         if (notIgnored(affectedFile)) {
251           filesToRefresh.add(affectedFile);
252         }
253       }
254     }
255
256     // if index has changed, no need to refresh specific files - we get the full status of all files
257     if (allChanged) {
258       LOG.debug(String.format("GitUntrackedFilesHolder: total refresh is needed, marking %s recursively dirty", myRoot));
259       myDirtyScopeManager.dirDirtyRecursively(myRoot);
260       synchronized (LOCK) {
261         myReady = false;
262       }
263     } else {
264       synchronized (LOCK) {
265         myPossiblyUntrackedFiles.addAll(filesToRefresh);
266       }
267     }
268   }
269
270   private boolean totalRefreshNeeded(@NotNull String path) {
271     return indexChanged(path) || externallyCommitted(path) || headMoved(path) ||
272            headChanged(path) || currentBranchChanged(path) || gitignoreChanged(path);
273   }
274
275   private boolean headChanged(@NotNull String path) {
276     return myRepositoryFiles.isHeadFile(path);
277   }
278
279   private boolean currentBranchChanged(@NotNull String path) {
280     GitLocalBranch currentBranch = myRepository.getCurrentBranch();
281     return currentBranch != null && myRepositoryFiles.isBranchFile(path, currentBranch.getFullName());
282   }
283
284   private boolean headMoved(@NotNull String path) {
285     return myRepositoryFiles.isOrigHeadFile(path);
286   }
287
288   private boolean indexChanged(@NotNull String path) {
289     return myRepositoryFiles.isIndexFile(path);
290   }
291
292   private boolean externallyCommitted(@NotNull String path) {
293     return myRepositoryFiles.isCommitMessageFile(path);
294   }
295
296   private boolean gitignoreChanged(@NotNull String path) {
297     // TODO watch file stored in core.excludesfile
298     return path.endsWith(".gitignore") || myRepositoryFiles.isExclude(path);
299   }
300
301   @Nullable
302   private static VirtualFile getAffectedFile(@NotNull VFileEvent event) {
303     if (event instanceof VFileCreateEvent || event instanceof VFileDeleteEvent || event instanceof VFileMoveEvent || isRename(event)) {
304       return event.getFile();
305     } else if (event instanceof VFileCopyEvent) {
306       VFileCopyEvent copyEvent = (VFileCopyEvent) event;
307       return copyEvent.getNewParent().findChild(copyEvent.getNewChildName());
308     }
309     return null;
310   }
311
312   private static boolean isRename(@NotNull VFileEvent event) {
313     return event instanceof VFilePropertyChangeEvent && ((VFilePropertyChangeEvent)event).getPropertyName().equals(VirtualFile.PROP_NAME);
314   }
315
316   private boolean notIgnored(@Nullable VirtualFile file) {
317     return file != null && belongsToThisRepository(file) && !myChangeListManager.isIgnoredFile(file);
318   }
319
320   private boolean belongsToThisRepository(VirtualFile file) {
321     // this check should be quick
322     // we shouldn't create a full instance repository here because it may lead to SOE while many unversioned files will be processed
323     GitRepository repository = myRepositoryManager.getRepositoryForRootQuick(myVcsManager.getVcsRootFor(file));
324     return repository != null && repository.getRoot().equals(myRoot);
325   }
326 }