820749a286c66391683350c2f197dd7d96399bc3
[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.openapi.progress.ProgressIndicator;
19 import com.intellij.openapi.progress.ProgressManager;
20 import com.intellij.openapi.progress.Task;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.ui.DialogWrapper;
23 import com.intellij.openapi.ui.Messages;
24 import com.intellij.openapi.util.Key;
25 import com.intellij.openapi.vcs.VcsException;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.ui.DocumentAdapter;
28 import com.intellij.util.Consumer;
29 import git4idea.GitBranch;
30 import git4idea.GitRevisionNumber;
31 import git4idea.GitVcs;
32 import git4idea.actions.GitShowAllSubmittedFilesAction;
33 import git4idea.commands.*;
34 import git4idea.config.GitVersionSpecialty;
35 import git4idea.i18n.GitBundle;
36 import git4idea.stash.GitStashUtils;
37 import git4idea.validators.GitBranchNameValidator;
38 import org.jetbrains.annotations.NotNull;
39
40 import javax.swing.*;
41 import javax.swing.event.DocumentEvent;
42 import javax.swing.event.ListSelectionEvent;
43 import javax.swing.event.ListSelectionListener;
44 import java.awt.event.ActionEvent;
45 import java.awt.event.ActionListener;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Set;
49 import java.util.concurrent.atomic.AtomicBoolean;
50
51 /**
52  * The unstash dialog
53  */
54 public class GitUnstashDialog extends DialogWrapper {
55   /**
56    * Git root selector
57    */
58   private JComboBox myGitRootComboBox;
59   /**
60    * The current branch label
61    */
62   private JLabel myCurrentBranch;
63   /**
64    * The view stash button
65    */
66   private JButton myViewButton;
67   /**
68    * The drop stash button
69    */
70   private JButton myDropButton;
71   /**
72    * The clear stashes button
73    */
74   private JButton myClearButton;
75   /**
76    * The pop stash checkbox
77    */
78   private JCheckBox myPopStashCheckBox;
79   /**
80    * The branch text field
81    */
82   private JTextField myBranchTextField;
83   /**
84    * The root panel of the dialog
85    */
86   private JPanel myPanel;
87   /**
88    * The stash list
89    */
90   private JList myStashList;
91   /**
92    * If this checkbox is selected, the index is reinstated as well as working tree
93    */
94   private JCheckBox myReinstateIndexCheckBox;
95   /**
96    * Set of branches for the current root
97    */
98   private final HashSet<String> myBranches = new HashSet<String>();
99
100   /**
101    * The project
102    */
103   private final Project myProject;
104   private GitVcs myVcs;
105
106   /**
107    * A constructor
108    *
109    * @param project     the project
110    * @param roots       the list of the roots
111    * @param defaultRoot the default root to select
112    */
113   public GitUnstashDialog(final Project project, final List<VirtualFile> roots, final VirtualFile defaultRoot) {
114     super(project, true);
115     myProject = project;
116     myVcs = GitVcs.getInstance(project);
117     setTitle(GitBundle.getString("unstash.title"));
118     setOKButtonText(GitBundle.getString("unstash.button.apply"));
119     GitUIUtil.setupRootChooser(project, roots, defaultRoot, myGitRootComboBox, myCurrentBranch);
120     myStashList.setModel(new DefaultListModel());
121     refreshStashList();
122     myGitRootComboBox.addActionListener(new ActionListener() {
123       public void actionPerformed(final ActionEvent e) {
124         refreshStashList();
125         updateDialogState();
126       }
127     });
128     myStashList.addListSelectionListener(new ListSelectionListener() {
129       public void valueChanged(final ListSelectionEvent e) {
130         updateDialogState();
131       }
132     });
133     myBranchTextField.getDocument().addDocumentListener(new DocumentAdapter() {
134       protected void textChanged(final DocumentEvent e) {
135         updateDialogState();
136       }
137     });
138     myPopStashCheckBox.addActionListener(new ActionListener() {
139       public void actionPerformed(ActionEvent e) {
140         updateDialogState();
141       }
142     });
143     myClearButton.addActionListener(new ActionListener() {
144       public void actionPerformed(final ActionEvent e) {
145         if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(),
146                                                      GitBundle.message("git.unstash.clear.confirmation.message"),
147                                                      GitBundle.message("git.unstash.clear.confirmation.title"), Messages.getWarningIcon())) {
148           GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH);
149           h.setNoSSH(true);
150           h.addParameters("clear");
151           GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.clearing.stashes"), h.printableCommandLine());
152           refreshStashList();
153           updateDialogState();
154         }
155       }
156     });
157     myDropButton.addActionListener(new ActionListener() {
158       public void actionPerformed(final ActionEvent e) {
159         final StashInfo stash = getSelectedStash();
160         if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(),
161                                                      GitBundle.message("git.unstash.drop.confirmation.message", stash.getStash(), stash.getMessage()),
162                                                      GitBundle.message("git.unstash.drop.confirmation.title", stash.getStash()), Messages.getQuestionIcon())) {
163           ProgressManager.getInstance().run(new Task.Modal(myProject, "Removing stash " + stash.getStash(), false) {
164             @Override
165             public void run(@NotNull ProgressIndicator indicator) {
166               GitSimpleHandler h = dropHandler(stash.getStash());
167               try {
168                 h.run();
169                 h.unsilence();
170               }
171               catch (VcsException ex) {
172                 try {
173                   //noinspection HardCodedStringLiteral
174                   if (ex.getMessage().startsWith("fatal: Needed a single revision")) {
175                     h = dropHandler(translateStash(stash.getStash()));
176                     h.run();
177                   }
178                   else {
179                     h.unsilence();
180                     throw ex;
181                   }
182                 }
183                 catch (VcsException ex2) {
184                   GitUIUtil.showOperationError(myProject, ex, h.printableCommandLine());
185                   return;
186                 }
187               }
188             }
189           });
190           refreshStashList();
191           updateDialogState();
192         }
193       }
194
195       private GitSimpleHandler dropHandler(String stash) {
196         GitSimpleHandler h = new GitSimpleHandler(myProject, getGitRoot(), GitCommand.STASH);
197         h.setNoSSH(true);
198         h.addParameters("drop", stash);
199         return h;
200       }
201     });
202     myViewButton.addActionListener(new ActionListener() {
203       public void actionPerformed(final ActionEvent e) {
204         final VirtualFile root = getGitRoot();
205         String resolvedStash;
206         String selectedStash = getSelectedStash().getStash();
207         try {
208           resolvedStash = GitRevisionNumber.resolve(myProject, root, selectedStash).asString();
209         }
210         catch (VcsException ex) {
211           try {
212             //noinspection HardCodedStringLiteral
213             if (ex.getMessage().startsWith("fatal: bad revision 'stash@")) {
214               selectedStash = translateStash(selectedStash);
215               resolvedStash = GitRevisionNumber.resolve(myProject, root, selectedStash).asString();
216             }
217             else {
218               throw ex;
219             }
220           }
221           catch (VcsException ex2) {
222             GitUIUtil.showOperationError(myProject, ex, "resolving revision");
223             return;
224           }
225         }
226         GitShowAllSubmittedFilesAction.showSubmittedFiles(myProject, resolvedStash, root);
227       }
228     });
229     init();
230     updateDialogState();
231   }
232
233   /**
234    * Translate stash name so that { } are escaped.
235    *
236    * @param selectedStash a selected stash
237    * @return translated name
238    */
239   private static String translateStash(String selectedStash) {
240     return selectedStash.replaceAll("([\\{}])", "\\\\$1");
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   }
323
324   /**
325    * @return the selected git root
326    */
327   private VirtualFile getGitRoot() {
328     return (VirtualFile)myGitRootComboBox.getSelectedItem();
329   }
330
331   /**
332    * @param escaped if true stash name will be escaped
333    * @return unstash handler
334    */
335   private GitLineHandler handler(boolean escaped) {
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     if (escaped) {
350       selectedStash = translateStash(selectedStash);
351     } else if (GitVersionSpecialty.NEEDS_QUOTES_IN_STASH_NAME.existsIn(myVcs.getVersion())) { // else if, because escaping {} also solves the issue
352       selectedStash = "\"" + selectedStash + "\"";
353     }
354     h.addParameters(selectedStash);
355     return h;
356   }
357
358   /**
359    * @return selected stash
360    * @throws NullPointerException if no stash is selected
361    */
362   private StashInfo getSelectedStash() {
363     return (StashInfo)myStashList.getSelectedValue();
364   }
365
366   /**
367    * {@inheritDoc}
368    */
369   protected JComponent createCenterPanel() {
370     return myPanel;
371   }
372
373   /**
374    * {@inheritDoc}
375    */
376   @Override
377   protected String getDimensionServiceKey() {
378     return getClass().getName();
379   }
380
381   /**
382    * {@inheritDoc}
383    */
384   @Override
385   protected String getHelpId() {
386     return "reference.VersionControl.Git.Unstash";
387   }
388
389   /**
390    * Show unstash dialog and process its result
391    *
392    * @param project       the context project
393    * @param gitRoots      the git roots
394    * @param defaultRoot   the default git root
395    * @param affectedRoots the affected roots
396    */
397   public static void showUnstashDialog(Project project,
398                                        List<VirtualFile> gitRoots,
399                                        VirtualFile defaultRoot,
400                                        Set<VirtualFile> affectedRoots) {
401     GitUnstashDialog d = new GitUnstashDialog(project, gitRoots, defaultRoot);
402     d.show();
403     if (!d.isOK()) {
404       return;
405     }
406     affectedRoots.add(d.getGitRoot());
407     GitLineHandler h = d.handler(false);
408     final AtomicBoolean needToEscapedBraces = new AtomicBoolean(false);
409     h.addLineListener(new GitLineHandlerAdapter() {
410       public void onLineAvailable(String line, Key outputType) {
411         if (line.startsWith("fatal: Needed a single revision")) {
412           needToEscapedBraces.set(true);
413         }
414       }
415     });
416     int rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false);
417     if (needToEscapedBraces.get()) {
418       h = d.handler(true);
419       rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false);
420     }
421     if (rc != 0) {
422       GitUIUtil.showOperationErrors(project, h.errors(), h.printableCommandLine());
423     }
424   }
425 }