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