[Git] IDEA-51187 Auto-merge on unstash.
[idea/community.git] / plugins / git4idea / src / git4idea / merge / GitMergeConflictResolver.java
1 /*
2  * Copyright 2000-2011 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.notification.Notification;
19 import com.intellij.notification.NotificationListener;
20 import com.intellij.notification.NotificationType;
21 import com.intellij.notification.Notifications;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.vcs.AbstractVcsHelper;
26 import com.intellij.openapi.vcs.VcsException;
27 import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
28 import com.intellij.openapi.vcs.merge.MergeProvider;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.util.ui.UIUtil;
31 import git4idea.GitVcs;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import javax.swing.event.HyperlinkEvent;
36 import java.util.ArrayList;
37 import java.util.Collection;
38
39 /**
40  * @author Kirill Likhodedov
41  */
42 public class GitMergeConflictResolver {
43
44   private static final Logger LOG = Logger.getInstance(GitMergeConflictResolver.class);
45   protected final @NotNull Project myProject;
46   private final boolean myReverseMerge;
47   private final @NotNull String myErrorNotificationTitle;
48   private final @NotNull String myErrorNotificationAdditionalDescription;
49   private final @NotNull MergeDialogCustomizer myMergeDialogCustomizer;
50   private final AbstractVcsHelper myVcsHelper;
51   private final GitVcs myVcs;
52
53   /**
54    * @param reverseMerge specify if reverse merge provider has to be used for merging - it is the case of rebase or stash.
55    */
56   public GitMergeConflictResolver(@NotNull Project project, boolean reverseMerge, @Nullable String mergeDialogTitle, @NotNull String errorNotificationTitle, @NotNull String errorNotificationAdditionalDescription) {
57     this(project, reverseMerge, new SimpleMergeDialogCustomizer(mergeDialogTitle), errorNotificationTitle, errorNotificationAdditionalDescription);
58   }
59
60   public GitMergeConflictResolver(@NotNull Project project, boolean reverseMerge, @NotNull MergeDialogCustomizer mergeDialogCustomizer, @NotNull String errorNotificationTitle, @NotNull String errorNotificationAdditionalDescription) {
61     myProject = project;
62     myReverseMerge = reverseMerge;
63     myErrorNotificationTitle = errorNotificationTitle;
64     myErrorNotificationAdditionalDescription = errorNotificationAdditionalDescription;
65     myMergeDialogCustomizer = mergeDialogCustomizer;
66     myVcsHelper = AbstractVcsHelper.getInstance(project);
67     myVcs = GitVcs.getInstance(project);
68   }
69
70   /**
71    * Goes throw the procedure of merging conflicts via MergeTool for different types of operations.
72    *
73    * 1. Checks if there are unmerged files. If not, executes {@link #proceedIfNothingToMerge()}
74    * 2. Otherwise shows {@link com.intellij.openapi.vcs.merge.MultipleFileMergeDialog} where user merges files.
75    * 3. After dialog is closed, checks if unmerged files remain. If not, executes {@link #proceedAfterAllMerged()}.
76    * Otherwise shows a notification.
77    *
78    * @param roots Git repositories to look for unmerged files.
79    * @return true if there is nothing to merge anymore, false if unmerged files remain or in the case of error.
80    */
81   public final boolean merge(@NotNull final Collection<VirtualFile> roots) {
82     return merge(roots, false);
83   }
84
85   /**
86    * Does the same as {@link #merge(java.util.Collection)}, but just returns the result of merging without proceeding with update
87    * or other operation. Also notifications are a bit different.
88    * @return true if all conflicts were merged, false if unmerged files remain or in the case of error.
89    */
90   protected boolean justMerge(@NotNull final Collection<VirtualFile> roots) {
91     return merge(roots, true);
92   }
93
94   /**
95    * This is executed from {@link #merge(java.util.Collection)} if the initial check tells that there is nothing to merge.
96    * @return Return value is returned from {@link #merge(java.util.Collection)}
97    */
98   protected boolean proceedIfNothingToMerge() throws VcsException {
99     return true;
100   }
101
102   /**
103    * This is executed from {@link #merge(java.util.Collection)} after all conflicts are resolved.
104    * @return Return value is returned from {@link #merge(java.util.Collection)}
105    */
106   protected boolean proceedAfterAllMerged() throws VcsException {
107     return true;
108   }
109
110   private boolean merge(@NotNull final Collection<VirtualFile> roots, boolean mergeDialogInvokedFromNotification) {
111     try {
112       Collection<VirtualFile> unmergedFiles = GitMergeUtil.getUnmergedFiles(myProject, roots);
113       if (unmergedFiles.isEmpty()) {
114         LOG.info("merge no unmerged files");
115         return mergeDialogInvokedFromNotification ? true : proceedIfNothingToMerge();
116       } else {
117         final Collection<VirtualFile> finalUnmergedFiles = unmergedFiles;
118         UIUtil.invokeAndWaitIfNeeded(new Runnable() {
119           @Override public void run() {
120             final MergeProvider mergeProvider = myReverseMerge ? myVcs.getReverseMergeProvider() : myVcs.getMergeProvider();
121             myVcsHelper.showMergeDialog(new ArrayList<VirtualFile>(finalUnmergedFiles), mergeProvider, myMergeDialogCustomizer);
122           }
123         });
124
125         unmergedFiles = GitMergeUtil.getUnmergedFiles(myProject, roots);
126         if (unmergedFiles.isEmpty()) {
127           LOG.info("merge no more unmerged files");
128           return mergeDialogInvokedFromNotification ? true : proceedAfterAllMerged();
129         } else {
130           LOG.info("mergeFiles unmerged files remain: " + unmergedFiles);
131           if (mergeDialogInvokedFromNotification) {
132             Notifications.Bus.notify(new Notification(GitVcs.IMPORTANT_ERROR_NOTIFICATION, "Not all conflicts resolved",
133                                                       "You should <a href='resolve'>resolve</a> all conflicts before update. <br/>" + myErrorNotificationAdditionalDescription, NotificationType.WARNING,
134                                                       new ResolveNotificationListener(roots)), myProject);
135
136           } else {
137             notifyUnresolvedRemain(roots);
138           }
139         }
140       }
141     } catch (VcsException e) {
142       LOG.info("mergeFiles ", e);
143       final String description = mergeDialogInvokedFromNotification
144                                  ? "Be sure to resolve all conflicts before update. <br/>"
145                                  : "Be sure to resolve all conflicts first. ";
146       Notifications.Bus.notify(new Notification(GitVcs.IMPORTANT_ERROR_NOTIFICATION, "Not all conflicts resolved",
147                                                 description + myErrorNotificationAdditionalDescription + "<br/>" +
148                                                 e.getLocalizedMessage(), NotificationType.ERROR), myProject);
149     }
150     return false;
151
152   }
153
154   /**
155    * Shows notification that not all conflicts were resolved.
156    * @param roots             Roots that were merged.
157    */
158   protected void notifyUnresolvedRemain(Collection<VirtualFile> roots) {
159     Notifications.Bus.notify(new Notification(GitVcs.IMPORTANT_ERROR_NOTIFICATION, myErrorNotificationTitle,
160                                               "You have to <a href='resolve'>resolve</a> all conflicts first." + myErrorNotificationAdditionalDescription, NotificationType.WARNING,
161                                               new ResolveNotificationListener(roots)), myProject);
162   }
163
164   private class ResolveNotificationListener implements NotificationListener {
165     private final Collection<VirtualFile> myRoots;
166
167     public ResolveNotificationListener(Collection<VirtualFile> roots) {
168       myRoots = roots;
169     }
170
171     @Override public void hyperlinkUpdate(@NotNull final Notification notification, @NotNull HyperlinkEvent event) {
172       if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) {
173         notification.expire();
174         ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
175           @Override public void run() {
176             justMerge(myRoots);
177           }
178         });
179       }
180     }
181   }
182
183   private static class SimpleMergeDialogCustomizer extends MergeDialogCustomizer {
184     private final String myMergeDialogTitle;
185
186     public SimpleMergeDialogCustomizer(String mergeDialogTitle) {
187       myMergeDialogTitle = mergeDialogTitle;
188     }
189
190     @Override
191     public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
192       return myMergeDialogTitle;
193     }
194   }
195 }