@@ -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 \n bravo_x\n charlie_x\n \n " ;
390+ let ours = b"\n \n bravo_x\n charlie_x\n " ;
391+ let theirs = b"alpha_x\n \n charlie_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