Merge branch 'master' into multiroot_branch
[idea/community.git] / plugins / git4idea / src / git4idea / merge / GitMergeProvider.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.merge;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.vcs.FilePath;
21 import com.intellij.openapi.vcs.VcsException;
22 import com.intellij.openapi.vcs.merge.MergeData;
23 import com.intellij.openapi.vcs.merge.MergeProvider2;
24 import com.intellij.openapi.vcs.merge.MergeSession;
25 import com.intellij.openapi.vfs.VirtualFile;
26 import com.intellij.util.ArrayUtil;
27 import com.intellij.util.ui.ColumnInfo;
28 import com.intellij.vcsUtil.VcsFileUtil;
29 import com.intellij.vcsUtil.VcsRunnable;
30 import com.intellij.vcsUtil.VcsUtil;
31 import git4idea.GitFileRevision;
32 import git4idea.GitRevisionNumber;
33 import git4idea.util.GitUtil;
34 import git4idea.commands.GitCommand;
35 import git4idea.util.GitFileUtils;
36 import git4idea.commands.GitSimpleHandler;
37 import git4idea.util.StringScanner;
38 import git4idea.i18n.GitBundle;
39 import org.jetbrains.annotations.NotNull;
40
41 import java.io.IOException;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45
46 /**
47  * Merge-changes provider for Git, used by IDEA internal 3-way merge tool
48  */
49 public class GitMergeProvider implements MergeProvider2 {
50   /**
51    * the logger
52    */
53   private static final Logger log = Logger.getInstance(GitMergeProvider.class.getName());
54   /**
55    * The project instance
56    */
57   private final Project myProject;
58   /**
59    * If true the merge provider has a reverse meaning
60    */
61   private final boolean myReverse;
62   /**
63    * The revision that designates common parent for the files during the merge
64    */
65   private static final int ORIGINAL_REVISION_NUM = 1;
66   /**
67    * The revision that designates the file on the local branch
68    */
69   private static final int YOURS_REVISION_NUM = 2;
70   /**
71    * The revision that designates the remote file being merged
72    */
73   private static final int THEIRS_REVISION_NUM = 3;
74
75   /**
76    * A merge provider
77    *
78    * @param project a project for the provider
79    */
80   public GitMergeProvider(Project project) {
81     this(project, false);
82   }
83
84   /**
85    * A merge provider
86    *
87    * @param project a project for the provider
88    * @param reverse if true, yours and theirs take a reverse meaning
89    */
90   public GitMergeProvider(Project project, boolean reverse) {
91     myProject = project;
92     myReverse = reverse;
93   }
94
95   /**
96    * {@inheritDoc}
97    */
98   @NotNull
99   public MergeData loadRevisions(final VirtualFile file) throws VcsException {
100     final MergeData mergeData = new MergeData();
101     if (file == null) return mergeData;
102     final VirtualFile root = GitUtil.getGitRoot(file);
103     final FilePath path = VcsUtil.getFilePath(file.getPath());
104
105     VcsRunnable runnable = new VcsRunnable() {
106       @SuppressWarnings({"ConstantConditions"})
107       public void run() throws VcsException {
108         GitFileRevision original = new GitFileRevision(myProject, path, new GitRevisionNumber(":" + ORIGINAL_REVISION_NUM), true);
109         GitFileRevision current = new GitFileRevision(myProject, path, new GitRevisionNumber(":" + yoursRevision()), true);
110         GitFileRevision last = new GitFileRevision(myProject, path, new GitRevisionNumber(":" + theirsRevision()), true);
111         try {
112           try {
113             mergeData.ORIGINAL = original.getContent();
114           }
115           catch (Exception ex) {
116             /// unable to load original revision, use the current instead
117             /// This could happen in case if rebasing.
118             mergeData.ORIGINAL = file.contentsToByteArray();
119           }
120           mergeData.CURRENT = loadRevisionCatchingErrors(current);
121           mergeData.LAST = loadRevisionCatchingErrors(last);
122           try {
123             mergeData.LAST_REVISION_NUMBER = GitRevisionNumber.resolve(myProject, root, myReverse ? "HEAD" : "MERGE_HEAD");
124           }
125           catch (VcsException e) {
126             // ignore exception, the null value will be used
127           }
128         }
129         catch (IOException e) {
130           throw new IllegalStateException("Failed to load file content", e);
131         }
132       }
133     };
134     VcsUtil.runVcsProcessWithProgress(runnable, GitBundle.message("merge.load.files"), false, myProject);
135     return mergeData;
136   }
137
138   private byte[] loadRevisionCatchingErrors(final GitFileRevision revision) throws VcsException, IOException {
139     try {
140       return revision.getContent();
141     } catch (VcsException e) {
142       String m = e.getMessage().trim();
143       if (m.startsWith("fatal: ambiguous argument ")
144           || (m.startsWith("fatal: Path '") && m.contains("' exists on disk, but not in '"))
145           || (m.contains("is in the index, but not at stage "))) {
146         return ArrayUtil.EMPTY_BYTE_ARRAY;
147       }
148       else {
149         throw e;
150       }
151     }
152   }
153
154   /**
155    * @return number for "yours" revision  (taking {@code revsere} flag in account)
156    */
157   private int yoursRevision() {
158     return myReverse ? THEIRS_REVISION_NUM : YOURS_REVISION_NUM;
159   }
160
161   /**
162    * @return number for "theirs" revision (taking {@code revsere} flag in account)
163    */
164   private int theirsRevision() {
165     return myReverse ? YOURS_REVISION_NUM : THEIRS_REVISION_NUM;
166   }
167
168   /**
169    * {@inheritDoc}
170    */
171   public void conflictResolvedForFile(VirtualFile file) {
172     if (file == null) return;
173     try {
174       GitFileUtils.addFiles(myProject, GitUtil.getGitRoot(file), file);
175     }
176     catch (VcsException e) {
177       log.error("Confirming conflict resolution failed", e);
178     }
179   }
180
181   /**
182    * {@inheritDoc}
183    */
184   public boolean isBinary(VirtualFile file) {
185     return file.getFileType().isBinary();
186   }
187
188   @NotNull
189   public MergeSession createMergeSession(List<VirtualFile> files) {
190     return new MyMergeSession(files);
191   }
192
193
194   /**
195    * The conflict descriptor
196    */
197   private static class Conflict {
198     /**
199      * the file in the conflict
200      */
201     VirtualFile myFile;
202     /**
203      * the root for the file
204      */
205     VirtualFile myRoot;
206     /**
207      * the status of theirs revision
208      */
209     Status myStatusTheirs;
210     /**
211      * the status
212      */
213     Status myStatusYours;
214
215     /**
216      * @return true if the merge operation can be applied
217      */
218     boolean isMergeable() {
219       return true;
220     }
221
222     /**
223      * The conflict status
224      */
225     enum Status {
226       /**
227        * the file was modified on the branch
228        */
229       MODIFIED,
230       /**
231        * the file was deleted on the branch
232        */
233       DELETED,
234     }
235   }
236
237
238   /**
239    * The merge session, it queries conflict information .
240    */
241   private class MyMergeSession implements MergeSession {
242     /**
243      * the map with conflicts
244      */
245     Map<VirtualFile, Conflict> myConflicts = new HashMap<VirtualFile, Conflict>();
246
247     /**
248      * A constructor from list of the files
249      *
250      * @param filesToMerge the files to process using merge dialog.
251      */
252     MyMergeSession(List<VirtualFile> filesToMerge) {
253       // get conflict type by the file
254       try {
255         for (Map.Entry<VirtualFile, List<VirtualFile>> e : GitUtil.sortFilesByGitRoot(filesToMerge).entrySet()) {
256           Map<String, Conflict> cs = new HashMap<String, Conflict>();
257           VirtualFile root = e.getKey();
258           List<VirtualFile> files = e.getValue();
259           GitSimpleHandler h = new GitSimpleHandler(myProject, root, GitCommand.LS_FILES);
260           h.setNoSSH(true);
261           h.setStdoutSuppressed(true);
262           h.setSilent(true);
263           h.addParameters("--exclude-standard", "--unmerged", "-t", "-z");
264           h.endOptions();
265           String output = h.run();
266           StringScanner s = new StringScanner(output);
267           while (s.hasMoreData()) {
268             if (!"M".equals(s.spaceToken())) {
269               s.boundedToken('\u0000');
270               continue;
271             }
272             s.spaceToken(); // permissions
273             s.spaceToken(); // commit hash
274             int source = Integer.parseInt(s.tabToken());
275             String file = s.boundedToken('\u0000');
276             Conflict c = cs.get(file);
277             if (c == null) {
278               c = new Conflict();
279               c.myRoot = root;
280               cs.put(file, c);
281             }
282             if (source == theirsRevision()) {
283               c.myStatusTheirs = Conflict.Status.MODIFIED;
284             }
285             else if (source == yoursRevision()) {
286               c.myStatusYours = Conflict.Status.MODIFIED;
287             }
288             else if (source != ORIGINAL_REVISION_NUM) {
289               throw new IllegalStateException("Unknown revision " + source + " for the file: " + file);
290             }
291           }
292           for (VirtualFile f : files) {
293             String path = VcsFileUtil.relativePath(root, f);
294             Conflict c = cs.get(path);
295             assert c != null : "The conflict not found for the file: " + f.getPath() + "(" + path + ")";
296             c.myFile = f;
297             if (c.myStatusTheirs == null) {
298               c.myStatusTheirs = Conflict.Status.DELETED;
299             }
300             if (c.myStatusYours == null) {
301               c.myStatusYours = Conflict.Status.DELETED;
302             }
303             myConflicts.put(f, c);
304           }
305         }
306       }
307       catch (VcsException ex) {
308         throw new IllegalStateException("The git operation should not fail in this context", ex);
309       }
310     }
311
312     public ColumnInfo[] getMergeInfoColumns() {
313       return new ColumnInfo[]{new StatusColumn(false), new StatusColumn(true)};
314     }
315
316     public boolean canMerge(VirtualFile file) {
317       Conflict c = myConflicts.get(file);
318       return c != null;
319     }
320
321     public void conflictResolvedForFile(VirtualFile file, Resolution resolution) {
322       Conflict c = myConflicts.get(file);
323       assert c != null : "Conflict was not loaded for the file: " + file.getPath();
324       try {
325         Conflict.Status status;
326         switch (resolution) {
327           case AcceptedTheirs:
328             status = c.myStatusTheirs;
329             break;
330           case AcceptedYours:
331             status = c.myStatusYours;
332             break;
333           case Merged:
334             status = Conflict.Status.MODIFIED;
335             break;
336           default:
337             throw new IllegalArgumentException("Unsupported resolution for unmergable files(" + file.getPath() + "): " + resolution);
338         }
339         switch (status) {
340           case MODIFIED:
341             GitFileUtils.addFiles(myProject, c.myRoot, file);
342             break;
343           case DELETED:
344             GitFileUtils.deleteFiles(myProject, c.myRoot, file);
345             break;
346           default:
347             throw new IllegalArgumentException("Unsupported status(" + file.getPath() + "): " + status);
348         }
349       }
350       catch (VcsException e) {
351         log.error("Unexpected exception during the git operation (" + file.getPath() + ")", e);
352       }
353     }
354
355     /**
356      * The status column, the column shows either "yours" or "theirs" status
357      */
358     class StatusColumn extends ColumnInfo<VirtualFile, String> {
359       /**
360        * if false, "yours" status is displayed, otherwise "theirs"
361        */
362       private final boolean myIsTheirs;
363
364       /**
365        * The constructor
366        *
367        * @param isTheirs if true columns represents status in 'theirs' revision, if false in 'ours'
368        */
369       public StatusColumn(boolean isTheirs) {
370         super(isTheirs ? GitBundle.message("merge.tool.column.theirs.status") : GitBundle.message("merge.tool.column.yours.status"));
371         myIsTheirs = isTheirs;
372       }
373
374       /**
375        * {@inheritDoc}
376        */
377       public String valueOf(VirtualFile file) {
378         Conflict c = myConflicts.get(file);
379         assert c != null : "No conflict for the file " + file;
380         Conflict.Status s = myIsTheirs ? c.myStatusTheirs : c.myStatusYours;
381         switch (s) {
382           case MODIFIED:
383             return GitBundle.message("merge.tool.column.status.modified");
384           case DELETED:
385             return GitBundle.message("merge.tool.column.status.deleted");
386           default:
387             throw new IllegalStateException("Unknown status " + s + " for file " + file.getPath());
388         }
389       }
390
391       @Override
392       public String getMaxStringValue() {
393         return GitBundle.message("merge.tool.column.status.modified");
394       }
395
396       @Override
397       public int getAdditionalWidth() {
398         return 10;
399       }
400     }
401   }
402 }