/* * Copyright 2000-2011 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.update; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.FilePathImpl; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vcs.changes.ContentRevision; import com.intellij.openapi.vcs.changes.LocalChangeList; import com.intellij.openapi.vcs.changes.ui.ChangeListViewerDialog; import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; import git4idea.GitUtil; import git4idea.branch.GitBranchPair; import git4idea.commands.*; import git4idea.merge.GitConflictResolver; import git4idea.merge.GitMerger; import git4idea.repo.GitRepository; import git4idea.util.GitUIUtil; import git4idea.util.UntrackedFilesNotifier; import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * Handles git pull via merge. */ public class GitMergeUpdater extends GitUpdater { private static final Logger LOG = Logger.getInstance(GitMergeUpdater.class); private final ChangeListManager myChangeListManager; public GitMergeUpdater(Project project, @NotNull Git git, VirtualFile root, final Map trackedBranches, ProgressIndicator progressIndicator, UpdatedFiles updatedFiles) { super(project, git, root, trackedBranches, progressIndicator, updatedFiles); myChangeListManager = ChangeListManager.getInstance(myProject); } @Override @NotNull protected GitUpdateResult doUpdate() { LOG.info("doUpdate "); final GitMerger merger = new GitMerger(myProject); final GitLineHandler mergeHandler = new GitLineHandler(myProject, myRoot, GitCommand.MERGE); mergeHandler.addParameters("--no-stat", "-v"); mergeHandler.addParameters(myTrackedBranches.get(myRoot).getDest().getName()); final MergeLineListener mergeLineListener = new MergeLineListener(); mergeHandler.addLineListener(mergeLineListener); GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = new GitUntrackedFilesOverwrittenByOperationDetector(myRoot); mergeHandler.addLineListener(untrackedFilesDetector); String progressTitle = makeProgressTitle("Merging"); final GitTask mergeTask = new GitTask(myProject, mergeHandler, progressTitle); mergeTask.setProgressIndicator(myProgressIndicator); mergeTask.setProgressAnalyzer(new GitStandardProgressAnalyzer()); final AtomicReference updateResult = new AtomicReference(); final AtomicBoolean failure = new AtomicBoolean(); mergeTask.executeInBackground(true, new GitTaskResultHandlerAdapter() { @Override protected void onSuccess() { updateResult.set(GitUpdateResult.SUCCESS); } @Override protected void onCancel() { cancel(); updateResult.set(GitUpdateResult.CANCEL); } @Override protected void onFailure() { failure.set(true); } }); if (failure.get()) { updateResult.set(handleMergeFailure(mergeLineListener, untrackedFilesDetector, merger, mergeHandler)); } return updateResult.get(); } @NotNull private GitUpdateResult handleMergeFailure(MergeLineListener mergeLineListener, GitMessageWithFilesDetector untrackedFilesWouldBeOverwrittenByMergeDetector, final GitMerger merger, GitLineHandler mergeHandler) { final MergeError error = mergeLineListener.getMergeError(); LOG.info("merge error: " + error); if (error == MergeError.CONFLICT) { LOG.info("Conflict detected"); final boolean allMerged = new MyConflictResolver(myProject, myGit, merger, myRoot).merge(); return allMerged ? GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS : GitUpdateResult.INCOMPLETE; } else if (error == MergeError.LOCAL_CHANGES) { LOG.info("Local changes would be overwritten by merge"); final List paths = getFilesOverwrittenByMerge(mergeLineListener.getOutput()); final Collection changes = getLocalChangesFilteredByFiles(paths); UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { ChangeListViewerDialog dialog = new ChangeListViewerDialog(myProject, changes, false) { @Override protected String getDescription() { return "Your local changes to the following files would be overwritten by merge.
" + "Please, commit your changes or stash them before you can merge."; } }; dialog.show(); } }); return GitUpdateResult.ERROR; } else if (untrackedFilesWouldBeOverwrittenByMergeDetector.wasMessageDetected()) { LOG.info("handleMergeFailure: untracked files would be overwritten by merge"); UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, myRoot, untrackedFilesWouldBeOverwrittenByMergeDetector.getRelativeFilePaths(), "merge", null); return GitUpdateResult.ERROR; } else { String errors = GitUIUtil.stringifyErrors(mergeHandler.errors()); LOG.info("Unknown error: " + errors); GitUIUtil.notifyImportantError(myProject, "Error merging", errors); return GitUpdateResult.ERROR; } } @Override public boolean isSaveNeeded() { try { if (GitUtil.hasLocalChanges(true, myProject, myRoot)) { return true; } } catch (VcsException e) { LOG.info("isSaveNeeded failed to check staging area", e); return true; } // git log --name-status master..origin/master GitBranchPair gitBranchPair = myTrackedBranches.get(myRoot); String currentBranch = gitBranchPair.getBranch().getName(); String remoteBranch = gitBranchPair.getDest().getName(); try { GitRepository repository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(myRoot); if (repository == null) { LOG.error("Repository is null for root " + myRoot); return true; // fail safe } final Collection remotelyChanged = GitUtil.getPathsDiffBetweenRefs(ServiceManager.getService(Git.class), repository, currentBranch, remoteBranch); final List locallyChanged = myChangeListManager.getAffectedPaths(); for (final File localPath : locallyChanged) { if (ContainerUtil.exists(remotelyChanged, new Condition() { @Override public boolean value(String remotelyChangedPath) { return FileUtil.pathsEqual(localPath.getPath(), remotelyChangedPath); } })) { // found a file which was changed locally and remotely => need to save return true; } } return false; } catch (VcsException e) { LOG.info("failed to get remotely changed files for " + currentBranch + ".." + remoteBranch, e); return true; // fail safe } } private void cancel() { try { GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.RESET); h.addParameters("--merge"); h.run(); } catch (VcsException e) { LOG.info("cancel git reset --merge", e); GitUIUtil.notifyImportantError(myProject, "Couldn't reset merge", e.getLocalizedMessage()); } } // parses the output of merge conflict returning files which would be overwritten by merge. These files will be stashed. private List getFilesOverwrittenByMerge(@NotNull List mergeOutput) { final List paths = new ArrayList(); for (String line : mergeOutput) { if (StringUtil.isEmptyOrSpaces(line)) { continue; } if (line.contains("Please, commit your changes or stash them before you can merge")) { break; } line = line.trim(); final String path; try { path = myRoot.getPath() + "/" + GitUtil.unescapePath(line); final File file = new File(path); if (file.exists()) { paths.add(new FilePathImpl(file, false)); } } catch (VcsException e) { // just continue } } return paths; } private Collection getLocalChangesFilteredByFiles(List paths) { final Collection changes = new HashSet(); for(LocalChangeList list : myChangeListManager.getChangeLists()) { for (Change change : list.getChanges()) { final ContentRevision afterRevision = change.getAfterRevision(); final ContentRevision beforeRevision = change.getBeforeRevision(); if ((afterRevision != null && paths.contains(afterRevision.getFile())) || (beforeRevision != null && paths.contains(beforeRevision.getFile()))) { changes.add(change); } } } return changes; } @Override public String toString() { return "Merge updater"; } private enum MergeError { CONFLICT, LOCAL_CHANGES, OTHER } private static class MergeLineListener extends GitLineHandlerAdapter { private MergeError myMergeError; private List myOutput = new ArrayList(); private boolean myLocalChangesError = false; @Override public void onLineAvailable(String line, Key outputType) { if (myLocalChangesError) { myOutput.add(line); } else if (line.contains("Automatic merge failed; fix conflicts and then commit the result")) { myMergeError = MergeError.CONFLICT; } else if (line.contains("Your local changes to the following files would be overwritten by merge")) { myMergeError = MergeError.LOCAL_CHANGES; myLocalChangesError = true; } } public MergeError getMergeError() { return myMergeError; } public List getOutput() { return myOutput; } } private static class MyConflictResolver extends GitConflictResolver { private final GitMerger myMerger; private final VirtualFile myRoot; public MyConflictResolver(Project project, @NotNull Git git, GitMerger merger, VirtualFile root) { super(project, git, ServiceManager.getService(git4idea.GitPlatformFacade.class), Collections.singleton(root), makeParams()); myMerger = merger; myRoot = root; } private static Params makeParams() { Params params = new Params(); params.setErrorNotificationTitle("Can't complete update"); params.setMergeDescription("Merge conflicts detected. Resolve them before continuing update."); return params; } @Override protected boolean proceedIfNothingToMerge() throws VcsException { myMerger.mergeCommit(myRoot); return true; } @Override protected boolean proceedAfterAllMerged() throws VcsException { myMerger.mergeCommit(myRoot); return true; } } }