From ff2905d592bda972df7741d54c07e14b9c9fbfa5 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Wed, 8 Feb 2012 17:11:55 +0400 Subject: [PATCH] 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); } } }