IDEA-131357 git: speedup bulk merge resolve using "accept ours/theirs"

It is notably faster to checkout files in one git command.
This commit is contained in:
Aleksey Pivovarov
2018-06-11 17:27:33 +03:00
parent df0e78d47c
commit d3c7fa5994
5 changed files with 147 additions and 80 deletions
@@ -15,7 +15,6 @@
*/
package com.intellij.openapi.vcs.merge;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.ColumnInfo;
import org.jetbrains.annotations.NotNull;
@@ -58,15 +57,4 @@ public interface MergeSession {
* @param resolution the used resolution.
*/
void conflictResolvedForFile(@NotNull VirtualFile file, @NotNull Resolution resolution);
/**
* Called before {@link #conflictResolvedForFile} when the user executes "Accept Theirs" or "Accept Ours" action
* to update file content to the selected version.
*
* @param resolution AcceptedYours or AcceptedTheirs
* @return true if operation was performed, false if we should fall back to generic implementation.
*/
default boolean acceptFileRevision(@NotNull VirtualFile file, @NotNull MergeSession.Resolution resolution) throws VcsException {
return false;
}
}
@@ -0,0 +1,33 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.vcs.merge;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Represents the state of a multiple file merge operation.
*
* @author yole
* @see MergeProvider2#createMergeSession
* @since 8.1
*/
public interface MergeSessionEx extends MergeSession {
/**
* Called when user executes "Accept Theirs" or "Accept Ours" action to update file content on disk to the selected version.
*
* @param resolution AcceptedYours or AcceptedTheirs
*/
void acceptFilesRevisions(@NotNull List<VirtualFile> files, @NotNull MergeSessionEx.Resolution resolution) throws VcsException;
/**
* Called after conflict resolution for conflicting files.
* @see #conflictResolvedForFile
*
* @param file the conflicting file.
* @param resolution the used resolution.
*/
void conflictResolvedForFiles(@NotNull List<VirtualFile> files, @NotNull Resolution resolution);
}
@@ -271,8 +271,6 @@ public class MultipleFileMergeDialog extends DialogWrapper {
private void acceptFileRevision(@NotNull VirtualFile file, @NotNull MergeSession.Resolution resolution) throws Exception {
if (myMergeSession != null && !myMergeSession.canMerge(file)) return;
if (myMergeSession != null && myMergeSession.acceptFileRevision(file, resolution)) return;
if (!DiffUtil.makeWritable(myProject, file)) {
throw new IOException("File is read-only: " + file.getPresentableName());
}
@@ -237,6 +237,8 @@ open class MultipleFileMergeDialog2(
override fun getDimensionServiceKey(): String = "MultipleFileMergeDialog"
private fun acceptRevision(resolution: MergeSession.Resolution) {
assert(resolution == MergeSession.Resolution.AcceptedYours || resolution == MergeSession.Resolution.AcceptedTheirs)
FileDocumentManager.getInstance().saveAllDocuments()
val files = getSelectedFiles()
if (!beforeResolve(files)) {
@@ -244,10 +246,23 @@ open class MultipleFileMergeDialog2(
}
try {
for (file in files) {
acceptFileRevision(file, resolution)
checkMarkModifiedProject(file)
markFileProcessed(file, resolution)
if (mergeSession is MergeSessionEx) {
val supportedFiles = files.filter { file -> mergeSession.canMerge(file) }
mergeSession.acceptFilesRevisions(supportedFiles, resolution)
for (file in supportedFiles) {
checkMarkModifiedProject(file)
}
markFilesProcessed(supportedFiles, resolution)
}
else {
for (file in files) {
acceptFileRevision(file, resolution)
checkMarkModifiedProject(file)
markFileProcessed(file, resolution)
}
}
}
catch (e: Exception) {
@@ -261,8 +276,6 @@ open class MultipleFileMergeDialog2(
private fun acceptFileRevision(file: VirtualFile, resolution: MergeSession.Resolution) {
if (mergeSession?.canMerge(file) == false) return
if (mergeSession?.acceptFileRevision(file, resolution) == true) return
if (!DiffUtil.makeWritable(project, file)) {
throw IOException("File is read-only: " + file.presentableName)
}
@@ -280,16 +293,27 @@ open class MultipleFileMergeDialog2(
}
}
private fun markFileProcessed(file: VirtualFile, resolution: MergeSession.Resolution) {
unresolvedFiles.remove(file)
if (mergeSession != null) {
mergeSession.conflictResolvedForFile(file, resolution)
private fun markFilesProcessed(files: List<VirtualFile>, resolution: MergeSession.Resolution) {
unresolvedFiles.removeAll(files)
if (mergeSession is MergeSessionEx) {
mergeSession.conflictResolvedForFiles(files, resolution)
}
else if (mergeSession != null) {
files.forEach {
mergeSession.conflictResolvedForFile(it, resolution)
}
}
else {
mergeProvider.conflictResolvedForFile(file)
files.forEach {
mergeProvider.conflictResolvedForFile(it)
}
}
processedFiles.add(file)
VcsDirtyScopeManager.getInstance(project).fileDirty(file)
processedFiles.addAll(files)
VcsDirtyScopeManager.getInstance(project).filesDirty(files, emptyList())
}
private fun markFileProcessed(file: VirtualFile, resolution: MergeSession.Resolution) {
markFilesProcessed(listOf(file), resolution)
}
private fun updateModelFromFiles() {
@@ -13,10 +13,12 @@ import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.merge.*;
import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.vcs.log.Hash;
import com.intellij.vcs.log.impl.HashImpl;
@@ -528,7 +530,7 @@ public class GitMergeProvider implements MergeProvider2 {
/**
* The merge session, it queries conflict information.
*/
private class MyMergeSession implements MergeSession {
private class MyMergeSession implements MergeSessionEx {
Map<VirtualFile, Conflict> myConflicts = new HashMap<>();
String currentBranchName;
String mergeHeadBranchName;
@@ -613,74 +615,96 @@ public class GitMergeProvider implements MergeProvider2 {
@Override
public void conflictResolvedForFile(@NotNull VirtualFile file, @NotNull Resolution resolution) {
Conflict c = myConflicts.get(file);
if (c == null) {
LOG.error("Conflict was not loaded for the file: " + file.getPath());
return;
}
try {
Conflict.Status status;
switch (resolution) {
case AcceptedTheirs:
status = c.myStatusTheirs;
break;
case AcceptedYours:
status = c.myStatusYours;
break;
case Merged:
status = Conflict.Status.MODIFIED;
break;
default:
throw new IllegalArgumentException("Unsupported resolution for unmergable files(" + file.getPath() + "): " + resolution);
conflictResolvedForFiles(Collections.singletonList(file), resolution);
}
@Override
public void conflictResolvedForFiles(@NotNull List<VirtualFile> files, @NotNull Resolution resolution) {
MultiMap<VirtualFile, Conflict> byRoot = groupConflictsByRoot(files);
for (VirtualFile root : byRoot.keySet()) {
Collection<Conflict> conflicts = byRoot.get(root);
List<VirtualFile> toAdd = new ArrayList<>();
List<VirtualFile> toDelete = new ArrayList<>();
for (Conflict c: conflicts) {
Conflict.Status status;
switch (resolution) {
case AcceptedTheirs:
status = c.myStatusTheirs;
break;
case AcceptedYours:
status = c.myStatusYours;
break;
case Merged:
status = Conflict.Status.MODIFIED;
break;
default:
throw new IllegalArgumentException("Unsupported resolution: " + resolution);
}
if (status == Conflict.Status.MODIFIED) {
toAdd.add(c.myFile);
}
else {
toDelete.add(c.myFile);
}
}
switch (status) {
case MODIFIED:
GitFileUtils.addFiles(myProject, c.myRoot, file);
break;
case DELETED:
GitFileUtils.deleteFiles(myProject, c.myRoot, file);
break;
default:
throw new IllegalArgumentException("Unsupported status(" + file.getPath() + "): " + status);
try {
GitFileUtils.addFiles(myProject, root, toAdd);
GitFileUtils.deleteFiles(myProject, root, toDelete);
}
catch (VcsException e) {
LOG.error(String.format("Unexpected exception during the git operation: modified - %s deleted - %s)", toAdd, toDelete), e);
}
}
catch (VcsException e) {
LOG.error("Unexpected exception during the git operation (" + file.getPath() + ")", e);
}
}
@Override
public boolean acceptFileRevision(@NotNull VirtualFile file, @NotNull MergeSession.Resolution resolution) throws VcsException {
if (resolution != Resolution.AcceptedYours && resolution != Resolution.AcceptedTheirs) return false;
public void acceptFilesRevisions(@NotNull List<VirtualFile> files, @NotNull Resolution resolution) throws VcsException {
assert resolution == Resolution.AcceptedYours || resolution == Resolution.AcceptedTheirs;
MultiMap<VirtualFile, Conflict> byRoot = groupConflictsByRoot(files);
boolean isCurrent = resolution == Resolution.AcceptedYours;
Conflict c = myConflicts.get(file);
if (c == null) {
LOG.error("Conflict was not loaded for the file: " + file.getPath());
return false;
}
for (VirtualFile root : byRoot.keySet()) {
Collection<Conflict> conflicts = byRoot.get(root);
Conflict.Status status = isCurrent ? c.myStatusYours : c.myStatusTheirs;
switch (status) {
case MODIFIED:
String parameter = myReverseRoots.contains(c.myRoot)
? isCurrent ? "--theirs" : "--ours"
: isCurrent ? "--ours" : "--theirs";
List<VirtualFile> filesToCheckout = ContainerUtil.mapNotNull(conflicts, c -> {
Conflict.Status status = isCurrent ? c.myStatusYours : c.myStatusTheirs;
return status == Conflict.Status.MODIFIED ? c.myFile : null;
});
GitLineHandler handler = new GitLineHandler(myProject, c.myRoot, GitCommand.CHECKOUT);
String parameter = myReverseRoots.contains(root)
? isCurrent ? "--theirs" : "--ours"
: isCurrent ? "--ours" : "--theirs";
for (List<String> paths : VcsFileUtil.chunkFiles(root, filesToCheckout)) {
GitLineHandler handler = new GitLineHandler(myProject, root, GitCommand.CHECKOUT);
handler.addParameters(parameter);
handler.endOptions();
handler.addRelativeFiles(Collections.singletonList(c.myFile));
handler.addParameters(paths);
GitCommandResult result = Git.getInstance().runCommand(handler);
if (!result.success()) throw new VcsException(result.getErrorOutputAsJoinedString());
break;
case DELETED:
break;
default:
throw new IllegalArgumentException("Unsupported status(" + file.getPath() + "): " + status);
}
}
}
return true;
@NotNull
private MultiMap<VirtualFile, Conflict> groupConflictsByRoot(@NotNull List<VirtualFile> files) {
MultiMap<VirtualFile, Conflict> byRoot = MultiMap.create();
for (VirtualFile file: files) {
Conflict c = myConflicts.get(file);
if (c == null) {
LOG.error("Conflict was not loaded for the file: " + file.getPath());
continue;
}
byRoot.putValue(c.myRoot, c);
}
return byRoot;
}
/**