Skip to content

Commit 399fea9

Browse files
committed
fix: Adjust blob-merge baseline to also test the reverse of each operation
This also fixes an issue with blob merge computations.
1 parent f86cacc commit 399fea9

File tree

5 files changed

+159
-86
lines changed

5 files changed

+159
-86
lines changed

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

+31-26
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::blob::builtin_driver::text::utils::{
55
};
66
use crate::blob::builtin_driver::text::{Conflict, ConflictStyle, Labels, Options};
77
use crate::blob::Resolution;
8+
use std::ops::Range;
89

910
/// Merge `current` and `other` with `ancestor` as base according to `opts`.
1011
///
@@ -36,7 +37,7 @@ pub fn merge<'a>(
3637
input.update_before(tokens(ancestor));
3738
input.update_after(tokens(current));
3839

39-
let current_hunks = imara_diff::diff(
40+
let hunks = imara_diff::diff(
4041
opts.diff_algorithm,
4142
input,
4243
CollectHunks {
@@ -53,7 +54,7 @@ pub fn merge<'a>(
5354
input,
5455
CollectHunks {
5556
side: Side::Other,
56-
hunks: current_hunks,
57+
hunks,
5758
},
5859
);
5960

@@ -62,33 +63,28 @@ pub fn merge<'a>(
6263
let mut intersecting = Vec::new();
6364
let mut ancestor_integrated_until = 0;
6465
let mut resolution = Resolution::Complete;
65-
let mut filled_hunks = Vec::with_capacity(2);
66-
while let Some(hunk) = hunks.next() {
67-
if take_intersecting(&hunk, &mut hunks, &mut intersecting) {
68-
fill_ancestor(&hunk.before, &mut intersecting);
69-
70-
let filled_hunks_side = hunk.side;
71-
filled_hunks.clear();
72-
filled_hunks.push(hunk);
73-
fill_ancestor(
74-
&intersecting
75-
.first()
76-
.zip(intersecting.last())
77-
.map(|(f, l)| f.before.start..l.before.end)
78-
.expect("at least one entry"),
79-
&mut filled_hunks,
80-
);
66+
let mut current_hunks = Vec::with_capacity(2);
67+
while take_intersecting(&mut hunks, &mut current_hunks, &mut intersecting).is_some() {
68+
if !intersecting.is_empty() {
69+
let filled_hunks_side = current_hunks.first().expect("at least one hunk").side;
70+
{
71+
let filled_hunks_range = before_range_from_hunks(&current_hunks);
72+
let intersecting_range = before_range_from_hunks(&intersecting);
73+
let extended_range = filled_hunks_range.start..intersecting_range.end.max(filled_hunks_range.end);
74+
fill_ancestor(&extended_range, &mut current_hunks);
75+
fill_ancestor(&extended_range, &mut intersecting);
76+
}
8177
match opts.conflict {
8278
Conflict::Keep { style, marker_size } => {
8379
let (hunks_front_and_back, num_hunks_front) = match style {
8480
ConflictStyle::Merge | ConflictStyle::ZealousDiff3 => {
85-
zealously_contract_hunks(&mut filled_hunks, &mut intersecting, input, &current_tokens)
81+
zealously_contract_hunks(&mut current_hunks, &mut intersecting, input, &current_tokens)
8682
}
8783
ConflictStyle::Diff3 => (Vec::new(), 0),
8884
};
8985
let (our_hunks, their_hunks) = match filled_hunks_side {
90-
Side::Current => (&filled_hunks, &intersecting),
91-
Side::Other => (&intersecting, &filled_hunks),
86+
Side::Current => (&current_hunks, &intersecting),
87+
Side::Other => (&intersecting, &current_hunks),
9288
Side::Ancestor => {
9389
unreachable!("initial hunks are never ancestors")
9490
}
@@ -173,8 +169,8 @@ pub fn merge<'a>(
173169
}
174170
Conflict::ResolveWithOurs | Conflict::ResolveWithTheirs => {
175171
let (our_hunks, their_hunks) = match filled_hunks_side {
176-
Side::Current => (&filled_hunks, &intersecting),
177-
Side::Other => (&intersecting, &filled_hunks),
172+
Side::Current => (&current_hunks, &intersecting),
173+
Side::Other => (&intersecting, &current_hunks),
178174
Side::Ancestor => {
179175
unreachable!("initial hunks are never ancestors")
180176
}
@@ -194,11 +190,11 @@ pub fn merge<'a>(
194190
}
195191
Conflict::ResolveWithUnion => {
196192
let (hunks_front_and_back, num_hunks_front) =
197-
zealously_contract_hunks(&mut filled_hunks, &mut intersecting, input, &current_tokens);
193+
zealously_contract_hunks(&mut current_hunks, &mut intersecting, input, &current_tokens);
198194

199195
let (our_hunks, their_hunks) = match filled_hunks_side {
200-
Side::Current => (&filled_hunks, &intersecting),
201-
Side::Other => (&intersecting, &filled_hunks),
196+
Side::Current => (&current_hunks, &intersecting),
197+
Side::Other => (&intersecting, &current_hunks),
202198
Side::Ancestor => {
203199
unreachable!("initial hunks are never ancestors")
204200
}
@@ -228,6 +224,7 @@ pub fn merge<'a>(
228224
}
229225
}
230226
} else {
227+
let hunk = current_hunks.pop().expect("always pushed during intersection check");
231228
write_ancestor(input, ancestor_integrated_until, hunk.before.start as usize, out);
232229
ancestor_integrated_until = hunk.before.end;
233230
write_hunks(std::slice::from_ref(&hunk), input, &current_tokens, out);
@@ -237,3 +234,11 @@ pub fn merge<'a>(
237234

238235
resolution
239236
}
237+
238+
fn before_range_from_hunks(hunks: &[Hunk]) -> Range<u32> {
239+
hunks
240+
.first()
241+
.zip(hunks.last())
242+
.map(|(f, l)| f.before.start..l.before.end)
243+
.expect("at least one entry")
244+
}

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

+39-14
Original file line numberDiff line numberDiff line change
@@ -414,21 +414,46 @@ fn write_tokens(
414414
}
415415

416416
/// Find all hunks in `iter` which aren't from the same side as `hunk` and intersect with it.
417-
/// Return `true` if `out` is non-empty after the operation, indicating overlapping hunks were found.
418-
pub fn take_intersecting(hunk: &Hunk, iter: &mut Peekable<impl Iterator<Item = Hunk>>, out: &mut Vec<Hunk>) -> bool {
419-
out.clear();
420-
while iter
421-
.peek()
422-
.filter(|b_hunk| {
423-
b_hunk.side != hunk.side
424-
&& (hunk.before.contains(&b_hunk.before.start)
425-
|| (hunk.before.is_empty() && hunk.before.start == b_hunk.before.start))
426-
})
427-
.is_some()
428-
{
429-
out.extend(iter.next());
417+
/// Also put `hunk` into `input` so it's the first item, and possibly put more hunks of the side of `hunk` so
418+
/// `iter` doesn't have any overlapping hunks left.
419+
/// Return `true` if `intersecting` is non-empty after the operation, indicating overlapping hunks were found.
420+
pub fn take_intersecting(
421+
iter: &mut Peekable<impl Iterator<Item = Hunk>>,
422+
input: &mut Vec<Hunk>,
423+
intersecting: &mut Vec<Hunk>,
424+
) -> Option<()> {
425+
input.clear();
426+
input.push(iter.next()?);
427+
intersecting.clear();
428+
429+
fn left_overlaps_right(left: &Hunk, right: &Hunk) -> bool {
430+
left.side != right.side
431+
&& (right.before.contains(&left.before.start)
432+
|| (right.before.is_empty() && right.before.start == left.before.start))
433+
}
434+
435+
loop {
436+
let hunk = input.last().expect("just pushed");
437+
while iter.peek().filter(|b_hunk| left_overlaps_right(b_hunk, hunk)).is_some() {
438+
intersecting.extend(iter.next());
439+
}
440+
// The hunks that overlap might themselves overlap with a following hunk of the other side.
441+
// If so, split it so it doesn't overlap anymore.
442+
let mut found_more_intersections = false;
443+
while intersecting
444+
.last_mut()
445+
.zip(iter.peek_mut())
446+
.filter(|(last_intersecting, candidate)| left_overlaps_right(candidate, last_intersecting))
447+
.is_some()
448+
{
449+
input.extend(iter.next());
450+
found_more_intersections = true;
451+
}
452+
if !found_more_intersections {
453+
break;
454+
}
430455
}
431-
!out.is_empty()
456+
Some(())
432457
}
433458

434459
pub fn tokens(input: &[u8]) -> imara_diff::sources::ByteLines<'_, true> {
Binary file not shown.

gix-merge/tests/fixtures/text-baseline.sh

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ function baseline() {
1212

1313
shift 4
1414
git merge-file --stdout "$@" "$ours" "$base" "$theirs" > "$output" || true
15-
1615
echo "$ours" "$base" "$theirs" "$output" "$@" >> baseline.cases
16+
17+
local output="${output}-reversed"
18+
git merge-file --stdout "$@" "$theirs" "$base" "$ours" > "${output}" || true
19+
echo "$theirs" "$base" "$ours" "${output}" "$@" >> baseline-reversed.cases
1720
}
1821

1922
mkdir simple

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

+85-45
Original file line numberDiff line numberDiff line change
@@ -71,60 +71,100 @@ mod text {
7171
"complex/spurious-c-conflicts/zdiff3-histogram.merged",
7272
];
7373

74+
/// Should be a copy of `DIVERGING` once the reverse operation truly works like before
75+
const DIVERGING_REVERSED: &[&str] = &[
76+
// expected cases
77+
// TODO: double-check them, maybe some could actually work, they seem to have similar failure modes as above
78+
"zdiff3-middlecommon/merge.merged-reversed",
79+
"zdiff3-middlecommon/merge-union.merged-reversed",
80+
"zdiff3-interesting/merge.merged-reversed",
81+
"zdiff3-interesting/merge-theirs.merged-reversed",
82+
"zdiff3-interesting/diff3.merged-reversed",
83+
"zdiff3-interesting/diff3-histogram.merged-reversed",
84+
"zdiff3-interesting/zdiff3.merged-reversed",
85+
"zdiff3-interesting/zdiff3-histogram.merged-reversed",
86+
"zdiff3-interesting/merge-union.merged-reversed",
87+
"zdiff3-evil/merge.merged-reversed",
88+
"zdiff3-evil/merge-union.merged-reversed",
89+
"complex/missing-LF-at-EOF/merge.merged-reversed",
90+
"complex/missing-LF-at-EOF/diff3.merged-reversed",
91+
"complex/missing-LF-at-EOF/diff3-histogram.merged-reversed",
92+
"complex/missing-LF-at-EOF/zdiff3.merged-reversed",
93+
"complex/missing-LF-at-EOF/zdiff3-histogram.merged-reversed",
94+
"complex/missing-LF-at-EOF/merge-ours.merged-reversed",
95+
"complex/missing-LF-at-EOF/merge-theirs.merged-reversed",
96+
"complex/missing-LF-at-EOF/merge-union.merged-reversed",
97+
"complex/auto-simplification/merge.merged-reversed",
98+
"complex/auto-simplification/merge-union.merged-reversed",
99+
"complex/marker-newline-handling-lf2/zdiff3.merged-reversed",
100+
"complex/marker-newline-handling-lf2/zdiff3-histogram.merged-reversed",
101+
"complex/spurious-c-conflicts/merge.merged-reversed",
102+
"complex/spurious-c-conflicts/merge-union.merged-reversed",
103+
"complex/spurious-c-conflicts/diff3-histogram.merged-reversed",
104+
"complex/spurious-c-conflicts/zdiff3-histogram.merged-reversed",
105+
];
106+
74107
// TODO: fix all of these eventually
75-
fn is_case_diverging(case: &baseline::Expectation) -> bool {
76-
DIVERGING.iter().any(|name| case.name == *name)
108+
fn is_case_diverging(case: &baseline::Expectation, diverging: &[&str]) -> bool {
109+
diverging.iter().any(|name| case.name == *name)
77110
}
78111

79112
#[test]
80113
fn run_baseline() -> crate::Result {
81114
let root = gix_testtools::scripted_fixture_read_only("text-baseline.sh")?;
82-
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
83-
let mut out = Vec::new();
84-
let mut num_diverging = 0;
85-
let mut num_cases = 0;
86-
for case in baseline::Expectations::new(&root, &cases) {
87-
num_cases += 1;
88-
let mut input = imara_diff::intern::InternedInput::default();
89-
let actual = gix_merge::blob::builtin_driver::text(
90-
&mut out,
91-
&mut input,
92-
case.labels(),
93-
&case.ours,
94-
&case.base,
95-
&case.theirs,
96-
case.options,
97-
);
98-
if is_case_diverging(&case) {
99-
num_diverging += 1;
100-
} else {
101-
let expected_resolution = if case.expected.contains_str("<<<<<<<") {
102-
Resolution::Conflict
103-
} else {
104-
Resolution::Complete
105-
};
106-
assert_eq!(out.as_bstr(), case.expected);
107-
assert_str_eq!(
108-
out.as_bstr().to_str_lossy(),
109-
case.expected.to_str_lossy(),
110-
"{}: output mismatch\n{}",
111-
case.name,
112-
out.as_bstr()
115+
for (baseline, diverging, expected_percentage) in [
116+
("baseline-reversed.cases", DIVERGING_REVERSED, 11),
117+
("baseline.cases", DIVERGING, 11),
118+
] {
119+
let cases = std::fs::read_to_string(root.join(baseline))?;
120+
let mut out = Vec::new();
121+
let mut num_diverging = 0;
122+
let mut num_cases = 0;
123+
for case in baseline::Expectations::new(&root, &cases)
124+
// .filter(|case| case.name == "partial-match/merge-union.merged-reversed")
125+
{
126+
num_cases += 1;
127+
let mut input = imara_diff::intern::InternedInput::default();
128+
let actual = gix_merge::blob::builtin_driver::text(
129+
&mut out,
130+
&mut input,
131+
case.labels(),
132+
&case.ours,
133+
&case.base,
134+
&case.theirs,
135+
case.options,
113136
);
114-
assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name,);
137+
if is_case_diverging(&case, diverging) {
138+
num_diverging += 1;
139+
} else {
140+
let expected_resolution = if case.expected.contains_str("<<<<<<<") {
141+
Resolution::Conflict
142+
} else {
143+
Resolution::Complete
144+
};
145+
assert_str_eq!(
146+
out.as_bstr().to_str_lossy(),
147+
case.expected.to_str_lossy(),
148+
"{}: output mismatch\n{}",
149+
case.name,
150+
out.as_bstr()
151+
);
152+
assert_eq!(out.as_bstr(), case.expected);
153+
assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name,);
154+
}
115155
}
116-
}
117156

118-
assert_eq!(
119-
num_diverging,
120-
DIVERGING.len(),
121-
"Number of expected diverging cases must match the actual one - probably the implementation improved"
122-
);
123-
assert_eq!(
124-
((num_diverging as f32 / num_cases as f32) * 100.0) as usize,
125-
11,
126-
"Just to show the percentage of skipped tests - this should get better"
127-
);
157+
assert_eq!(
158+
num_diverging,
159+
diverging.len(),
160+
"Number of expected diverging cases must match the actual one - probably the implementation improved"
161+
);
162+
assert_eq!(
163+
((num_diverging as f32 / num_cases as f32) * 100.0) as usize,
164+
expected_percentage,
165+
"Just to show the percentage of skipped tests - this should get better"
166+
);
167+
}
128168
Ok(())
129169
}
130170

0 commit comments

Comments
 (0)