replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / merge / MergeChangeCollector.java
1 /*
2  * Copyright 2000-2014 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.merge;
17
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.util.io.FileUtil;
20 import com.intellij.openapi.vcs.VcsException;
21 import com.intellij.openapi.vcs.VcsKey;
22 import com.intellij.openapi.vcs.update.FileGroup;
23 import com.intellij.openapi.vcs.update.UpdatedFiles;
24 import com.intellij.openapi.vfs.CharsetToolkit;
25 import com.intellij.openapi.vfs.VirtualFile;
26 import git4idea.GitRevisionNumber;
27 import git4idea.GitUtil;
28 import git4idea.GitVcs;
29 import git4idea.commands.GitCommand;
30 import git4idea.commands.GitSimpleHandler;
31 import git4idea.repo.GitRepository;
32 import git4idea.util.StringScanner;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35
36 import java.io.File;
37 import java.io.IOException;
38 import java.util.*;
39
40 import static com.intellij.util.ObjectUtils.assertNotNull;
41
42 /**
43  * Collect change for merge or pull operations
44  */
45 public class MergeChangeCollector {
46   private final HashSet<String> myUnmergedPaths = new HashSet<>();
47   private final Project myProject;
48   private final VirtualFile myRoot;
49   private final GitRevisionNumber myStart; // Revision number before update (used for diff)
50   @NotNull private final GitRepository myRepository;
51
52   public MergeChangeCollector(final Project project, final VirtualFile root, final GitRevisionNumber start) {
53     myStart = start;
54     myProject = project;
55     myRoot = root;
56     myRepository = assertNotNull(GitUtil.getRepositoryManager(project).getRepositoryForRoot(root));
57   }
58
59   /**
60    * Collects changed files during or after merge operation to the supplied {@code updates} container.
61    */
62   public void collect(final UpdatedFiles updates, List<VcsException> exceptions) {
63     try {
64       // collect unmerged
65       Set<String> paths = getUnmergedPaths();
66       addAll(updates, FileGroup.MERGED_WITH_CONFLICT_ID, paths);
67
68       // collect other changes (ignoring unmerged)
69       TreeSet<String> updated = new TreeSet<>();
70       TreeSet<String> created = new TreeSet<>();
71       TreeSet<String> removed = new TreeSet<>();
72
73       String revisionsForDiff = getRevisionsForDiff();
74       if (revisionsForDiff ==  null) {
75         return;
76       }
77       getChangedFilesExceptUnmerged(updated, created, removed, revisionsForDiff);
78       addAll(updates, FileGroup.UPDATED_ID, updated);
79       addAll(updates, FileGroup.CREATED_ID, created);
80       addAll(updates, FileGroup.REMOVED_FROM_REPOSITORY_ID, removed);
81     } catch (VcsException e) {
82       exceptions.add(e);
83     }
84   }
85
86   /**
87    * Returns absolute paths to files which are currently unmerged, and also populates myUnmergedPaths with relative paths.
88    */
89   public @NotNull Set<String> getUnmergedPaths() throws VcsException {
90     String root = myRoot.getPath();
91     final GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.LS_FILES);
92     h.setSilent(true);
93     h.addParameters("--unmerged");
94     final String result = h.run();
95
96     final Set<String> paths = new HashSet<>();
97     for (StringScanner s = new StringScanner(result); s.hasMoreData();) {
98       if (s.isEol()) {
99         s.nextLine();
100         continue;
101       }
102       s.boundedToken('\t');
103       final String relative = s.line();
104       if (!myUnmergedPaths.add(relative)) {
105         continue;
106       }
107       String path = root + "/" + GitUtil.unescapePath(relative);
108       paths.add(path);
109     }
110     return paths;
111   }
112
113   /**
114    * @return The revision range which will be used to find merge diff (merge may be just finished, or in progress)
115    * or null in case of error or inconsistency.
116    */
117   @Nullable
118   public String getRevisionsForDiff() throws VcsException {
119     String root = myRoot.getPath();
120     GitRevisionNumber currentHead = GitRevisionNumber.resolve(myProject, myRoot, "HEAD");
121     if (currentHead.equals(myStart)) {
122       // The head has not advanced. This means that this is a merge that did not commit.
123       // This could be caused by --no-commit option or by failed two-head merge. The MERGE_HEAD
124       // should be available. In case of --no-commit option, the MERGE_HEAD might contain
125       // multiple heads separated by newline. The changes are collected separately for each head
126       // and they are merged using TreeSet class (that also sorts the changes).
127       File mergeHeadsFile = myRepository.getRepositoryFiles().getMergeHeadFile();
128       try {
129         if (mergeHeadsFile.exists()) {
130           String mergeHeads = new String(FileUtil.loadFileText(mergeHeadsFile, CharsetToolkit.UTF8));
131           for (StringScanner s = new StringScanner(mergeHeads); s.hasMoreData();) {
132             String head = s.line();
133             if (head.length() == 0) {
134               continue;
135             }
136             // note that "..." cause the diff to start from common parent between head and merge head
137             return myStart.getRev() + "..." + head;
138           }
139         }
140       } catch (IOException e) {
141         //noinspection ThrowableInstanceNeverThrown
142         throw new VcsException("Unable to read the file " + mergeHeadsFile + ": " + e.getMessage(), e);
143       }
144     } else {
145       // Otherwise this is a merge that did created a commit. And because of this the incoming changes
146       // are diffs between old head and new head. The commit could have been multihead commit,
147       // and the expression below considers it as well.
148       return myStart.getRev() + "..HEAD";
149     }
150     return null;
151   }
152
153   /**
154    * Populates the supplied collections of modified, created and removed files returned by 'git diff #revisions' command,
155    * where revisions is the range of revisions to check.
156    */
157   public void getChangedFilesExceptUnmerged(Collection<String> updated, Collection<String> created, Collection<String> removed, String revisions)
158     throws VcsException {
159     if (revisions == null) {
160       return;
161     }
162     String root = myRoot.getPath();
163     GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.DIFF);
164     h.setSilent(true);
165     // note that moves are not detected here
166     h.addParameters("--name-status", "--diff-filter=ADMRUX", "--no-renames", revisions);
167     for (StringScanner s = new StringScanner(h.run()); s.hasMoreData();) {
168       if (s.isEol()) {
169         s.nextLine();
170         continue;
171       }
172       char status = s.peek();
173       s.boundedToken('\t');
174       final String relative = s.line();
175       // eliminate conflicts
176       if (myUnmergedPaths.contains(relative)) {
177         continue;
178       }
179       String path = root + "/" + GitUtil.unescapePath(relative);
180       switch (status) {
181         case 'M':
182           updated.add(path);
183           break;
184         case 'A':
185           created.add(path);
186           break;
187         case 'D':
188           removed.add(path);
189           break;
190         default:
191           throw new IllegalStateException("Unexpected status: " + status);
192       }
193     }
194   }
195
196   /**
197    * Add all paths to the group
198    */
199   private static void addAll(final UpdatedFiles updates, String group_id, Set<String> paths) {
200     FileGroup fileGroup = updates.getGroupById(group_id);
201     final VcsKey vcsKey = GitVcs.getKey();
202     for (String path : paths) {
203       fileGroup.add(path, vcsKey, null);
204     }
205   }
206 }