blob: 214c026307d517bfd8139990acf0b6d81b818f45 [file] [log] [blame]
/*
* 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.merge;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
import com.intellij.openapi.vcs.merge.MergeProvider;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.util.StringScanner;
import org.jetbrains.annotations.NotNull;
import javax.swing.event.HyperlinkEvent;
import java.util.*;
/**
*
* The class is highly customizable, since the procedure of resolving conflicts is very common in Git operations.
* @author Kirill Likhodedov
*/
public class GitConflictResolver {
private static final Logger LOG = Logger.getInstance(GitConflictResolver.class);
@NotNull protected final Project myProject;
@NotNull private final Git myGit;
@NotNull private final GitPlatformFacade myPlatformFacade;
private final Collection<VirtualFile> myRoots;
private final Params myParams;
@NotNull private final GitRepositoryManager myRepositoryManager;
private final AbstractVcsHelper myVcsHelper;
/**
* Customizing parameters - mostly String notification texts, etc.
*/
public static class Params {
private boolean reverse;
private String myErrorNotificationTitle = "";
private String myErrorNotificationAdditionalDescription = "";
private String myMergeDescription = "";
private MergeDialogCustomizer myMergeDialogCustomizer = new MergeDialogCustomizer() {
@Override public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
return myMergeDescription;
}
};
/**
* @param reverseMerge specify {@code true} if reverse merge provider has to be used for merging - it is the case of rebase or stash.
*/
public Params setReverse(boolean reverseMerge) {
reverse = reverseMerge;
return this;
}
public Params setErrorNotificationTitle(String errorNotificationTitle) {
myErrorNotificationTitle = errorNotificationTitle;
return this;
}
public Params setErrorNotificationAdditionalDescription(String errorNotificationAdditionalDescription) {
myErrorNotificationAdditionalDescription = errorNotificationAdditionalDescription;
return this;
}
public Params setMergeDescription(String mergeDescription) {
myMergeDescription = mergeDescription;
return this;
}
public Params setMergeDialogCustomizer(MergeDialogCustomizer mergeDialogCustomizer) {
myMergeDialogCustomizer = mergeDialogCustomizer;
return this;
}
}
public GitConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull GitPlatformFacade platformFacade,
@NotNull Collection<VirtualFile> roots, @NotNull Params params) {
myProject = project;
myGit = git;
myPlatformFacade = platformFacade;
myRoots = roots;
myParams = params;
myRepositoryManager = myPlatformFacade.getRepositoryManager(myProject);
myVcsHelper = myPlatformFacade.getVcsHelper(project);
}
/**
* <p>
* Goes throw the procedure of merging conflicts via MergeTool for different types of operations.
* <ul>
* <li>Checks if there are unmerged files. If not, executes {@link #proceedIfNothingToMerge()}</li>
* <li>Otherwise shows a {@link com.intellij.openapi.vcs.merge.MultipleFileMergeDialog} where user is able to merge files.</li>
* <li>After the dialog is closed, checks if unmerged files remain.
* If everything is merged, executes {@link #proceedAfterAllMerged()}. Otherwise shows a notification.</li>
* </ul>
* </p>
* <p>
* If a Git error happens during seeking for unmerged files or in other cases,
* the method shows a notification and returns {@code false}.
* </p>
*
* @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() {
return merge(false);
}
/**
* This is executed from {@link #merge()} if the initial check tells that there is nothing to merge.
* In the basic implementation no action is performed, {@code true} is returned.
* @return Return value is returned from {@link #merge()}
*/
protected boolean proceedIfNothingToMerge() throws VcsException {
return true;
}
/**
* This is executed from {@link #merge()} after all conflicts are resolved.
* In the basic implementation no action is performed, {@code true} is returned.
* @return Return value is returned from {@link #merge()}
*/
protected boolean proceedAfterAllMerged() throws VcsException {
return true;
}
/**
* Invoke the merge dialog, but execute nothing after merge is completed.
* @return true if all changes were merged, false if unresolved merges remain.
*/
public final boolean mergeNoProceed() {
return merge(true);
}
/**
* Shows notification that not all conflicts were resolved.
*/
protected void notifyUnresolvedRemain() {
notifyWarning(myParams.myErrorNotificationTitle,
"You have to <a href='resolve'>resolve</a> all conflicts first." + myParams.myErrorNotificationAdditionalDescription);
}
/**
* Shows notification that some conflicts were still not resolved - after user invoked the conflict resolver by pressing the link on the
* notification.
*/
private void notifyUnresolvedRemainAfterNotification() {
notifyWarning("Not all conflicts resolved",
"You should <a href='resolve'>resolve</a> all conflicts before update. <br>" +
myParams.myErrorNotificationAdditionalDescription);
}
private void notifyWarning(String title, String content) {
VcsNotifier.getInstance(myProject).notifyImportantWarning(title, content, new ResolveNotificationListener());
}
private boolean merge(boolean mergeDialogInvokedFromNotification) {
try {
final Collection<VirtualFile> initiallyUnmergedFiles = getUnmergedFiles(myRoots);
if (initiallyUnmergedFiles.isEmpty()) {
LOG.info("merge: no unmerged files");
return mergeDialogInvokedFromNotification ? true : proceedIfNothingToMerge();
}
else {
showMergeDialog(initiallyUnmergedFiles);
final Collection<VirtualFile> unmergedFilesAfterResolve = getUnmergedFiles(myRoots);
if (unmergedFilesAfterResolve.isEmpty()) {
LOG.info("merge no more unmerged files");
return mergeDialogInvokedFromNotification ? true : proceedAfterAllMerged();
} else {
LOG.info("mergeFiles unmerged files remain: " + unmergedFilesAfterResolve);
if (mergeDialogInvokedFromNotification) {
notifyUnresolvedRemainAfterNotification();
} else {
notifyUnresolvedRemain();
}
}
}
} catch (VcsException e) {
if (((GitVcs)myPlatformFacade.getVcs(myProject)).getExecutableValidator().checkExecutableAndNotifyIfNeeded()) {
notifyException(e);
}
}
return false;
}
private void showMergeDialog(final Collection<VirtualFile> initiallyUnmergedFiles) {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override public void run() {
final MergeProvider mergeProvider = myParams.reverse ?
new GitMergeProvider(myProject, true) : new GitMergeProvider(myProject, false);
myVcsHelper.showMergeDialog(new ArrayList<VirtualFile>(initiallyUnmergedFiles), mergeProvider, myParams.myMergeDialogCustomizer);
}
});
}
private void notifyException(VcsException e) {
LOG.info("mergeFiles ", e);
final String description = "Couldn't check the working tree for unmerged files because of an error.";
VcsNotifier.getInstance(myProject).notifyError(myParams.myErrorNotificationTitle,
description + myParams.myErrorNotificationAdditionalDescription + "<br/>" +
e.getLocalizedMessage(),
new ResolveNotificationListener()
);
}
@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")) {
notification.expire();
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override public void run() {
mergeNoProceed();
}
});
}
}
}
/**
* @return unmerged files in the given Git roots, all in a single collection.
* @see #getUnmergedFiles(com.intellij.openapi.vfs.VirtualFile)
*/
private Collection<VirtualFile> getUnmergedFiles(@NotNull Collection<VirtualFile> roots) throws VcsException {
final Collection<VirtualFile> unmergedFiles = new HashSet<VirtualFile>();
for (VirtualFile root : roots) {
unmergedFiles.addAll(getUnmergedFiles(root));
}
return unmergedFiles;
}
/**
* @return unmerged files in the given Git root.
* @see #getUnmergedFiles(java.util.Collection
*/
private Collection<VirtualFile> getUnmergedFiles(@NotNull VirtualFile root) throws VcsException {
return unmergedFiles(root);
}
/**
* Parse changes from lines
*
*
* @param root the git root
* @return a set of unmerged files
* @throws com.intellij.openapi.vcs.VcsException if the input format does not matches expected format
*/
private List<VirtualFile> unmergedFiles(VirtualFile root) throws VcsException {
HashSet<VirtualFile> unmerged = new HashSet<VirtualFile>();
String rootPath = root.getPath();
GitRepository repository = myRepositoryManager.getRepositoryForRoot(root);
if (repository == null) {
LOG.error("Repository not found for root " + root);
return Collections.emptyList();
}
GitCommandResult result = myGit.getUnmergedFiles(repository);
if (!result.success()) {
throw new VcsException(result.getErrorOutputAsJoinedString());
}
String output = StringUtil.join(result.getOutput(), "\n");
LocalFileSystem lfs = myPlatformFacade.getLocalFileSystem();
for (StringScanner s = new StringScanner(output); s.hasMoreData();) {
if (s.isEol()) {
s.nextLine();
continue;
}
s.boundedToken('\t');
final String relative = s.line();
String path = rootPath + "/" + GitUtil.unescapePath(relative);
VirtualFile file = lfs.refreshAndFindFileByPath(path);
if (file != null) {
// the file name is in the delete- or rename- conflict, so it is shown in the list of unmerged files,
// but the file itself doesn't exist. In that case we just ignore the file.
file.refresh(false, false);
unmerged.add(file);
}
}
if (unmerged.size() == 0) {
return Collections.emptyList();
}
else {
ArrayList<VirtualFile> rc = new ArrayList<VirtualFile>(unmerged.size());
rc.addAll(unmerged);
Collections.sort(rc, GitUtil.VIRTUAL_FILE_COMPARATOR);
return rc;
}
}
}