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).
This commit is contained in:
Kirill Likhodedov
2012-02-08 17:11:55 +04:00
parent b6b074e3a2
commit ff2905d592
17 changed files with 679 additions and 140 deletions
@@ -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.<br/>You should stash or commit them.<br/>", 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<GitRepository>, List<Change>> getConflictingRepositoriesAndAffectedChanges(
@NotNull GitRepository currentRepository, @NotNull GitMessageWithFilesDetector localChangesOverwrittenBy,
String currentBranch, String nextBranch) {
// get changes overwritten by checkout from the error message captured from Git
List<Change> 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<GitRepository, List<Change>> conflictingChangesInRepositories =
collectLocalChangesConflictingWithBranch(myProject, getRemainingRepositoriesExceptGiven(currentRepository), currentBranch, nextBranch);
Set<GitRepository> otherProblematicRepositories = conflictingChangesInRepositories.keySet();
List<GitRepository> allConflictingRepositories = new ArrayList<GitRepository>(otherProblematicRepositories);
allConflictingRepositories.add(currentRepository);
for (List<Change> changes : conflictingChangesInRepositories.values()) {
affectedChanges.addAll(changes);
}
return Pair.create(allConflictingRepositories, affectedChanges);
}
}
@@ -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<GitRepository, String> revisions = new HashMap<GitRepository, String>();
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.
*/
@@ -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()) {
@@ -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<Change> 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<GitRepository, List<Change>> conflictingChangesInRepositories =
collectLocalChangesConflictingWithBranch(myProject, getRemainingRepositoriesExceptGiven(repository), myPreviousBranch, myStartPointReference);
Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myPreviousBranch, myStartPointReference);
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
List<Change> affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond();
Set<GitRepository> otherProblematicRepositories = conflictingChangesInRepositories.keySet();
List<GitRepository> allConflictingRepositories = new ArrayList<GitRepository>(otherProblematicRepositories);
allConflictingRepositories.add(repository);
for (List<Change> 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.<br/>Stash or commit them before checking out a branch.<br/>";
if (wereSuccessful()) {
showFatalErrorDialogWithRollback(title, message);
}
else {
showFatalNotification(title, message);
}
}
@NotNull
@Override
protected String getRollbackProposal() {
@@ -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<GitRepository, String> myCurrentRevisionsBeforeMerge;
// true in value if unstash will be needed after resolving conflicts.
@NotNull private final Map<GitRepository, Boolean> myConflictedRepositories = new HashMap<GitRepository, Boolean>();
private GitPreservingProcess myPreservingProcess;
protected GitMergeOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
@NotNull String branchToMerge,
@NotNull String currentBranch,
@NotNull Map<GitRepository, String> 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. <a href='resolve'>Resolve now.</a>", NotificationType.WARNING,
getResolveLinkListener());
}
};
return conflictResolver.merge();
}
return true;
}
private boolean proposeSmartMergePerformAndNotify(@NotNull GitRepository repository,
@NotNull GitMessageWithFilesDetector localChangesOverwrittenByMerge) {
Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranch, myBranchToMerge);
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
List<Change> 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<GitRepository> 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<Boolean>() {
@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<GitRepository> 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<GitRepository> repositoriesForSmartRollback = new ArrayList<GitRepository>();
Collection<GitRepository> repositoriesForSimpleRollback = new ArrayList<GitRepository>();
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<GitRepository> 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 <b><code>%s</code></b> to <b><code>%s</code></b>", myBranchToMerge, myCurrentBranch);
}
@NotNull
@Override
protected String getRollbackProposal() {
return "However merge has succeeded for the following " + repositories() + ":<br/>" +
successfulRepositoriesJoined() +
"<br/>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);
}
}
}
@@ -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<Change> 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<Change> changes) {
static int showAndGetAnswer(@NotNull final Project project, @NotNull final List<Change> 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<Change> changes) {
private GitSmartOperationDialog(@NotNull Project project, @NotNull List<Change> 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);
}
}
@@ -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 &lt;reference&gt;} <br/>
* {@code git checkout -b &lt;newBranch&gt; &lt;reference&gt;}
@@ -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.<br/>
* {@code git rev-list -1 <branchName>}
@@ -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"),
@@ -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;
@@ -118,7 +118,6 @@ public class GitConflictResolver {
* the method shows a notification and returns {@code false}.
* </p>
*
* @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")) {
@@ -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<VirtualFile> myMergingRoots;
private final boolean myRefreshAfterCommit;
private final GitMerger myMerger;
public GitMergeCommittingConflictResolver(Project project, GitMerger merger, Collection<VirtualFile> 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;
}
}
@@ -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<GitRepository> repositories,
@NotNull String branchName,
GitRepository selectedRepository) {
public CompareAction(@NotNull Project project, @NotNull List<GitRepository> 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<GitRepository> myRepositories;
private final String myBranchName;
private final GitRepository mySelectedRepository;
public MergeAction(@NotNull Project project, @NotNull List<GitRepository> 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);
}
}
}
@@ -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<VirtualFile> myMergingRoots;
private final GitMerger myMerger;
public MergeCommittingConflictResolver(Project project, GitMerger merger, Collection<VirtualFile> mergingRoots, Params params) {
super(project, mergingRoots, params);
myMerger = merger;
myMergingRoots = mergingRoots;
}
@Override protected boolean proceedAfterAllMerged() throws VcsException {
myMerger.mergeCommit(myMergingRoots);
return true;
}
}
}
@@ -147,7 +147,7 @@ public class GitPreservingProcess {
}
}
private void load() {
public void load() {
try {
mySaver.load();
}
@@ -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<GitWouldBeOverwrittenByCheckoutDialog>() {
myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler<GitSmartOperationDialog>() {
@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<GitWouldBeOverwrittenByCheckoutDialog>() {
myDialogManager.registerDialogHandler(GitSmartOperationDialog.class,
new TestDialogHandler<GitSmartOperationDialog>() {
@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<GitWouldBeOverwrittenByCheckoutDialog>() {
myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler<GitSmartOperationDialog>() {
@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<GitWouldBeOverwrittenByCheckoutDialog>() {
myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler<GitSmartOperationDialog>() {
@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.<br/>Stash or commit them before checking out a branch.<br/>" +
assertMessage("Couldn't checkout feature",
"Local changes would be overwritten by checkout.<br/>You should stash or commit them.<br/>" +
"However checkout has succeeded for the following repository:<br/>" +
myUltimate.getPresentableUrl() +
"<br/>You may rollback (checkout back to master) not to let branches diverge.",
myUltimate.getPresentableUrl() +
"<br/>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<GitBranchIsNotFullyMergedDialog>() {
@@ -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<GitBranchIsNotFullyMergedDialog>() {
@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);
@@ -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);
@@ -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);
}
}
}