5f1c8b1a5a2315ecf784d215db117d85371a83b4
[idea/community.git] / plugins / git4idea / src / git4idea / ui / GitUnstashDialog.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.ui;
17
18 import com.intellij.notification.Notification;
19 import com.intellij.notification.NotificationListener;
20 import com.intellij.notification.NotificationType;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.application.ModalityState;
23 import com.intellij.openapi.components.ServiceManager;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.progress.ProgressIndicator;
26 import com.intellij.openapi.progress.ProgressManager;
27 import com.intellij.openapi.progress.Task;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.ui.DialogWrapper;
30 import com.intellij.openapi.ui.Messages;
31 import com.intellij.openapi.util.Key;
32 import com.intellij.openapi.vcs.VcsException;
33 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
34 import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
35 import com.intellij.openapi.vfs.VirtualFile;
36 import com.intellij.ui.DocumentAdapter;
37 import com.intellij.util.Consumer;
38 import git4idea.GitBranch;
39 import git4idea.GitRevisionNumber;
40 import git4idea.GitVcs;
41 import git4idea.PlatformFacade;
42 import git4idea.actions.GitShowAllSubmittedFilesAction;
43 import git4idea.commands.*;
44 import git4idea.config.GitVersionSpecialty;
45 import git4idea.i18n.GitBundle;
46 import git4idea.merge.GitConflictResolver;
47 import git4idea.stash.GitStashUtils;
48 import git4idea.util.GitUIUtil;
49 import git4idea.validators.GitBranchNameValidator;
50 import org.jetbrains.annotations.NotNull;
51
52 import javax.swing.*;
53 import javax.swing.event.DocumentEvent;
54 import javax.swing.event.HyperlinkEvent;
55 import javax.swing.event.ListSelectionEvent;
56 import javax.swing.event.ListSelectionListener;
57 import java.awt.event.ActionEvent;
58 import java.awt.event.ActionListener;
59 import java.util.*;
60 import java.util.concurrent.atomic.AtomicBoolean;
61
62 /**
63  * The unstash dialog
64  */
65 public class GitUnstashDialog extends DialogWrapper {
66   /**
67    * Git root selector
68    */
69   private JComboBox myGitRootComboBox;
70   /**
71    * The current branch label
72    */
73   private JLabel myCurrentBranch;
74   /**
75    * The view stash button
76    */
77   private JButton myViewButton;
78   /**
79    * The drop stash button
80    */
81   private JButton myDropButton;
82   /**
83    * The clear stashes button
84    */
85   private JButton myClearButton;
86   /**
87    * The pop stash checkbox
88    */
89   private JCheckBox myPopStashCheckBox;
90   /**
91    * The branch text field
92    */
93   private JTextField myBranchTextField;
94   /**
95    * The root panel of the dialog
96    */
97   private JPanel myPanel;
98   /**
99    * The stash list
100    */
101   private JList myStashList;
102   /**
103    * If this checkbox is selected, the index is reinstated as well as working tree
104    */
105   private JCheckBox myReinstateIndexCheckBox;
106   /**
107    * Set of branches for the current root
108    */
109   private final HashSet<String> myBranches = new HashSet<String>();
110
111   /**
112    * The project
113    */
114   private final Project myProject;
115   private GitVcs myVcs;
116   private static final Logger LOG = Logger.getInstance(GitUnstashDialog.class);
117
118   /**
119    * A constructor
120    *
121    * @param project     the project
122    * @param roots       the list of the roots
123    * @param defaultRoot the default root to select
124    */
125   public GitUnstashDialog(final Project project, final List<VirtualFile> roots, final VirtualFile defaultRoot) {
126     super(project, true);
127     setModal(false);
128     myProject = project;
129     myVcs = GitVcs.getInstance(project);
130     setTitle(GitBundle.getString("unstash.title"));
131     setOKButtonText(GitBundle.getString("unstash.button.apply"));
132     GitUIUtil.setupRootChooser(project, roots, defaultRoot, myGitRootComboBox, myCurrentBranch);
133     myStashList.setModel(new DefaultListModel());
134     refreshStashList();
135     myGitRootComboBox.addActionListener(new ActionListener() {
136       public void actionPerformed(final ActionEvent e) {
137         refreshStashList();
138         updateDialogState();
139       }
140     });
141     myStashList.addListSelectionListener(new ListSelectionListener() {
142       public void valueChanged(final ListSelectionEvent e) {
143         updateDialogState();
144       }
145     });
146     myBranchTextField.getDocument().addDocumentListener(new DocumentAdapter() {
147       protected void textChanged(final DocumentEvent e) {
148         updateDialogState();
149       }
150     });
151     myPopStashCheckBox.addActionListener(new ActionListener() {
152       public void actionPerformed(ActionEvent e) {
153         updateDialogState();
154       }
155     });
156     myClearButton.addActionListener(new ActionListener() {
157       public void actionPerformed(final ActionEvent e) {
158         if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(),
159                                                      GitBundle.message("git.unstash.clear.confirmation.message"),
160                                                      GitBundle.message("git.unstash.clear.confirmation.title"), Messages.getWarningIcon())) {
161           GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH);
162           h.setNoSSH(true);
163           h.addParameters("clear");
164           GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.clearing.stashes"), h.printableCommandLine());
165           refreshStashList();
166           updateDialogState();
167         }
168       }
169     });
170     myDropButton.addActionListener(new ActionListener() {
171       public void actionPerformed(final ActionEvent e) {
172         final StashInfo stash = getSelectedStash();
173         if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(),
174                                                      GitBundle.message("git.unstash.drop.confirmation.message", stash.getStash(), stash.getMessage()),
175                                                      GitBundle.message("git.unstash.drop.confirmation.title", stash.getStash()), Messages.getQuestionIcon())) {
176           final ModalityState current = ModalityState.current();
177           ProgressManager.getInstance().run(new Task.Modal(myProject, "Removing stash " + stash.getStash(), false) {
178             @Override
179             public void run(@NotNull ProgressIndicator indicator) {
180               final GitSimpleHandler h = dropHandler(stash.getStash());
181               try {
182                 h.run();
183                 h.unsilence();
184               }
185               catch (final VcsException ex) {
186                 ApplicationManager.getApplication().invokeLater(new Runnable() {
187                   @Override
188                   public void run() {
189                     GitUIUtil.showOperationError(myProject, ex, h.printableCommandLine());
190                   }
191                 }, current);
192               }
193             }
194           });
195           refreshStashList();
196           updateDialogState();
197         }
198       }
199
200       private GitSimpleHandler dropHandler(String stash) {
201         GitSimpleHandler h = new GitSimpleHandler(myProject, getGitRoot(), GitCommand.STASH);
202         h.setNoSSH(true);
203         h.addParameters("drop");
204         addStashParameter(h, stash);
205         return h;
206       }
207     });
208     myViewButton.addActionListener(new ActionListener() {
209       public void actionPerformed(final ActionEvent e) {
210         final VirtualFile root = getGitRoot();
211         String resolvedStash;
212         String selectedStash = getSelectedStash().getStash();
213         try {
214           GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.REV_LIST);
215           h.setNoSSH(true);
216           h.setSilent(true);
217           h.addParameters("--timestamp", "--max-count=1");
218           addStashParameter(h, selectedStash);
219           h.endOptions();
220           final String output = h.run();
221           resolvedStash = GitRevisionNumber.parseRevlistOutputAsRevisionNumber(h, output).asString();
222         }
223         catch (VcsException ex) {
224           GitUIUtil.showOperationError(myProject, ex, "resolving revision");
225           return;
226         }
227         GitShowAllSubmittedFilesAction.showSubmittedFiles(myProject, resolvedStash, root, true, false);
228       }
229     });
230     init();
231     updateDialogState();
232   }
233
234   /**
235    * Adds {@code stash@{x}} parameter to the handler, quotes it if needed.
236    */
237   private void addStashParameter(@NotNull GitHandler handler, @NotNull String stash) {
238     if (GitVersionSpecialty.NEEDS_QUOTES_IN_STASH_NAME.existsIn(myVcs.getVersion())) {
239       handler.addParameters("\"" + stash + "\"");
240       handler.dontEscapeQuotes();
241     }
242     else {
243       handler.addParameters(stash);
244     }
245   }
246
247   /**
248    * Update state dialog depending on the current state of the fields
249    */
250   private void updateDialogState() {
251     String branch = myBranchTextField.getText();
252     if (branch.length() != 0) {
253       setOKButtonText(GitBundle.getString("unstash.button.branch"));
254       myPopStashCheckBox.setEnabled(false);
255       myPopStashCheckBox.setSelected(true);
256       myReinstateIndexCheckBox.setEnabled(false);
257       myReinstateIndexCheckBox.setSelected(true);
258       if (!GitBranchNameValidator.INSTANCE.checkInput(branch)) {
259         setErrorText(GitBundle.getString("unstash.error.invalid.branch.name"));
260         setOKActionEnabled(false);
261         return;
262       }
263       if (myBranches.contains(branch)) {
264         setErrorText(GitBundle.getString("unstash.error.branch.exists"));
265         setOKActionEnabled(false);
266         return;
267       }
268     }
269     else {
270       if (!myPopStashCheckBox.isEnabled()) {
271         myPopStashCheckBox.setSelected(false);
272       }
273       myPopStashCheckBox.setEnabled(true);
274       setOKButtonText(
275         myPopStashCheckBox.isSelected() ? GitBundle.getString("unstash.button.pop") : GitBundle.getString("unstash.button.apply"));
276       if (!myReinstateIndexCheckBox.isEnabled()) {
277         myReinstateIndexCheckBox.setSelected(false);
278       }
279       myReinstateIndexCheckBox.setEnabled(true);
280     }
281     if (myStashList.getModel().getSize() == 0) {
282       myViewButton.setEnabled(false);
283       myDropButton.setEnabled(false);
284       myClearButton.setEnabled(false);
285       setErrorText(null);
286       setOKActionEnabled(false);
287       return;
288     }
289     else {
290       myClearButton.setEnabled(true);
291     }
292     if (myStashList.getSelectedIndex() == -1) {
293       myViewButton.setEnabled(false);
294       myDropButton.setEnabled(false);
295       setErrorText(null);
296       setOKActionEnabled(false);
297       return;
298     }
299     else {
300       myViewButton.setEnabled(true);
301       myDropButton.setEnabled(true);
302     }
303     setErrorText(null);
304     setOKActionEnabled(true);
305   }
306
307   /**
308    * Refresh stash list
309    */
310   private void refreshStashList() {
311     final DefaultListModel listModel = (DefaultListModel)myStashList.getModel();
312     listModel.clear();
313     GitStashUtils.loadStashStack(myProject, getGitRoot(), new Consumer<StashInfo>() {
314       @Override
315       public void consume(StashInfo stashInfo) {
316         listModel.addElement(stashInfo);
317       }
318     });
319     myBranches.clear();
320     try {
321       GitBranch.listAsStrings(myProject, getGitRoot(), false, true, myBranches, null);
322     }
323     catch (VcsException e) {
324       // ignore error
325     }
326     myStashList.setSelectedIndex(0);
327   }
328
329   /**
330    * @return the selected git root
331    */
332   private VirtualFile getGitRoot() {
333     return (VirtualFile)myGitRootComboBox.getSelectedItem();
334   }
335
336   /**
337    * @return unstash handler
338    */
339   private GitLineHandler handler() {
340     GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH);
341     h.setNoSSH(true);
342     String branch = myBranchTextField.getText();
343     if (branch.length() == 0) {
344       h.addParameters(myPopStashCheckBox.isSelected() ? "pop" : "apply");
345       if (myReinstateIndexCheckBox.isSelected()) {
346         h.addParameters("--index");
347       }
348     }
349     else {
350       h.addParameters("branch", branch);
351     }
352     String selectedStash = getSelectedStash().getStash();
353     addStashParameter(h, selectedStash);
354     return h;
355   }
356
357   /**
358    * @return selected stash
359    * @throws NullPointerException if no stash is selected
360    */
361   private StashInfo getSelectedStash() {
362     return (StashInfo)myStashList.getSelectedValue();
363   }
364
365   /**
366    * {@inheritDoc}
367    */
368   protected JComponent createCenterPanel() {
369     return myPanel;
370   }
371
372   /**
373    * {@inheritDoc}
374    */
375   @Override
376   protected String getDimensionServiceKey() {
377     return getClass().getName();
378   }
379
380   /**
381    * {@inheritDoc}
382    */
383   @Override
384   protected String getHelpId() {
385     return "reference.VersionControl.Git.Unstash";
386   }
387
388   @Override
389   public JComponent getPreferredFocusedComponent() {
390     return myStashList;
391   }
392
393   @Override
394   protected void doOKAction() {
395     VirtualFile root = getGitRoot();
396     GitLineHandler h = handler();
397     final AtomicBoolean conflict = new AtomicBoolean();
398
399     h.addLineListener(new GitLineHandlerAdapter() {
400       public void onLineAvailable(String line, Key outputType) {
401         if (line.contains("Merge conflict")) {
402           conflict.set(true);
403         }
404       }
405     });
406     int rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false);
407     root.refresh(true, true);
408
409     if (conflict.get()) {
410       boolean conflictsResolved = new UnstashConflictResolver(myProject, root, getSelectedStash()).merge();
411       LOG.info("loadRoot " + root + ", conflictsResolved: " + conflictsResolved);
412     } else if (rc != 0) {
413       GitUIUtil.showOperationErrors(myProject, h.errors(), h.printableCommandLine());
414     }
415     super.doOKAction();
416   }
417
418   public static void showUnstashDialog(Project project, List<VirtualFile> gitRoots, VirtualFile defaultRoot) {
419     new GitUnstashDialog(project, gitRoots, defaultRoot).show();
420     // d is not modal=> everything else in doOKAction.
421   }
422
423   private static class UnstashConflictResolver extends GitConflictResolver {
424
425     private final VirtualFile myRoot;
426     private final StashInfo myStashInfo;
427
428     public UnstashConflictResolver(Project project, VirtualFile root, StashInfo stashInfo) {
429       super(project, ServiceManager.getService(Git.class), ServiceManager.getService(PlatformFacade.class),
430             Collections.singleton(root), makeParams(stashInfo));
431       myRoot = root;
432       myStashInfo = stashInfo;
433     }
434     
435     private static Params makeParams(StashInfo stashInfo) {
436       Params params = new Params();
437       params.setErrorNotificationTitle("Unstashed with conflicts");
438       params.setMergeDialogCustomizer(new UnstashMergeDialogCustomizer(stashInfo));
439       return params;
440     }
441
442     @Override
443     protected void notifyUnresolvedRemain() {
444       GitVcs.IMPORTANT_ERROR_NOTIFICATION.createNotification("Conflicts were not resolved during unstash",
445                                                 "Unstash is not complete, you have unresolved merges in your working tree<br/>" +
446                                                 "<a href='resolve'>Resolve</a> conflicts.",
447                                                 NotificationType.WARNING, new NotificationListener() {
448           @Override
449           public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
450             if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
451               if (event.getDescription().equals("resolve")) {
452                 new UnstashConflictResolver(myProject, myRoot, myStashInfo).mergeNoProceed();
453               }
454             }
455           }
456       }).notify(myProject);
457     }
458   }
459
460   private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer {
461
462     private final StashInfo myStashInfo;
463
464     public UnstashMergeDialogCustomizer(StashInfo stashInfo) {
465       myStashInfo = stashInfo;
466     }
467
468     @Override
469     public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
470       return "<html>Conflicts during unstashing <code>" + myStashInfo.getStash() + "\"" + myStashInfo.getMessage() + "\"</code></html>";
471     }
472
473     @Override
474     public String getLeftPanelTitle(VirtualFile file) {
475       return "Local changes";
476     }
477
478     @Override
479     public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
480       return "Changes from stash";
481     }
482   }
483 }