From 4b73e06c20d1af4245aae4f637f85c413dc56565 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 6 Feb 2012 16:25:06 +0400 Subject: [PATCH 01/16] remove commented code --- .../ui/branch/GitBranchPopupActions.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java b/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java index efdd34f94929..4ae9edab67a2 100644 --- a/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java +++ b/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java @@ -53,7 +53,6 @@ class GitBranchPopupActions { ActionGroup createActions(@Nullable DefaultActionGroup toInsert) { DefaultActionGroup popupGroup = new DefaultActionGroup(null, false); - //popupGroup.addAction(new CurrentBranchAction(GitBranchUiUtil.getDisplayableBranchText(myRepository), "in root " + GitUIUtil.getShortRepositoryName(myRepository))); popupGroup.addAction(new NewBranchAction(myProject, Collections.singletonList(myRepository), myRepository)); popupGroup.addAction(new CheckoutRevisionActions(myProject, myRepository)); @@ -80,23 +79,6 @@ class GitBranchPopupActions { return popupGroup; } - ///** - // * "Current branch:" item which is disabled and is just a label to display the current branch. - // */ - //static class CurrentBranchAction extends DumbAwareAction { - // CurrentBranchAction(@NotNull String currentBranchName, @Nullable String rootName) { - // super("", String.format("Current branch is %s %s", currentBranchName, rootName), null); - // getTemplatePresentation().setText("Current Branch: " + currentBranchName, false); // no mnemonics - // } - // - // @Override public void actionPerformed(AnActionEvent e) { - // } - // - // @Override public void update(AnActionEvent e) { - // e.getPresentation().setEnabled(false); // this action works as a label - // } - //} - static class NewBranchAction extends DumbAwareAction { private final Project myProject; private final List myRepositories; From e4448bd74e3d3f31ccdbc54c494b01f3beabff94 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 6 Feb 2012 16:30:13 +0400 Subject: [PATCH 02/16] Git branch delete: remove extra repository refresh, it is done in the GitBranchOperation --- .../src/git4idea/branch/GitBranchOperationsProcessor.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java index df1989fdc3de..271d18a93d68 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java @@ -172,11 +172,7 @@ public final class GitBranchOperationsProcessor { } private void doDelete(final String branchName, ProgressIndicator indicator) { - GitDeleteBranchOperation operation = new GitDeleteBranchOperation(myProject, myRepositories, branchName, getCurrentBranch(), indicator); - operation.execute(); - for (GitRepository repository : myRepositories) { - repository.update(GitRepository.TrackedTopic.BRANCHES, GitRepository.TrackedTopic.CONFIG); - } + new GitDeleteBranchOperation(myProject, myRepositories, branchName, getCurrentBranch(), indicator).execute(); } /** From 74777152571d82fe4f9f7a27bed4608eb5637fae Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 6 Feb 2012 16:33:59 +0400 Subject: [PATCH 03/16] Git branches: Remove public where not needed --- .../src/git4idea/branch/GitCheckoutNewBranchOperation.java | 4 ++-- .../git4idea/src/git4idea/branch/GitCheckoutOperation.java | 4 ++-- .../src/git4idea/branch/GitDeleteBranchOperation.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java index ec9f340839e0..70529ec6cae0 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java @@ -37,13 +37,13 @@ import static git4idea.util.GitUIUtil.code; * * @author Kirill Likhodedov */ -public class GitCheckoutNewBranchOperation extends GitBranchOperation { +class GitCheckoutNewBranchOperation extends GitBranchOperation { @NotNull private final Project myProject; @NotNull private final String myNewBranchName; @NotNull private final String myPreviousBranch; - public GitCheckoutNewBranchOperation(@NotNull Project project, @NotNull Collection repositories, + GitCheckoutNewBranchOperation(@NotNull Project project, @NotNull Collection repositories, @NotNull String newBranchName, @NotNull String previousBranch, @NotNull ProgressIndicator indicator) { super(project, repositories, indicator); diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 06f93d1c9233..5b2638c045ea 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -68,7 +68,7 @@ import static git4idea.util.GitUIUtil.code; * * @author Kirill Likhodedov */ -public class GitCheckoutOperation extends GitBranchOperation { +class GitCheckoutOperation extends GitBranchOperation { private static final Logger LOG = Logger.getInstance(GitCheckoutOperation.class); @@ -76,7 +76,7 @@ public class GitCheckoutOperation extends GitBranchOperation { @Nullable private final String myNewBranch; @NotNull private final String myPreviousBranch; - public GitCheckoutOperation(@NotNull Project project, @NotNull Collection repositories, + GitCheckoutOperation(@NotNull Project project, @NotNull Collection repositories, @NotNull String startPointReference, @Nullable String newBranch, @NotNull String previousBranch, @NotNull ProgressIndicator indicator) { super(project, repositories, indicator); diff --git a/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java index dc2b5ef1fcdc..e7231d774814 100644 --- a/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java @@ -52,7 +52,7 @@ class GitDeleteBranchOperation extends GitBranchOperation { private final String myBranchName; private final String myCurrentBranch; - public GitDeleteBranchOperation(@NotNull Project project, @NotNull Collection repositories, + GitDeleteBranchOperation(@NotNull Project project, @NotNull Collection repositories, @NotNull String branchName, @NotNull String currentBranch, @NotNull ProgressIndicator indicator) { super(project, repositories, indicator); myBranchName = branchName; From 4744780fd81858620f82e494505c363799808d2a Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 6 Feb 2012 16:47:08 +0400 Subject: [PATCH 04/16] Move refreshRoot and fatalUntrackedFilesError from GitCheckoutOperation up to GitBranchOperation - will be reused in merge. --- .../git4idea/branch/GitBranchOperation.java | 76 ++++++++++++++++++- .../git4idea/branch/GitCheckoutOperation.java | 55 -------------- 2 files changed, 72 insertions(+), 59 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index 2c2d519fe3f1..fd1aefc70cae 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -21,23 +21,28 @@ import com.intellij.notification.NotificationType; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.VerticalFlowLayout; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.components.JBLabel; import com.intellij.util.Function; import com.intellij.util.ui.UIUtil; -import git4idea.GitUtil; -import git4idea.GitVcs; -import git4idea.MessageManager; -import git4idea.NotificationManager; +import git4idea.*; import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; +import git4idea.util.UntrackedFilesNotifier; import org.jetbrains.annotations.NotNull; +import javax.swing.*; import javax.swing.event.HyperlinkEvent; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static com.intellij.openapi.util.text.StringUtil.pluralize; +import static com.intellij.openapi.util.text.StringUtil.stripHtml; /** * Common class for Git operations with branches aware of multi-root configuration, @@ -227,4 +232,67 @@ abstract class GitBranchOperation { }); } + /** + * Asynchronously refreshes the VFS root directory of the given repository. + */ + protected static void refreshRoot(@NotNull GitRepository repository) { + repository.getRoot().refresh(true, true); + } + + /** + * Shows the error "The following untracked working tree files would be overwritten by checkout/merge". + * If there were no repositories that succeeded the operation, shows a notification with a link to the list of these untracked files. + * If some repositories succeeded, shows a dialog with the list of these files and a proposal to rollback the operation of those + * repositories. + */ + protected void fatalUntrackedFilesError(@NotNull Collection untrackedFiles) { + if (wereSuccessful()) { + showUntrackedFilesDialogWithRollback(untrackedFiles); + } + else { + showUntrackedFilesNotification(untrackedFiles); + } + } + + private void showUntrackedFilesNotification(@NotNull Collection untrackedFiles) { + UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, "checkout"); + } + + private void showUntrackedFilesDialogWithRollback(@NotNull Collection untrackedFiles) { + String title = "Couldn't checkout"; + String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription("checkout", true); + + final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, new ArrayList(untrackedFiles), + stripHtml(description, true)); + dialog.setTitle(title); + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + DialogManager.getInstance(myProject).showDialog(dialog); + } + }); + + if (dialog.isOK()) { + rollback(); + } + } + + private class UntrackedFilesDialog extends SelectFilesDialog { + + public UntrackedFilesDialog(@NotNull Project project, @NotNull List originalFiles, @NotNull String prompt) { + super(project, originalFiles, prompt, null, false, false); + setOKButtonText("Rollback"); + setCancelButtonText("Don't rollback"); + } + + @Override + protected JComponent createSouthPanel() { + JComponent buttons = super.createSouthPanel(); + JPanel panel = new JPanel(new VerticalFlowLayout()); + panel.add(new JBLabel("" + getRollbackProposal() + "")); + panel.add(buttons); + return panel; + } + } + } diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 5b2638c045ea..a8ee67c2e539 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -189,56 +189,6 @@ class GitCheckoutOperation extends GitBranchOperation { return collectChangesConflictingWithCheckout(remainingRepositories); } - private void fatalUntrackedFilesError(@NotNull Collection untrackedFiles) { - if (wereSuccessful()) { - showUntrackedFilesDialogWithRollback(untrackedFiles); - } - else { - showUntrackedFilesNotification(untrackedFiles); - } - } - - private void showUntrackedFilesNotification(@NotNull Collection untrackedFiles) { - UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, "checkout"); - } - - private void showUntrackedFilesDialogWithRollback(@NotNull Collection untrackedFiles) { - String title = "Couldn't checkout"; - String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription("checkout", true); - - final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, new ArrayList(untrackedFiles), - stripHtml(description, true)); - dialog.setTitle(title); - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - DialogManager.getInstance(myProject).showDialog(dialog); - } - }); - - if (dialog.isOK()) { - rollback(); - } - } - - private class UntrackedFilesDialog extends SelectFilesDialog { - - public UntrackedFilesDialog(@NotNull Project project, @NotNull List originalFiles, @NotNull String prompt) { - super(project, originalFiles, prompt, null, false, false); - setOKButtonText("Rollback"); - setCancelButtonText("Don't rollback"); - } - - @Override - protected JComponent createSouthPanel() { - JComponent buttons = super.createSouthPanel(); - JPanel panel = new JPanel(new VerticalFlowLayout()); - panel.add(new JBLabel("" + getRollbackProposal() + "")); - panel.add(buttons); - return panel; - } - } - @NotNull @Override protected String getRollbackProposal() { @@ -437,9 +387,4 @@ class GitCheckoutOperation extends GitBranchOperation { // repository state will be auto-updated with this VFS refresh => no need to call GitRepository#update(). } } - - private static void refreshRoot(GitRepository repository) { - repository.getRoot().refresh(true, true); - } - } From 0ff9c62ff371ba8867efdbcb723e28f712c776d6 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 6 Feb 2012 18:59:23 +0400 Subject: [PATCH 05/16] Generalize messages in GitBranchOperation by introducing getOperationName(). Fix the test. --- .../git4idea/branch/GitBranchOperation.java | 56 ++++++++++++++----- .../branch/GitCheckoutNewBranchOperation.java | 6 ++ .../git4idea/branch/GitCheckoutOperation.java | 15 +++-- .../branch/GitDeleteBranchOperation.java | 6 ++ .../branch/GitBranchOperationsTest.java | 44 ++++++++++----- 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index fd1aefc70cae..8c5ece93054c 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -52,11 +52,6 @@ import static com.intellij.openapi.util.text.StringUtil.stripHtml; */ abstract class GitBranchOperation { - static final String UNMERGED_FILES_ERROR_TITLE = "Can't checkout because of unmerged files"; - static final String UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION = - "You have to resolve all merge conflicts before checkout.
" + - "After resolving conflicts you also probably would want to commit your files to the current branch."; - @NotNull protected final Project myProject; @NotNull private final Collection myRepositories; @NotNull private final ProgressIndicator myIndicator; @@ -83,6 +78,14 @@ abstract class GitBranchOperation { @NotNull protected abstract String getRollbackProposal(); + /** + * Returns a short downcased name of the operation. + * It is used by some dialogs or notifications which are common to several operations. + * Some operations (like checkout new branch) can be not mentioned in these dialogs, so their operation names would be not used. + */ + @NotNull + protected abstract String getOperationName(); + /** * @return next repository that wasn't handled (e.g. checked out) yet. */ @@ -171,6 +174,27 @@ abstract class GitBranchOperation { } } + @NotNull + private String unmergedFilesErrorTitle() { + return unmergedFilesErrorTitle(getOperationName()); + } + + @NotNull + private static String unmergedFilesErrorTitle(String operationName) { + return "Can't " + operationName + " because of unmerged files"; + } + + @NotNull + private String unmergedFilesErrorNotificationDescription() { + return unmergedFilesErrorNotificationDescription(getOperationName()); + } + + @NotNull + private static String unmergedFilesErrorNotificationDescription(String operationName) { + return "You have to resolve all merge conflicts before " + operationName + ".
" + + "After resolving conflicts you also probably would want to commit your files to the current branch."; + } + protected void showFatalNotification(@NotNull String title, @NotNull String message) { notifyError(title, message); } @@ -206,10 +230,12 @@ abstract class GitBranchOperation { final AtomicBoolean ok = new AtomicBoolean(); UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { - String description = "You have to resolve all merge conflicts before checkout.
" + getRollbackProposal() + ""; + String description = "You have to resolve all merge conflicts before " + getOperationName() + ".
" + + getRollbackProposal() + ""; // suppressing: this message looks ugly if capitalized by words //noinspection DialogTitleCapitalization - ok.set(Messages.OK == MessageManager.showYesNoDialog(myProject, description, UNMERGED_FILES_ERROR_TITLE, "Rollback", "Don't rollback", Messages.getErrorIcon())); + ok.set(Messages.OK == MessageManager.showYesNoDialog(myProject, description, unmergedFilesErrorTitle(), + "Rollback", "Don't rollback", Messages.getErrorIcon())); } }); if (ok.get()) { @@ -218,13 +244,15 @@ abstract class GitBranchOperation { } private void showUnmergedFilesNotification() { - String title = UNMERGED_FILES_ERROR_TITLE; - String description = UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION; - NotificationManager.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, description, NotificationType.ERROR, new NotificationListener() { + String title = unmergedFilesErrorTitle(); + String description = unmergedFilesErrorNotificationDescription(); + NotificationManager.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, description, NotificationType.ERROR, + new NotificationListener() { @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) { GitConflictResolver.Params params = new GitConflictResolver.Params(). - setMergeDescription("The following files have unresolved conflicts. You need to resolve them before checking out."). + setMergeDescription("The following files have unresolved conflicts. You need to resolve them before " + + getOperationName() + "."). setErrorNotificationTitle("Unresolved files remain."); new GitConflictResolver(myProject, GitUtil.getRoots(getRepositories()), params).merge(); } @@ -255,12 +283,12 @@ abstract class GitBranchOperation { } private void showUntrackedFilesNotification(@NotNull Collection untrackedFiles) { - UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, "checkout"); + UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, getOperationName()); } private void showUntrackedFilesDialogWithRollback(@NotNull Collection untrackedFiles) { - String title = "Couldn't checkout"; - String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription("checkout", true); + String title = "Couldn't " + getOperationName(); + String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(getOperationName(), true); final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, new ArrayList(untrackedFiles), stripHtml(description, true)); diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java index 70529ec6cae0..e6e7fb39a1a3 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java @@ -98,6 +98,12 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { "
You may rollback (checkout back to " + myPreviousBranch + " and delete " + myNewBranchName + ") not to let branches diverge."; } + @NotNull + @Override + protected String getOperationName() { + return "checkout"; + } + @Override protected void rollback() { GitCompoundResult checkoutResult = new GitCompoundResult(myProject); diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index a8ee67c2e539..8b14bdab75bd 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -19,25 +19,20 @@ import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.VerticalFlowLayout; import com.intellij.openapi.util.Clock; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; -import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.ui.components.JBLabel; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.continuation.ContinuationContext; import com.intellij.util.text.DateFormatUtil; -import com.intellij.util.ui.UIUtil; import com.intellij.vcsUtil.VcsUtil; -import git4idea.DialogManager; import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.commands.*; @@ -46,15 +41,13 @@ import git4idea.repo.GitRepository; import git4idea.stash.GitChangesSaver; import git4idea.update.GitComplexProcess; import git4idea.util.GitUIUtil; -import git4idea.util.UntrackedFilesNotifier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.swing.*; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; -import static com.intellij.openapi.util.text.StringUtil.*; +import static com.intellij.openapi.util.text.StringUtil.join; import static git4idea.commands.GitMessageWithFilesDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT; import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILES_OVERWRITTEN_BY; import static git4idea.util.GitUIUtil.code; @@ -197,6 +190,12 @@ class GitCheckoutOperation extends GitBranchOperation { "
You may rollback (checkout back to " + myPreviousBranch + ") not to let branches diverge."; } + @NotNull + @Override + protected String getOperationName() { + return "checkout"; + } + @Override protected void rollback() { GitCompoundResult checkoutResult = new GitCompoundResult(myProject); diff --git a/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java index e7231d774814..0c4c89fa3f69 100644 --- a/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java @@ -142,6 +142,12 @@ class GitDeleteBranchOperation extends GitBranchOperation { "
You may rollback (recreate " + myBranchName + " in these roots) not to let branches diverge."; } + @NotNull + @Override + protected String getOperationName() { + return "branch deletion"; + } + @NotNull private GitCompoundResult forceDelete(@NotNull String branchName, @NotNull Collection possibleFailedRepositories) { GitCompoundResult compoundResult = new GitCompoundResult(myProject); diff --git a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java b/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java index 2209fdc834d9..656b18655d13 100644 --- a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java +++ b/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java @@ -141,14 +141,14 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { public void create_new_branch_with_unmerged_files_in_first_repo_should_show_notification() throws Exception { GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate); doCheckoutNewBranch(); - assertNotify(NotificationType.ERROR, GitBranchOperation.UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION); + assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("checkout")); } - + @Test public void create_new_branch_with_unmerged_files_in_second_repo_should_propose_to_rollback() throws Exception { GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib); doCheckoutNewBranch(); - assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE); + assertMessage(unmergedFilesErrorTitle("checkout")); } @Test @@ -156,7 +156,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib); myMessageManager.nextAnswer(Messages.OK); doCheckoutNewBranch(); - assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE); + assertMessage(unmergedFilesErrorTitle("checkout")); assertBranch("master"); assertTrue(!branch(myUltimate).contains(NEW_BRANCH)); } @@ -166,7 +166,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib); myMessageManager.nextAnswer(Messages.CANCEL); doCheckoutNewBranch(); - assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE); + assertMessage(unmergedFilesErrorTitle("checkout")); assertBranch(myUltimate, NEW_BRANCH); assertBranch(myCommunity, MASTER); @@ -185,7 +185,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { prepareBranchForSimpleCheckout(); GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate); doCheckout("feature", null); - assertNotify(NotificationType.ERROR, GitBranchOperation.UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION); + assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("checkout")); } @Test @@ -193,7 +193,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { prepareBranchForSimpleCheckout(); GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); doCheckout("feature", null); - assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE); + assertMessage(unmergedFilesErrorTitle("checkout")); } @Test @@ -202,7 +202,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); myMessageManager.nextAnswer(Messages.OK); doCheckout("feature", null); - assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE); + assertMessage(unmergedFilesErrorTitle("checkout")); assertBranch("master"); } @@ -212,7 +212,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); myMessageManager.nextAnswer(Messages.CANCEL); doCheckout("feature", null); - assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE); + assertMessage(unmergedFilesErrorTitle("checkout")); assertBranch(myUltimate, "feature"); assertBranch(myCommunity, "master"); assertBranch(myContrib, "master"); @@ -234,7 +234,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { branch(myUltimate, "feature"); branch(myContrib, "feature"); - Class gitCheckoutOperationClass = Class.forName("git4idea.branch.GitCheckoutOperation"); + Class gitCheckoutOperationClass = Class.forName("git4idea.branch.GitBranchOperation"); Class[] classes = gitCheckoutOperationClass.getDeclaredClasses(); Class untrackedFilesDialogClass = null; for (Class aClass : classes) { @@ -335,7 +335,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { doCheckout("feature", null); assertMessage("Couldn't checkout feature", "Local changes would be overwritten by checkout.
Stash or commit them before checking out a branch.
" + - "However checkout has succeeded for the following repositories:
" + + "However checkout has succeeded for the following repository:
" + myUltimate.getPresentableUrl() + "
You may rollback (checkout back to master) not to let branches diverge.", "Rollback", "Don't rollback"); @@ -347,7 +347,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); myMessageManager.nextAnswer(Messages.OK); doCheckout("feature", "newBranch"); - assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE); + assertMessage(unmergedFilesErrorTitle("checkout")); assertBranch("master"); for (GitRepository repository : myRepositories) { assertFalse(branch(repository).contains("newBranch"), "Branch newBranch wasn't deleted from repository " + getShortRepositoryName( @@ -423,7 +423,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); doDeleteBranch("unmerged_branch"); - assertNotify(NotificationType.ERROR, "Branch unmerged_branch wasn't deleted", "This branch is not fully merged to master"); + assertNotify(NotificationType.ERROR, "Branch unmerged_branch wasn't deleted", "This branch is not fully merged to master."); } @Test @@ -488,6 +488,20 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { callPrivateBranchOperationsProcessorMethod("doCheckoutNewBranch", NEW_BRANCH); } + private static String unmergedFilesErrorNotificationDescription(String operation) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = GitBranchOperation.class.getDeclaredMethod("unmergedFilesErrorNotificationDescription", String.class); + method.setAccessible(true); + return (String) method.invoke(null, operation); + } + + private static String unmergedFilesErrorTitle(String operation) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = GitBranchOperation.class.getDeclaredMethod("unmergedFilesErrorTitle", String.class); + method.setAccessible(true); + return (String) method.invoke(null, operation); + } + private void callPrivateBranchOperationsProcessorMethod(String methodName, String branchName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { // call private doCheckoutNewBranch instead of public checkoutNewBranch to avoid dealing with background process creation // same for other branch operations @@ -547,10 +561,10 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { TestMessageManager.Message message = myMessageManager.getLastMessage(); assertNotNull(message); if (title != null) { - assertEquals(message.getTitle(), title); + assertEquals(stripHtmlAndBreaks(message.getTitle()), stripHtmlAndBreaks(title)); } if (description != null) { - assertEquals(message.getDescription(), description); + assertEquals(stripHtmlAndBreaks(message.getDescription()), stripHtmlAndBreaks(description)); } if (yesButton != null) { assertEquals(message.getYesText(), yesButton); From 7c88bd0cd51e667a0f94f2dfee2917c5227dfa44 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 6 Feb 2012 21:22:43 +0400 Subject: [PATCH 06/16] Change GitVcs.IMPORTANT_ERROR_NOTIFICATION NotificationGroup id, because it was changed to non-sticky balloon some time ago, and changing to sticky-balloon in the code has no effect on those users. --- plugins/git4idea/src/git4idea/GitVcs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/git4idea/src/git4idea/GitVcs.java b/plugins/git4idea/src/git4idea/GitVcs.java index 960ae602b6b9..e4f48069e5e3 100644 --- a/plugins/git4idea/src/git4idea/GitVcs.java +++ b/plugins/git4idea/src/git4idea/GitVcs.java @@ -91,7 +91,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; */ public class GitVcs extends AbstractVcs { public static final NotificationGroup NOTIFICATION_GROUP_ID = NotificationGroup.toolWindowGroup("Git Messages", ChangesViewContentManager.TOOLWINDOW_ID, true); - public static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup("Git Important Errors", NotificationDisplayType.STICKY_BALLOON, true); + public static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup("Git Important Messages", NotificationDisplayType.STICKY_BALLOON, true); static { NotificationsConfigurationImpl.remove("Git"); From b83ebb1f9570dd74cc3b74112de8db0b48bc1823 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Tue, 7 Feb 2012 15:58:33 +0400 Subject: [PATCH 07/16] GitCheckoutOperation: simplify get remaining repos except the given one --- .../git4idea/src/git4idea/branch/GitBranchOperation.java | 7 +++++++ .../src/git4idea/branch/GitCheckoutOperation.java | 9 +-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index 8c5ece93054c..d6218c96c3d9 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -143,6 +143,13 @@ abstract class GitBranchOperation { return myRemainingRepositories; } + @NotNull + protected List getRemainingRepositoriesExceptGiven(@NotNull final GitRepository currentRepository) { + List repositories = new ArrayList(myRemainingRepositories); + repositories.remove(currentRepository); + return repositories; + } + protected void notifySuccess() { NotificationManager.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", getSuccessMessage(), NotificationType.INFORMATION); } diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 8b14bdab75bd..9c1843f529fb 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -20,7 +20,6 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Clock; -import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; @@ -29,7 +28,6 @@ import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; -import com.intellij.util.containers.ContainerUtil; import com.intellij.util.continuation.ContinuationContext; import com.intellij.util.text.DateFormatUtil; import com.intellij.vcsUtil.VcsUtil; @@ -173,12 +171,7 @@ class GitCheckoutOperation extends GitBranchOperation { @NotNull private Map> collectLocalChangesOnAllOtherRepositories(@NotNull final GitRepository currentRepository) { // get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout - List remainingRepositories = ContainerUtil.filter(getRepositories(), new Condition() { - @Override - public boolean value(GitRepository repo) { - return !repo.equals(currentRepository) && !getSuccessfulRepositories().contains(repo); - } - }); + List remainingRepositories = getRemainingRepositoriesExceptGiven(currentRepository); return collectChangesConflictingWithCheckout(remainingRepositories); } From 6c33525e2b01c93c41494741e4d3c54c8933fe32 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Tue, 7 Feb 2012 17:24:26 +0400 Subject: [PATCH 08/16] GitFreezingProcess as a simple more linear alternative to GitComplexProcess --- .../git4idea/update/GitComplexProcess.java | 15 +-- .../src/git4idea/util/GitFreezingProcess.java | 118 ++++++++++++++++++ 2 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 plugins/git4idea/src/git4idea/util/GitFreezingProcess.java diff --git a/plugins/git4idea/src/git4idea/update/GitComplexProcess.java b/plugins/git4idea/src/git4idea/update/GitComplexProcess.java index 612133003765..607f6a422370 100644 --- a/plugins/git4idea/src/git4idea/update/GitComplexProcess.java +++ b/plugins/git4idea/src/git4idea/update/GitComplexProcess.java @@ -15,14 +15,12 @@ */ package git4idea.update; -import com.intellij.ide.SaveAndSyncHandler; -import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ex.ProjectManagerEx; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.util.continuation.*; import git4idea.repo.GitRepository; import git4idea.repo.GitRepositoryManager; +import git4idea.util.GitFreezingProcess; /** * This class executes a Git task, surrounding it with a couple of preparation and completion tasks, such as: @@ -50,16 +48,12 @@ public class GitComplexProcess { private final Operation myOperation; private final String myFreezeReason; - private final ProjectManagerEx myProjectManager; private final GitRepositoryManager myRepositoryManager; private final ChangeListManager myChangeListManager; private final TaskDescriptor BLOCK = new TaskDescriptor("", Where.AWT) { @Override public void run(ContinuationContext context) { - myProjectManager.blockReloadingProjectOnExternalChanges(); - FileDocumentManager.getInstance().saveAllDocuments(); - SaveAndSyncHandler.getInstance().blockSaveOnFrameDeactivation(); - SaveAndSyncHandler.getInstance().blockSyncOnFrameActivation(); + GitFreezingProcess.saveAndBlock(); } }; @@ -77,9 +71,7 @@ public class GitComplexProcess { private final TaskDescriptor UNBLOCK = new TaskDescriptor("", Where.AWT) { @Override public void run(ContinuationContext context) { - myProjectManager.unblockReloadingProjectOnExternalChanges(); - SaveAndSyncHandler.getInstance().unblockSaveOnFrameDeactivation(); - SaveAndSyncHandler.getInstance().unblockSyncOnFrameActivation(); + GitFreezingProcess.unblock(); } @Override public boolean isHaveMagicCure() { @@ -99,7 +91,6 @@ public class GitComplexProcess { myOperation = operation; myFreezeReason = "Local changes are not available until Git " + myTitle + " is finished."; - myProjectManager = ProjectManagerEx.getInstanceEx(); myRepositoryManager = GitRepositoryManager.getInstance(project); myChangeListManager = ChangeListManager.getInstance(myProject); diff --git a/plugins/git4idea/src/git4idea/util/GitFreezingProcess.java b/plugins/git4idea/src/git4idea/util/GitFreezingProcess.java new file mode 100644 index 000000000000..1d3ee9f838a2 --- /dev/null +++ b/plugins/git4idea/src/git4idea/util/GitFreezingProcess.java @@ -0,0 +1,118 @@ +/* + * Copyright 2000-2012 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.util; + +import com.intellij.ide.SaveAndSyncHandler; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ex.ProjectManagerEx; +import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.util.continuation.GatheringContinuationContext; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * Executes an action surrounding it with freezing-unfreezing operations. + * It is a simple linear alternative to {@link git4idea.update.GitComplexProcess} performing tasks in a single method instead of + * using continuations logic. + * + * @author Kirill Likhodedov + */ +public class GitFreezingProcess { + + @NotNull private final String myOperationTitle; + @NotNull private final Runnable myRunnable; + @NotNull private final ChangeListManager myChangeListManager; + + public GitFreezingProcess(@NotNull Project project, @NotNull String operationTitle, @NotNull Runnable runnable) { + myOperationTitle = operationTitle; + myRunnable = runnable; + myChangeListManager = ChangeListManager.getInstance(project); + } + + public void execute() { + try { + saveAndBlockInAwt(); + freeze(); + try { + myRunnable.run(); + } + finally { + unfreeze(); + } + } + finally { + unblockInAwt(); + } + } + + public static void saveAndBlock() { + ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges(); + FileDocumentManager.getInstance().saveAllDocuments(); + SaveAndSyncHandler.getInstance().blockSaveOnFrameDeactivation(); + SaveAndSyncHandler.getInstance().blockSyncOnFrameActivation(); + } + + private static void saveAndBlockInAwt() { + final AtomicReference exception = new AtomicReference(); + // if an error happens, let it be thrown there (in awt) + throw it here to call unblock if anything has already been blocked + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + try { + saveAndBlock(); + } + catch (Throwable t) { + RuntimeException re = new RuntimeException(t); + exception.set(re); + throw re; + } + } + }); + + RuntimeException re = exception.get(); + if (re != null) { + throw re; + } + } + + private static void unblockInAwt() { + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + unblock(); + } + }); + } + + public static void unblock() { + ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges(); + SaveAndSyncHandler.getInstance().unblockSaveOnFrameDeactivation(); + SaveAndSyncHandler.getInstance().unblockSyncOnFrameActivation(); + } + + private void freeze() { + myChangeListManager.freeze(new GatheringContinuationContext(), + "Local changes are not available until Git " + myOperationTitle + " is finished."); + } + + private void unfreeze() { + myChangeListManager.letGo(); + } + +} From a3046728933dfcb36ed9c34b775585f57ba9cdc1 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Tue, 7 Feb 2012 19:24:55 +0400 Subject: [PATCH 09/16] GitPreservingProcess: stash -> perform operation -> unstash Move the stuff from GitCheckoutOperation there. (Preparing to GitMergeOperation). --- .../src/git4idea/NotificationManager.java | 4 + .../git4idea/branch/GitCheckoutOperation.java | 80 +-------- .../git4idea/stash/GitStashChangesSaver.java | 21 ++- .../git4idea/util/GitPreservingProcess.java | 161 ++++++++++++++++++ 4 files changed, 184 insertions(+), 82 deletions(-) create mode 100644 plugins/git4idea/src/git4idea/util/GitPreservingProcess.java diff --git a/plugins/git4idea/src/git4idea/NotificationManager.java b/plugins/git4idea/src/git4idea/NotificationManager.java index 93ad28e5b8cd..7977bca38c30 100644 --- a/plugins/git4idea/src/git4idea/NotificationManager.java +++ b/plugins/git4idea/src/git4idea/NotificationManager.java @@ -48,6 +48,10 @@ public class NotificationManager { notify(notificationGroup, title, message, type, null); } + public void notifyError(String title, String message) { + notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, message, NotificationType.ERROR, null); + } + protected static Notification createNotification(NotificationGroup notificationGroup, String title, String message, NotificationType type, NotificationListener listener) { return notificationGroup.createNotification(title, message, type, listener); diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 9c1843f529fb..f1161b0e35c3 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -19,25 +19,18 @@ import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Clock; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; -import com.intellij.openapi.vcs.history.VcsRevisionNumber; -import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; -import com.intellij.util.continuation.ContinuationContext; -import com.intellij.util.text.DateFormatUtil; import com.intellij.vcsUtil.VcsUtil; import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.commands.*; -import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; -import git4idea.stash.GitChangesSaver; -import git4idea.update.GitComplexProcess; +import git4idea.util.GitPreservingProcess; import git4idea.util.GitUIUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,7 +38,6 @@ import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; -import static com.intellij.openapi.util.text.StringUtil.join; import static git4idea.commands.GitMessageWithFilesDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT; import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILES_OVERWRITTEN_BY; import static git4idea.util.GitUIUtil.code; @@ -257,76 +249,18 @@ class GitCheckoutOperation extends GitBranchOperation { // stash - checkout - unstash private boolean smartCheckout(@NotNull final List repositories, @NotNull final String reference, @Nullable final String newBranch, @NotNull ProgressIndicator indicator) { - final GitChangesSaver saver = configureSaver(reference, indicator); final AtomicBoolean result = new AtomicBoolean(); - GitComplexProcess.Operation checkoutOperation = new GitComplexProcess.Operation() { - @Override public void run(ContinuationContext context) { - boolean savedSuccessfully = save(repositories, saver); - if (savedSuccessfully) { - try { - result.set(checkoutOrNotify(repositories, reference, newBranch, false)); - } finally { - saver.restoreLocalChanges(context); - } - } + GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, repositories, "checkout", reference, indicator, new Runnable() { + @Override + public void run() { + result.set(checkoutOrNotify(repositories, reference, newBranch, false)); } - }; - GitComplexProcess.execute(myProject, "checkout", checkoutOperation); + }); + preservingProcess.execute(); return result.get(); } - /** - * Configures the saver, actually notifications and texts in the GitConflictResolver used inside. - */ - private GitChangesSaver configureSaver(final String reference, ProgressIndicator indicator) { - GitChangesSaver saver = GitChangesSaver.getSaver(myProject, indicator, String.format("Checkout %s at %s", - reference, - DateFormatUtil.formatDateTime(Clock.getTime()))); - MergeDialogCustomizer mergeDialogCustomizer = new MergeDialogCustomizer() { - @Override - public String getMultipleFileMergeDescription(Collection files) { - return String.format( - "Uncommitted changes that were saved before checkout have conflicts with files from %s", - reference); - } - - @Override - public String getLeftPanelTitle(VirtualFile file) { - return "Uncommitted changes"; - } - - @Override - public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) { - return String.format("Changes from %s", reference); - } - }; - - GitConflictResolver.Params params = new GitConflictResolver.Params(). - setReverse(true). - setMergeDialogCustomizer(mergeDialogCustomizer). - setErrorNotificationTitle("Local changes were not restored"); - - saver.setConflictResolverParams(params); - return saver; - } - - /** - * Saves local changes. In case of error shows a notification and returns false. - */ - private boolean save(@NotNull Collection repositories, @NotNull GitChangesSaver saver) { - try { - saver.saveLocalChanges(GitUtil.getRoots(repositories)); - return true; - } catch (VcsException e) { - LOG.info("Couldn't save local changes", e); - notifyError("Couldn't save uncommitted changes.", - String.format("Tried to save uncommitted changes in %s before checkout, but failed with an error.
%s", - saver.getSaverName(), join(e.getMessages()))); - return false; - } - } - /** * Checks out or shows an error message. */ diff --git a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java index b369b4455717..9ce2fe859bde 100644 --- a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java @@ -67,21 +67,24 @@ public class GitStashChangesSaver extends GitChangesSaver { } @Override - protected void load(ContinuationContext context) { + protected void load(@NotNull ContinuationContext context) { + try { + load(); + } + catch (VcsException e) { + context.handleException(e); + } + } + + public void load() throws VcsException { for (VirtualFile root : myStashedRoots) { - try { - loadRoot(root); - } - catch (VcsException e) { - context.handleException(e); - return; - } + loadRoot(root); } // we'll refresh more but this way we needn't compute what files under roots etc LocalFileSystem.getInstance().refreshIoFiles(myChangeManager.getAffectedPaths()); } - @Override + @Override protected boolean wereChangesSaved() { return !myStashedRoots.isEmpty(); } diff --git a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java new file mode 100644 index 000000000000..8a4acd1832d1 --- /dev/null +++ b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java @@ -0,0 +1,161 @@ +/* + * Copyright 2000-2012 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.util; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Clock; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.text.StringUtil; +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.util.text.DateFormatUtil; +import git4idea.GitUtil; +import git4idea.NotificationManager; +import git4idea.merge.GitConflictResolver; +import git4idea.repo.GitRepository; +import git4idea.stash.GitStashChangesSaver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +import static com.intellij.openapi.util.text.StringUtil.join; + +/** + * Executes a Git operation on a number of repositories surrounding it by stash-unstash procedure. + * I.e. stashes changes, executes the operation and then unstashes it. + * + * @author Kirill Likhodedov + */ +public class GitPreservingProcess { + + private static final Logger LOG = Logger.getInstance(GitPreservingProcess.class); + + @NotNull private final Project myProject; + @NotNull private final Collection myRepositories; + @NotNull private final String myOperationTitle; + @NotNull private final String myDestinationName; + @NotNull private final ProgressIndicator myProgressIndicator; + @NotNull private final Runnable myOperation; + @NotNull private final String myStashMessage; + + private GitStashChangesSaver mySaver; + + public GitPreservingProcess(@NotNull Project project, @NotNull Collection repositories, + @NotNull String operationTitle, @NotNull String destinationName, + @NotNull ProgressIndicator indicator, @NotNull Runnable operation) { + myProject = project; + myRepositories = repositories; + myOperationTitle = operationTitle; + myDestinationName = destinationName; + myProgressIndicator = indicator; + myOperation = operation; + myStashMessage = String.format("%s %s at %s", StringUtil.capitalize(myOperationTitle), myDestinationName, + DateFormatUtil.formatDateTime(Clock.getTime())); + } + + public void execute() { + execute(null); + } + + public void execute(@Nullable final Computable autoLoadDecision) { + Runnable operation = new Runnable() { + @Override + public void run() { + mySaver = configureSaver(); + boolean savedSuccessfully = save(); + if (savedSuccessfully) { + try { + myOperation.run(); + } + finally { + if (autoLoadDecision == null || autoLoadDecision.compute()) { + load(); + } + } + } + } + }; + + new GitFreezingProcess(myProject, myOperationTitle, operation).execute(); + } + + /** + * Configures the saver, actually notifications and texts in the GitConflictResolver used inside. + */ + private GitStashChangesSaver configureSaver() { + GitStashChangesSaver saver = new GitStashChangesSaver(myProject, myProgressIndicator, myStashMessage); + MergeDialogCustomizer mergeDialogCustomizer = new MergeDialogCustomizer() { + @Override + public String getMultipleFileMergeDescription(Collection files) { + return String.format( + "Uncommitted changes that were saved before %s have conflicts with files from %s", + myOperationTitle, myDestinationName); + } + + @Override + public String getLeftPanelTitle(VirtualFile file) { + return "Uncommitted changes from stash"; + } + + @Override + public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) { + return String.format("Changes from %s", myDestinationName); + } + }; + + GitConflictResolver.Params params = new GitConflictResolver.Params(). + setReverse(true). + setMergeDialogCustomizer(mergeDialogCustomizer). + setErrorNotificationTitle("Local changes were not restored"); + + saver.setConflictResolverParams(params); + return saver; + } + + /** + * Saves local changes. In case of error shows a notification and returns false. + */ + private boolean save() { + try { + mySaver.saveLocalChanges(GitUtil.getRoots(myRepositories)); + return true; + } catch (VcsException e) { + LOG.info("Couldn't save local changes", e); + NotificationManager.getInstance(myProject).notifyError( + "Couldn't save uncommitted changes.", + String.format("Tried to save uncommitted changes in stash before %s, but failed with an error.
%s", + myOperationTitle, join(e.getMessages()))); + return false; + } + } + + private void load() { + try { + mySaver.load(); + } + catch (VcsException e) { + LOG.info("Couldn't load local changes", e); + NotificationManager.getInstance(myProject).notifyError("Couldn't restore uncommitted changes", + String.format("Tried to unstash uncommitted changes, but failed with error.
%s",join(e.getMessages()))); + } + } + +} From 8126f0ac1866cb762e444c7d89a617d8bfcdd2a4 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Tue, 7 Feb 2012 19:45:26 +0400 Subject: [PATCH 10/16] Extract some methods from GitCheckoutOperation to make them available for merge. --- plugins/git4idea/src/git4idea/GitUtil.java | 33 +++++++++++ .../git4idea/branch/GitBranchOperation.java | 37 +++++++++++- .../git4idea/branch/GitCheckoutOperation.java | 58 +------------------ 3 files changed, 69 insertions(+), 59 deletions(-) diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index 6fa5f09f2b45..c49f36644ff4 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -18,10 +18,13 @@ package git4idea; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vcs.changes.FilePathsHelper; import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile; import com.intellij.openapi.vfs.LocalFileSystem; @@ -673,4 +676,34 @@ public class GitUtil { } return remoteChanges; } + + /** + * Given the list of paths converts them to the list of {@link Change Changes} found in the {@link ChangeListManager}, + * i.e. this works only for local changes. + * Paths can be absolute or relative to the repository. + * If a path is not in the local changes, it is ignored. + */ + @NotNull + public static List convertPathsToChanges(@NotNull GitRepository repository, + @NotNull Collection affectedPaths, boolean relativePaths) { + ChangeListManager changeListManager = ChangeListManager.getInstance(repository.getProject()); + List affectedChanges = new ArrayList(); + for (String path : affectedPaths) { + VirtualFile file; + if (relativePaths) { + file = repository.getRoot().findFileByRelativePath(FileUtil.toSystemIndependentName(path)); + } + else { + file = VcsUtil.getVirtualFile(path); + } + + if (file != null) { + Change change = changeListManager.getChange(file); + if (change != null) { + affectedChanges.add(change); + } + } + } + return affectedChanges; + } } diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index d6218c96c3d9..c311c30dd5fb 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -18,11 +18,14 @@ package git4idea.branch; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.VerticalFlowLayout; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.components.JBLabel; @@ -36,9 +39,7 @@ import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.event.HyperlinkEvent; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import static com.intellij.openapi.util.text.StringUtil.pluralize; @@ -52,6 +53,8 @@ import static com.intellij.openapi.util.text.StringUtil.stripHtml; */ abstract class GitBranchOperation { + private static final Logger LOG = Logger.getInstance(GitBranchOperation.class); + @NotNull protected final Project myProject; @NotNull private final Collection myRepositories; @NotNull private final ProgressIndicator myIndicator; @@ -312,6 +315,34 @@ abstract class GitBranchOperation { } } + /** + * TODO this is non-optimal and even incorrect, since such diff shows the difference between committed changes + * For each of the given repositories looks to the diff between current branch and the given branch and converts it to the list of + * local changes. + */ + @NotNull + static Map> collectLocalChangesConflictingWithBranch(@NotNull Project project, + @NotNull Collection repositories, + @NotNull String currentBranch, + @NotNull String otherBranch) { + Map> changes = new HashMap>(); + for (GitRepository repository : repositories) { + try { + Collection diff = GitUtil.getPathsDiffBetweenRefs(currentBranch, otherBranch, project, repository.getRoot()); + List changesInRepo = GitUtil.convertPathsToChanges(repository, diff, false); + if (!changesInRepo.isEmpty()) { + changes.put(repository, changesInRepo); + } + } + catch (VcsException e) { + // ignoring the exception: this is not fatal if we won't collect such a diff from other repositories. + // At worst, use will get double dialog proposing the smart checkout. + LOG.warn(String.format("Couldn't collect diff between %s and %s in %s", currentBranch, otherBranch, repository.getRoot()), e); + } + } + return changes; + } + private class UntrackedFilesDialog extends SelectFilesDialog { public UntrackedFilesDialog(@NotNull Project project, @NotNull List originalFiles, @NotNull String prompt) { diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index f1161b0e35c3..ca1974c9c31f 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -19,13 +19,9 @@ import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; -import com.intellij.vcsUtil.VcsUtil; import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.commands.*; @@ -113,7 +109,7 @@ class GitCheckoutOperation extends GitBranchOperation { private boolean smartCheckoutOrNotify(@NotNull GitRepository repository, @NotNull GitMessageWithFilesDetector localChangesOverwrittenByCheckout) { // get changes overwritten by checkout from the error message captured from Git - List affectedChanges = getChangesAffectedByCheckout(repository, localChangesOverwrittenByCheckout.getRelativeFilePaths(), true); + List affectedChanges = GitUtil.convertPathsToChanges(repository, localChangesOverwrittenByCheckout.getRelativeFilePaths(), true); // get all other conflicting changes Map> conflictingChangesInRepositories = collectLocalChangesOnAllOtherRepositories(repository); Set otherProblematicRepositories = conflictingChangesInRepositories.keySet(); @@ -164,7 +160,7 @@ class GitCheckoutOperation extends GitBranchOperation { private Map> collectLocalChangesOnAllOtherRepositories(@NotNull final GitRepository currentRepository) { // get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout List remainingRepositories = getRemainingRepositoriesExceptGiven(currentRepository); - return collectChangesConflictingWithCheckout(remainingRepositories); + return collectLocalChangesConflictingWithBranch(myProject, remainingRepositories, myPreviousBranch, myStartPointReference); } @NotNull @@ -218,26 +214,6 @@ class GitCheckoutOperation extends GitBranchOperation { return "Couldn't checkout " + myStartPointReference; } - @NotNull - private Map> collectChangesConflictingWithCheckout(@NotNull Collection repositories) { - Map> changes = new HashMap>(); - for (GitRepository repository : repositories) { - try { - Collection diff = GitUtil.getPathsDiffBetweenRefs(myPreviousBranch, myStartPointReference, myProject, repository.getRoot()); - List changesInRepo = getChangesAffectedByCheckout(repository, diff, false); - if (!changesInRepo.isEmpty()) { - changes.put(repository, changesInRepo); - } - } - catch (VcsException e) { - // ignoring the exception: this is not fatal if we won't collect such a diff from other repositories. - // At worst, use will get double dialog proposing the smart checkout. - LOG.warn(String.format("Couldn't collect diff between %s and %s in %s", myPreviousBranch, myStartPointReference, repository.getRoot()), e); - } - } - return changes; - } - @NotNull @Override public String getSuccessMessage() { @@ -277,36 +253,6 @@ class GitCheckoutOperation extends GitBranchOperation { return false; } - /** - * Forms the list of the changes, that would be overwritten by checkout. - * - * @param repository - * @param affectedPaths paths returned by Git. - * @param relativePaths Are the paths specified relative or absolute. - * @return List of Changes is these paths. - */ - private List getChangesAffectedByCheckout(@NotNull GitRepository repository, @NotNull Collection affectedPaths, boolean relativePaths) { - ChangeListManager changeListManager = ChangeListManager.getInstance(myProject); - List affectedChanges = new ArrayList(); - for (String path : affectedPaths) { - VirtualFile file; - if (relativePaths) { - file = repository.getRoot().findFileByRelativePath(FileUtil.toSystemIndependentName(path)); - } - else { - file = VcsUtil.getVirtualFile(path); - } - - if (file != null) { - Change change = changeListManager.getChange(file); - if (change != null) { - affectedChanges.add(change); - } - } - } - return affectedChanges; - } - private static void refresh(GitRepository... repositories) { for (GitRepository repository : repositories) { refreshRoot(repository); From b6b074e3a25d24323b2baafc0a20dddadb3041b9 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Tue, 7 Feb 2012 20:22:37 +0400 Subject: [PATCH 11/16] Inline --- .../src/git4idea/branch/GitCheckoutOperation.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index ca1974c9c31f..64077d856333 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -111,7 +111,10 @@ class GitCheckoutOperation extends GitBranchOperation { // get changes overwritten by checkout from the error message captured from Git List affectedChanges = GitUtil.convertPathsToChanges(repository, localChangesOverwrittenByCheckout.getRelativeFilePaths(), true); // get all other conflicting changes - Map> conflictingChangesInRepositories = collectLocalChangesOnAllOtherRepositories(repository); + // get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout + Map> conflictingChangesInRepositories = + collectLocalChangesConflictingWithBranch(myProject, getRemainingRepositoriesExceptGiven(repository), myPreviousBranch, myStartPointReference); + Set otherProblematicRepositories = conflictingChangesInRepositories.keySet(); List allConflictingRepositories = new ArrayList(otherProblematicRepositories); allConflictingRepositories.add(repository); @@ -156,13 +159,6 @@ class GitCheckoutOperation extends GitBranchOperation { } } - @NotNull - private Map> collectLocalChangesOnAllOtherRepositories(@NotNull final GitRepository currentRepository) { - // get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout - List remainingRepositories = getRemainingRepositoriesExceptGiven(currentRepository); - return collectLocalChangesConflictingWithBranch(myProject, remainingRepositories, myPreviousBranch, myStartPointReference); - } - @NotNull @Override protected String getRollbackProposal() { From ff2905d592bda972df7741d54c07e14b9c9fbfa5 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 8 Feb 2012 17:11:55 +0400 Subject: [PATCH 12/16] GitMergeOperation Add "Merge" operation to the GitBranchPopup. Extract more methods on "local changes overwritten by" error from GitCheckoutOperation, and reuse them in GitMergeOperation. Add some tests on GitMergeOperation; DRY GitBranchOperationsTest a bit. Rename GitWouldBeOverwrittenByCheckoutDialog to GitSmartOperationDialog. Make "force" button optional - it has no sense for merge. Extract GitMergeCommittingConflictResolver from GitUpdateProcess to reuse in GitMergeOperation. Refresh the root after commit if needed (to understand that the merge has been committed; this is not needed in case of update, because the ChangeListManager is frozen and then refreshed). --- .../git4idea/branch/GitBranchOperation.java | 52 ++- .../branch/GitBranchOperationsProcessor.java | 18 ++ .../branch/GitCheckoutNewBranchOperation.java | 2 +- .../git4idea/branch/GitCheckoutOperation.java | 54 +--- .../git4idea/branch/GitMergeOperation.java | 306 ++++++++++++++++++ ...alog.java => GitSmartOperationDialog.java} | 44 ++- .../git4idea/src/git4idea/commands/Git.java | 20 ++ .../commands/GitMessageWithFilesDetector.java | 3 + .../commands/GitSimpleEventDetector.java | 7 +- .../git4idea/merge/GitConflictResolver.java | 6 +- .../GitMergeCommittingConflictResolver.java | 51 +++ .../ui/branch/GitBranchPopupActions.java | 32 +- .../src/git4idea/update/GitUpdateProcess.java | 23 +- .../git4idea/util/GitPreservingProcess.java | 2 +- .../branch/GitBranchOperationsTest.java | 181 ++++++++--- .../git4idea/tests/git4idea/test/GitExec.java | 4 + .../test/GitTestScenarioGenerator.java | 14 +- 17 files changed, 679 insertions(+), 140 deletions(-) create mode 100644 plugins/git4idea/src/git4idea/branch/GitMergeOperation.java rename plugins/git4idea/src/git4idea/branch/{GitWouldBeOverwrittenByCheckoutDialog.java => GitSmartOperationDialog.java} (66%) create mode 100644 plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index c311c30dd5fb..d9676498e5e1 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -23,6 +23,7 @@ import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.VerticalFlowLayout; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; @@ -32,6 +33,7 @@ import com.intellij.ui.components.JBLabel; import com.intellij.util.Function; import com.intellij.util.ui.UIUtil; import git4idea.*; +import git4idea.commands.GitMessageWithFilesDetector; import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; import git4idea.util.UntrackedFilesNotifier; @@ -153,8 +155,12 @@ abstract class GitBranchOperation { return repositories; } + protected void notifySuccess(@NotNull String message) { + NotificationManager.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", message, NotificationType.INFORMATION); + } + protected void notifySuccess() { - NotificationManager.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", getSuccessMessage(), NotificationType.INFORMATION); + notifySuccess(getSuccessMessage()); } /** @@ -277,6 +283,17 @@ abstract class GitBranchOperation { repository.getRoot().refresh(true, true); } + protected void fatalLocalChangesError(@NotNull String reference) { + String title = String.format("Couldn't %s %s", getOperationName(), reference); + String message = String.format("Local changes would be overwritten by %s.
You should stash or commit them.
", getOperationName()); + if (wereSuccessful()) { + showFatalErrorDialogWithRollback(title, message); + } + else { + showFatalNotification(title, message); + } + } + /** * Shows the error "The following untracked working tree files would be overwritten by checkout/merge". * If there were no repositories that succeeded the operation, shows a notification with a link to the list of these untracked files. @@ -361,4 +378,37 @@ abstract class GitBranchOperation { } } + /** + * When checkout or merge operation on a repository fails with the error "local changes would be overwritten by...", + * affected local files are captured by the {@link git4idea.commands.GitMessageWithFilesDetector detector}. + * Then all remaining (non successful repositories) are searched if they are about to fail with the same problem. + * All collected local changes which prevent the operation, together with these repositories, are returned. + * @param currentRepository The first repository which failed the operation. + * @param localChangesOverwrittenBy The detector of local changes would be overwritten by merge/checkout. + * @param currentBranch Current branch. + * @param nextBranch Branch to compare with (the branch to be checked out, or the branch to be merged). + * @return Repositories that have failed or would fail with the "local changes" error, together with these local changes. + */ + @NotNull + protected Pair, List> getConflictingRepositoriesAndAffectedChanges( + @NotNull GitRepository currentRepository, @NotNull GitMessageWithFilesDetector localChangesOverwrittenBy, + String currentBranch, String nextBranch) { + + // get changes overwritten by checkout from the error message captured from Git + List affectedChanges = GitUtil.convertPathsToChanges(currentRepository, localChangesOverwrittenBy.getRelativeFilePaths(), true); + // get all other conflicting changes + // get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout + Map> conflictingChangesInRepositories = + collectLocalChangesConflictingWithBranch(myProject, getRemainingRepositoriesExceptGiven(currentRepository), currentBranch, nextBranch); + + Set otherProblematicRepositories = conflictingChangesInRepositories.keySet(); + List allConflictingRepositories = new ArrayList(otherProblematicRepositories); + allConflictingRepositories.add(currentRepository); + for (List changes : conflictingChangesInRepositories.values()) { + affectedChanges.addAll(changes); + } + + return Pair.create(allConflictingRepositories, affectedChanges); + } + } diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java index 271d18a93d68..e066aa825424 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperationsProcessor.java @@ -39,7 +39,9 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Executor of Git branching operations. @@ -232,6 +234,22 @@ public final class GitBranchOperationsProcessor { } } + public void merge(@NotNull final String branchName) { + new CommonBackgroundTask(myProject, "Merging " + branchName, myCallInAwtAfterExecution) { + @Override public void execute(@NotNull ProgressIndicator indicator) { + doMerge(branchName, indicator); + } + }.runInBackground(); + } + + private void doMerge(@NotNull String branchName, @NotNull ProgressIndicator indicator) { + Map revisions = new HashMap(); + for (GitRepository repository : myRepositories) { + revisions.put(repository, repository.getCurrentRevision()); + } + new GitMergeOperation(myProject, myRepositories, branchName, getCurrentBranch(), revisions, indicator).execute(); + } + /** * Executes common operations before/after executing the actual branch operation. */ diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java index e6e7fb39a1a3..27f135bac9a3 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java @@ -58,7 +58,7 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { while (hasMoreRepositories() && !fatalErrorHappened) { final GitRepository repository = next(); - GitSimpleEventDetector unmergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED); + GitSimpleEventDetector unmergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT); GitCommandResult result = Git.checkoutNewBranch(repository, myNewBranchName, unmergedDetector); if (result.success()) { diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 64077d856333..1340ba4e582f 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -19,10 +19,9 @@ import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.util.ArrayUtil; -import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.commands.*; import git4idea.repo.GitRepository; @@ -31,7 +30,8 @@ import git4idea.util.GitUIUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collection; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static git4idea.commands.GitMessageWithFilesDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT; @@ -72,7 +72,7 @@ class GitCheckoutOperation extends GitBranchOperation { VirtualFile root = repository.getRoot(); GitMessageWithFilesDetector localChangesOverwrittenByCheckout = new GitMessageWithFilesDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT, root); - GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED); + GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT); GitMessageWithFilesDetector untrackedOverwrittenByCheckout = new GitMessageWithFilesDetector(UNTRACKED_FILES_OVERWRITTEN_BY, root); GitCommandResult result = Git.checkout(repository, myStartPointReference, myNewBranch, false, @@ -108,30 +108,19 @@ class GitCheckoutOperation extends GitBranchOperation { private boolean smartCheckoutOrNotify(@NotNull GitRepository repository, @NotNull GitMessageWithFilesDetector localChangesOverwrittenByCheckout) { - // get changes overwritten by checkout from the error message captured from Git - List affectedChanges = GitUtil.convertPathsToChanges(repository, localChangesOverwrittenByCheckout.getRelativeFilePaths(), true); - // get all other conflicting changes - // get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout - Map> conflictingChangesInRepositories = - collectLocalChangesConflictingWithBranch(myProject, getRemainingRepositoriesExceptGiven(repository), myPreviousBranch, myStartPointReference); + Pair, List> conflictingRepositoriesAndAffectedChanges = + getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myPreviousBranch, myStartPointReference); + List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); + List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); - Set otherProblematicRepositories = conflictingChangesInRepositories.keySet(); - List allConflictingRepositories = new ArrayList(otherProblematicRepositories); - allConflictingRepositories.add(repository); - for (List changes : conflictingChangesInRepositories.values()) { - affectedChanges.addAll(changes); - } - - int smartCheckoutDecision = GitWouldBeOverwrittenByCheckoutDialog.showAndGetAnswer(myProject, affectedChanges); - if (smartCheckoutDecision == GitWouldBeOverwrittenByCheckoutDialog.SMART_CHECKOUT) { + int smartCheckoutDecision = GitSmartOperationDialog.showAndGetAnswer(myProject, affectedChanges, "checkout", true); + if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) { boolean smartCheckedOutSuccessfully = smartCheckout(allConflictingRepositories, myStartPointReference, myNewBranch, getIndicator()); if (smartCheckedOutSuccessfully) { - GitRepository[] otherRepositories = ArrayUtil.toObjectArray(otherProblematicRepositories, GitRepository.class); - - markSuccessful(repository); - markSuccessful(otherRepositories); - refresh(repository); - refresh(otherRepositories); + for (GitRepository conflictingRepository : allConflictingRepositories) { + markSuccessful(conflictingRepository); + refresh(conflictingRepository); + } return true; } else { @@ -139,26 +128,15 @@ class GitCheckoutOperation extends GitBranchOperation { return false; } } - else if (smartCheckoutDecision == GitWouldBeOverwrittenByCheckoutDialog.FORCE_CHECKOUT_EXIT_CODE) { + else if (smartCheckoutDecision == GitSmartOperationDialog.FORCE_EXIT_CODE) { return checkoutOrNotify(allConflictingRepositories, myStartPointReference, myNewBranch, true); } else { - fatalLocalChangesError(); + fatalLocalChangesError(myStartPointReference); return false; } } - private void fatalLocalChangesError() { - String title = "Couldn't checkout " + myStartPointReference; - String message = "Local changes would be overwritten by checkout.
Stash or commit them before checking out a branch.
"; - if (wereSuccessful()) { - showFatalErrorDialogWithRollback(title, message); - } - else { - showFatalNotification(title, message); - } - } - @NotNull @Override protected String getRollbackProposal() { diff --git a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java new file mode 100644 index 000000000000..b323bb2913e8 --- /dev/null +++ b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java @@ -0,0 +1,306 @@ +/* + * Copyright 2000-2012 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.branch; + +import com.intellij.notification.NotificationType; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitUtil; +import git4idea.GitVcs; +import git4idea.NotificationManager; +import git4idea.commands.*; +import git4idea.merge.GitConflictResolver; +import git4idea.merge.GitMergeCommittingConflictResolver; +import git4idea.merge.GitMerger; +import git4idea.repo.GitRepository; +import git4idea.util.GitPreservingProcess; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import static git4idea.commands.GitMessageWithFilesDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_MERGE; +import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILES_OVERWRITTEN_BY; + +/** + * @author Kirill Likhodedov + */ +class GitMergeOperation extends GitBranchOperation { + + private static final Logger LOG = Logger.getInstance(GitMergeOperation.class); + + @NotNull private final ChangeListManager myChangeListManager; + @NotNull private final String myBranchToMerge; + @NotNull private final String myCurrentBranch; + @NotNull private final Map myCurrentRevisionsBeforeMerge; + + // true in value if unstash will be needed after resolving conflicts. + @NotNull private final Map myConflictedRepositories = new HashMap(); + private GitPreservingProcess myPreservingProcess; + + protected GitMergeOperation(@NotNull Project project, @NotNull Collection repositories, + @NotNull String branchToMerge, + @NotNull String currentBranch, + @NotNull Map currentRevisionsBeforeMerge, + @NotNull ProgressIndicator indicator) { + super(project, repositories, indicator); + myBranchToMerge = branchToMerge; + myCurrentBranch = currentBranch; + myCurrentRevisionsBeforeMerge = currentRevisionsBeforeMerge; + myChangeListManager = ChangeListManager.getInstance(myProject); + } + + @Override + protected void execute() { + boolean fatalErrorHappened = false; + boolean notAlreadyUpToDate = false; + while (hasMoreRepositories() && !fatalErrorHappened) { + final GitRepository repository = next(); + + VirtualFile root = repository.getRoot(); + GitMessageWithFilesDetector localChangesOverwrittenByMerge = new GitMessageWithFilesDetector(LOCAL_CHANGES_OVERWRITTEN_BY_MERGE, root); + GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_MERGE); + GitMessageWithFilesDetector untrackedOverwrittenByMerge = new GitMessageWithFilesDetector(UNTRACKED_FILES_OVERWRITTEN_BY, root); + GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT); + GitSimpleEventDetector alreadyUpToDateDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.ALREADY_UP_TO_DATE); + + GitCommandResult result = Git.merge(repository, myBranchToMerge, + localChangesOverwrittenByMerge, unmergedFiles, untrackedOverwrittenByMerge, mergeConflict, + alreadyUpToDateDetector); + if (result.success()) { + refresh(repository); + markSuccessful(repository); + if (!alreadyUpToDateDetector.hasHappened()) { + notAlreadyUpToDate = true; + } + } + else if (unmergedFiles.hasHappened()) { + fatalUnmergedFilesError(); + fatalErrorHappened = true; + } + else if (localChangesOverwrittenByMerge.wasMessageDetected()) { + boolean smartMergeSucceeded = proposeSmartMergePerformAndNotify(repository, localChangesOverwrittenByMerge); + if (!smartMergeSucceeded) { + fatalErrorHappened = true; + } + } + else if (mergeConflict.hasHappened()) { + myConflictedRepositories.put(repository, Boolean.FALSE); + refresh(repository); + markSuccessful(repository); + } + else if (untrackedOverwrittenByMerge.wasMessageDetected()) { + fatalUntrackedFilesError(untrackedOverwrittenByMerge.getFiles()); + fatalErrorHappened = true; + } + else { + fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString()); + fatalErrorHappened = true; + } + } + + boolean allConflictsResolved = resolveConflicts(); + + if (!fatalErrorHappened && allConflictsResolved) { + if (notAlreadyUpToDate) { + notifySuccess(); + } + else { + notifySuccess("Already up-to-date."); + } + } + + restoreLocalChanges(); + } + + private boolean resolveConflicts() { + if (!myConflictedRepositories.isEmpty()) { + GitMerger merger = new GitMerger(myProject); + GitConflictResolver.Params params = new GitConflictResolver.Params(); + GitMergeCommittingConflictResolver conflictResolver = + new GitMergeCommittingConflictResolver(myProject, merger, GitUtil.getRoots(myConflictedRepositories.keySet()), params, true) { + @Override + protected void notifyUnresolvedRemain() { + NotificationManager.getInstance(myProject).notify( + GitVcs.IMPORTANT_ERROR_NOTIFICATION, "Merged branch " + myBranchToMerge + " with conflicts", + "Unresolved conflicts remain in the project. Resolve now.", NotificationType.WARNING, + getResolveLinkListener()); + } + }; + return conflictResolver.merge(); + } + return true; + } + + private boolean proposeSmartMergePerformAndNotify(@NotNull GitRepository repository, + @NotNull GitMessageWithFilesDetector localChangesOverwrittenByMerge) { + Pair, List> conflictingRepositoriesAndAffectedChanges = + getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranch, myBranchToMerge); + List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); + List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); + + int smartCheckoutDecision = GitSmartOperationDialog.showAndGetAnswer(myProject, affectedChanges, "merge", false); + if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) { + return doSmartMerge(allConflictingRepositories); + } + else { + fatalLocalChangesError(myBranchToMerge); + return false; + } + } + + private void restoreLocalChanges() { + if (myPreservingProcess != null) { + myPreservingProcess.load(); + } + } + + private boolean doSmartMerge(@NotNull final Collection repositories) { + final AtomicBoolean success = new AtomicBoolean(); + myPreservingProcess = new GitPreservingProcess(myProject, repositories, "merge", myBranchToMerge, getIndicator(), + new Runnable() { + @Override + public void run() { + success.set(doMerge(repositories)); + } + }); + myPreservingProcess.execute(new Computable() { + @Override + public Boolean compute() { + return myConflictedRepositories.isEmpty(); + } + }); + return success.get(); + } + + /** + * Performs merge in the given repositories. + * Handle only merge conflict situation: all other cases should have been handled before and are treated as errors. + * Conflict is treated as a success: the repository with conflict is remembered and will be handled later along with all other conflicts. + * If an error happens in one repository, the method doesn't go further in others, and shows a notification. + * + * @return true if merge has succeeded without errors (but possibly with conflicts) in all repositories; + * false if it failed at least in one of them. + */ + private boolean doMerge(@NotNull Collection repositories) { + for (GitRepository repository : repositories) { + GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT); + GitCommandResult result = Git.merge(repository, myBranchToMerge, mergeConflict); + if (!result.success()) { + if (mergeConflict.hasHappened()) { + myConflictedRepositories.put(repository, Boolean.TRUE); + refresh(repository); + markSuccessful(repository); + } + else { + fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString()); + return false; + } + } + else { + refresh(repository); + markSuccessful(repository); + } + } + return true; + } + + @NotNull + private String getCommonErrorTitle() { + return "Couldn't merge " + myBranchToMerge; + } + + @Override + protected void rollback() { + Collection repositoriesForSmartRollback = new ArrayList(); + Collection repositoriesForSimpleRollback = new ArrayList(); + for (GitRepository repository : getSuccessfulRepositories()) { + if (thereAreLocalChangesIn(repository)) { + repositoriesForSmartRollback.add(repository); + } + else { + repositoriesForSimpleRollback.add(repository); + } + } + + GitCompoundResult result = smartRollback(repositoriesForSmartRollback); + for (GitRepository repository : repositoriesForSimpleRollback) { + result.append(repository, rollback(repository)); + } + + if (!result.totalSuccess()) { + NotificationManager.getInstance(myProject).notifyError("Error during rollback", result.getErrorOutputWithReposIndication()); + } + } + + @NotNull + private GitCompoundResult smartRollback(@NotNull final Collection repositories) { + final GitCompoundResult result = new GitCompoundResult(myProject); + myPreservingProcess = new GitPreservingProcess(myProject, repositories, "merge", myBranchToMerge, getIndicator(), + new Runnable() { + @Override public void run() { + for (GitRepository repository : repositories) { + result.append(repository, rollback(repository)); + } + } + }); + myPreservingProcess.execute(); + return result; + } + + @NotNull + private GitCommandResult rollback(@NotNull GitRepository repository) { + return Git.hardReset(repository, myCurrentRevisionsBeforeMerge.get(repository)); + } + + private boolean thereAreLocalChangesIn(@NotNull GitRepository repository) { + return !myChangeListManager.getChangesIn(repository.getRoot()).isEmpty(); + } + + @NotNull + @Override + public String getSuccessMessage() { + return String.format("Merged %s to %s", myBranchToMerge, myCurrentBranch); + } + + @NotNull + @Override + protected String getRollbackProposal() { + return "However merge has succeeded for the following " + repositories() + ":
" + + successfulRepositoriesJoined() + + "
You may rollback (reset to the commit before merging) not to let branches diverge."; + } + + @NotNull + @Override + protected String getOperationName() { + return "merge"; + } + + private static void refresh(GitRepository... repositories) { + for (GitRepository repository : repositories) { + refreshRoot(repository); + repository.update(GitRepository.TrackedTopic.ALL_CURRENT); + } + } + +} diff --git a/plugins/git4idea/src/git4idea/branch/GitWouldBeOverwrittenByCheckoutDialog.java b/plugins/git4idea/src/git4idea/branch/GitSmartOperationDialog.java similarity index 66% rename from plugins/git4idea/src/git4idea/branch/GitWouldBeOverwrittenByCheckoutDialog.java rename to plugins/git4idea/src/git4idea/branch/GitSmartOperationDialog.java index ad60e17fcc44..af3e986ea62f 100644 --- a/plugins/git4idea/src/git4idea/branch/GitWouldBeOverwrittenByCheckoutDialog.java +++ b/plugins/git4idea/src/git4idea/branch/GitSmartOperationDialog.java @@ -31,29 +31,33 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** - * The dialog that is shown when the error "The following files would be overwritten by checkout" happens. - * Displays the list of these files and proposes to make a "smart" checkout. + * The dialog that is shown when the error + * "Your local changes to the following files would be overwritten by merge/checkout" + * happens. + * Displays the list of these files and proposes to make a "smart" merge or checkout. * * @author Kirill Likhodedov */ -// TODO "don't ask again" option -class GitWouldBeOverwrittenByCheckoutDialog extends DialogWrapper { +class GitSmartOperationDialog extends DialogWrapper { - public static final int SMART_CHECKOUT = OK_EXIT_CODE; - public static final int FORCE_CHECKOUT_EXIT_CODE = NEXT_USER_EXIT_CODE; + public static final int SMART_EXIT_CODE = OK_EXIT_CODE; + public static final int FORCE_EXIT_CODE = NEXT_USER_EXIT_CODE; private final Project myProject; private final List myChanges; + @NotNull private final String myOperationTitle; + private final boolean myForceButton; /** - * @return true if smart checkout has to be performed, false if user doesn't want to checkout. + * Shows the dialog with the list of local changes preventing merge/checkout and returns the dialog exit code. */ - static int showAndGetAnswer(@NotNull final Project project, @NotNull final List changes) { + static int showAndGetAnswer(@NotNull final Project project, @NotNull final List changes, @NotNull final String operationTitle, + final boolean forceButton) { final AtomicInteger exitCode = new AtomicInteger(); UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { - GitWouldBeOverwrittenByCheckoutDialog dialog = new GitWouldBeOverwrittenByCheckoutDialog(project, changes); + GitSmartOperationDialog dialog = new GitSmartOperationDialog(project, changes, operationTitle, forceButton); DialogManager.getInstance(project).showDialog(dialog); exitCode.set(dialog.getExitCode()); } @@ -61,19 +65,25 @@ class GitWouldBeOverwrittenByCheckoutDialog extends DialogWrapper { return exitCode.get(); } - private GitWouldBeOverwrittenByCheckoutDialog(@NotNull Project project, @NotNull List changes) { + private GitSmartOperationDialog(@NotNull Project project, @NotNull List changes, @NotNull String operationTitle, + boolean forceButton) { super(project); myProject = project; myChanges = changes; - setOKButtonText("Smart checkout"); - setCancelButtonText("Don't checkout"); + myOperationTitle = operationTitle; + myForceButton = forceButton; + setOKButtonText("Smart " + operationTitle); + setCancelButtonText("Don't " + operationTitle); getCancelAction().putValue(FOCUSED_ACTION, Boolean.TRUE); init(); } @Override protected Action[] createLeftSideActions() { - return new Action[] {new ForceCheckoutAction() }; + if (myForceButton) { + return new Action[] {new ForceCheckoutAction(myOperationTitle) }; + } + return new Action[0]; } @Override @@ -94,19 +104,19 @@ class GitWouldBeOverwrittenByCheckoutDialog extends DialogWrapper { @Override protected String getDimensionServiceKey() { - return GitWouldBeOverwrittenByCheckoutDialog.class.getName(); + return GitSmartOperationDialog.class.getName(); } private class ForceCheckoutAction extends AbstractAction { - ForceCheckoutAction() { - super("Force checkout"); + ForceCheckoutAction(@NotNull String operationTitle) { + super("Force " + operationTitle); } @Override public void actionPerformed(ActionEvent e) { - close(FORCE_CHECKOUT_EXIT_CODE); + close(FORCE_EXIT_CODE); } } diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index c52978d918dd..27f94bf775b9 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -131,6 +131,19 @@ public class Git { return run(handler, true); } + @NotNull + public static GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, + @NotNull GitLineHandlerListener... listeners) { + final GitLineHandler mergeHandler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.MERGE); + mergeHandler.setSilent(false); + mergeHandler.addParameters(branchToMerge); + for (GitLineHandlerListener listener : listeners) { + mergeHandler.addLineListener(listener); + } + return run(mergeHandler); + } + + /** * {@code git checkout <reference>}
* {@code git checkout -b <newBranch> <reference>} @@ -225,6 +238,13 @@ public class Git { return run(h); } + @NotNull + public static GitCommandResult hardReset(@NotNull GitRepository repository, @NotNull String revision) { + final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.RESET); + handler.addParameters("--hard", revision); + return run(handler); + } + /** * Returns the last (tip) commit on the given branch.
* {@code git rev-list -1 } diff --git a/plugins/git4idea/src/git4idea/commands/GitMessageWithFilesDetector.java b/plugins/git4idea/src/git4idea/commands/GitMessageWithFilesDetector.java index 4b597df64bd7..a3c8910ab94b 100644 --- a/plugins/git4idea/src/git4idea/commands/GitMessageWithFilesDetector.java +++ b/plugins/git4idea/src/git4idea/commands/GitMessageWithFilesDetector.java @@ -40,6 +40,9 @@ public class GitMessageWithFilesDetector implements GitLineHandlerListener { private final VirtualFile myRoot; public enum Event { + LOCAL_CHANGES_OVERWRITTEN_BY_MERGE( + "Your local changes to the following files would be overwritten by merge", + "commit your changes or stash them before"), LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT( "Your local changes to the following files would be overwritten by checkout", "commit your changes or stash them before"), diff --git a/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java b/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java index 34c612e3b466..216db2ad3aa8 100644 --- a/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java +++ b/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java @@ -26,8 +26,11 @@ public class GitSimpleEventDetector implements GitLineHandlerListener { private boolean myHappened; public enum Event { - UNMERGED("you need to resolve your current index first"), - BRANCH_NOT_FULLY_MERGED("is not fully merged"); + UNMERGED_PREVENTING_CHECKOUT("you need to resolve your current index first"), + UNMERGED_PREVENTING_MERGE("is not possible because you have unmerged files"), + BRANCH_NOT_FULLY_MERGED("is not fully merged"), + MERGE_CONFLICT("Automatic merge failed; fix conflicts and then commit the result"), + ALREADY_UP_TO_DATE("Already up-to-date"); private final String myDetectionString; diff --git a/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java b/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java index c1987199ce7d..351bb674b5cf 100644 --- a/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java +++ b/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java @@ -118,7 +118,6 @@ public class GitConflictResolver { * the method shows a notification and returns {@code false}. *

* - * @param roots Git repositories to look for unmerged files. * @return {@code true} if there is nothing to merge anymore, {@code false} if unmerged files remain or in the case of error. */ public final boolean merge() { @@ -221,6 +220,11 @@ public class GitConflictResolver { } + @NotNull + protected NotificationListener getResolveLinkListener() { + return new ResolveNotificationListener(); + } + private class ResolveNotificationListener implements NotificationListener { @Override public void hyperlinkUpdate(@NotNull final Notification notification, @NotNull HyperlinkEvent event) { if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) { diff --git a/plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java b/plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java new file mode 100644 index 000000000000..4c48ab6dd0dd --- /dev/null +++ b/plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2012 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.merge; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vfs.VirtualFile; + +import java.util.Collection; + +/** + * Conflict resolver that makes a merge commit after all conflicts are resolved. + * + * @author Kirill Likhodedov + */ +public class GitMergeCommittingConflictResolver extends GitConflictResolver { + private final Collection myMergingRoots; + private final boolean myRefreshAfterCommit; + private final GitMerger myMerger; + + public GitMergeCommittingConflictResolver(Project project, GitMerger merger, Collection mergingRoots, Params params, + boolean refreshAfterCommit) { + super(project, mergingRoots, params); + myMerger = merger; + myMergingRoots = mergingRoots; + myRefreshAfterCommit = refreshAfterCommit; + } + + @Override protected boolean proceedAfterAllMerged() throws VcsException { + myMerger.mergeCommit(myMergingRoots); + if (myRefreshAfterCommit) { + for (VirtualFile root : myMergingRoots) { + root.refresh(true, true); + } + } + return true; + } +} diff --git a/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java b/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java index 4ae9edab67a2..ba4d118967ea 100644 --- a/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java +++ b/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java @@ -187,6 +187,7 @@ class GitBranchPopupActions { new CheckoutAction(myProject, myRepositories, myBranchName, mySelectedRepository), new CheckoutAsNewBranch(myProject, myRepositories, myBranchName, mySelectedRepository), new CompareAction(myProject, myRepositories, myBranchName, mySelectedRepository), + new MergeAction(myProject, myRepositories, myBranchName, mySelectedRepository), new DeleteAction(myProject, myRepositories, myBranchName, mySelectedRepository) }; } @@ -290,6 +291,7 @@ class GitBranchPopupActions { return new AnAction[] { new CheckoutRemoteBranchAction(myProject, myRepositories, myBranchName, mySelectedRepository), new CompareAction(myProject, myRepositories, myBranchName, mySelectedRepository), + new MergeAction(myProject, myRepositories, myBranchName, mySelectedRepository), }; } @@ -324,7 +326,6 @@ class GitBranchPopupActions { return myRemoteBranchName.substring(slashPosition+1); } } - } private static class CompareAction extends DumbAwareAction { @@ -334,10 +335,8 @@ class GitBranchPopupActions { private final String myBranchName; private final GitRepository mySelectedRepository; - public CompareAction(@NotNull Project project, - @NotNull List repositories, - @NotNull String branchName, - GitRepository selectedRepository) { + public CompareAction(@NotNull Project project, @NotNull List repositories, @NotNull String branchName, + @NotNull GitRepository selectedRepository) { super("Compare"); myProject = project; myRepositories = repositories; @@ -351,4 +350,27 @@ class GitBranchPopupActions { } } + + private static class MergeAction extends DumbAwareAction { + + private final Project myProject; + private final List myRepositories; + private final String myBranchName; + private final GitRepository mySelectedRepository; + + public MergeAction(@NotNull Project project, @NotNull List repositories, @NotNull String branchName, + @NotNull GitRepository selectedRepository) { + super("Merge"); + myProject = project; + myRepositories = repositories; + myBranchName = branchName; + mySelectedRepository = selectedRepository; + } + + @Override + public void actionPerformed(AnActionEvent e) { + new GitBranchOperationsProcessor(myProject, myRepositories, mySelectedRepository).merge(myBranchName); + } + + } } diff --git a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java index 6f3b1fff17c2..6f4c81c8aeb6 100644 --- a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java +++ b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java @@ -31,6 +31,7 @@ import git4idea.GitBranch; import git4idea.GitVcs; import git4idea.branch.GitBranchPair; import git4idea.merge.GitConflictResolver; +import git4idea.merge.GitMergeCommittingConflictResolver; import git4idea.merge.GitMerger; import git4idea.rebase.GitRebaser; import git4idea.stash.GitChangesSaver; @@ -297,7 +298,7 @@ public class GitUpdateProcess { GitConflictResolver.Params params = new GitConflictResolver.Params(); params.setErrorNotificationTitle("Can't update"); params.setMergeDescription("You have unfinished merge. These conflicts must be resolved before update."); - return !new MergeCommittingConflictResolver(myProject, myMerger, mergingRoots, params).merge(); + return !new GitMergeCommittingConflictResolver(myProject, myMerger, mergingRoots, params, false).merge(); } /** @@ -308,7 +309,7 @@ public class GitUpdateProcess { GitConflictResolver.Params params = new GitConflictResolver.Params(); params.setErrorNotificationTitle("Can't update"); params.setMergeDescription("Unmerged files detected. These conflicts must be resolved before update."); - return !new MergeCommittingConflictResolver(myProject, myMerger, myRoots, params).merge(); + return !new GitMergeCommittingConflictResolver(myProject, myMerger, myRoots, params, false).merge(); } /** @@ -338,22 +339,4 @@ public class GitUpdateProcess { } }.merge(); } - - // conflict resolver that makes a merge commit after all conflicts are resolved - private static class MergeCommittingConflictResolver extends GitConflictResolver { - private final Collection myMergingRoots; - private final GitMerger myMerger; - - public MergeCommittingConflictResolver(Project project, GitMerger merger, Collection mergingRoots, Params params) { - super(project, mergingRoots, params); - myMerger = merger; - myMergingRoots = mergingRoots; - } - - @Override protected boolean proceedAfterAllMerged() throws VcsException { - myMerger.mergeCommit(myMergingRoots); - return true; - } - } - } diff --git a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java index 8a4acd1832d1..f778e761c140 100644 --- a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java +++ b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java @@ -147,7 +147,7 @@ public class GitPreservingProcess { } } - private void load() { + public void load() { try { mySaver.load(); } diff --git a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java b/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java index 656b18655d13..659b07d4e708 100644 --- a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java +++ b/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java @@ -172,25 +172,25 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { assertBranch(myCommunity, MASTER); assertBranch(myContrib, MASTER); } - + @Test public void checkout_without_problems() throws Exception { - prepareBranchForSimpleCheckout(); + prepareBranchWithCommit("feature"); doCheckout("feature", null); assertNotify(NotificationType.INFORMATION, "Checked out feature"); } - + @Test public void checkout_with_unmerged_files_in_first_repo_should_show_notification() throws Exception { - prepareBranchForSimpleCheckout(); + prepareBranchWithCommit("feature"); GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate); doCheckout("feature", null); assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("checkout")); } - + @Test public void checkout_with_unmerged_file_in_second_repo_should_propose_to_rollback() throws Exception { - prepareBranchForSimpleCheckout(); + prepareBranchWithCommit("feature"); GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); doCheckout("feature", null); assertMessage(unmergedFilesErrorTitle("checkout")); @@ -198,7 +198,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { @Test public void rollback_checkout_should_return_to_previous_branch() throws Exception { - prepareBranchForSimpleCheckout(); + prepareBranchWithCommit("feature"); GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); myMessageManager.nextAnswer(Messages.OK); doCheckout("feature", null); @@ -208,7 +208,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { @Test public void deny_rollback_checkout_should_do_nothing() throws Exception { - prepareBranchForSimpleCheckout(); + prepareBranchWithCommit("feature"); GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); myMessageManager.nextAnswer(Messages.CANCEL); doCheckout("feature", null); @@ -243,7 +243,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { } } assertNotNull(untrackedFilesDialogClass); - + final AtomicBoolean dialogShown = new AtomicBoolean(); final Class finalUntrackedFilesDialogClass = untrackedFilesDialogClass; myDialogManager.registerDialogHandler(untrackedFilesDialogClass, new TestDialogHandler() { @@ -267,9 +267,9 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { branch(myContrib, "feature"); final AtomicBoolean dialogShown = new AtomicBoolean(); - myDialogManager.registerDialogHandler(GitWouldBeOverwrittenByCheckoutDialog.class, new TestDialogHandler() { + myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler() { @Override - public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) { + public int handleDialog(GitSmartOperationDialog dialog) { dialogShown.set(true); return DialogWrapper.CANCEL_EXIT_CODE; } @@ -284,10 +284,10 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate); prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myCommunity); prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myContrib); - myDialogManager.registerDialogHandler(GitWouldBeOverwrittenByCheckoutDialog.class, - new TestDialogHandler() { + myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, + new TestDialogHandler() { @Override - public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) { + public int handleDialog(GitSmartOperationDialog dialog) { return DialogWrapper.OK_EXIT_CODE; } }); @@ -307,16 +307,16 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { branch(myCommunity, "feature"); branch(myContrib, "feature"); - myDialogManager.registerDialogHandler(GitWouldBeOverwrittenByCheckoutDialog.class, new TestDialogHandler() { + myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler() { @Override - public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) { + public int handleDialog(GitSmartOperationDialog dialog) { return DialogWrapper.CANCEL_EXIT_CODE; } }); doCheckout("feature", null); assertNotify(NotificationType.ERROR, "Couldn't checkout feature", stripHtmlAndBreaks("Local changes would be overwritten by checkout." + - "Stash or commit them before checking out a branch.")); + "You should stash or commit them.")); assertBranch("master"); } @@ -325,25 +325,25 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myCommunity); branch(myUltimate, "feature"); branch(myContrib, "feature"); - myDialogManager.registerDialogHandler(GitWouldBeOverwrittenByCheckoutDialog.class, new TestDialogHandler() { + myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler() { @Override - public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) { + public int handleDialog(GitSmartOperationDialog dialog) { return DialogWrapper.CANCEL_EXIT_CODE; } }); doCheckout("feature", null); - assertMessage("Couldn't checkout feature", - "Local changes would be overwritten by checkout.
Stash or commit them before checking out a branch.
" + + assertMessage("Couldn't checkout feature", + "Local changes would be overwritten by checkout.
You should stash or commit them.
" + "However checkout has succeeded for the following repository:
" + - myUltimate.getPresentableUrl() + - "
You may rollback (checkout back to master) not to let branches diverge.", + myUltimate.getPresentableUrl() + + "
You may rollback (checkout back to master) not to let branches diverge.", "Rollback", "Don't rollback"); } - + @Test public void rollback_checkout_branch_as_new_branch_should_delete_branches() throws Exception { - prepareBranchForSimpleCheckout(); + prepareBranchWithCommit("feature"); GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); myMessageManager.nextAnswer(Messages.OK); doCheckout("feature", "newBranch"); @@ -372,14 +372,6 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { create(repository, "untracked.txt"); } - private void prepareBranchForSimpleCheckout() throws IOException { - for (GitRepository repository : myRepositories) { - checkout(repository, "-b", "feature"); - createAddCommit(repository, "feature_file.txt"); - checkout(repository, "master"); - } - } - @Test public void delete_branch_without_problems() throws Exception { for (GitRepository repository : myRepositories) { @@ -389,10 +381,10 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { doDeleteBranch("master1"); assertNotify(NotificationType.INFORMATION, "Deleted branch master1"); } - + @Test public void delete_unmerged_branch_should_show_dialog() throws Exception { - prepareUnmergedBranch(myUltimate, myCommunity, myContrib); + prepareBranchWithCommit("unmerged_branch", myUltimate, myCommunity, myContrib); final AtomicBoolean dialogShown = new AtomicBoolean(); myDialogManager.registerDialogHandler(GitBranchIsNotFullyMergedDialog.class, new TestDialogHandler() { @@ -405,41 +397,41 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { doDeleteBranch("unmerged_branch"); assertTrue(dialogShown.get()); } - + @Test public void ok_in_unmerged_branch_dialog_should_force_delete_branch() throws Exception { - prepareUnmergedBranch(myUltimate, myCommunity, myContrib); + prepareBranchWithCommit("unmerged_branch", myUltimate, myCommunity, myContrib); registerNotFullyMergedDialog(DialogWrapper.OK_EXIT_CODE); doDeleteBranch("unmerged_branch"); for (GitRepository repository : myRepositories) { assertTrue(!branch(repository).contains("unmerged_branch")); } } - + @Test public void cancel_in_unmerged_branch_dialog_in_first_repository_should_show_notification() throws Exception { - prepareUnmergedBranch(myUltimate, myContrib); + prepareBranchWithCommit("unmerged_branch", myUltimate, myContrib); branch(myCommunity, "unmerged_branch"); registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); doDeleteBranch("unmerged_branch"); assertNotify(NotificationType.ERROR, "Branch unmerged_branch wasn't deleted", "This branch is not fully merged to master."); } - + @Test public void cancel_in_unmerged_branch_dialog_in_not_first_repository_should_show_rollback_proposal() throws Exception { branch(myUltimate, "unmerged_branch"); - prepareUnmergedBranch(myCommunity, myContrib); + prepareBranchWithCommit("unmerged_branch", myCommunity, myContrib); registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); doDeleteBranch("unmerged_branch"); assertMessage(String.format("Branch %s wasn't deleted", "unmerged_branch")); } - + @Test public void rollback_delete_branch_should_recreate_branches() throws Exception { branch(myUltimate, "unmerged_branch"); - prepareUnmergedBranch(myCommunity); + prepareBranchWithCommit("unmerged_branch", myCommunity); branch(myContrib, "unmerged_branch"); registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); @@ -450,13 +442,13 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { assertTrue(branch(repository).contains("unmerged_branch")); } } - + @Test public void deny_rollback_delete_branch_should_do_nothing() throws Exception { branch(myUltimate, "unmerged_branch"); - prepareUnmergedBranch(myCommunity); + prepareBranchWithCommit("unmerged_branch", myCommunity); branch(myContrib, "unmerged_branch"); - + registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); myMessageManager.nextAnswer(Messages.CANCEL); doDeleteBranch("unmerged_branch"); @@ -466,6 +458,88 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { assertTrue(!branch(myUltimate).contains("unmerged_branch")); } + @Test + public void merge_simple_without_problems() throws Exception { + prepareBranchWithCommit("feature", myUltimate, myCommunity, myContrib); + doMerge("feature"); + assertNotify(NotificationType.INFORMATION, "Merged feature to master"); + + assertFile(myUltimate, "unmerged_branch_file", "content"); + assertFile(myCommunity, "unmerged_branch_file", "content"); + assertFile(myContrib, "unmerged_branch_file", "content"); + } + + private static void assertFile(GitRepository repository, String path, String content) throws IOException { + VirtualFile branchFile = repository.getRoot().findChild(path); + assertNotNull(branchFile); + assertTrue(branchFile.exists()); + assertEquals(new String(branchFile.contentsToByteArray()), content); + } + + @Test + public void merge_up_to_date_branch() throws Exception { + branch(myUltimate, "master2"); + branch(myCommunity, "master2"); + branch(myContrib, "master2"); + + doMerge("master2"); + + assertNotify(NotificationType.INFORMATION, "Already up-to-date."); + } + + @Test + public void merge_one_simple_and_other_up_to_date() throws Exception { + branch(myUltimate, "master2"); + branch(myContrib, "master2"); + prepareBranchWithCommit("master2", myCommunity); + + doMerge("master2"); + + assertNotify(NotificationType.INFORMATION, "Merged master2 to master"); + assertFile(myCommunity, "unmerged_branch_file", "content"); + assertNull(myUltimate.getRoot().findChild("unmerged_branch_file")); + } + + @Test + public void merge_with_unmerged_files_in_first_repo_should_show_notification() throws Exception { + prepareBranchWithCommit("feature"); + GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate); + doMerge("feature"); + assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("merge")); + } + + @Test + public void merge_with_unmerged_files_in_second_repo_should_propose_to_rollback() throws Exception { + prepareBranchWithCommit("feature"); + GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); + doMerge("feature"); + assertMessage(unmergedFilesErrorTitle("merge")); + } + + @Test + public void rollback_merge_should_reset_merge() throws Exception { + prepareBranchWithCommit("feature"); + String ultimateTip = tip(myUltimate); + GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); + myMessageManager.nextAnswer(Messages.OK); + doMerge("feature"); + assertMessage(unmergedFilesErrorTitle("merge")); + assertBranch("master"); + assertEquals(tip(myUltimate), ultimateTip); + } + + @Test + public void deny_rollback_merge_should_leave_as_is() throws Exception { + prepareBranchWithCommit("feature"); + String ultimateTip = tip(myUltimate); + GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); + myMessageManager.nextAnswer(Messages.CANCEL); + doMerge("feature"); + assertMessage(unmergedFilesErrorTitle("merge")); + assertBranch("master"); + assertFalse(tip(myUltimate).equals(ultimateTip)); + } + private void registerNotFullyMergedDialog(final int answer) { myDialogManager.registerDialogHandler(GitBranchIsNotFullyMergedDialog.class, new TestDialogHandler() { @Override @@ -475,9 +549,12 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { }); } - private static void prepareUnmergedBranch(GitRepository... repositories) throws IOException { + private void prepareBranchWithCommit(String branch, GitRepository... repositories) throws IOException { + if (repositories.length == 0) { + repositories = new GitRepository[] { myUltimate, myCommunity, myContrib }; + } for (GitRepository repository : repositories) { - checkout(repository, "-b", "unmerged_branch"); + checkout(repository, "-b", branch); createAddCommit(repository, "unmerged_branch_file"); checkout(repository, "master"); refresh(repository); @@ -488,6 +565,14 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { callPrivateBranchOperationsProcessorMethod("doCheckoutNewBranch", NEW_BRANCH); } + private void doMerge(String branch) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + callPrivateBranchOperationsProcessorMethod("doMerge", branch); + // sync refresh is needed, because the refresh inside GitMergeOperation is asynchronous. + for (GitRepository repository : myRepositories) { + repository.getRoot().refresh(false, true); + } + } + private static String unmergedFilesErrorNotificationDescription(String operation) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method method = GitBranchOperation.class.getDeclaredMethod("unmergedFilesErrorNotificationDescription", String.class); diff --git a/plugins/git4idea/tests/git4idea/test/GitExec.java b/plugins/git4idea/tests/git4idea/test/GitExec.java index e7c709f79fe2..6cabf8c9cb3d 100644 --- a/plugins/git4idea/tests/git4idea/test/GitExec.java +++ b/plugins/git4idea/tests/git4idea/test/GitExec.java @@ -121,6 +121,10 @@ public class GitExec { run(repository, "merge", branch); } + public static String tip(@NotNull GitRepository repository) throws IOException { + return run(repository, "rev-list", "-1", "HEAD"); + } + @NotNull private static String run(@NotNull GitRepository repository, @NotNull String command, String... params) throws IOException { return new GitTestRunEnv(new File(repository.getRoot().getPath())).run(command, params); diff --git a/plugins/git4idea/tests/git4idea/test/GitTestScenarioGenerator.java b/plugins/git4idea/tests/git4idea/test/GitTestScenarioGenerator.java index 021535ebad9e..10cb1fa5e5e2 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestScenarioGenerator.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestScenarioGenerator.java @@ -27,13 +27,15 @@ import static git4idea.test.GitExec.*; */ public class GitTestScenarioGenerator { + private static final String BRANCH_FOR_UNMERGED_CONFLICTS = "unmerged_files_branch_" + Math.random(); + public static void prepareUnmergedFiles(@NotNull GitRepository... repositories) throws IOException { for (GitRepository repository : repositories) { String unmergedFile = "unmerged"; create(repository, unmergedFile, "master content"); addCommit(repository, unmergedFile); - checkoutFeatureBranch(repository); + checkoutBranchForUnmergedConflicts(repository); edit(repository, unmergedFile, "feature content"); addCommit(repository, unmergedFile); @@ -41,17 +43,17 @@ public class GitTestScenarioGenerator { edit(repository, unmergedFile, "master feature"); addCommit(repository, unmergedFile); - merge(repository, "feature"); + merge(repository, BRANCH_FOR_UNMERGED_CONFLICTS); refresh(repository); } } - private static void checkoutFeatureBranch(GitRepository repository) throws IOException { + private static void checkoutBranchForUnmergedConflicts(GitRepository repository) throws IOException { String branches = branch(repository); - if (!branches.contains("feature")) { - checkout(repository, "-b", "feature"); + if (!branches.contains(BRANCH_FOR_UNMERGED_CONFLICTS)) { + checkout(repository, "-b", BRANCH_FOR_UNMERGED_CONFLICTS); } else { - checkout(repository, "feature"); + checkout(repository, BRANCH_FOR_UNMERGED_CONFLICTS); } } } From 47087d9a6fae8ee9d58bf3076a66cadc8a9237d6 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 8 Feb 2012 19:36:50 +0400 Subject: [PATCH 13/16] More tests on GitMergeOperation --- .../git4idea/branch/GitCheckoutOperation.java | 3 +- .../git4idea/branch/GitMergeOperation.java | 3 +- .../branch/GitBranchOperationsTest.java | 110 +++++++++++++++--- 3 files changed, 95 insertions(+), 21 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 1340ba4e582f..6585ce74fd73 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -50,6 +50,7 @@ import static git4idea.util.GitUIUtil.code; class GitCheckoutOperation extends GitBranchOperation { private static final Logger LOG = Logger.getInstance(GitCheckoutOperation.class); + public static final String ROLLBACK_PROPOSAL_FORMAT = "You may rollback (checkout back to %s) not to let branches diverge."; @NotNull private final String myStartPointReference; @Nullable private final String myNewBranch; @@ -142,7 +143,7 @@ class GitCheckoutOperation extends GitBranchOperation { protected String getRollbackProposal() { return "However checkout has succeeded for the following " + repositories() + ":
" + successfulRepositoriesJoined() + - "
You may rollback (checkout back to " + myPreviousBranch + ") not to let branches diverge."; + "
" + String.format(ROLLBACK_PROPOSAL_FORMAT, myPreviousBranch); } @NotNull diff --git a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java index b323bb2913e8..1a83aff9e779 100644 --- a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java @@ -47,6 +47,7 @@ import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILE class GitMergeOperation extends GitBranchOperation { private static final Logger LOG = Logger.getInstance(GitMergeOperation.class); + public static final String ROLLBACK_PROPOSAL = "You may rollback (reset to the commit before merging) not to let branches diverge."; @NotNull private final ChangeListManager myChangeListManager; @NotNull private final String myBranchToMerge; @@ -287,7 +288,7 @@ class GitMergeOperation extends GitBranchOperation { protected String getRollbackProposal() { return "However merge has succeeded for the following " + repositories() + ":
" + successfulRepositoriesJoined() + - "
You may rollback (reset to the commit before merging) not to let branches diverge."; + "
" + ROLLBACK_PROPOSAL; } @NotNull diff --git a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java b/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java index 659b07d4e708..958213bbd41f 100644 --- a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java +++ b/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java @@ -220,16 +220,25 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { @Test public void checkout_with_untracked_files_overwritten_by_checkout_in_first_repo_should_show_notification() throws Exception { + test_untracked_files_overwritten_by_in_first_repo(true); + } + + private void test_untracked_files_overwritten_by_in_first_repo(boolean checkout) throws Exception { prepareUntrackedFilesAndBranchWithSameTrackedFiles(myUltimate); branch(myCommunity, "feature"); branch(myContrib, "feature"); - doCheckout("feature", null); - assertNotify(NotificationType.ERROR, UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription("checkout", false)); + doCheckoutOrMerge(checkout, "feature"); + String operation = checkout ? "checkout" : "merge"; + assertNotify(NotificationType.ERROR, UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(operation, false)); } @Test public void checkout_with_untracked_files_overwritten_by_checkout_in_second_repo_should_show_rollback_proposal_with_file_list() throws Exception { + test_checkout_with_untracked_files_overwritten_by_in_second_repo(true); + } + + public void test_checkout_with_untracked_files_overwritten_by_in_second_repo(boolean checkout) throws Exception { prepareUntrackedFilesAndBranchWithSameTrackedFiles(myCommunity); branch(myUltimate, "feature"); branch(myContrib, "feature"); @@ -256,12 +265,16 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { } }); - doCheckout("feature", null); + doCheckoutOrMerge(checkout, "feature"); assertTrue(dialogShown.get()); } @Test public void checkout_with_local_changes_overwritten_by_checkout_should_show_smart_checkout_dialog() throws Exception { + test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog(true); + } + + public void test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog(boolean checkout) throws Exception { prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate); branch(myCommunity, "feature"); branch(myContrib, "feature"); @@ -275,12 +288,22 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { } }); - doCheckout("feature", null); + doCheckoutOrMerge(checkout, "feature"); assertTrue(dialogShown.get()); } @Test public void agree_to_smart_checkout_should_smart_checkout() throws Exception { + prepare_agree_to_smart_operation(true); + assertBranch("feature"); + for (GitRepository repository : myRepositories) { + refresh(repository); + assertBranch(repository, "feature"); + assertEquals(read(repository, "local.txt"), "master\ninitial content\nfeature content\n"); + } + } + + public void prepare_agree_to_smart_operation(boolean checkout) throws Exception { prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate); prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myCommunity); prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myContrib); @@ -292,17 +315,15 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { } }); - doCheckout("feature", null); - assertBranch("feature"); - for (GitRepository repository : myRepositories) { - refresh(repository); - assertBranch(repository, "feature"); - assertEquals(read(repository, "local.txt"), "master\ninitial content\nfeature content\n"); - } + doCheckoutOrMerge(checkout, "feature"); } @Test public void deny_to_smart_checkout_in_first_repo_should_show_notification() throws Exception { + test_deny_to_smart_operation_in_first_repo_should_show_notification(true); + } + + public void test_deny_to_smart_operation_in_first_repo_should_show_notification(boolean checkout) throws Exception { prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate); branch(myCommunity, "feature"); branch(myContrib, "feature"); @@ -314,14 +335,19 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { } }); - doCheckout("feature", null); - assertNotify(NotificationType.ERROR, "Couldn't checkout feature", stripHtmlAndBreaks("Local changes would be overwritten by checkout." + - "You should stash or commit them.")); + doCheckoutOrMerge(checkout, "feature"); + String operation = checkout ? "checkout" : "merge"; + assertNotify(NotificationType.ERROR, "Couldn't " + operation + " feature", + stripHtmlAndBreaks("Local changes would be overwritten by " + operation + "." + "You should stash or commit them.")); assertBranch("master"); } @Test public void deny_to_smart_checkout_in_second_repo_should_show_rollback_proposal() throws Exception { + test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal(true); + } + + public void test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal(boolean checkout) throws Exception { prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myCommunity); branch(myUltimate, "feature"); branch(myContrib, "feature"); @@ -332,12 +358,16 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { } }); - doCheckout("feature", null); - assertMessage("Couldn't checkout feature", - "Local changes would be overwritten by checkout.
You should stash or commit them.
" + - "However checkout has succeeded for the following repository:
" + + doCheckoutOrMerge(checkout, "feature"); + String operationName = checkout ? "checkout" : "merge"; + String rollbackProposal = checkout ? + String.format(GitCheckoutOperation.ROLLBACK_PROPOSAL_FORMAT, "master") : + GitMergeOperation.ROLLBACK_PROPOSAL; + assertMessage("Couldn't " + operationName + " feature", + "Local changes would be overwritten by " + operationName + ".
You should stash or commit them.
" + + "However " + operationName + " has succeeded for the following repository:
" + myUltimate.getPresentableUrl() + - "
You may rollback (checkout back to master) not to let branches diverge.", + "
" + rollbackProposal, "Rollback", "Don't rollback"); } @@ -540,6 +570,39 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { assertFalse(tip(myUltimate).equals(ultimateTip)); } + @Test + public void merge_with_untracked_files_overwritten_by_merge_in_first_repo_should_show_notification() throws Exception { + test_untracked_files_overwritten_by_in_first_repo(false); + } + + @Test + public void merge_with_untracked_files_overwritten_by_merge_in_second_repo_should_show_rollback_proposal_with_file_list() throws Exception { + test_checkout_with_untracked_files_overwritten_by_in_second_repo(false); + } + + @Test + public void merge_with_local_changes_overwritten_by_merge_should_show_smart_checkout_dialog() throws Exception { + test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog(false); + } + + @Test + public void agree_to_smart_merge_should_smart_merge() throws Exception { + prepare_agree_to_smart_operation(false); + for (GitRepository repository : myRepositories) { + assertEquals(read(repository, "local.txt"), "master\ninitial content\nfeature content\n"); + } + } + + @Test + public void deny_to_smart_merge_in_first_repo_should_show_notification() throws Exception { + test_deny_to_smart_operation_in_first_repo_should_show_notification(false); + } + + @Test + public void deny_to_smart_merge_in_second_repo_should_show_rollback_proposal() throws Exception { + test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal(false); + } + private void registerNotFullyMergedDialog(final int answer) { myDialogManager.registerDialogHandler(GitBranchIsNotFullyMergedDialog.class, new TestDialogHandler() { @Override @@ -573,6 +636,15 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase { } } + private void doCheckoutOrMerge(boolean checkout, String branch) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + if (checkout) { + doCheckout(branch, null); + } + else { + doMerge(branch); + } + } + private static String unmergedFilesErrorNotificationDescription(String operation) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method method = GitBranchOperation.class.getDeclaredMethod("unmergedFilesErrorNotificationDescription", String.class); From 054f70493b319b2b659278d8c1ac2ac983291054 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 8 Feb 2012 19:37:41 +0400 Subject: [PATCH 14/16] GitPreservingProcess: Don't load the changes if they already have been loaded. --- .../git4idea/util/GitPreservingProcess.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java index f778e761c140..4162719c18f5 100644 --- a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java +++ b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java @@ -57,6 +57,8 @@ public class GitPreservingProcess { @NotNull private final String myStashMessage; private GitStashChangesSaver mySaver; + private boolean myLoaded; + private final Object LOAD_LOCK = new Object(); public GitPreservingProcess(@NotNull Project project, @NotNull Collection repositories, @NotNull String operationTitle, @NotNull String destinationName, @@ -148,13 +150,19 @@ public class GitPreservingProcess { } public void load() { - try { - mySaver.load(); - } - catch (VcsException e) { - LOG.info("Couldn't load local changes", e); - NotificationManager.getInstance(myProject).notifyError("Couldn't restore uncommitted changes", - String.format("Tried to unstash uncommitted changes, but failed with error.
%s",join(e.getMessages()))); + synchronized (LOAD_LOCK) { + if (myLoaded) { + return; + } + try { + mySaver.load(); + myLoaded = true; + } + catch (VcsException e) { + LOG.info("Couldn't load local changes", e); + NotificationManager.getInstance(myProject).notifyError("Couldn't restore uncommitted changes", + String.format("Tried to unstash uncommitted changes, but failed with error.
%s",join(e.getMessages()))); + } } } From 2c51f02d36290e97b7d7ff209dba15c8094b61b6 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 8 Feb 2012 19:56:14 +0400 Subject: [PATCH 15/16] GitMergeOperation: fix "already up-to-date" situation detection --- .../git4idea/src/git4idea/branch/GitMergeOperation.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java index 1a83aff9e779..fce48e4c0643 100644 --- a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java @@ -73,7 +73,7 @@ class GitMergeOperation extends GitBranchOperation { @Override protected void execute() { boolean fatalErrorHappened = false; - boolean notAlreadyUpToDate = false; + int alreadyUpToDateRepositories = 0; while (hasMoreRepositories() && !fatalErrorHappened) { final GitRepository repository = next(); @@ -90,8 +90,8 @@ class GitMergeOperation extends GitBranchOperation { if (result.success()) { refresh(repository); markSuccessful(repository); - if (!alreadyUpToDateDetector.hasHappened()) { - notAlreadyUpToDate = true; + if (alreadyUpToDateDetector.hasHappened()) { + alreadyUpToDateRepositories += 1; } } else if (unmergedFiles.hasHappened()) { @@ -122,7 +122,7 @@ class GitMergeOperation extends GitBranchOperation { boolean allConflictsResolved = resolveConflicts(); if (!fatalErrorHappened && allConflictsResolved) { - if (notAlreadyUpToDate) { + if (alreadyUpToDateRepositories < getRepositories().size()) { notifySuccess(); } else { From 26ef832e9b185ccfbfbb29a9034a3fa0f403ac12 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 8 Feb 2012 21:18:39 +0400 Subject: [PATCH 16/16] capitalize text on buttons --- .../src/git4idea/branch/GitSmartOperationDialog.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/git4idea/src/git4idea/branch/GitSmartOperationDialog.java b/plugins/git4idea/src/git4idea/branch/GitSmartOperationDialog.java index af3e986ea62f..0d8a187664e4 100644 --- a/plugins/git4idea/src/git4idea/branch/GitSmartOperationDialog.java +++ b/plugins/git4idea/src/git4idea/branch/GitSmartOperationDialog.java @@ -17,6 +17,7 @@ package git4idea.branch; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ui.ChangesBrowser; import com.intellij.ui.IdeBorderFactory; @@ -45,7 +46,7 @@ class GitSmartOperationDialog extends DialogWrapper { private final Project myProject; private final List myChanges; - @NotNull private final String myOperationTitle; + @NotNull private final String myCapitalizedOperationTitle; private final boolean myForceButton; /** @@ -70,10 +71,10 @@ class GitSmartOperationDialog extends DialogWrapper { super(project); myProject = project; myChanges = changes; - myOperationTitle = operationTitle; + myCapitalizedOperationTitle = StringUtil.capitalize(operationTitle); myForceButton = forceButton; - setOKButtonText("Smart " + operationTitle); - setCancelButtonText("Don't " + operationTitle); + setOKButtonText("Smart " + myCapitalizedOperationTitle); + setCancelButtonText("Don't " + myCapitalizedOperationTitle); getCancelAction().putValue(FOCUSED_ACTION, Boolean.TRUE); init(); } @@ -81,7 +82,7 @@ class GitSmartOperationDialog extends DialogWrapper { @Override protected Action[] createLeftSideActions() { if (myForceButton) { - return new Action[] {new ForceCheckoutAction(myOperationTitle) }; + return new Action[] {new ForceCheckoutAction(myCapitalizedOperationTitle) }; } return new Action[0]; }