2 * Copyright 2000-2011 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package git4idea.stash;
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;
51 import javax.swing.event.HyperlinkEvent;
54 import java.util.concurrent.atomic.AtomicBoolean;
56 import static com.intellij.notification.NotificationType.WARNING;
59 * @author Kirill Likhodedov
61 public class GitStashChangesSaver extends GitChangesSaver {
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
66 public GitStashChangesSaver(Project project, ProgressIndicator progressIndicator, String stashMessage) {
67 super(project, progressIndicator, stashMessage);
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());
79 protected void load(@Nullable final Runnable restoreListsRunnable, ContinuationContext context) {
80 for (VirtualFile root : myStashedRoots) {
84 catch (VcsException e) {
85 context.handleException(e);
89 final List<File> files = ObjectsConvertor.fp2jiof(getChangedFiles());
90 LocalFileSystem.getInstance().refreshIoFiles(files);
91 if (restoreListsRunnable != null) {
92 UIUtil.invokeLaterIfNeeded(new Runnable() {
94 myChangeManager.invokeAfterUpdate(restoreListsRunnable, InvokeAfterUpdateMode.BACKGROUND_NOT_CANCELLABLE,
95 GitBundle.getString("update.restoring.change.lists"), ModalityState.NON_MODAL);
102 protected boolean wereChangesSaved() {
103 return !myStashedRoots.isEmpty();
106 @Override public String getSaverName() {
110 @Override protected void showSavedChanges() {
111 GitUnstashDialog.showUnstashDialog(myProject, new ArrayList<VirtualFile>(myStashedRoots), myStashedRoots.iterator().next(), new HashSet<VirtualFile>());
114 private void stash(Collection<VirtualFile> roots) throws VcsException {
115 for (VirtualFile root : roots) {
116 final String message = GitHandlerUtil.formatOperationName("Stashing changes from", root);
118 final String oldProgressTitle = myProgressIndicator.getText();
119 myProgressIndicator.setText(message);
120 if (GitStashUtils.saveStash(myProject, root, myStashMessage)) {
121 myStashedRoots.add(root);
123 myProgressIndicator.setText(oldProgressTitle);
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);
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");
146 final AtomicBoolean conflict = new AtomicBoolean();
147 handler.addLineListener(new GitLineHandlerAdapter() {
149 public void onLineAvailable(String line, Key outputType) {
150 if (line.contains("Merge conflict")) {
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() {
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);
169 @Override protected void onFailure() {
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");
182 LOG.info("unstash failed " + handler.errors());
183 GitUIUtil.notifyImportantError(myProject, "Couldn't unstash", "<br/>" + GitUIUtil.stringifyErrors(handler.errors()));
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;
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());
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);
218 return sortedChanges;
221 private static void storeChangeInMap(Map<VirtualFile, Collection<Change>> sortedChanges,
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);
236 private class UnstashConflictResolver extends GitMergeConflictResolver {
237 public UnstashConflictResolver() {
238 super(GitStashChangesSaver.this.myProject, true, new UnstashMergeDialogCustomizer(), "Local changes were not restored", "");
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'>" +
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() {
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);
267 private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer {
270 public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
271 return "Uncommitted changes that were stashed before update have conflicts with updated files.";
275 public String getLeftPanelTitle(VirtualFile file) {
276 return "Your uncommitted changes";
280 public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
281 return "Changes from remote";