fix [VCS, merge]: Don't auto resolve modified_deleted conflicts IJPL-252505

GitOrigin-RevId: bbdc4b3c172b4bbec0791b737ecb2f04839ed254
This commit is contained in:
Radu Nedelcu
2026-09-14 13:07:03 +00:00
committed by intellij-monorepo-bot
parent 28e0bf1a91
commit 0f1cb3ea5e
7 changed files with 88 additions and 7 deletions
+1
View File
@@ -351,6 +351,7 @@ f:com.intellij.diff.merge.MergeTool$ToolbarComponents
a:com.intellij.diff.merge.TextMergeRequest
- com.intellij.diff.merge.ThreesideMergeRequest
- <init>():V
- getConflictType():com.intellij.diff.merge.ConflictType
- a:getContents():java.util.List
- a:getOutputContent():com.intellij.diff.contents.DocumentContent
a:com.intellij.diff.merge.ThreesideMergeRequest
@@ -12,4 +12,8 @@ public abstract class TextMergeRequest extends ThreesideMergeRequest {
@Override
public abstract @NotNull DocumentContent getOutputContent();
public @NotNull ConflictType getConflictType() {
return ConflictType.DEFAULT;
}
}
@@ -333,7 +333,8 @@ public class DiffRequestFactoryImpl extends DiffRequestFactory {
List<DocumentContent> contents = DiffUtil.getDocumentContentsForViewer(project, byteContents, output, conflictType);
return new TextMergeRequestImpl(project, outputContent, originalContent, contents, title, contentTitles);
ConflictType effectiveConflictType = conflictType != null ? conflictType : ConflictType.DEFAULT;
return new TextMergeRequestImpl(project, outputContent, originalContent, contents, effectiveConflictType, title, contentTitles);
}
@Override
@@ -355,8 +355,9 @@ class MergeConflictModel(
}
fun canResolveChangeAutomatically(changeIndex: Int, side: ThreeSide): Boolean {
val change = getByIndex(changeIndex)
if (mergeRequest.conflictType.isModifyDeleteFileConflict()) return false
val change = getByIndex(changeIndex)
return if (change.isConflict) {
side == ThreeSide.BASE &&
change.conflictType.canBeResolved() &&
@@ -371,6 +372,11 @@ class MergeConflictModel(
}
}
private fun ConflictType.isModifyDeleteFileConflict() = when (this) {
ConflictType.MODIFIED_DELETED, ConflictType.DELETED_MODIFIED -> true
ConflictType.DEFAULT, ConflictType.ADDED_ADDED -> false
}
private fun TextMergeRequest.resetOutputContent() {
val text = ThreeSide.BASE.select(contents).document.text
outputContent.document.setText(text)
@@ -2,6 +2,7 @@
package com.intellij.diff.requests;
import com.intellij.diff.contents.DocumentContent;
import com.intellij.diff.merge.ConflictType;
import com.intellij.diff.merge.MergeCallback;
import com.intellij.diff.merge.MergeResult;
import com.intellij.diff.merge.MergeUtil;
@@ -25,6 +26,7 @@ public class TextMergeRequestImpl extends TextMergeRequest {
private final @NotNull List<DocumentContent> myContents;
private final @NotNull CharSequence myOriginalContent;
private final @NotNull ConflictType myConflictType;
private final @Nullable @NlsContexts.DialogTitle String myTitle;
private final @NotNull List<String> myTitles;
@@ -35,12 +37,23 @@ public class TextMergeRequestImpl extends TextMergeRequest {
@NotNull List<DocumentContent> contents,
@Nullable @NlsContexts.DialogTitle String title,
@NotNull List<@Nls String> contentTitles) {
this(project, output, originalContent, contents, ConflictType.DEFAULT, title, contentTitles);
}
public TextMergeRequestImpl(@Nullable Project project,
@NotNull DocumentContent output,
@NotNull CharSequence originalContent,
@NotNull List<DocumentContent> contents,
@NotNull ConflictType conflictType,
@Nullable @NlsContexts.DialogTitle String title,
@NotNull List<@Nls String> contentTitles) {
assert contents.size() == 3;
assert contentTitles.size() == 3;
myProject = project;
myOutput = output;
myOriginalContent = originalContent;
myConflictType = conflictType;
myContents = contents;
myTitles = contentTitles;
@@ -57,6 +70,11 @@ public class TextMergeRequestImpl extends TextMergeRequest {
return myContents;
}
@Override
public @NotNull ConflictType getConflictType() {
return myConflictType;
}
@Override
public @Nullable String getTitle() {
return myTitle;
@@ -813,6 +813,38 @@ class MergeTest : MergeTestBase() {
}
}
fun testDoNotAutoResolveDeletedByUsFileConflict() {
// "deleted by us": ours (left) removed the file, theirs (right) kept the base content (e.g. a rename).
doTest("", "A_B_C", "A_B_C", 1, ConflictType.DELETED_MODIFIED) {
assertFalse(0.canResolveConflict())
runApplyNonConflictsAction(ThreeSide.BASE)
0.assertResolved(NONE)
assertContent("A_B_C")
}
}
fun testDoNotAutoResolveDeletedByThemFileConflict() {
// "deleted by them": theirs (right) removed the file, ours (left) kept the base content.
doTest("A_B_C", "A_B_C", "", 1, ConflictType.MODIFIED_DELETED) {
assertFalse(0.canResolveConflict())
runApplyNonConflictsAction(ThreeSide.BASE)
0.assertResolved(NONE)
assertContent("A_B_C")
}
}
fun testAutoResolveDeletionWithoutFileConflictType() {
// Control: without the modify/delete ConflictType, the same one-sided change still auto-resolves.
doTest("", "A_B_C", "A_B_C", 1) {
assertTrue(0.canResolveConflict())
runApplyNonConflictsAction(ThreeSide.BASE)
0.assertResolved(BOTH)
}
}
fun testApplySeveralActions() {
doTest("X_1_Y_2_Z_3_4_U_W_",
"X_a_Y_b_Z_c_U_d_W_",
@@ -48,7 +48,21 @@ abstract class MergeTestBase : HeavyDiffTestCase() {
doTest(left, base, right, changesCount, IgnorePolicy.DEFAULT, f)
}
fun doTest(
left: String, base: String, right: String, changesCount: Int,
conflictType: ConflictType, f: TestBuilder.() -> Unit,
) {
doTest(left, base, right, changesCount, IgnorePolicy.DEFAULT, conflictType, f)
}
fun doTest(left: String, base: String, right: String, changesCount: Int, policy: IgnorePolicy, f: TestBuilder.() -> Unit) {
doTest(left, base, right, changesCount, policy, ConflictType.DEFAULT, f)
}
fun doTest(
left: String, base: String, right: String, changesCount: Int, policy: IgnorePolicy,
conflictType: ConflictType, f: TestBuilder.() -> Unit,
) {
val contentFactory = DiffContentFactoryImpl()
val leftContent: DocumentContent = contentFactory.create(parseSource(left))
val baseContent: DocumentContent = contentFactory.create(parseSource(base))
@@ -57,7 +71,7 @@ abstract class MergeTestBase : HeavyDiffTestCase() {
outputContent.document.setReadOnly(false)
val context = MockMergeContext(project)
val request = MockMergeRequest(leftContent, baseContent, rightContent, outputContent)
val request = MockMergeRequest(leftContent, baseContent, rightContent, outputContent, conflictType)
val settings = TextDiffSettings()
settings.ignorePolicy = policy
@@ -376,10 +390,13 @@ abstract class MergeTestBase : HeavyDiffTestCase() {
}
}
private class MockMergeRequest(val left: DocumentContent,
val base: DocumentContent,
val right: DocumentContent,
val output: DocumentContent) : TextMergeRequest() {
private class MockMergeRequest(
val left: DocumentContent,
val base: DocumentContent,
val right: DocumentContent,
val output: DocumentContent,
private val requestConflictType: ConflictType = ConflictType.DEFAULT,
) : TextMergeRequest() {
override fun getTitle(): String? = null
override fun applyResult(result: MergeResult) {
@@ -390,6 +407,8 @@ abstract class MergeTestBase : HeavyDiffTestCase() {
override fun getOutputContent(): DocumentContent = output
override fun getContentTitles(): List<String?> = listOf(null, null, null)
override fun getConflictType(): ConflictType = requestConflictType
}
enum class SidesState {