diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/merge/MergeSession.java b/platform/vcs-api/src/com/intellij/openapi/vcs/merge/MergeSession.java index cd2790658ff3..53daace201b2 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/merge/MergeSession.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/merge/MergeSession.java @@ -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; - } } diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/merge/MergeSessionEx.java b/platform/vcs-api/src/com/intellij/openapi/vcs/merge/MergeSessionEx.java new file mode 100644 index 000000000000..18f21ac4feb1 --- /dev/null +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/merge/MergeSessionEx.java @@ -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 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 files, @NotNull Resolution resolution); +} diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog.java index 176c1a509446..3ee7b26b621d 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog.java @@ -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()); } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog2.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog2.kt index 24e584351382..75572f8c75ed 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog2.kt +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/merge/MultipleFileMergeDialog2.kt @@ -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, 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() { diff --git a/plugins/git4idea/src/git4idea/merge/GitMergeProvider.java b/plugins/git4idea/src/git4idea/merge/GitMergeProvider.java index c5bfba7343f7..e93be6adbe8d 100644 --- a/plugins/git4idea/src/git4idea/merge/GitMergeProvider.java +++ b/plugins/git4idea/src/git4idea/merge/GitMergeProvider.java @@ -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 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 files, @NotNull Resolution resolution) { + MultiMap byRoot = groupConflictsByRoot(files); + + for (VirtualFile root : byRoot.keySet()) { + Collection conflicts = byRoot.get(root); + + List toAdd = new ArrayList<>(); + List 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 files, @NotNull Resolution resolution) throws VcsException { + assert resolution == Resolution.AcceptedYours || resolution == Resolution.AcceptedTheirs; + + MultiMap 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 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 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 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 groupConflictsByRoot(@NotNull List files) { + MultiMap 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; } /**