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