replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / status / GitOldChangesCollector.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.status;
17
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.util.Comparing;
20 import com.intellij.openapi.vcs.*;
21 import com.intellij.openapi.vcs.changes.Change;
22 import com.intellij.openapi.vcs.changes.ChangeListManager;
23 import com.intellij.openapi.vcs.changes.ContentRevision;
24 import com.intellij.openapi.vcs.changes.VcsDirtyScope;
25 import com.intellij.openapi.vfs.VirtualFile;
26 import git4idea.GitContentRevision;
27 import git4idea.GitRevisionNumber;
28 import git4idea.GitUtil;
29 import git4idea.changes.GitChangeUtils;
30 import git4idea.commands.GitCommand;
31 import git4idea.commands.GitSimpleHandler;
32 import git4idea.util.StringScanner;
33 import org.jetbrains.annotations.NotNull;
34
35 import java.util.*;
36
37 /**
38  * <p>
39  *   Collects changes from the Git repository in the specified {@link com.intellij.openapi.vcs.changes.VcsDirtyScope}
40  *   using the older technique that is replaced by {@link GitNewChangesCollector} for Git later than 1.7.0 inclusive.
41  *   This class is used for Git older than 1.7.0 not inclusive, that don't have {@code 'git status --porcelain'}.
42  * </p>
43  * <p>
44  *   The method used by this class is less efficient and more error-prone than {@link GitNewChangesCollector} method.
45  *   Thus this class is considered as a legacy code for Git 1.6.*. Read further for the implementation details and the ground for
46  *   transferring to use {@code 'git status --porcelain'}.
47  * </p>
48  * <p>
49  *   The following Git commands are called to get the changes, i.e. the state of the working tree combined with the state of index.
50  *   <ul>
51  *     <li>
52  *       <b>{@code 'git update-index --refresh'}</b> (called on the whole repository) - probably unnecessary (especially before 'git diff'),
53  *       but is left not to break some older Gits occasionally. See the following links for some details:
54  *       <a href="http://us.generation-nt.com/answer/bug-596126-git-status-does-not-refresh-index-fixed-since-1-7-1-1-please-consider-upgrading-1-7-1-2-squeeze-help-200234171.html">
55  *       gitk doesn't refresh the index statinfo</a>;
56  *       <a href="http://thread.gmane.org/gmane.comp.version-control.git/144176/focus">
57  *       "Most git porcelain silently refreshes stat-dirty index entries"</a>;
58  *       <a href="https://git.wiki.kernel.org/index.php/GitFaq#Can_I_import_from_tar_files_.28archives.29.3">update-index to import from tar files</a>.
59  *     </li>
60  *     <li>
61  *       <b>{@code 'git ls-files --unmerged'}</b> (called on the whole repository) - to get the list of unmerged files.
62  *       It is not clear why it should be called on the whole repository. The decision to call it on the whole repository was made in
63  *       <code>45687fe "<a href="http://youtrack.jetbrains.net/issue/IDEA-50573">IDEADEV-40577</a>: The ignored unmerged files are now reported"</code>,
64  *       but neither the rollback & test, nor the analysis didn't recover the need for that. It is left however, since it is a legacy code.
65  *     </li>
66  *     <li>
67  *       <b>{@code 'git ls-files --others --exclude-standard'}</b> (called on the dirty scope) - to get the list of unversioned files.
68  *       Note that this command is the only way to get the list of unversioned files, besides {@code 'git status'}.
69  *     </li>
70  *     <li>
71  *       <b>{@code 'git diff --name-status -M HEAD -- }</b> (called on the dirty scope) - to get all other changes (except unversioned and
72  *       unmerged).
73  *       Note that there is also no way to get all tracked changes by a single command (except {@code 'git status'}), since
74  *       {@code 'git diff'} returns either only not-staged changes, either ({@code 'git diff HEAD'}) treats unmerged as modified.
75  *     </li>
76  *   </ul>
77  * </p>
78  * <p>
79  *   <b>Performance measurement</b>
80  *   was performed on a large repository (like IntelliJ IDEA), on a single machine, after several "warm-ups" when {@code 'git status'} duration
81  *   stabilizes.
82  *   For the whole repository:
83  *   {@code 'git status'} takes ~ 1300 ms while these 4 commands take ~ 1870 ms
84  *   ('update-index' ~ 270 ms, 'ls-files --unmerged' ~ 46 ms, 'ls files --others' ~ 820 ms, 'diff' ~ 650 ms)
85  *   ; for a single file:
86  *   {@code 'git status'} takes ~ 375 ms, these 4 commands take ~ 750 ms.
87  * </p>
88  * <p>
89  * The class is immutable: collect changes and get the instance from where they can be retrieved by {@link #collect}.
90  * </p>
91  *
92  * @author Constantine Plotnikov
93  * @author Kirill Likhodedov
94  */
95 class GitOldChangesCollector extends GitChangesCollector {
96
97   private final List<VirtualFile> myUnversioned = new ArrayList<>(); // Unversioned files
98   private final Set<String> myUnmergedNames = new HashSet<>(); // Names of unmerged files
99   private final List<Change> myChanges = new ArrayList<>(); // all changes
100
101   /**
102    * Collects the changes from git command line and returns the instance of GitNewChangesCollector from which these changes can be retrieved.
103    * This may be lengthy.
104    */
105   @NotNull
106   static GitOldChangesCollector collect(@NotNull Project project, @NotNull ChangeListManager changeListManager,
107                                         @NotNull ProjectLevelVcsManager vcsManager, @NotNull AbstractVcs vcs,
108                                         @NotNull VcsDirtyScope dirtyScope, @NotNull VirtualFile vcsRoot) throws VcsException {
109     return new GitOldChangesCollector(project, changeListManager, vcsManager, vcs, dirtyScope, vcsRoot);
110   }
111
112   @NotNull
113   @Override
114   Collection<VirtualFile> getUnversionedFiles() {
115     return myUnversioned;
116   }
117
118   @NotNull
119   @Override
120   Collection<Change> getChanges(){
121     return myChanges;
122   }
123
124   private GitOldChangesCollector(@NotNull Project project, @NotNull ChangeListManager changeListManager,
125                                  @NotNull ProjectLevelVcsManager vcsManager, @NotNull AbstractVcs vcs, @NotNull VcsDirtyScope dirtyScope,
126                                  @NotNull VirtualFile vcsRoot) throws VcsException {
127     super(project, changeListManager, vcsManager, vcs, dirtyScope, vcsRoot);
128     updateIndex();
129     collectUnmergedAndUnversioned();
130     collectDiffChanges();
131   }
132
133   private void updateIndex() throws VcsException {
134     GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.UPDATE_INDEX);
135     handler.addParameters("--refresh", "--ignore-missing");
136     handler.setSilent(true);
137     handler.setStdoutSuppressed(true);
138     handler.ignoreErrorCode(1);
139     handler.run();
140   }
141
142   /**
143    * Collect diff with head
144    *
145    * @throws VcsException if there is a problem with running git
146    */
147   private void collectDiffChanges() throws VcsException {
148     Collection<FilePath> dirtyPaths = dirtyPaths(true);
149     if (dirtyPaths.isEmpty()) {
150       return;
151     }
152     try {
153       String output = GitChangeUtils.getDiffOutput(myProject, myVcsRoot, "HEAD", dirtyPaths);
154       GitChangeUtils.parseChanges(myProject, myVcsRoot, null, GitChangeUtils.resolveReference(myProject, myVcsRoot, "HEAD"), output, myChanges,
155                                   myUnmergedNames);
156     }
157     catch (VcsException ex) {
158       if (!GitChangeUtils.isHeadMissing(ex)) {
159         throw ex;
160       }
161       GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
162       handler.addParameters("--cached");
163       handler.setSilent(true);
164       handler.setStdoutSuppressed(true);
165       // During init diff does not works because HEAD
166       // will appear only after the first commit.
167       // In that case added files are cached in index.
168       String output = handler.run();
169       if (output.length() > 0) {
170         StringTokenizer tokenizer = new StringTokenizer(output, "\n\r");
171         while (tokenizer.hasMoreTokens()) {
172           final String s = tokenizer.nextToken();
173           Change ch = new Change(null, GitContentRevision.createRevision(myVcsRoot, s, null, myProject, false, true), FileStatus.ADDED);
174           myChanges.add(ch);
175         }
176       }
177     }
178   }
179
180   /**
181    * Collect unversioned and unmerged files
182    *
183    * @throws VcsException if there is a problem with running git
184    */
185   private void collectUnmergedAndUnversioned() throws VcsException {
186     Collection<FilePath> dirtyPaths = dirtyPaths(false);
187     if (dirtyPaths.isEmpty()) {
188       return;
189     }
190     // prepare handler
191     GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
192     handler.addParameters("-v", "--unmerged");
193     handler.setSilent(true);
194     handler.setStdoutSuppressed(true);
195     // run handler and collect changes
196     parseFiles(handler.run());
197     // prepare handler
198     handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
199     handler.addParameters("-v", "--others", "--exclude-standard");
200     handler.setSilent(true);
201     handler.setStdoutSuppressed(true);
202     handler.endOptions();
203     handler.addRelativePaths(dirtyPaths);
204     if(handler.isLargeCommandLine()) {
205       handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
206       handler.addParameters("-v", "--others", "--exclude-standard");
207       handler.setSilent(true);
208       handler.setStdoutSuppressed(true);
209       handler.endOptions();
210     }
211     // run handler and collect changes
212     parseFiles(handler.run());
213   }
214
215   private void parseFiles(String list) throws VcsException {
216     for (StringScanner sc = new StringScanner(list); sc.hasMoreData();) {
217       if (sc.isEol()) {
218         sc.nextLine();
219         continue;
220       }
221       char status = sc.peek();
222       sc.skipChars(2);
223       if ('?' == status) {
224         VirtualFile file = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(sc.line()));
225         if (Comparing.equal(GitUtil.gitRootOrNull(file), myVcsRoot)) {
226           myUnversioned.add(file);
227         }
228       }
229       else { //noinspection HardCodedStringLiteral
230         if ('M' == status) {
231           sc.boundedToken('\t');
232           String file = GitUtil.unescapePath(sc.line());
233           VirtualFile vFile = myVcsRoot.findFileByRelativePath(file);
234           if (!Comparing.equal(GitUtil.gitRootOrNull(vFile), myVcsRoot)) {
235             continue;
236           }
237           if (!myUnmergedNames.add(file)) {
238             continue;
239           }
240           // assume modify-modify conflict
241           ContentRevision before = GitContentRevision.createRevision(myVcsRoot, file, new GitRevisionNumber("orig_head"), myProject, true,
242                                                                      true);
243           ContentRevision after = GitContentRevision.createRevision(myVcsRoot, file, null, myProject, false, true);
244           myChanges.add(new Change(before, after, FileStatus.MERGED_WITH_CONFLICTS));
245         }
246         else {
247           throw new VcsException("Unsupported type of the merge conflict detected: " + status);
248         }
249       }
250     }
251   }
252 }