| /* |
| * 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 <code>git pull</code> 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<VirtualFile, GitBranchPair> 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<GitUpdateResult> updateResult = new AtomicReference<GitUpdateResult>(); |
| 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<FilePath> paths = getFilesOverwrittenByMerge(mergeLineListener.getOutput()); |
| final Collection<Change> 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.<br/>" + |
| "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<String> remotelyChanged = GitUtil.getPathsDiffBetweenRefs(ServiceManager.getService(Git.class), repository, |
| currentBranch, remoteBranch); |
| final List<File> locallyChanged = myChangeListManager.getAffectedPaths(); |
| for (final File localPath : locallyChanged) { |
| if (ContainerUtil.exists(remotelyChanged, new Condition<String>() { |
| @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<FilePath> getFilesOverwrittenByMerge(@NotNull List<String> mergeOutput) { |
| final List<FilePath> paths = new ArrayList<FilePath>(); |
| 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<Change> getLocalChangesFilteredByFiles(List<FilePath> paths) { |
| final Collection<Change> changes = new HashSet<Change>(); |
| 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<String> myOutput = new ArrayList<String>(); |
| 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<String> 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; |
| } |
| } |
| } |