git4idea: Fixed performance problem with checking for changes in big projects
[idea/community.git] / plugins / git4idea / src / git4idea / changes / ChangeCollector.java
1 /*
2  * Copyright 2000-2007 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.changes;
17
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.vcs.FilePath;
20 import com.intellij.openapi.vcs.FileStatus;
21 import com.intellij.openapi.vcs.VcsException;
22 import com.intellij.openapi.vcs.changes.Change;
23 import com.intellij.openapi.vcs.changes.ChangeListManager;
24 import com.intellij.openapi.vcs.changes.ContentRevision;
25 import com.intellij.openapi.vcs.changes.VcsDirtyScope;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.vcsUtil.VcsUtil;
28 import git4idea.GitContentRevision;
29 import git4idea.GitRevisionNumber;
30 import git4idea.GitUtil;
31 import git4idea.commands.GitHandler;
32 import git4idea.commands.GitSimpleHandler;
33 import git4idea.commands.StringScanner;
34
35 import java.util.*;
36
37 /**
38  * A collector for changes in the Git. It is introduced because changes are not
39  * cannot be got as a sum of stateless operations.
40  */
41 class ChangeCollector {
42   /**
43    * The dirty scope used in the collector
44    */
45   private VcsDirtyScope myDirtyScope;
46   /**
47    * a vcs root for changes
48    */
49   private final VirtualFile myVcsRoot;
50   /**
51    * a project for change collection
52    */
53   private final Project myProject;
54   /**
55    * Unversioned files
56    */
57   private final List<VirtualFile> myUnversioned = new ArrayList<VirtualFile>();
58   /**
59    * Names that are listed as unmerged
60    */
61   private final Set<String> myUnmergedNames = new HashSet<String>();
62   /**
63    * Names that are listed as unmerged
64    */
65   private final List<Change> myChanges = new ArrayList<Change>();
66   /**
67    * This flag indicates that collecting changes has been failed.
68    */
69   private boolean myIsFailed = true;
70   /**
71    * This flag indicates that collecting changes has been started
72    */
73   private boolean myIsCollected = false;
74
75   /**
76    * A constructor
77    *
78    * @param project    a project
79    * @param dirtyScope the dirty scope to check
80    * @param vcsRoot    a vcs root
81    */
82   public ChangeCollector(final Project project, VcsDirtyScope dirtyScope, final VirtualFile vcsRoot) {
83     myDirtyScope = dirtyScope;
84     myVcsRoot = vcsRoot;
85     myProject = project;
86   }
87
88   /**
89    * Get unversioned files
90    *
91    * @return an unversioned files
92    * @throws VcsException if there is a problem with executing Git
93    */
94   public Collection<VirtualFile> unversioned() throws VcsException {
95     ensureCollected();
96     return myUnversioned;
97   }
98
99   /**
100    * Get changes
101    *
102    * @return an unversioned files
103    * @throws VcsException if there is a problem with executing Git
104    */
105   public Collection<Change> changes() throws VcsException {
106     ensureCollected();
107     return myChanges;
108   }
109
110
111   /**
112    * Ensure that changes has been collected.
113    *
114    * @throws VcsException an exception
115    */
116   private void ensureCollected() throws VcsException {
117     if (myIsCollected) {
118       if (myIsFailed) {
119         throw new IllegalStateException("The method should not be called after after exception has been thrown.");
120       }
121       else {
122         return;
123       }
124     }
125     myIsCollected = true;
126     collectUnmergedAndUnversioned();
127     collectDiffChanges();
128     myIsFailed = false;
129   }
130
131   /**
132    * Collect dirty file paths
133    *
134    * @param includeChanges if true, previous changes are included in collection
135    * @return the set of dirty paths to check, the paths are automatically collapsed if the summary length more than limit
136    */
137   private Collection<FilePath> dirtyPaths(boolean includeChanges) {
138     // TODO collapse paths with common prefix
139     ArrayList<FilePath> paths = new ArrayList<FilePath>();
140     FilePath rootPath = VcsUtil.getFilePath(myVcsRoot.getPath(), true);
141     for (FilePath p : myDirtyScope.getRecursivelyDirtyDirectories()) {
142       addToPaths(rootPath, paths, p);
143     }
144     ArrayList<FilePath> candidatePaths = new ArrayList<FilePath>();
145     candidatePaths.addAll(myDirtyScope.getDirtyFilesNoExpand());
146     if (includeChanges) {
147       try {
148         ChangeListManager cm = ChangeListManager.getInstance(myProject);
149         for (Change c : cm.getChangesIn(myVcsRoot)) {
150           switch (c.getType()) {
151             case NEW:
152             case DELETED:
153             case MOVED:
154               if (c.getAfterRevision() != null) {
155                 addToPaths(rootPath, paths, c.getAfterRevision().getFile());
156               }
157               if (c.getBeforeRevision() != null) {
158                 addToPaths(rootPath, paths, c.getBeforeRevision().getFile());
159               }
160             default:
161               // do nothing
162           }
163         }
164       }
165       catch (Exception t) {
166         // ignore exceptions
167       }
168     }
169     for (FilePath p : candidatePaths) {
170       addToPaths(rootPath, paths, p);
171     }
172     return paths;
173   }
174
175   /**
176    * Add path to the collection of the paths to check for this vcs root
177    *
178    * @param root  the root path
179    * @param paths the existing paths
180    * @param toAdd the path to add
181    */
182   void addToPaths(FilePath root, Collection<FilePath> paths, FilePath toAdd) {
183     if (GitUtil.getGitRootOrNull(toAdd) != myVcsRoot) {
184       return;
185     }
186     if (root.isUnder(toAdd, true)) {
187       toAdd = root;
188     }
189     for (Iterator<FilePath> i = paths.iterator(); i.hasNext();) {
190       FilePath p = i.next();
191       if (p.isUnder(toAdd, true)) {
192         i.remove();
193       }
194       if (toAdd.isUnder(p, false)) {
195         return;
196       }
197     }
198     paths.add(toAdd);
199   }
200
201   /**
202    * Collect diff with head
203    *
204    * @throws VcsException if there is a problem with running git
205    */
206   private void collectDiffChanges() throws VcsException {
207     GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitHandler.DIFF);
208     handler.addParameters("--name-status", "--diff-filter=ADCMRUX", "-M", "HEAD");
209     handler.setNoSSH(true);
210     handler.setSilent(true);
211     handler.setStdoutSuppressed(true);
212     handler.endOptions();
213     handler.addRelativePaths(dirtyPaths(true));
214     try {
215       String output = handler.run();
216       GitChangeUtils.parseChanges(myProject, myVcsRoot, null, GitChangeUtils.loadRevision(myProject, myVcsRoot, "HEAD"), output, myChanges,
217                                   myUnmergedNames);
218     }
219     catch (VcsException ex) {
220       if (!GitChangeUtils.isHeadMissing(ex)) {
221         throw ex;
222       }
223       handler = new GitSimpleHandler(myProject, myVcsRoot, GitHandler.LS_FILES);
224       handler.addParameters("--cached");
225       handler.setNoSSH(true);
226       handler.setSilent(true);
227       handler.setStdoutSuppressed(true);
228       // During init diff does not works because HEAD
229       // will appear only after the first commit.
230       // In that case added files are cached in index.
231       String output = handler.run();
232       if (output.length() > 0) {
233         StringTokenizer tokenizer = new StringTokenizer(output, "\n\r");
234         while (tokenizer.hasMoreTokens()) {
235           final String s = tokenizer.nextToken();
236           Change ch = new Change(null, GitContentRevision.createRevision(myVcsRoot, s, null, myProject, false), FileStatus.ADDED);
237           myChanges.add(ch);
238         }
239       }
240     }
241   }
242
243   /**
244    * Collect unversioned and unmerged files
245    *
246    * @throws VcsException if there is a problem with running git
247    */
248   private void collectUnmergedAndUnversioned() throws VcsException {
249     // prepare handler
250     GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitHandler.LS_FILES);
251     handler.addParameters("-v", "--others", "--unmerged", "--exclude-standard");
252     handler.setSilent(true);
253     handler.setNoSSH(true);
254     handler.setStdoutSuppressed(true);
255     handler.addRelativePaths(dirtyPaths(false));
256     // run handler and collect changes
257     String list = handler.run();
258     for (StringScanner sc = new StringScanner(list); sc.hasMoreData();) {
259       if (sc.isEol()) {
260         sc.nextLine();
261         continue;
262       }
263       char status = sc.peek();
264       sc.skipChars(2);
265       if ('?' == status) {
266         VirtualFile file = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(sc.line()));
267         if (GitUtil.gitRootOrNull(file) == myVcsRoot) {
268           myUnversioned.add(file);
269         }
270       }
271       else { //noinspection HardCodedStringLiteral
272         if ('M' == status) {
273           sc.boundedToken('\t');
274           String file = GitUtil.unescapePath(sc.line());
275           VirtualFile vFile = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(sc.line()));
276           if (GitUtil.gitRootOrNull(vFile) != myVcsRoot) {
277             continue;
278           }
279           if (!myUnmergedNames.add(file)) {
280             continue;
281           }
282           // TODO handle conflict rename-modify
283           // TODO handle conflict copy-modify
284           // TODO handle conflict delete-modify
285           // TODO handle conflict rename-delete
286           // assume modify-modify conflict
287           ContentRevision before = GitContentRevision.createRevision(myVcsRoot, file, new GitRevisionNumber("orig_head"), myProject, false);
288           ContentRevision after = GitContentRevision.createRevision(myVcsRoot, file, null, myProject, false);
289           myChanges.add(new Change(before, after, FileStatus.MERGED_WITH_CONFLICTS));
290         }
291         else {
292           throw new VcsException("Unsupported type of the merge conflict detected: " + status);
293         }
294       }
295     }
296   }
297 }