IDEA-80510
[idea/community.git] / plugins / git4idea / src / git4idea / branch / GitBranchOperation.java
1 /*
2  * Copyright 2000-2012 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.branch;
17
18 import com.intellij.notification.Notification;
19 import com.intellij.notification.NotificationListener;
20 import com.intellij.notification.NotificationType;
21 import com.intellij.openapi.progress.ProgressIndicator;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.ui.Messages;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.util.Function;
26 import com.intellij.util.ui.UIUtil;
27 import git4idea.GitUtil;
28 import git4idea.GitVcs;
29 import git4idea.MessageManager;
30 import git4idea.NotificationManager;
31 import git4idea.merge.GitConflictResolver;
32 import git4idea.repo.GitRepository;
33 import org.jetbrains.annotations.NotNull;
34
35 import javax.swing.event.HyperlinkEvent;
36 import java.util.ArrayList;
37 import java.util.Collection;
38 import java.util.concurrent.atomic.AtomicBoolean;
39
40 /**
41  * Common class for Git operations with branches aware of multi-root configuration,
42  * which means showing combined error information, proposing to rollback, etc.
43  *
44  * @author Kirill Likhodedov
45  */
46 abstract class GitBranchOperation {
47
48   static final String UNMERGED_FILES_ERROR_TITLE = "Can't checkout because of unmerged files";
49   static final String UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION =
50     "You have to <a href='resolve'>resolve</a> all merge conflicts before checkout.<br/>" +
51     "After resolving conflicts you also probably would want to commit your files to the current branch.";
52
53   @NotNull protected final Project myProject;
54   @NotNull private final Collection<GitRepository> myRepositories;
55   @NotNull private final ProgressIndicator myIndicator;
56
57   @NotNull private final Collection<GitRepository> mySuccessfulRepositories;
58   @NotNull private final Collection<GitRepository> myRemainingRepositories;
59
60   protected GitBranchOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
61                                @NotNull ProgressIndicator indicator) {
62     myProject = project;
63     myRepositories = repositories;
64     myIndicator = indicator;
65     mySuccessfulRepositories = new ArrayList<GitRepository>();
66     myRemainingRepositories = new ArrayList<GitRepository>(myRepositories);
67   }
68
69   protected abstract void execute();
70
71   protected abstract void rollback();
72
73   @NotNull
74   public abstract String getSuccessMessage();
75
76   @NotNull
77   protected abstract String getRollbackProposal();
78
79   /**
80    * @return next repository that wasn't handled (e.g. checked out) yet.
81    */
82   @NotNull
83   protected GitRepository next() {
84     return myRemainingRepositories.iterator().next();
85   }
86
87   /**
88    * @return true if there are more repositories on which the operation wasn't executed yet.
89    */
90   protected boolean hasMoreRepositories() {
91     return !myRemainingRepositories.isEmpty();
92   }
93
94   /**
95    * Marks repositories as successful, i.e. they won't be handled again.
96    */
97   protected void markSuccessful(GitRepository... repositories) {
98     for (GitRepository repository : repositories) {
99       mySuccessfulRepositories.add(repository);
100       myRemainingRepositories.remove(repository);
101     }
102   }
103
104   /**
105    * @return true if the operation has already succeeded in at least one of repositories.
106    */
107   protected boolean wereSuccessful() {
108     return !mySuccessfulRepositories.isEmpty();
109   }
110   
111   @NotNull
112   protected Collection<GitRepository> getSuccessfulRepositories() {
113     return mySuccessfulRepositories;
114   }
115   
116   @NotNull
117   protected String successfulRepositoriesJoined() {
118     return StringUtil.join(mySuccessfulRepositories, new Function<GitRepository, String>() {
119       @Override
120       public String fun(GitRepository repository) {
121         return repository.getPresentableUrl();
122       }
123     }, "<br/>");
124   }
125   
126   @NotNull
127   protected Collection<GitRepository> getRepositories() {
128     return myRepositories;
129   }
130
131   @NotNull
132   protected Collection<GitRepository> getRemainingRepositories() {
133     return myRemainingRepositories;
134   }
135
136   protected void notifySuccess() {
137     NotificationManager.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", getSuccessMessage(), NotificationType.INFORMATION);
138   }
139
140   /**
141    * Show fatal error as a notification or as a dialog with rollback proposal.
142    */
143   protected void fatalError(@NotNull String title, @NotNull String message) {
144     if (wereSuccessful())  {
145       showFatalErrorDialogWithRollback(title, message);
146     }
147     else {
148       showFatalNotification(title, message);
149     }
150   }
151
152   protected void showFatalErrorDialogWithRollback(@NotNull final String title, @NotNull final String message) {
153     final AtomicBoolean ok = new AtomicBoolean();
154     UIUtil.invokeAndWaitIfNeeded(new Runnable() {
155       @Override
156       public void run() {
157         String description = message + getRollbackProposal();
158         ok.set(Messages.OK ==
159                MessageManager.showYesNoDialog(myProject, description, title, "Rollback", "Don't rollback", Messages.getErrorIcon()));
160       }
161     });
162     if (ok.get()) {
163       rollback();
164     }
165   }
166
167   protected void showFatalNotification(@NotNull String title, @NotNull String message) {
168     notifyError(title, message);
169   }
170
171   protected void notifyError(@NotNull String title, @NotNull String message) {
172     NotificationManager.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, message, NotificationType.ERROR);
173   }
174
175   @NotNull
176   protected ProgressIndicator getIndicator() {
177     return myIndicator;
178   }
179
180   /**
181    * Display the error saying that the operation can't be performed because there are unmerged files in a repository.
182    * Such error prevents checking out and creating new branch.
183    */
184   protected void fatalUnmergedFilesError() {
185     if (wereSuccessful()) {
186       showUnmergedFilesDialogWithRollback();
187     }
188     else {
189       showUnmergedFilesNotification();
190     }
191   }
192
193   private void showUnmergedFilesDialogWithRollback() {
194     final AtomicBoolean ok = new AtomicBoolean();
195     UIUtil.invokeAndWaitIfNeeded(new Runnable() {
196       @Override public void run() {
197         String description = "You have to resolve all merge conflicts before checkout.<br/>" + getRollbackProposal();
198         // suppressing: this message looks ugly if capitalized by words
199         //noinspection DialogTitleCapitalization
200         ok.set(Messages.OK == MessageManager.showYesNoDialog(myProject, description, UNMERGED_FILES_ERROR_TITLE, "Rollback", "Don't rollback", Messages.getErrorIcon()));
201       }
202     });
203     if (ok.get()) {
204       rollback();
205     }
206   }
207
208   private void showUnmergedFilesNotification() {
209     String title = UNMERGED_FILES_ERROR_TITLE;
210     String description = UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION;
211     NotificationManager.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, description, NotificationType.ERROR, new NotificationListener() {
212       @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
213         if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) {
214           GitConflictResolver.Params params = new GitConflictResolver.Params().
215                 setMergeDescription("The following files have unresolved conflicts. You need to resolve them before checking out.").
216                 setErrorNotificationTitle("Unresolved files remain.");
217           new GitConflictResolver(myProject, GitUtil.getRoots(getRepositories()), params).merge();
218         }
219       }
220     });
221   }
222
223 }