Skip to content

Commit 172bd22

Browse files
authored
Merge pull request #2476 from mtsgrd/fix/false-conflict-empty-insertion-overlap
fix: coalesce split Myers hunks to prevent false merge conflicts
2 parents 6f47e98 + 7b82f3a commit 172bd22

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

gix-merge/src/blob/builtin_driver/text/utils.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,9 @@ pub fn collect_hunks(
479479
side: Side,
480480
mut hunks: Vec<Hunk>,
481481
) -> Vec<Hunk> {
482-
hunks.extend(imara_diff::Diff::compute(algorithm, input).hunks().map(|hunk| Hunk {
482+
let mut diff = imara_diff::Diff::compute(algorithm, input);
483+
diff.postprocess_lines(input);
484+
hunks.extend(diff.hunks().map(|hunk| Hunk {
483485
before: hunk.before,
484486
after: hunk.after,
485487
side,

gix-merge/tests/merge/blob/builtin_driver.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,73 @@ mod text {
369369
}
370370
}
371371

372+
mod false_conflict {
373+
use gix_merge::blob::{builtin_driver, builtin_driver::text::Conflict, Resolution};
374+
use imara_diff::InternedInput;
375+
376+
/// Minimal reproduction: Myers produces a false conflict where git merge-file resolves cleanly.
377+
///
378+
/// base: alpha_x / (blank) / bravo_x / charlie_x / (blank)
379+
/// ours: (blank) / (blank) / bravo_x / charlie_x
380+
/// theirs: alpha_x / (blank) / charlie_x / (blank)
381+
///
382+
/// base→ours: alpha_x deleted (replaced by blank), trailing blank removed
383+
/// base→theirs: bravo_x deleted
384+
///
385+
/// These are non-overlapping changes that git merges cleanly.
386+
/// See https://github.com/GitoxideLabs/gitoxide/issues/2475
387+
#[test]
388+
fn myers_false_conflict_with_blank_line_ambiguity() {
389+
let base = b"alpha_x\n\nbravo_x\ncharlie_x\n\n";
390+
let ours = b"\n\nbravo_x\ncharlie_x\n";
391+
let theirs = b"alpha_x\n\ncharlie_x\n\n";
392+
393+
let labels = builtin_driver::text::Labels {
394+
ancestor: Some("base".into()),
395+
current: Some("ours".into()),
396+
other: Some("theirs".into()),
397+
};
398+
399+
// Histogram resolves cleanly.
400+
{
401+
let options = builtin_driver::text::Options {
402+
diff_algorithm: imara_diff::Algorithm::Histogram,
403+
conflict: Conflict::Keep {
404+
style: builtin_driver::text::ConflictStyle::Merge,
405+
marker_size: 7.try_into().unwrap(),
406+
},
407+
};
408+
let mut out = Vec::new();
409+
let mut input = InternedInput::default();
410+
let res = builtin_driver::text(&mut out, &mut input, labels, ours, base, theirs, options);
411+
assert_eq!(res, Resolution::Complete, "Histogram should resolve cleanly");
412+
}
413+
414+
// Myers should also resolve cleanly (it used to produce a false conflict because
415+
// imara-diff's Myers splits the ours change into two hunks — a deletion at base[0]
416+
// and an empty insertion at base[2] — and the insertion collided with theirs'
417+
// deletion at base[2]).
418+
{
419+
let options = builtin_driver::text::Options {
420+
diff_algorithm: imara_diff::Algorithm::Myers,
421+
conflict: Conflict::Keep {
422+
style: builtin_driver::text::ConflictStyle::Merge,
423+
marker_size: 7.try_into().unwrap(),
424+
},
425+
};
426+
let mut out = Vec::new();
427+
let mut input = InternedInput::default();
428+
let res = builtin_driver::text(&mut out, &mut input, labels, ours, base, theirs, options);
429+
assert_eq!(
430+
res,
431+
Resolution::Complete,
432+
"Myers should resolve cleanly (git merge-file does). Output:\n{}",
433+
String::from_utf8_lossy(&out)
434+
);
435+
}
436+
}
437+
}
438+
372439
mod baseline {
373440
use std::path::Path;
374441

0 commit comments

Comments
 (0)