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