blob: 1b8f382d1dd8442967dbfa02520b9c8b91eb82bb [file] [log] [blame]
/*
* 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.dvcs.DvcsUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.commands.*;
import git4idea.merge.GitMergeCommittingConflictResolver;
import git4idea.merge.GitMerger;
import git4idea.repo.GitRepository;
import git4idea.reset.GitResetMode;
import git4idea.util.GitPreservingProcess;
import org.jetbrains.annotations.NotNull;
import javax.swing.event.HyperlinkEvent;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
class GitMergeOperation extends GitBranchOperation {
private static final Logger LOG = Logger.getInstance(GitMergeOperation.class);
public static final String ROLLBACK_PROPOSAL = "You may rollback (reset to the commit before merging) not to let branches diverge.";
@NotNull private final ChangeListManager myChangeListManager;
@NotNull private final String myBranchToMerge;
private final GitBrancher.DeleteOnMergeOption myDeleteOnMerge;
@NotNull private final Map<GitRepository, String> myCurrentRevisionsBeforeMerge;
// true in value, if we've stashed local changes before merge and will need to unstash after resolving conflicts.
@NotNull private final Map<GitRepository, Boolean> myConflictedRepositories = new HashMap<GitRepository, Boolean>();
private GitPreservingProcess myPreservingProcess;
GitMergeOperation(@NotNull Project project, GitPlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler,
@NotNull Collection<GitRepository> repositories,
@NotNull String branchToMerge, GitBrancher.DeleteOnMergeOption deleteOnMerge,
@NotNull Map<GitRepository, String> currentRevisionsBeforeMerge) {
super(project, facade, git, uiHandler, repositories);
myBranchToMerge = branchToMerge;
myDeleteOnMerge = deleteOnMerge;
myCurrentRevisionsBeforeMerge = currentRevisionsBeforeMerge;
myChangeListManager = myFacade.getChangeListManager(myProject);
}
@Override
protected void execute() {
LOG.info("starting");
saveAllDocuments();
boolean fatalErrorHappened = false;
int alreadyUpToDateRepositories = 0;
GitUtil.workingTreeChangeStarted(myProject);
try {
while (hasMoreRepositories() && !fatalErrorHappened) {
final GitRepository repository = next();
LOG.info("next repository: " + repository);
VirtualFile root = repository.getRoot();
GitLocalChangesWouldBeOverwrittenDetector localChangesDetector =
new GitLocalChangesWouldBeOverwrittenDetector(root, GitLocalChangesWouldBeOverwrittenDetector.Operation.MERGE);
GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_MERGE);
GitUntrackedFilesOverwrittenByOperationDetector untrackedOverwrittenByMerge =
new GitUntrackedFilesOverwrittenByOperationDetector(root);
GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT);
GitSimpleEventDetector alreadyUpToDateDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.ALREADY_UP_TO_DATE);
GitCommandResult result = myGit.merge(repository, myBranchToMerge, Collections.<String>emptyList(),
localChangesDetector, unmergedFiles, untrackedOverwrittenByMerge, mergeConflict,
alreadyUpToDateDetector);
if (result.success()) {
LOG.info("Merged successfully");
refresh(repository);
markSuccessful(repository);
if (alreadyUpToDateDetector.hasHappened()) {
alreadyUpToDateRepositories += 1;
}
}
else if (unmergedFiles.hasHappened()) {
LOG.info("Unmerged files error!");
fatalUnmergedFilesError();
fatalErrorHappened = true;
}
else if (localChangesDetector.wasMessageDetected()) {
LOG.info("Local changes would be overwritten by merge!");
boolean smartMergeSucceeded = proposeSmartMergePerformAndNotify(repository, localChangesDetector);
if (!smartMergeSucceeded) {
fatalErrorHappened = true;
}
}
else if (mergeConflict.hasHappened()) {
LOG.info("Merge conflict");
myConflictedRepositories.put(repository, Boolean.FALSE);
refresh(repository);
markSuccessful(repository);
}
else if (untrackedOverwrittenByMerge.wasMessageDetected()) {
LOG.info("Untracked files would be overwritten by merge!");
fatalUntrackedFilesError(repository.getRoot(), untrackedOverwrittenByMerge.getRelativeFilePaths());
fatalErrorHappened = true;
}
else {
LOG.info("Unknown error. " + result);
fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString());
fatalErrorHappened = true;
}
}
if (fatalErrorHappened) {
notifyAboutRemainingConflicts();
}
else {
boolean allConflictsResolved = resolveConflicts();
if (allConflictsResolved) {
if (alreadyUpToDateRepositories < getRepositories().size()) {
notifySuccess();
}
else {
notifySuccess("Already up-to-date");
}
}
}
restoreLocalChanges();
}
finally {
GitUtil.workingTreeChangeFinished(myProject);
}
}
private void notifyAboutRemainingConflicts() {
if (!myConflictedRepositories.isEmpty()) {
new MyMergeConflictResolver().notifyUnresolvedRemain();
}
}
@Override
protected void notifySuccess(@NotNull String message) {
switch (myDeleteOnMerge) {
case DELETE:
super.notifySuccess(message);
UIUtil.invokeLaterIfNeeded(new Runnable() { // bg process needs to be started from the EDT
@Override
public void run() {
GitBrancher brancher = ServiceManager.getService(myProject, GitBrancher.class);
brancher.deleteBranch(myBranchToMerge, new ArrayList<GitRepository>(getRepositories()));
}
});
break;
case PROPOSE:
String description = message + "<br/><a href='delete'>Delete " + myBranchToMerge + "</a>";
VcsNotifier.getInstance(myProject).notifySuccess("", description, new DeleteMergedLocalBranchNotificationListener());
break;
case NOTHING:
super.notifySuccess(message);
break;
}
}
private boolean resolveConflicts() {
if (!myConflictedRepositories.isEmpty()) {
return new MyMergeConflictResolver().merge();
}
return true;
}
private boolean proposeSmartMergePerformAndNotify(@NotNull GitRepository repository,
@NotNull GitMessageWithFilesDetector localChangesOverwrittenByMerge) {
Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentHeads.get(repository),
myBranchToMerge);
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
List<Change> affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond();
Collection<String> absolutePaths = GitUtil.toAbsolute(repository.getRoot(), localChangesOverwrittenByMerge.getRelativeFilePaths());
int smartCheckoutDecision = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, absolutePaths, "merge", null);
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, myFacade, myGit, 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 = myGit.merge(repository, myBranchToMerge, Collections.<String>emptyList(), 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() {
LOG.info("starting rollback...");
Collection<GitRepository> repositoriesForSmartRollback = new ArrayList<GitRepository>();
Collection<GitRepository> repositoriesForSimpleRollback = new ArrayList<GitRepository>();
Collection<GitRepository> repositoriesForMergeRollback = new ArrayList<GitRepository>();
for (GitRepository repository : getSuccessfulRepositories()) {
if (myConflictedRepositories.containsKey(repository)) {
repositoriesForMergeRollback.add(repository);
}
else if (thereAreLocalChangesIn(repository)) {
repositoriesForSmartRollback.add(repository);
}
else {
repositoriesForSimpleRollback.add(repository);
}
}
LOG.info("for smart rollback: " + DvcsUtil.getShortNames(repositoriesForSmartRollback) +
"; for simple rollback: " + DvcsUtil.getShortNames(repositoriesForSimpleRollback) +
"; for merge rollback: " + DvcsUtil.getShortNames(repositoriesForMergeRollback));
GitCompoundResult result = smartRollback(repositoriesForSmartRollback);
for (GitRepository repository : repositoriesForSimpleRollback) {
result.append(repository, rollback(repository));
}
for (GitRepository repository : repositoriesForMergeRollback) {
result.append(repository, rollbackMerge(repository));
}
myConflictedRepositories.clear();
if (!result.totalSuccess()) {
VcsNotifier.getInstance(myProject).notifyError("Error during rollback", result.getErrorOutputWithReposIndication());
}
LOG.info("rollback finished.");
}
@NotNull
private GitCompoundResult smartRollback(@NotNull final Collection<GitRepository> repositories) {
LOG.info("Starting smart rollback...");
final GitCompoundResult result = new GitCompoundResult(myProject);
GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myFacade, myGit, repositories, "merge", myBranchToMerge,
getIndicator(),
new Runnable() {
@Override public void run() {
for (GitRepository repository : repositories) {
result.append(repository, rollback(repository));
}
}
});
preservingProcess.execute();
LOG.info("Smart rollback completed.");
return result;
}
@NotNull
private GitCommandResult rollback(@NotNull GitRepository repository) {
return myGit.reset(repository, GitResetMode.HARD, myCurrentRevisionsBeforeMerge.get(repository));
}
@NotNull
private GitCommandResult rollbackMerge(@NotNull GitRepository repository) {
GitCommandResult result = myGit.resetMerge(repository, null);
refresh(repository);
return result;
}
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, stringifyBranchesByRepos(myCurrentHeads));
}
@NotNull
@Override
protected String getRollbackProposal() {
return "However merge has succeeded for the following " + repositories() + ":<br/>" +
successfulRepositoriesJoined() +
"<br/>" + ROLLBACK_PROPOSAL;
}
@NotNull
@Override
protected String getOperationName() {
return "merge";
}
private void refresh(GitRepository... repositories) {
for (GitRepository repository : repositories) {
refreshRoot(repository);
repository.update();
}
}
private class MyMergeConflictResolver extends GitMergeCommittingConflictResolver {
public MyMergeConflictResolver() {
super(GitMergeOperation.this.myProject, myGit, new GitMerger(GitMergeOperation.this.myProject),
GitUtil.getRootsFromRepositories(GitMergeOperation.this.myConflictedRepositories.keySet()), new Params(), true);
}
@Override
protected void notifyUnresolvedRemain() {
VcsNotifier.getInstance(myProject).notifyImportantWarning("Merged branch " + myBranchToMerge + " with conflicts",
"Unresolved conflicts remain in the project. <a href='resolve'>Resolve now.</a>",
getResolveLinkListener());
}
}
private class DeleteMergedLocalBranchNotificationListener implements NotificationListener {
@Override
public void hyperlinkUpdate(@NotNull Notification notification,
@NotNull HyperlinkEvent event) {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equalsIgnoreCase("delete")) {
GitBrancher brancher = ServiceManager.getService(myProject, GitBrancher.class);
brancher.deleteBranch(myBranchToMerge, new ArrayList<GitRepository>(getRepositories()));
}
}
}
}