Skip to content

Commit 75b41c6

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. It's breaking because the marker-size was reduced to `u8`.
1 parent bc8f9b4 commit 75b41c6

File tree

11 files changed

+446
-226
lines changed

11 files changed

+446
-226
lines changed

gix-merge/fuzz/.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
target
2+
corpus
3+
artifacts
4+
5+
# These usually involve a lot of local CPU time, keep them.
6+
$artifacts
7+
$corpus

gix-merge/fuzz/Cargo.toml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "gix-merge-fuzz"
3+
version = "0.0.0"
4+
authors = ["Automatically generated"]
5+
publish = false
6+
edition = "2021"
7+
8+
[package.metadata]
9+
cargo-fuzz = true
10+
11+
[dependencies]
12+
anyhow = "1.0.76"
13+
libfuzzer-sys = "0.4"
14+
arbitrary = { version = "1.3.2", features = ["derive"] }
15+
imara-diff = { version = "0.1.7" }
16+
gix-merge = { path = ".." }
17+
18+
# Prevent this from interfering with workspaces
19+
[workspace]
20+
members = ["."]
21+
22+
[[bin]]
23+
name = "blob"
24+
path = "fuzz_targets/blob.rs"
25+
test = false
26+
doc = false

gix-merge/fuzz/fuzz_targets/blob.rs

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#![no_main]
2+
use anyhow::Result;
3+
use arbitrary::Arbitrary;
4+
use gix_merge::blob::builtin_driver::text::{Conflict, ConflictStyle, Options};
5+
use gix_merge::blob::Resolution;
6+
use libfuzzer_sys::fuzz_target;
7+
use std::hint::black_box;
8+
use std::num::NonZero;
9+
10+
fn fuzz_text_merge(
11+
Ctx {
12+
base,
13+
ours,
14+
theirs,
15+
marker_size,
16+
}: Ctx,
17+
) -> Result<()> {
18+
let mut buf = Vec::new();
19+
let mut input = imara_diff::intern::InternedInput::default();
20+
for diff_algorithm in [
21+
imara_diff::Algorithm::Histogram,
22+
imara_diff::Algorithm::Myers,
23+
imara_diff::Algorithm::MyersMinimal,
24+
] {
25+
let mut opts = Options {
26+
diff_algorithm,
27+
conflict: Default::default(),
28+
};
29+
for (left, right) in [(ours, theirs), (theirs, ours)] {
30+
let resolution = gix_merge::blob::builtin_driver::text(
31+
&mut buf,
32+
&mut input,
33+
Default::default(),
34+
left,
35+
base,
36+
right,
37+
opts,
38+
);
39+
if resolution == Resolution::Conflict {
40+
for conflict in [
41+
Conflict::ResolveWithOurs,
42+
Conflict::ResolveWithTheirs,
43+
Conflict::ResolveWithUnion,
44+
Conflict::Keep {
45+
style: ConflictStyle::Diff3,
46+
marker_size,
47+
},
48+
Conflict::Keep {
49+
style: ConflictStyle::ZealousDiff3,
50+
marker_size,
51+
},
52+
] {
53+
opts.conflict = conflict;
54+
gix_merge::blob::builtin_driver::text(
55+
&mut buf,
56+
&mut input,
57+
Default::default(),
58+
left,
59+
base,
60+
right,
61+
opts,
62+
);
63+
}
64+
}
65+
}
66+
}
67+
Ok(())
68+
}
69+
70+
#[derive(Debug, Arbitrary)]
71+
struct Ctx<'a> {
72+
base: &'a [u8],
73+
ours: &'a [u8],
74+
theirs: &'a [u8],
75+
marker_size: NonZero<u8>,
76+
}
77+
78+
fuzz_target!(|ctx: Ctx<'_>| {
79+
_ = black_box(fuzz_text_merge(ctx));
80+
});

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

+164-152
Large diffs are not rendered by default.

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use bstr::BStr;
2+
use std::num::NonZeroU8;
23

34
/// The way the built-in [text driver](crate::blob::BuiltinDriver::Text) will express
45
/// merge conflicts in the resulting file.
@@ -87,7 +88,7 @@ pub enum Conflict {
8788
/// How to visualize conflicts in merged files.
8889
style: ConflictStyle,
8990
/// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<`
90-
marker_size: usize,
91+
marker_size: NonZeroU8,
9192
},
9293
/// Chose our side to resolve a conflict.
9394
ResolveWithOurs,
@@ -99,12 +100,13 @@ pub enum Conflict {
99100

100101
impl Conflict {
101102
/// The amount of conflict marker characters to print by default.
102-
pub const DEFAULT_MARKER_SIZE: usize = 7;
103+
// TODO: use NonZeroU8::new().unwrap() here once the MSRV supports it.
104+
pub const DEFAULT_MARKER_SIZE: u8 = 7;
103105

104106
/// The amount of conflict markers to print if this instance contains them, or `None` otherwise
105-
pub fn marker_size(&self) -> Option<usize> {
107+
pub fn marker_size(&self) -> Option<u8> {
106108
match self {
107-
Conflict::Keep { marker_size, .. } => Some(*marker_size),
109+
Conflict::Keep { marker_size, .. } => Some(marker_size.get()),
108110
Conflict::ResolveWithOurs | Conflict::ResolveWithTheirs | Conflict::ResolveWithUnion => None,
109111
}
110112
}
@@ -114,7 +116,7 @@ impl Default for Conflict {
114116
fn default() -> Self {
115117
Conflict::Keep {
116118
style: Default::default(),
117-
marker_size: Conflict::DEFAULT_MARKER_SIZE,
119+
marker_size: Conflict::DEFAULT_MARKER_SIZE.try_into().unwrap(),
118120
}
119121
}
120122
}

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

+43-18
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ pub fn assure_ends_with_nl(out: &mut Vec<u8>, nl: &BStr) {
9393
}
9494
}
9595

96-
pub fn write_conflict_marker(out: &mut Vec<u8>, marker: u8, label: Option<&BStr>, marker_size: usize, nl: &BStr) {
96+
pub fn write_conflict_marker(out: &mut Vec<u8>, marker: u8, label: Option<&BStr>, marker_size: u8, nl: &BStr) {
9797
assure_ends_with_nl(out, nl);
98-
out.extend(std::iter::repeat(marker).take(marker_size));
98+
out.extend(std::iter::repeat(marker).take(marker_size as usize));
9999
if let Some(label) = label {
100100
out.push(b' ');
101101
out.extend_from_slice(label);
@@ -132,8 +132,8 @@ pub fn fill_ancestor(Range { start, end }: &Range<u32>, in_out: &mut Vec<Hunk>)
132132
for (idx, next_idx) in (first_idx..in_out.len()).map(|idx| (idx, idx + 1)) {
133133
let Some(next_hunk) = in_out.get(next_idx) else { break };
134134
let hunk = &in_out[idx];
135-
if let Some(lines_to_add) = next_hunk.after.start.checked_sub(hunk.after.end).filter(is_nonzero) {
136-
in_out.push(ancestor_hunk(hunk.after.end, lines_to_add));
135+
if let Some(lines_to_add) = next_hunk.before.start.checked_sub(hunk.before.end).filter(is_nonzero) {
136+
in_out.push(ancestor_hunk(hunk.before.end, lines_to_add));
137137
added_hunks = true;
138138
}
139139
}
@@ -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

0 commit comments

Comments
 (0)