| /* |
| * 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.notification.NotificationType; |
| 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.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.*; |
| 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<String> myBranches = new HashSet<String>(); |
| |
| /** |
| * 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<VirtualFile> 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<StashInfo>() { |
| @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<VirtualFile> 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() { |
| GitVcs.IMPORTANT_ERROR_NOTIFICATION.createNotification("Conflicts were not resolved during unstash", |
| "Unstash is not complete, you have unresolved merges in your working tree<br/>" + |
| "<a href='resolve'>Resolve</a> conflicts.", |
| NotificationType.WARNING, 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(); |
| } |
| } |
| } |
| }).notify(myProject); |
| } |
| } |
| |
| private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer { |
| |
| private final StashInfo myStashInfo; |
| |
| public UnstashMergeDialogCustomizer(StashInfo stashInfo) { |
| myStashInfo = stashInfo; |
| } |
| |
| @Override |
| public String getMultipleFileMergeDescription(Collection<VirtualFile> files) { |
| return "<html>Conflicts during unstashing <code>" + myStashInfo.getStash() + "\"" + myStashInfo.getMessage() + "\"</code></html>"; |
| } |
| |
| @Override |
| public String getLeftPanelTitle(VirtualFile file) { |
| return "Local changes"; |
| } |
| |
| @Override |
| public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) { |
| return "Changes from stash"; |
| } |
| } |
| } |