[Git] Fix merge dialog sides for unstash and cherry-pick. Relates to IDEA-51187
[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.notification.Notifications;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.progress.ProgressIndicator;
24 import com.intellij.openapi.progress.ProgressManager;
25 import com.intellij.openapi.progress.Task;
26 import com.intellij.openapi.project.Project;
27 import com.intellij.openapi.ui.DialogWrapper;
28 import com.intellij.openapi.ui.Messages;
29 import com.intellij.openapi.util.Key;
30 import com.intellij.openapi.vcs.VcsException;
31 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
32 import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.ui.DocumentAdapter;
35 import com.intellij.util.Consumer;
36 import git4idea.GitBranch;
37 import git4idea.GitRevisionNumber;
38 import git4idea.GitVcs;
39 import git4idea.actions.GitShowAllSubmittedFilesAction;
40 import git4idea.commands.*;
41 import git4idea.config.GitVersionSpecialty;
42 import git4idea.i18n.GitBundle;
43 import git4idea.merge.GitMergeConflictResolver;
44 import git4idea.stash.GitStashUtils;
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     myProject = project;
124     myVcs = GitVcs.getInstance(project);
125     setTitle(GitBundle.getString("unstash.title"));
126     setOKButtonText(GitBundle.getString("unstash.button.apply"));
127     GitUIUtil.setupRootChooser(project, roots, defaultRoot, myGitRootComboBox, myCurrentBranch);
128     myStashList.setModel(new DefaultListModel());
129     refreshStashList();
130     myGitRootComboBox.addActionListener(new ActionListener() {
131       public void actionPerformed(final ActionEvent e) {
132         refreshStashList();
133         updateDialogState();
134       }
135     });
136     myStashList.addListSelectionListener(new ListSelectionListener() {
137       public void valueChanged(final ListSelectionEvent e) {
138         updateDialogState();
139       }
140     });
141     myBranchTextField.getDocument().addDocumentListener(new DocumentAdapter() {
142       protected void textChanged(final DocumentEvent e) {
143         updateDialogState();
144       }
145     });
146     myPopStashCheckBox.addActionListener(new ActionListener() {
147       public void actionPerformed(ActionEvent e) {
148         updateDialogState();
149       }
150     });
151     myClearButton.addActionListener(new ActionListener() {
152       public void actionPerformed(final ActionEvent e) {
153         if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(),
154                                                      GitBundle.message("git.unstash.clear.confirmation.message"),
155                                                      GitBundle.message("git.unstash.clear.confirmation.title"), Messages.getWarningIcon())) {
156           GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH);
157           h.setNoSSH(true);
158           h.addParameters("clear");
159           GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.clearing.stashes"), h.printableCommandLine());
160           refreshStashList();
161           updateDialogState();
162         }
163       }
164     });
165     myDropButton.addActionListener(new ActionListener() {
166       public void actionPerformed(final ActionEvent e) {
167         final StashInfo stash = getSelectedStash();
168         if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(),
169                                                      GitBundle.message("git.unstash.drop.confirmation.message", stash.getStash(), stash.getMessage()),
170                                                      GitBundle.message("git.unstash.drop.confirmation.title", stash.getStash()), Messages.getQuestionIcon())) {
171           ProgressManager.getInstance().run(new Task.Modal(myProject, "Removing stash " + stash.getStash(), false) {
172             @Override
173             public void run(@NotNull ProgressIndicator indicator) {
174               GitSimpleHandler h = dropHandler(stash.getStash());
175               try {
176                 h.run();
177                 h.unsilence();
178               }
179               catch (VcsException ex) {
180                 try {
181                   //noinspection HardCodedStringLiteral
182                   if (ex.getMessage().startsWith("fatal: Needed a single revision")) {
183                     h = dropHandler(translateStash(stash.getStash()));
184                     h.run();
185                   }
186                   else {
187                     h.unsilence();
188                     throw ex;
189                   }
190                 }
191                 catch (VcsException ex2) {
192                   GitUIUtil.showOperationError(myProject, ex, h.printableCommandLine());
193                   return;
194                 }
195               }
196             }
197           });
198           refreshStashList();
199           updateDialogState();
200         }
201       }
202
203       private GitSimpleHandler dropHandler(String stash) {
204         GitSimpleHandler h = new GitSimpleHandler(myProject, getGitRoot(), GitCommand.STASH);
205         h.setNoSSH(true);
206         h.addParameters("drop", stash);
207         return h;
208       }
209     });
210     myViewButton.addActionListener(new ActionListener() {
211       public void actionPerformed(final ActionEvent e) {
212         final VirtualFile root = getGitRoot();
213         String resolvedStash;
214         String selectedStash = getSelectedStash().getStash();
215         try {
216           resolvedStash = GitRevisionNumber.resolve(myProject, root, selectedStash).asString();
217         }
218         catch (VcsException ex) {
219           try {
220             //noinspection HardCodedStringLiteral
221             if (ex.getMessage().startsWith("fatal: bad revision 'stash@")) {
222               selectedStash = translateStash(selectedStash);
223               resolvedStash = GitRevisionNumber.resolve(myProject, root, selectedStash).asString();
224             }
225             else {
226               throw ex;
227             }
228           }
229           catch (VcsException ex2) {
230             GitUIUtil.showOperationError(myProject, ex, "resolving revision");
231             return;
232           }
233         }
234         GitShowAllSubmittedFilesAction.showSubmittedFiles(myProject, resolvedStash, root);
235       }
236     });
237     init();
238     updateDialogState();
239   }
240
241   /**
242    * Translate stash name so that { } are escaped.
243    *
244    * @param selectedStash a selected stash
245    * @return translated name
246    */
247   private static String translateStash(String selectedStash) {
248     return selectedStash.replaceAll("([\\{}])", "\\\\$1");
249   }
250
251   /**
252    * Update state dialog depending on the current state of the fields
253    */
254   private void updateDialogState() {
255     String branch = myBranchTextField.getText();
256     if (branch.length() != 0) {
257       setOKButtonText(GitBundle.getString("unstash.button.branch"));
258       myPopStashCheckBox.setEnabled(false);
259       myPopStashCheckBox.setSelected(true);
260       myReinstateIndexCheckBox.setEnabled(false);
261       myReinstateIndexCheckBox.setSelected(true);
262       if (!GitBranchNameValidator.INSTANCE.checkInput(branch)) {
263         setErrorText(GitBundle.getString("unstash.error.invalid.branch.name"));
264         setOKActionEnabled(false);
265         return;
266       }
267       if (myBranches.contains(branch)) {
268         setErrorText(GitBundle.getString("unstash.error.branch.exists"));
269         setOKActionEnabled(false);
270         return;
271       }
272     }
273     else {
274       if (!myPopStashCheckBox.isEnabled()) {
275         myPopStashCheckBox.setSelected(false);
276       }
277       myPopStashCheckBox.setEnabled(true);
278       setOKButtonText(
279         myPopStashCheckBox.isSelected() ? GitBundle.getString("unstash.button.pop") : GitBundle.getString("unstash.button.apply"));
280       if (!myReinstateIndexCheckBox.isEnabled()) {
281         myReinstateIndexCheckBox.setSelected(false);
282       }
283       myReinstateIndexCheckBox.setEnabled(true);
284     }
285     if (myStashList.getModel().getSize() == 0) {
286       myViewButton.setEnabled(false);
287       myDropButton.setEnabled(false);
288       myClearButton.setEnabled(false);
289       setErrorText(null);
290       setOKActionEnabled(false);
291       return;
292     }
293     else {
294       myClearButton.setEnabled(true);
295     }
296     if (myStashList.getSelectedIndex() == -1) {
297       myViewButton.setEnabled(false);
298       myDropButton.setEnabled(false);
299       setErrorText(null);
300       setOKActionEnabled(false);
301       return;
302     }
303     else {
304       myViewButton.setEnabled(true);
305       myDropButton.setEnabled(true);
306     }
307     setErrorText(null);
308     setOKActionEnabled(true);
309   }
310
311   /**
312    * Refresh stash list
313    */
314   private void refreshStashList() {
315     final DefaultListModel listModel = (DefaultListModel)myStashList.getModel();
316     listModel.clear();
317     GitStashUtils.loadStashStack(myProject, getGitRoot(), new Consumer<StashInfo>() {
318       @Override
319       public void consume(StashInfo stashInfo) {
320         listModel.addElement(stashInfo);
321       }
322     });
323     myBranches.clear();
324     try {
325       GitBranch.listAsStrings(myProject, getGitRoot(), false, true, myBranches, null);
326     }
327     catch (VcsException e) {
328       // ignore error
329     }
330   }
331
332   /**
333    * @return the selected git root
334    */
335   private VirtualFile getGitRoot() {
336     return (VirtualFile)myGitRootComboBox.getSelectedItem();
337   }
338
339   /**
340    * @param escaped if true stash name will be escaped
341    * @return unstash handler
342    */
343   private GitLineHandler handler(boolean escaped) {
344     GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH);
345     h.setNoSSH(true);
346     String branch = myBranchTextField.getText();
347     if (branch.length() == 0) {
348       h.addParameters(myPopStashCheckBox.isSelected() ? "pop" : "apply");
349       if (myReinstateIndexCheckBox.isSelected()) {
350         h.addParameters("--index");
351       }
352     }
353     else {
354       h.addParameters("branch", branch);
355     }
356     String selectedStash = getSelectedStash().getStash();
357     if (escaped) {
358       selectedStash = translateStash(selectedStash);
359     } else if (GitVersionSpecialty.NEEDS_QUOTES_IN_STASH_NAME.existsIn(myVcs.getVersion())) { // else if, because escaping {} also solves the issue
360       selectedStash = "\"" + selectedStash + "\"";
361     }
362     h.addParameters(selectedStash);
363     return h;
364   }
365
366   /**
367    * @return selected stash
368    * @throws NullPointerException if no stash is selected
369    */
370   private StashInfo getSelectedStash() {
371     return (StashInfo)myStashList.getSelectedValue();
372   }
373
374   /**
375    * {@inheritDoc}
376    */
377   protected JComponent createCenterPanel() {
378     return myPanel;
379   }
380
381   /**
382    * {@inheritDoc}
383    */
384   @Override
385   protected String getDimensionServiceKey() {
386     return getClass().getName();
387   }
388
389   /**
390    * {@inheritDoc}
391    */
392   @Override
393   protected String getHelpId() {
394     return "reference.VersionControl.Git.Unstash";
395   }
396
397   /**
398    * Show unstash dialog and process its result
399    *
400    * @param project       the context project
401    * @param gitRoots      the git roots
402    * @param defaultRoot   the default git root
403    * @param affectedRoots the affected roots
404    */
405   public static void showUnstashDialog(Project project,
406                                        List<VirtualFile> gitRoots,
407                                        VirtualFile defaultRoot,
408                                        Set<VirtualFile> affectedRoots) {
409     GitUnstashDialog d = new GitUnstashDialog(project, gitRoots, defaultRoot);
410     d.show();
411     if (!d.isOK()) {
412       return;
413     }
414     affectedRoots.add(d.getGitRoot());
415     GitLineHandler h = d.handler(false);
416     final AtomicBoolean needToEscapedBraces = new AtomicBoolean(false);
417     final AtomicBoolean conflict = new AtomicBoolean();
418
419     h.addLineListener(new GitLineHandlerAdapter() {
420       public void onLineAvailable(String line, Key outputType) {
421         if (line.startsWith("fatal: Needed a single revision")) {
422           needToEscapedBraces.set(true);
423         } else if (line.contains("Merge conflict")) {
424           conflict.set(true);
425         }
426       }
427     });
428     int rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false);
429     if (needToEscapedBraces.get()) {
430       h = d.handler(true);
431       rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false);
432     }
433
434     if (conflict.get()) {
435       VirtualFile root = d.getGitRoot();
436       boolean conflictsResolved = new UnstashConflictResolver(project, d.getSelectedStash()).merge(Collections.singleton(root));
437       if (conflictsResolved) {
438         LOG.info("loadRoot " + root + " conflicts resolved, dropping stash");
439         GitStashUtils.dropStash(project, root);
440       }
441     } else if (rc != 0) {
442       GitUIUtil.showOperationErrors(project, h.errors(), h.printableCommandLine());
443     }
444   }
445
446   private static class UnstashConflictResolver extends GitMergeConflictResolver {
447     private StashInfo myStashInfo;
448
449     public UnstashConflictResolver(Project project, StashInfo stashInfo) {
450       super(project, false, new UnstashMergeDialogCustomizer(stashInfo), "Unstashed with conflicts", "");
451       myStashInfo = stashInfo;
452     }
453
454     @Override
455     protected void notifyUnresolvedRemain(final Collection<VirtualFile> roots) {
456       Notifications.Bus.notify(new Notification(GitVcs.IMPORTANT_ERROR_NOTIFICATION, "Conflicts were not resolved during unstash",
457                                                 "Unstash is not complete, you have unresolved merges in your working tree<br/>" +
458                                                 "<a href='resolve'>Resolve</a> conflicts.",
459                                                 NotificationType.WARNING, new NotificationListener() {
460           @Override
461           public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
462             if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
463               if (event.getDescription().equals("resolve")) {
464                 new UnstashConflictResolver(myProject, myStashInfo).justMerge(roots);
465               }
466             }
467           }
468       }));
469     }
470   }
471
472   private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer {
473
474     private final StashInfo myStashInfo;
475
476     public UnstashMergeDialogCustomizer(StashInfo stashInfo) {
477       myStashInfo = stashInfo;
478     }
479
480     @Override
481     public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
482       return "<html>Conflicts during unstashing <code>" + myStashInfo.getStash() + "\"" + myStashInfo.getMessage() + "\"</code></html>";
483     }
484
485     @Override
486     public String getLeftPanelTitle(VirtualFile file) {
487       return "Local changes";
488     }
489
490     @Override
491     public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
492       return "Changes from stash";
493     }
494   }
495 }