Skip to content

Blob merge with Myers algorithm produces false conflict where git merge-file resolves cleanly #2475

@mtsgrd

Description

Summary

gix_merge::blob::builtin_driver::text::merge with the Myers diff algorithm produces a false conflict on certain inputs where git merge-file (also Myers-based) resolves cleanly. Switching to Histogram resolves the issue.

Reproduction

Given three versions of a file:

  • base (71 lines) - original version
  • ours (164 lines) - heavily expanded version with ~90 new lines inserted before a comment
  • theirs (70 lines) - identical to base except one comment line deleted

The only change in base→theirs is the deletion of a single comment line:

--- base
+++ theirs
@@ -29,7 +29,6 @@
 	const uiState = inject(UI_STATE);
 	const claudeCodeService = inject(CLAUDE_CODE_SERVICE);

-	// ── MCP config modal ─────────────────────────────────────────────
 	let mcpConfigModal = $state<CodegenMcpConfigModal>();

In base→ours, this same comment exists but is now at line 102 (instead of 32) because ~70 lines of new code were inserted above it. The content around it (let mcpConfigModal, const mcpClaudeConfigQuery) is unchanged.

Expected behavior (matches git merge-file)

Clean merge: keep all the new code from ours, apply the comment deletion from theirs. git merge-file exits 0 with no conflict markers.

git merge-file -p ours.svelte base.svelte theirs.svelte
# exit 0, 163 lines, 0 conflict markers

Actual behavior

gix_merge::blob::builtin_driver::text::merge with Algorithm::Myers reports Resolution::Conflict:

<<<<<<< ours
	// ── Actions ──────────────────────────────────────────────────────
	async function onAbort() { ... }
	async function sendMessage(prompt) { ... }
	async function handleAnswerQuestion(answers) { ... }

	// ── MCP config modal ─────────────────────────────────────────────
=======
>>>>>>> theirs

gix sees the comment deletion as conflicting with the large insertion above it. The conflict region spans lines 78-105 of the merged output.

Key finding: Histogram algorithm works correctly

// Myers:     Resolution::Conflict (1 false conflict marker)
// Histogram: Resolution::Complete (0 conflicts) ✓

This suggests the Myers implementation in imara-diff produces different hunk boundaries than git's xdiff for this input, causing the 3-way merge to see overlapping regions where git doesn't.

Minimal reproduction code

use gix_merge::blob::builtin_driver::text;

let base = std::fs::read("base.svelte").unwrap();
let ours = std::fs::read("ours.svelte").unwrap();
let theirs = std::fs::read("theirs.svelte").unwrap();

let mut output = Vec::new();
let mut input = imara_diff::intern::InternedInput::new(&[][..], &[][..]);

// Myers: produces false conflict
let options = text::Options {
    diff_algorithm: imara_diff::Algorithm::Myers,
    conflict: text::Conflict::Keep {
        style: text::ConflictStyle::Merge,
        marker_size: std::num::NonZeroU8::new(7).unwrap(),
    },
};
let labels = text::Labels {
    ancestor: Some("base".into()),
    current: Some("ours".into()),
    other: Some("theirs".into()),
};
let resolution = text::function::merge(
    &mut output, &mut input, labels, &ours, &base, &theirs, options,
);
assert!(matches!(resolution, gix_merge::blob::Resolution::Conflict));
// ^ This should be Resolution::Complete to match git behavior

Impact

This causes GitButler's commit amend operation to reject valid changes with CherryPickMergeConflict when the 3-way merge (base=workspace tree, ours=target commit tree, theirs=workspace+change) hits this code path. The user is unable to amend their change into the correct commit even though the merge is semantically clean.

Environment

  • gix 0.80.0 (from main branch, commit 1a72380a)
  • imara-diff 0.1.8
  • macOS (Darwin 25.3.0, aarch64)

Files

I'll attach the three .svelte files (base/ours/theirs) needed to reproduce.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions