Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion gix-merge/src/blob/builtin_driver/text/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,9 @@ pub fn collect_hunks(
side: Side,
mut hunks: Vec<Hunk>,
) -> Vec<Hunk> {
hunks.extend(imara_diff::Diff::compute(algorithm, input).hunks().map(|hunk| Hunk {
let mut diff = imara_diff::Diff::compute(algorithm, input);
diff.postprocess_lines(input);
hunks.extend(diff.hunks().map(|hunk| Hunk {
Comment on lines -482 to +484
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now the actual fix, which was enabled by migrating to imara-diff v0.2 which comes with this kind of post-processing.
This also was facilitated by finally vendoring it, so the entire stack for diffing can be manipulated more swiftly and easily.

before: hunk.before,
after: hunk.after,
side,
Expand Down
67 changes: 67 additions & 0 deletions gix-merge/tests/merge/blob/builtin_driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,73 @@ mod text {
}
}

mod false_conflict {
use gix_merge::blob::{builtin_driver, builtin_driver::text::Conflict, Resolution};
use imara_diff::InternedInput;

/// Minimal reproduction: Myers produces a false conflict where git merge-file resolves cleanly.
///
/// base: alpha_x / (blank) / bravo_x / charlie_x / (blank)
/// ours: (blank) / (blank) / bravo_x / charlie_x
/// theirs: alpha_x / (blank) / charlie_x / (blank)
///
/// base→ours: alpha_x deleted (replaced by blank), trailing blank removed
/// base→theirs: bravo_x deleted
///
/// These are non-overlapping changes that git merges cleanly.
/// See https://github.com/GitoxideLabs/gitoxide/issues/2475
#[test]
fn myers_false_conflict_with_blank_line_ambiguity() {
let base = b"alpha_x\n\nbravo_x\ncharlie_x\n\n";
let ours = b"\n\nbravo_x\ncharlie_x\n";
let theirs = b"alpha_x\n\ncharlie_x\n\n";

let labels = builtin_driver::text::Labels {
ancestor: Some("base".into()),
current: Some("ours".into()),
other: Some("theirs".into()),
};

// Histogram resolves cleanly.
{
let options = builtin_driver::text::Options {
diff_algorithm: imara_diff::Algorithm::Histogram,
conflict: Conflict::Keep {
style: builtin_driver::text::ConflictStyle::Merge,
marker_size: 7.try_into().unwrap(),
},
};
let mut out = Vec::new();
let mut input = InternedInput::default();
let res = builtin_driver::text(&mut out, &mut input, labels, ours, base, theirs, options);
assert_eq!(res, Resolution::Complete, "Histogram should resolve cleanly");
}

// Myers should also resolve cleanly (it used to produce a false conflict because
// imara-diff's Myers splits the ours change into two hunks — a deletion at base[0]
// and an empty insertion at base[2] — and the insertion collided with theirs'
// deletion at base[2]).
{
let options = builtin_driver::text::Options {
diff_algorithm: imara_diff::Algorithm::Myers,
conflict: Conflict::Keep {
style: builtin_driver::text::ConflictStyle::Merge,
marker_size: 7.try_into().unwrap(),
},
};
let mut out = Vec::new();
let mut input = InternedInput::default();
let res = builtin_driver::text(&mut out, &mut input, labels, ours, base, theirs, options);
assert_eq!(
res,
Resolution::Complete,
"Myers should resolve cleanly (git merge-file does). Output:\n{}",
String::from_utf8_lossy(&out)
);
}
}
}

mod baseline {
use std::path::Path;

Expand Down
Loading