2 * Copyright 2000-2009 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.
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;
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;
56 import java.util.concurrent.atomic.AtomicBoolean;
61 public class GitUnstashDialog extends DialogWrapper {
65 private JComboBox myGitRootComboBox;
67 * The current branch label
69 private JLabel myCurrentBranch;
71 * The view stash button
73 private JButton myViewButton;
75 * The drop stash button
77 private JButton myDropButton;
79 * The clear stashes button
81 private JButton myClearButton;
83 * The pop stash checkbox
85 private JCheckBox myPopStashCheckBox;
87 * The branch text field
89 private JTextField myBranchTextField;
91 * The root panel of the dialog
93 private JPanel myPanel;
97 private JList myStashList;
99 * If this checkbox is selected, the index is reinstated as well as working tree
101 private JCheckBox myReinstateIndexCheckBox;
103 * Set of branches for the current root
105 private final HashSet<String> myBranches = new HashSet<String>();
110 private final Project myProject;
111 private GitVcs myVcs;
112 private static final Logger LOG = Logger.getInstance(GitUnstashDialog.class);
117 * @param project the project
118 * @param roots the list of the roots
119 * @param defaultRoot the default root to select
121 public GitUnstashDialog(final Project project, final List<VirtualFile> roots, final VirtualFile defaultRoot) {
122 super(project, true);
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());
130 myGitRootComboBox.addActionListener(new ActionListener() {
131 public void actionPerformed(final ActionEvent e) {
136 myStashList.addListSelectionListener(new ListSelectionListener() {
137 public void valueChanged(final ListSelectionEvent e) {
141 myBranchTextField.getDocument().addDocumentListener(new DocumentAdapter() {
142 protected void textChanged(final DocumentEvent e) {
146 myPopStashCheckBox.addActionListener(new ActionListener() {
147 public void actionPerformed(ActionEvent e) {
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);
158 h.addParameters("clear");
159 GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.clearing.stashes"), h.printableCommandLine());
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) {
173 public void run(@NotNull ProgressIndicator indicator) {
174 GitSimpleHandler h = dropHandler(stash.getStash());
179 catch (VcsException ex) {
181 //noinspection HardCodedStringLiteral
182 if (ex.getMessage().startsWith("fatal: Needed a single revision")) {
183 h = dropHandler(translateStash(stash.getStash()));
191 catch (VcsException ex2) {
192 GitUIUtil.showOperationError(myProject, ex, h.printableCommandLine());
203 private GitSimpleHandler dropHandler(String stash) {
204 GitSimpleHandler h = new GitSimpleHandler(myProject, getGitRoot(), GitCommand.STASH);
206 h.addParameters("drop", stash);
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();
216 resolvedStash = GitRevisionNumber.resolve(myProject, root, selectedStash).asString();
218 catch (VcsException ex) {
220 //noinspection HardCodedStringLiteral
221 if (ex.getMessage().startsWith("fatal: bad revision 'stash@")) {
222 selectedStash = translateStash(selectedStash);
223 resolvedStash = GitRevisionNumber.resolve(myProject, root, selectedStash).asString();
229 catch (VcsException ex2) {
230 GitUIUtil.showOperationError(myProject, ex, "resolving revision");
234 GitShowAllSubmittedFilesAction.showSubmittedFiles(myProject, resolvedStash, root);
242 * Translate stash name so that { } are escaped.
244 * @param selectedStash a selected stash
245 * @return translated name
247 private static String translateStash(String selectedStash) {
248 return selectedStash.replaceAll("([\\{}])", "\\\\$1");
252 * Update state dialog depending on the current state of the fields
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);
267 if (myBranches.contains(branch)) {
268 setErrorText(GitBundle.getString("unstash.error.branch.exists"));
269 setOKActionEnabled(false);
274 if (!myPopStashCheckBox.isEnabled()) {
275 myPopStashCheckBox.setSelected(false);
277 myPopStashCheckBox.setEnabled(true);
279 myPopStashCheckBox.isSelected() ? GitBundle.getString("unstash.button.pop") : GitBundle.getString("unstash.button.apply"));
280 if (!myReinstateIndexCheckBox.isEnabled()) {
281 myReinstateIndexCheckBox.setSelected(false);
283 myReinstateIndexCheckBox.setEnabled(true);
285 if (myStashList.getModel().getSize() == 0) {
286 myViewButton.setEnabled(false);
287 myDropButton.setEnabled(false);
288 myClearButton.setEnabled(false);
290 setOKActionEnabled(false);
294 myClearButton.setEnabled(true);
296 if (myStashList.getSelectedIndex() == -1) {
297 myViewButton.setEnabled(false);
298 myDropButton.setEnabled(false);
300 setOKActionEnabled(false);
304 myViewButton.setEnabled(true);
305 myDropButton.setEnabled(true);
308 setOKActionEnabled(true);
314 private void refreshStashList() {
315 final DefaultListModel listModel = (DefaultListModel)myStashList.getModel();
317 GitStashUtils.loadStashStack(myProject, getGitRoot(), new Consumer<StashInfo>() {
319 public void consume(StashInfo stashInfo) {
320 listModel.addElement(stashInfo);
325 GitBranch.listAsStrings(myProject, getGitRoot(), false, true, myBranches, null);
327 catch (VcsException e) {
333 * @return the selected git root
335 private VirtualFile getGitRoot() {
336 return (VirtualFile)myGitRootComboBox.getSelectedItem();
340 * @param escaped if true stash name will be escaped
341 * @return unstash handler
343 private GitLineHandler handler(boolean escaped) {
344 GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH);
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");
354 h.addParameters("branch", branch);
356 String selectedStash = getSelectedStash().getStash();
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 + "\"";
362 h.addParameters(selectedStash);
367 * @return selected stash
368 * @throws NullPointerException if no stash is selected
370 private StashInfo getSelectedStash() {
371 return (StashInfo)myStashList.getSelectedValue();
377 protected JComponent createCenterPanel() {
385 protected String getDimensionServiceKey() {
386 return getClass().getName();
393 protected String getHelpId() {
394 return "reference.VersionControl.Git.Unstash";
398 * Show unstash dialog and process its result
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
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);
414 affectedRoots.add(d.getGitRoot());
415 GitLineHandler h = d.handler(false);
416 final AtomicBoolean needToEscapedBraces = new AtomicBoolean(false);
417 final AtomicBoolean conflict = new AtomicBoolean();
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")) {
428 int rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false);
429 if (needToEscapedBraces.get()) {
431 rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false);
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);
441 } else if (rc != 0) {
442 GitUIUtil.showOperationErrors(project, h.errors(), h.printableCommandLine());
446 private static class UnstashConflictResolver extends GitMergeConflictResolver {
447 private StashInfo myStashInfo;
449 public UnstashConflictResolver(Project project, StashInfo stashInfo) {
450 super(project, false, new UnstashMergeDialogCustomizer(stashInfo), "Unstashed with conflicts", "");
451 myStashInfo = stashInfo;
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() {
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);
472 private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer {
474 private final StashInfo myStashInfo;
476 public UnstashMergeDialogCustomizer(StashInfo stashInfo) {
477 myStashInfo = stashInfo;
481 public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
482 return "<html>Conflicts during unstashing <code>" + myStashInfo.getStash() + "\"" + myStashInfo.getMessage() + "\"</code></html>";
486 public String getLeftPanelTitle(VirtualFile file) {
487 return "Local changes";
491 public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
492 return "Changes from stash";