ca8f4120ed408b73d26a96afd9ab4eaf1360ae60
[idea/community.git] / plugins / git4idea / src / git4idea / stash / GitStashChangesSaver.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.stash;
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.ModalityState;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.progress.ProgressIndicator;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.Key;
27 import com.intellij.openapi.vcs.ObjectsConvertor;
28 import com.intellij.openapi.vcs.VcsException;
29 import com.intellij.openapi.vcs.changes.Change;
30 import com.intellij.openapi.vcs.changes.ContentRevision;
31 import com.intellij.openapi.vcs.changes.InvokeAfterUpdateMode;
32 import com.intellij.openapi.vcs.changes.LocalChangeList;
33 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
34 import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
35 import com.intellij.openapi.vfs.LocalFileSystem;
36 import com.intellij.openapi.vfs.VirtualFile;
37 import com.intellij.util.continuation.ContinuationContext;
38 import com.intellij.util.ui.UIUtil;
39 import git4idea.GitUtil;
40 import git4idea.GitVcs;
41 import git4idea.commands.*;
42 import git4idea.config.GitVcsSettings;
43 import git4idea.convert.GitFileSeparatorConverter;
44 import git4idea.i18n.GitBundle;
45 import git4idea.merge.GitMergeConflictResolver;
46 import git4idea.ui.GitUIUtil;
47 import git4idea.ui.GitUnstashDialog;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50
51 import javax.swing.event.HyperlinkEvent;
52 import java.io.File;
53 import java.util.*;
54 import java.util.concurrent.atomic.AtomicBoolean;
55
56 import static com.intellij.notification.NotificationType.WARNING;
57
58 /**
59  * @author Kirill Likhodedov
60  */
61 public class GitStashChangesSaver extends GitChangesSaver {
62
63   private static final Logger LOG = Logger.getInstance(GitStashChangesSaver.class);
64   private final Set<VirtualFile> myStashedRoots = new HashSet<VirtualFile>(); // save stashed roots to unstash only them
65
66   public GitStashChangesSaver(Project project, ProgressIndicator progressIndicator, String stashMessage) {
67     super(project, progressIndicator, stashMessage);
68   }
69
70   @Override
71   protected void save(Collection<VirtualFile> rootsToSave) throws VcsException {
72     LOG.info("save " + rootsToSave);
73     Map<VirtualFile, Collection<Change>> changes = groupChangesByRoots(rootsToSave);
74     convertSeparatorsIfNeeded(changes);
75     stash(changes.keySet());
76   }
77
78   @Override
79   protected void load(@Nullable final Runnable restoreListsRunnable, ContinuationContext context) {
80     for (VirtualFile root : myStashedRoots) {
81       try {
82         loadRoot(root);
83       }
84       catch (VcsException e) {
85         context.handleException(e);
86         return;
87       }
88     }
89     final List<File> files = ObjectsConvertor.fp2jiof(getChangedFiles());
90     LocalFileSystem.getInstance().refreshIoFiles(files);
91     if (restoreListsRunnable != null) {
92       UIUtil.invokeLaterIfNeeded(new Runnable() {
93         public void run() {
94           myChangeManager.invokeAfterUpdate(restoreListsRunnable, InvokeAfterUpdateMode.BACKGROUND_NOT_CANCELLABLE,
95                                             GitBundle.getString("update.restoring.change.lists"), ModalityState.NON_MODAL);
96         }
97       });
98     }
99   }
100
101     @Override
102   protected boolean wereChangesSaved() {
103     return !myStashedRoots.isEmpty();
104   }
105
106   @Override public String getSaverName() {
107     return "stash";
108   }
109
110   @Override protected void showSavedChanges() {
111     GitUnstashDialog.showUnstashDialog(myProject, new ArrayList<VirtualFile>(myStashedRoots), myStashedRoots.iterator().next(), new HashSet<VirtualFile>());
112   }
113
114   private void stash(Collection<VirtualFile> roots) throws VcsException {
115     for (VirtualFile root : roots) {
116       final String message = GitHandlerUtil.formatOperationName("Stashing changes from", root);
117       LOG.info(message);
118       final String oldProgressTitle = myProgressIndicator.getText();
119       myProgressIndicator.setText(message);
120       if (GitStashUtils.saveStash(myProject, root, myStashMessage)) {
121         myStashedRoots.add(root);
122       }
123       myProgressIndicator.setText(oldProgressTitle);
124     }
125   }
126
127   private void convertSeparatorsIfNeeded(Map<VirtualFile, Collection<Change>> changes) throws VcsException {
128     LOG.info("convertSeparatorsIfNeeded ");
129     GitVcsSettings settings = GitVcsSettings.getInstance(myProject);
130     if (settings != null) {
131       List<VcsException> exceptions = new ArrayList<VcsException>(1);
132       GitFileSeparatorConverter.convertSeparatorsIfNeeded(myProject, settings, changes, exceptions);
133       if (!exceptions.isEmpty()) {
134         throw exceptions.get(0);
135       }
136     }
137   }
138
139   private void loadRoot(final VirtualFile root) throws VcsException {
140     LOG.info("loadRoot " + root);
141     myProgressIndicator.setText(GitHandlerUtil.formatOperationName("Unstashing changes to", root));
142     final GitLineHandler handler = new GitLineHandler(myProject, root, GitCommand.STASH);
143     handler.setNoSSH(true);
144     handler.addParameters("pop");
145
146     final AtomicBoolean conflict = new AtomicBoolean();
147     handler.addLineListener(new GitLineHandlerAdapter() {
148       @Override
149       public void onLineAvailable(String line, Key outputType) {
150         if (line.contains("Merge conflict")) {
151           conflict.set(true);
152         }
153       }
154     });
155
156     final GitTask task = new GitTask(myProject, handler, "Unstashing uncommitted changes");
157     task.setProgressIndicator(myProgressIndicator);
158     final AtomicBoolean failure = new AtomicBoolean();
159     task.executeInBackground(true, new GitTaskResultHandlerAdapter() {
160       @Override protected void onSuccess() {
161       }
162
163       @Override protected void onCancel() {
164         Notifications.Bus.notify(new Notification(GitVcs.NOTIFICATION_GROUP_ID, "Unstash cancelled",
165                                                   "You may view the stashed changes <a href='saver'>here</a>", WARNING,
166                                                   new ShowSavedChangesNotificationListener()), myProject);
167       }
168
169       @Override protected void onFailure() {
170         failure.set(true);
171       }
172     });
173
174     if (failure.get()) {
175       if (conflict.get()) {
176         boolean conflictsResolved = new UnstashConflictResolver().merge(Collections.singleton(root));
177         if (conflictsResolved) {
178           LOG.info("loadRoot " + root + " conflicts resolved, dropping stash");
179           dropStash(root);
180         }
181       } else {
182         LOG.info("unstash failed " + handler.errors());
183         GitUIUtil.notifyImportantError(myProject, "Couldn't unstash", "<br/>" + GitUIUtil.stringifyErrors(handler.errors()));
184       }
185     }
186   }
187
188   // drops stash (after completing conflicting merge during unstashing), shows a warning in case of error
189   private void dropStash(VirtualFile root) {
190     final GitSimpleHandler handler = new GitSimpleHandler(myProject, root, GitCommand.STASH);
191     handler.setNoSSH(true);
192     handler.addParameters("drop");
193     String output = null;
194     try {
195       output = handler.run();
196     } catch (VcsException e) {
197       LOG.info("dropStash " + output, e);
198       GitUIUtil.notifyMessage(myProject, "Couldn't drop stash",
199                               "Couldn't drop stash after resolving conflicts.<br/>Please drop stash manually.",
200                               WARNING, false, handler.errors());
201     }
202   }
203
204   // Sort changes from myChangesLists by their git roots.
205   // And use only supplied roots, ignoring changes from other roots.
206   private Map<VirtualFile, Collection<Change>> groupChangesByRoots(Collection<VirtualFile> rootsToSave) {
207     final Map<VirtualFile, Collection<Change>> sortedChanges = new HashMap<VirtualFile, Collection<Change>>();
208     for (LocalChangeList l : myChangeLists) {
209       final Collection<Change> changeCollection = l.getChanges();
210       for (Change c : changeCollection) {
211         if (c.getAfterRevision() != null) {
212           storeChangeInMap(sortedChanges, c, c.getAfterRevision(), rootsToSave);
213         } else if (c.getBeforeRevision() != null) {
214           storeChangeInMap(sortedChanges, c, c.getBeforeRevision(), rootsToSave);
215         }
216       }
217     }
218     return sortedChanges;
219   }
220
221   private static void storeChangeInMap(Map<VirtualFile, Collection<Change>> sortedChanges,
222                                        Change c,
223                                        ContentRevision contentRevision,
224                                        Collection<VirtualFile> rootsToSave) {
225     final VirtualFile root = GitUtil.getGitRootOrNull(contentRevision.getFile());
226     if (root != null && rootsToSave.contains(root)) {
227       Collection<Change> changes = sortedChanges.get(root);
228       if (changes == null) {
229         changes = new ArrayList<Change>();
230         sortedChanges.put(root, changes);
231       }
232       changes.add(c);
233     }
234   }
235
236   private class UnstashConflictResolver extends GitMergeConflictResolver {
237     public UnstashConflictResolver() {
238       super(GitStashChangesSaver.this.myProject, true, new UnstashMergeDialogCustomizer(), "Local changes were not restored", "");
239     }
240
241     @Override
242     protected void notifyUnresolvedRemain(final Collection<VirtualFile> roots) {
243       Notifications.Bus.notify(new Notification(GitVcs.IMPORTANT_ERROR_NOTIFICATION, "Local changes were restored with conflicts",
244                                                 "Before update your uncommitted changes were saved to <a href='saver'>" +
245                                                 getSaverName() +
246                                                 "</a><br/>" +
247                                                 "Unstash is not complete, you have unresolved merges in your working tree<br/>" +
248                                                 "<a href='resolve'>Resolve</a> conflicts and drop the stash.",
249                                                 NotificationType.WARNING, new NotificationListener() {
250           @Override
251           public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
252             if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
253               if (event.getDescription().equals("saver")) {
254                 // we don't use #showSavedChanges to specify unmerged root first
255                 GitUnstashDialog.showUnstashDialog(myProject, new ArrayList<VirtualFile>(myStashedRoots), roots.iterator().next(),
256                                                    new HashSet<VirtualFile>());
257               } else if (event.getDescription().equals("resolve")) {
258                 new UnstashConflictResolver().justMerge(roots);
259               }
260             }
261           }
262       }));
263     }
264
265   }
266
267   private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer {
268
269     @Override
270     public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
271       return "Uncommitted changes that were stashed before update have conflicts with updated files.";
272     }
273
274     @Override
275     public String getLeftPanelTitle(VirtualFile file) {
276       return "Your uncommitted changes";
277     }
278
279     @Override
280     public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
281       return "Changes from remote";
282     }
283   }
284 }