/* * Copyright 2000-2013 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package git4idea.ui; import com.intellij.CommonBundle; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Key; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.VcsNotifier; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.DocumentAdapter; import com.intellij.util.Consumer; import git4idea.GitPlatformFacade; import git4idea.GitRevisionNumber; import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.branch.GitBranchUtil; import git4idea.commands.*; import git4idea.config.GitVersionSpecialty; import git4idea.i18n.GitBundle; import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; import git4idea.stash.GitStashUtils; import git4idea.util.GitUIUtil; import git4idea.validators.GitBranchNameValidator; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.HyperlinkEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** * The unstash dialog */ public class GitUnstashDialog extends DialogWrapper { /** * Git root selector */ private JComboBox myGitRootComboBox; /** * The current branch label */ private JLabel myCurrentBranch; /** * The view stash button */ private JButton myViewButton; /** * The drop stash button */ private JButton myDropButton; /** * The clear stashes button */ private JButton myClearButton; /** * The pop stash checkbox */ private JCheckBox myPopStashCheckBox; /** * The branch text field */ private JTextField myBranchTextField; /** * The root panel of the dialog */ private JPanel myPanel; /** * The stash list */ private JList myStashList; /** * If this checkbox is selected, the index is reinstated as well as working tree */ private JCheckBox myReinstateIndexCheckBox; /** * Set of branches for the current root */ private final HashSet myBranches = new HashSet(); /** * The project */ private final Project myProject; private GitVcs myVcs; private static final Logger LOG = Logger.getInstance(GitUnstashDialog.class); /** * A constructor * * @param project the project * @param roots the list of the roots * @param defaultRoot the default root to select */ public GitUnstashDialog(final Project project, final List roots, final VirtualFile defaultRoot) { super(project, true); setModal(false); myProject = project; myVcs = GitVcs.getInstance(project); setTitle(GitBundle.getString("unstash.title")); setOKButtonText(GitBundle.getString("unstash.button.apply")); setCancelButtonText(CommonBundle.getCloseButtonText()); GitUIUtil.setupRootChooser(project, roots, defaultRoot, myGitRootComboBox, myCurrentBranch); myStashList.setModel(new DefaultListModel()); refreshStashList(); myGitRootComboBox.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { refreshStashList(); updateDialogState(); } }); myStashList.addListSelectionListener(new ListSelectionListener() { public void valueChanged(final ListSelectionEvent e) { updateDialogState(); } }); myBranchTextField.getDocument().addDocumentListener(new DocumentAdapter() { protected void textChanged(final DocumentEvent e) { updateDialogState(); } }); myPopStashCheckBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { updateDialogState(); } }); myClearButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(), GitBundle.message("git.unstash.clear.confirmation.message"), GitBundle.message("git.unstash.clear.confirmation.title"), Messages.getWarningIcon())) { GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH); h.addParameters("clear"); GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.clearing.stashes"), h.printableCommandLine()); refreshStashList(); updateDialogState(); } } }); myDropButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { final StashInfo stash = getSelectedStash(); if (Messages.YES == Messages.showYesNoDialog(GitUnstashDialog.this.getContentPane(), GitBundle.message("git.unstash.drop.confirmation.message", stash.getStash(), stash.getMessage()), GitBundle.message("git.unstash.drop.confirmation.title", stash.getStash()), Messages.getQuestionIcon())) { final ModalityState current = ModalityState.current(); ProgressManager.getInstance().run(new Task.Modal(myProject, "Removing stash " + stash.getStash(), false) { @Override public void run(@NotNull ProgressIndicator indicator) { final GitSimpleHandler h = dropHandler(stash.getStash()); try { h.run(); h.unsilence(); } catch (final VcsException ex) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { GitUIUtil.showOperationError(myProject, ex, h.printableCommandLine()); } }, current); } } }); refreshStashList(); updateDialogState(); } } private GitSimpleHandler dropHandler(String stash) { GitSimpleHandler h = new GitSimpleHandler(myProject, getGitRoot(), GitCommand.STASH); h.addParameters("drop"); addStashParameter(h, stash); return h; } }); myViewButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { final VirtualFile root = getGitRoot(); String resolvedStash; String selectedStash = getSelectedStash().getStash(); try { GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.REV_LIST); h.setSilent(true); h.addParameters("--timestamp", "--max-count=1"); addStashParameter(h, selectedStash); h.endOptions(); final String output = h.run(); resolvedStash = GitRevisionNumber.parseRevlistOutputAsRevisionNumber(h, output).asString(); } catch (VcsException ex) { GitUIUtil.showOperationError(myProject, ex, "resolving revision"); return; } GitUtil.showSubmittedFiles(myProject, resolvedStash, root, true, false); } }); init(); updateDialogState(); } /** * Adds {@code stash@{x}} parameter to the handler, quotes it if needed. */ private void addStashParameter(@NotNull GitHandler handler, @NotNull String stash) { if (GitVersionSpecialty.NEEDS_QUOTES_IN_STASH_NAME.existsIn(myVcs.getVersion())) { handler.addParameters(GeneralCommandLine.inescapableQuote(stash)); } else { handler.addParameters(stash); } } /** * Update state dialog depending on the current state of the fields */ private void updateDialogState() { String branch = myBranchTextField.getText(); if (branch.length() != 0) { setOKButtonText(GitBundle.getString("unstash.button.branch")); myPopStashCheckBox.setEnabled(false); myPopStashCheckBox.setSelected(true); myReinstateIndexCheckBox.setEnabled(false); myReinstateIndexCheckBox.setSelected(true); if (!GitBranchNameValidator.INSTANCE.checkInput(branch)) { setErrorText(GitBundle.getString("unstash.error.invalid.branch.name")); setOKActionEnabled(false); return; } if (myBranches.contains(branch)) { setErrorText(GitBundle.getString("unstash.error.branch.exists")); setOKActionEnabled(false); return; } } else { if (!myPopStashCheckBox.isEnabled()) { myPopStashCheckBox.setSelected(false); } myPopStashCheckBox.setEnabled(true); setOKButtonText( myPopStashCheckBox.isSelected() ? GitBundle.getString("unstash.button.pop") : GitBundle.getString("unstash.button.apply")); if (!myReinstateIndexCheckBox.isEnabled()) { myReinstateIndexCheckBox.setSelected(false); } myReinstateIndexCheckBox.setEnabled(true); } if (myStashList.getModel().getSize() == 0) { myViewButton.setEnabled(false); myDropButton.setEnabled(false); myClearButton.setEnabled(false); setErrorText(null); setOKActionEnabled(false); return; } else { myClearButton.setEnabled(true); } if (myStashList.getSelectedIndex() == -1) { myViewButton.setEnabled(false); myDropButton.setEnabled(false); setErrorText(null); setOKActionEnabled(false); return; } else { myViewButton.setEnabled(true); myDropButton.setEnabled(true); } setErrorText(null); setOKActionEnabled(true); } /** * Refresh stash list */ private void refreshStashList() { final DefaultListModel listModel = (DefaultListModel)myStashList.getModel(); listModel.clear(); VirtualFile root = getGitRoot(); GitStashUtils.loadStashStack(myProject, root, new Consumer() { @Override public void consume(StashInfo stashInfo) { listModel.addElement(stashInfo); } }); myBranches.clear(); GitRepository repository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(root); if (repository != null) { myBranches.addAll(GitBranchUtil.convertBranchesToNames(repository.getBranches().getLocalBranches())); } else { LOG.error("Repository is null for root " + root); } myStashList.setSelectedIndex(0); } /** * @return the selected git root */ private VirtualFile getGitRoot() { return (VirtualFile)myGitRootComboBox.getSelectedItem(); } /** * @return unstash handler */ private GitLineHandler handler() { GitLineHandler h = new GitLineHandler(myProject, getGitRoot(), GitCommand.STASH); String branch = myBranchTextField.getText(); if (branch.length() == 0) { h.addParameters(myPopStashCheckBox.isSelected() ? "pop" : "apply"); if (myReinstateIndexCheckBox.isSelected()) { h.addParameters("--index"); } } else { h.addParameters("branch", branch); } String selectedStash = getSelectedStash().getStash(); addStashParameter(h, selectedStash); return h; } /** * @return selected stash * @throws NullPointerException if no stash is selected */ private StashInfo getSelectedStash() { return (StashInfo)myStashList.getSelectedValue(); } /** * {@inheritDoc} */ protected JComponent createCenterPanel() { return myPanel; } /** * {@inheritDoc} */ @Override protected String getDimensionServiceKey() { return getClass().getName(); } /** * {@inheritDoc} */ @Override protected String getHelpId() { return "reference.VersionControl.Git.Unstash"; } @Override public JComponent getPreferredFocusedComponent() { return myStashList; } @Override protected void doOKAction() { VirtualFile root = getGitRoot(); GitLineHandler h = handler(); final AtomicBoolean conflict = new AtomicBoolean(); h.addLineListener(new GitLineHandlerAdapter() { public void onLineAvailable(String line, Key outputType) { if (line.contains("Merge conflict")) { conflict.set(true); } } }); int rc = GitHandlerUtil.doSynchronously(h, GitBundle.getString("unstash.unstashing"), h.printableCommandLine(), false); ServiceManager.getService(myProject, GitPlatformFacade.class).hardRefresh(root); if (conflict.get()) { boolean conflictsResolved = new UnstashConflictResolver(myProject, root, getSelectedStash()).merge(); LOG.info("loadRoot " + root + ", conflictsResolved: " + conflictsResolved); } else if (rc != 0) { GitUIUtil.showOperationErrors(myProject, h.errors(), h.printableCommandLine()); } super.doOKAction(); } public static void showUnstashDialog(Project project, List gitRoots, VirtualFile defaultRoot) { new GitUnstashDialog(project, gitRoots, defaultRoot).show(); // d is not modal=> everything else in doOKAction. } private static class UnstashConflictResolver extends GitConflictResolver { private final VirtualFile myRoot; private final StashInfo myStashInfo; public UnstashConflictResolver(Project project, VirtualFile root, StashInfo stashInfo) { super(project, ServiceManager.getService(Git.class), ServiceManager.getService(GitPlatformFacade.class), Collections.singleton(root), makeParams(stashInfo)); myRoot = root; myStashInfo = stashInfo; } private static Params makeParams(StashInfo stashInfo) { Params params = new Params(); params.setErrorNotificationTitle("Unstashed with conflicts"); params.setMergeDialogCustomizer(new UnstashMergeDialogCustomizer(stashInfo)); return params; } @Override protected void notifyUnresolvedRemain() { VcsNotifier.getInstance(myProject).notifyImportantWarning("Conflicts were not resolved during unstash", "Unstash is not complete, you have unresolved merges in your working tree
" + "Resolve conflicts.", new NotificationListener() { @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { if (event.getDescription().equals("resolve")) { new UnstashConflictResolver(myProject, myRoot, myStashInfo).mergeNoProceed(); } } } } ); } } private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer { private final StashInfo myStashInfo; public UnstashMergeDialogCustomizer(StashInfo stashInfo) { myStashInfo = stashInfo; } @Override public String getMultipleFileMergeDescription(Collection files) { return "Conflicts during unstashing " + myStashInfo.getStash() + "\"" + myStashInfo.getMessage() + "\""; } @Override public String getLeftPanelTitle(VirtualFile file) { return "Local changes"; } @Override public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) { return "Changes from stash"; } } }