| /* |
| * 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())); |
| } |
| } |
| } |
| } |