History

Implement `text` and `binary` merge algorithms, also with baseline tests for correctness.

Sebastian Thiel committed 2 years ago

Commit 07628461 parent 9efa09f16 files · +1787 −167Browse files

Unified · Split

16 files
  1. Cargo.lock +2 −0
  2. crate-status.md +10 −6
  3. gix-merge/Cargo.toml +4 −0
  4. gix-merge/src/blob/builtin_driver.rs +0 −156
  5. gix-merge/src/blob/builtin_driver/binary.rs +43 −0
  6. gix-merge/src/blob/builtin_driver/mod.rs +30 −0
  7. gix-merge/src/blob/builtin_driver/text/function.rs +248 −0
  8. gix-merge/src/blob/builtin_driver/text/mod.rs +89 −0
  9. gix-merge/src/blob/builtin_driver/text/utils.rs +474 −0
  10. gix-merge/src/blob/mod.rs +9 −5
  11. gix-merge/src/blob/platform.rs +1 −1
  12. gix-merge/tests/fixtures/generated-archives/text-baseline.tar +0 −0
  13. gix-merge/tests/fixtures/text-baseline.sh +659 −0
  14. gix-merge/tests/merge/blob/builtin_driver.rs +214 −0
  15. gix-merge/tests/merge/blob/mod.rs +1 −0
  16. gix-merge/tests/merge/main.rs +4 −0
Cargo.lock+2−0
@@ -2053,9 +2053,11 @@ dependencies = [ · 2052 unmodified lines
20532053 "gix-object 0.44.0",
20542054 "gix-path 0.10.11",
20552055 "gix-tempfile 14.0.2",
2056+ "gix-testtools",
20562057 "gix-trace 0.1.10",
20572058 "gix-worktree 0.36.0",
20582059 "imara-diff",
2060+ "pretty_assertions",
20592061 "serde",
20602062 "thiserror",
20612063 ]
crate-status.md+10−6
@@ -324,19 +324,23 @@ Check out the [performance discussion][gix-diff-performance] as well. · 323 unmodified lines
324324 * [x] prepare invocation of external diff program
325325 - [ ] pass meta-info
326326 * [ ] working with hunks of data
327+* [ ] diff-heuristics match Git perfectly
327328 * [x] API documentation
328329 * [ ] Examples
329330
331+[gix-diff-performance]: https://github.com/Byron/gitoxide/discussions/74
332+
330333 ### gix-merge
331334
332−* [ ] three-way merge analysis of blobs with choice of how to resolve conflicts
335+* [x] three-way merge analysis of blobs with choice of how to resolve conflicts
333336 - [ ] choose how to resolve conflicts on the data-structure
334337 - [ ] produce a new blob based on data-structure containing possible resolutions
335− - [ ] `merge` style
336− - [ ] `diff3` style
337− - [ ] `zdiff` style
338−
339−[gix-diff-performance]: https://github.com/Byron/gitoxide/discussions/74
338+ - [x] `merge` style
339+ - [x] `diff3` style
340+ - [x] `zdiff` style
341+* [ ] diff-heuristics match Git perfectly
342+* [x] API documentation
343+ * [ ] Examples
340344
341345 ### gix-traverse
342346
gix-merge/Cargo.toml+4−0
@@ -39,6 +39,10 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur · 38 unmodified lines
3939
4040 document-features = { version = "0.2.0", optional = true }
4141
42+[dev-dependencies]
43+gix-testtools = { path = "../tests/tools" }
44+pretty_assertions = "1.4.0"
45+
4246 [package.metadata.docs.rs]
4347 all-features = true
4448 features = ["document-features"]
gix-merge/src/blob/builtin_driver.rs+0−156
@@ -1,156 +0,0 @@
1−use crate::blob::BuiltinDriver;
2−
3−impl BuiltinDriver {
4− /// Return the name of this instance.
5− pub fn as_str(&self) -> &str {
6− match self {
7− BuiltinDriver::Text => "text",
8− BuiltinDriver::Binary => "binary",
9− BuiltinDriver::Union => "union",
10− }
11− }
12−
13− /// Get all available built-in drivers.
14− pub fn all() -> &'static [Self] {
15− &[BuiltinDriver::Text, BuiltinDriver::Binary, BuiltinDriver::Union]
16− }
17−
18− /// Try to match one of our variants to `name`, case-sensitive, and return its instance.
19− pub fn by_name(name: &str) -> Option<Self> {
20− Self::all().iter().find(|variant| variant.as_str() == name).copied()
21− }
22−}
23−
24−///
25−pub mod binary {
26− use crate::blob::Resolution;
27−
28− /// What to do when having to pick a side to resolve a conflict.
29− #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
30− pub enum ResolveWith {
31− /// Chose the ancestor to resolve a conflict.
32− Ancestor,
33− /// Chose our side to resolve a conflict.
34− Ours,
35− /// Chose their side to resolve a conflict.
36− Theirs,
37− }
38−
39− /// Tell the caller of [`merge()`] which side was picked
40− #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
41− pub enum Pick {
42− /// Chose the ancestor.
43− Ancestor,
44− /// Chose our side.
45− Ours,
46− /// Chose their side.
47− Theirs,
48− }
49−
50− /// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic.
51− ///
52− /// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`.
53− pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) {
54− match on_conflict {
55− None => (Pick::Ours, Resolution::Conflict),
56− Some(ResolveWith::Ours) => (Pick::Ours, Resolution::Complete),
57− Some(ResolveWith::Theirs) => (Pick::Theirs, Resolution::Complete),
58− Some(ResolveWith::Ancestor) => (Pick::Ancestor, Resolution::Complete),
59− }
60− }
61−}
62−
63−///
64−pub mod text {
65− use crate::blob::Resolution;
66−
67− /// The way the built-in [text driver](crate::blob::BuiltinDriver::Text) will express
68− /// merge conflicts in the resulting file.
69− #[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
70− pub enum ConflictStyle {
71− /// Only show the zealously minified conflicting lines of the local changes and the incoming (other) changes,
72− /// hiding the base version entirely.
73− ///
74− /// ```
75− /// line1-changed-by-both
76− /// <<<<<<< local
77− /// line2-to-be-changed-in-incoming
78− /// =======
79− /// line2-changed
80− /// >>>>>>> incoming
81− ///```
82− #[default]
83− Merge,
84− /// Show non-minimized hunks of local changes, the base, and the incoming (other) changes.
85− ///
86− /// This mode does not hide any information.
87− /// ```
88− /// <<<<<<< local
89− /// line1-changed-by-both
90− /// line2-to-be-changed-in-incoming
91− /// ||||||| 9a8d80c
92− /// line1-to-be-changed-by-both
93− /// line2-to-be-changed-in-incoming
94− /// =======
95− /// line1-changed-by-both
96− /// line2-changed
97− /// >>>>>>> incoming
98− ///```
99− Diff3,
100− /// Like [`Diff3](Self::Diff3), but will show *minimized* hunks of local change and the incoming (other) changes,
101− /// as well as non-minimized hunks of the base.
102− ///
103− /// ```
104− /// line1-changed-by-both
105− /// <<<<<<< local
106− /// line2-to-be-changed-in-incoming
107− /// ||||||| 9a8d80c
108− /// line1-to-be-changed-by-both
109− /// line2-to-be-changed-in-incoming
110− /// =======
111− /// line2-changed
112− /// >>>>>>> incoming
113− /// ```
114− ZealousDiff3,
115− }
116−
117− /// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text).
118− #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
119− pub struct Options {
120− /// How to visualize conflicts in merged files.
121− pub conflict_style: ConflictStyle,
122− /// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<`
123− pub marker_size: usize,
124− /// Decide what to do to automatically resolve conflicts.
125− /// If `None`, add conflict markers according to `conflict_style` and `marker_size`.
126− pub on_conflict: Option<ResolveWith>,
127− }
128−
129− impl Default for Options {
130− fn default() -> Self {
131− Options {
132− conflict_style: Default::default(),
133− marker_size: 7,
134− on_conflict: None,
135− }
136− }
137− }
138−
139− /// What to do to resolve a conflict.
140− #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
141− pub enum ResolveWith {
142− /// Chose our side to resolve a conflict.
143− Ours,
144− /// Chose their side to resolve a conflict.
145− Theirs,
146− /// Place our and their lines one after another, in any order
147− Union,
148− }
149−
150− /// Merge `current` and `other` with `ancestor` as base according to `opts`.
151− ///
152− /// Place the merged result in `out` and return the resolution.
153− pub fn merge(_out: &mut Vec<u8>, _current: &[u8], _ancestor: &[u8], _other: &[u8], _opts: Options) -> Resolution {
154− todo!("text merge");
155− }
156−}
gix-merge/src/blob/builtin_driver/binary.rs+43−0
@@ -0,0 +1,43 @@
1+/// What to do when having to pick a side to resolve a conflict.
2+#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
3+pub enum ResolveWith {
4+ /// Chose the ancestor to resolve a conflict.
5+ Ancestor,
6+ /// Chose our side to resolve a conflict.
7+ Ours,
8+ /// Chose their side to resolve a conflict.
9+ Theirs,
10+}
11+
12+/// Tell the caller of [`merge()`](function::merge) which side was picked.
13+#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
14+pub enum Pick {
15+ /// Chose the ancestor.
16+ Ancestor,
17+ /// Chose our side.
18+ Ours,
19+ /// Chose their side.
20+ Theirs,
21+}
22+
23+pub(super) mod function {
24+ use crate::blob::builtin_driver::binary::{Pick, ResolveWith};
25+ use crate::blob::Resolution;
26+
27+ /// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic.
28+ ///
29+ /// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`.
30+ pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) {
31+ match on_conflict {
32+ None => (Pick::Ours, Resolution::Conflict),
33+ Some(resolve) => (
34+ match resolve {
35+ ResolveWith::Ours => Pick::Ours,
36+ ResolveWith::Theirs => Pick::Theirs,
37+ ResolveWith::Ancestor => Pick::Ancestor,
38+ },
39+ Resolution::Complete,
40+ ),
41+ }
42+ }
43+}
gix-merge/src/blob/builtin_driver/mod.rs+30−0
@@ -0,0 +1,30 @@
1+use crate::blob::BuiltinDriver;
2+
3+impl BuiltinDriver {
4+ /// Return the name of this instance.
5+ pub fn as_str(&self) -> &str {
6+ match self {
7+ BuiltinDriver::Text => "text",
8+ BuiltinDriver::Binary => "binary",
9+ BuiltinDriver::Union => "union",
10+ }
11+ }
12+
13+ /// Get all available built-in drivers.
14+ pub fn all() -> &'static [Self] {
15+ &[BuiltinDriver::Text, BuiltinDriver::Binary, BuiltinDriver::Union]
16+ }
17+
18+ /// Try to match one of our variants to `name`, case-sensitive, and return its instance.
19+ pub fn by_name(name: &str) -> Option<Self> {
20+ Self::all().iter().find(|variant| variant.as_str() == name).copied()
21+ }
22+}
23+
24+///
25+pub mod binary;
26+pub use binary::function::merge as binary;
27+
28+///
29+pub mod text;
30+pub use text::function::merge as text;
gix-merge/src/blob/builtin_driver/text/function.rs+248−0
@@ -0,0 +1,248 @@
1+use crate::blob::builtin_driver::text::utils::{
2+ assure_ends_with_nl, contains_lines, detect_line_ending, detect_line_ending_or_nl, fill_ancestor,
3+ hunks_differ_in_diff3, take_intersecting, tokens, write_ancestor, write_conflict_marker, write_hunks,
4+ zealously_contract_hunks, CollectHunks, Hunk, Side,
5+};
6+use crate::blob::builtin_driver::text::{ConflictStyle, Options, ResolveWith};
7+use crate::blob::Resolution;
8+use bstr::BStr;
9+
10+/// Merge `current` and `other` with `ancestor` as base according to `opts`.
11+///
12+/// Use `current_label`, `other_label` and `ancestor_label` to annotate conflict sections.
13+///
14+/// `input` is for reusing memory for lists of tokens, but note that it grows indefinitely
15+/// while tokens for `current`, `ancestor` and `other` are added.
16+/// Place the merged result in `out` (cleared before use) and return the resolution.
17+///
18+/// # Important
19+///
20+/// *The caller* is responsible for clearing `input`, otherwise tokens will accumulate.
21+/// This idea is to save time if the input is known to be very similar.
22+#[allow(clippy::too_many_arguments)]
23+pub fn merge<'a>(
24+ out: &mut Vec<u8>,
25+ input: &mut imara_diff::intern::InternedInput<&'a [u8]>,
26+ current: &'a [u8],
27+ current_label: Option<&BStr>,
28+ ancestor: &'a [u8],
29+ ancestor_label: Option<&BStr>,
30+ other: &'a [u8],
31+ other_label: Option<&BStr>,
32+ opts: Options,
33+) -> Resolution {
34+ out.clear();
35+ input.update_before(tokens(ancestor));
36+ input.update_after(tokens(current));
37+
38+ let current_hunks = imara_diff::diff(
39+ opts.diff_algorithm,
40+ input,
41+ CollectHunks {
42+ side: Side::Current,
43+ hunks: Default::default(),
44+ },
45+ );
46+
47+ let current_tokens = std::mem::take(&mut input.after);
48+ input.update_after(tokens(other));
49+
50+ let mut hunks = imara_diff::diff(
51+ opts.diff_algorithm,
52+ input,
53+ CollectHunks {
54+ side: Side::Other,
55+ hunks: current_hunks,
56+ },
57+ );
58+
59+ hunks.sort_by(|a, b| a.before.start.cmp(&b.before.start));
60+ let mut hunks = hunks.into_iter().peekable();
61+ let mut intersecting = Vec::new();
62+ let mut ancestor_integrated_until = 0;
63+ let mut resolution = Resolution::Complete;
64+ let mut filled_hunks = Vec::with_capacity(2);
65+ while let Some(hunk) = hunks.next() {
66+ if take_intersecting(&hunk, &mut hunks, &mut intersecting) {
67+ fill_ancestor(&hunk.before, &mut intersecting);
68+
69+ let filled_hunks_side = hunk.side;
70+ filled_hunks.clear();
71+ filled_hunks.push(hunk);
72+ fill_ancestor(
73+ &intersecting
74+ .first()
75+ .zip(intersecting.last())
76+ .map(|(f, l)| f.before.start..l.before.end)
77+ .expect("at least one entry"),
78+ &mut filled_hunks,
79+ );
80+ match opts.on_conflict {
81+ None => {
82+ let (hunks_front_and_back, num_hunks_front) = match opts.conflict_style {
83+ ConflictStyle::Merge | ConflictStyle::ZealousDiff3 => {
84+ zealously_contract_hunks(&mut filled_hunks, &mut intersecting, input, &current_tokens)
85+ }
86+ ConflictStyle::Diff3 => (Vec::new(), 0),
87+ };
88+ let (our_hunks, their_hunks) = match filled_hunks_side {
89+ Side::Current => (&filled_hunks, &intersecting),
90+ Side::Other => (&intersecting, &filled_hunks),
91+ Side::Ancestor => {
92+ unreachable!("initial hunks are never ancestors")
93+ }
94+ };
95+ let (front_hunks, back_hunks) = hunks_front_and_back.split_at(num_hunks_front);
96+ let first_hunk = front_hunks
97+ .first()
98+ .or(our_hunks.first())
99+ .expect("at least one hunk to write");
100+ let last_hunk = back_hunks
101+ .last()
102+ .or(their_hunks.last())
103+ .or(our_hunks.last())
104+ .or(front_hunks.last())
105+ .expect("at least one hunk");
106+ write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out);
107+
108+ write_hunks(front_hunks, input, &current_tokens, out);
109+ if their_hunks.is_empty() {
110+ write_hunks(our_hunks, input, &current_tokens, out);
111+ } else if our_hunks.is_empty() {
112+ // TODO: assure we run into this - currently no test triggers this. Can this happen at all?
113+ write_hunks(their_hunks, input, &current_tokens, out);
114+ } else {
115+ // DEVIATION: this makes tests (mostly) pass, but probably is very different from what Git does.
116+ let hunk_storage;
117+ let nl = detect_line_ending(
118+ if front_hunks.is_empty() {
119+ hunk_storage = Hunk {
120+ before: ancestor_integrated_until..first_hunk.before.start,
121+ after: Default::default(),
122+ side: Side::Ancestor,
123+ };
124+ std::slice::from_ref(&hunk_storage)
125+ } else {
126+ front_hunks
127+ },
128+ input,
129+ &current_tokens,
130+ )
131+ .or_else(|| detect_line_ending(our_hunks, input, &current_tokens))
132+ .unwrap_or(b"\n".into());
133+ match opts.conflict_style {
134+ ConflictStyle::Merge => {
135+ if contains_lines(our_hunks) || contains_lines(their_hunks) {
136+ resolution = Resolution::Conflict;
137+ write_conflict_marker(out, b'<', current_label, opts.marker_size, nl);
138+ write_hunks(our_hunks, input, &current_tokens, out);
139+ write_conflict_marker(out, b'=', None, opts.marker_size, nl);
140+ write_hunks(their_hunks, input, &current_tokens, out);
141+ write_conflict_marker(out, b'>', other_label, opts.marker_size, nl);
142+ }
143+ }
144+ ConflictStyle::Diff3 | ConflictStyle::ZealousDiff3 => {
145+ if contains_lines(our_hunks) || contains_lines(their_hunks) {
146+ if hunks_differ_in_diff3(
147+ opts.conflict_style,
148+ our_hunks,
149+ their_hunks,
150+ input,
151+ &current_tokens,
152+ ) {
153+ resolution = Resolution::Conflict;
154+ write_conflict_marker(out, b'<', current_label, opts.marker_size, nl);
155+ write_hunks(our_hunks, input, &current_tokens, out);
156+ let ancestor_hunk = Hunk {
157+ before: first_hunk.before.start..last_hunk.before.end,
158+ after: Default::default(),
159+ side: Side::Ancestor,
160+ };
161+ let ancestor_hunk = std::slice::from_ref(&ancestor_hunk);
162+ let ancestor_nl =
163+ detect_line_ending_or_nl(ancestor_hunk, input, &current_tokens);
164+ write_conflict_marker(out, b'|', ancestor_label, opts.marker_size, ancestor_nl);
165+ write_hunks(ancestor_hunk, input, &current_tokens, out);
166+ write_conflict_marker(out, b'=', None, opts.marker_size, nl);
167+ write_hunks(their_hunks, input, &current_tokens, out);
168+ write_conflict_marker(out, b'>', other_label, opts.marker_size, nl);
169+ } else {
170+ write_hunks(our_hunks, input, &current_tokens, out);
171+ }
172+ }
173+ }
174+ }
175+ }
176+ write_hunks(back_hunks, input, &current_tokens, out);
177+ ancestor_integrated_until = last_hunk.before.end;
178+ }
179+ Some(resolve) => {
180+ match resolve {
181+ ResolveWith::Ours | ResolveWith::Theirs => {
182+ let (our_hunks, their_hunks) = match filled_hunks_side {
183+ Side::Current => (&filled_hunks, &intersecting),
184+ Side::Other => (&intersecting, &filled_hunks),
185+ Side::Ancestor => {
186+ unreachable!("initial hunks are never ancestors")
187+ }
188+ };
189+ let hunks_to_write = if resolve == ResolveWith::Ours {
190+ our_hunks
191+ } else {
192+ their_hunks
193+ };
194+ if let Some(first_hunk) = hunks_to_write.first() {
195+ write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out);
196+ }
197+ write_hunks(hunks_to_write, input, &current_tokens, out);
198+ if let Some(last_hunk) = hunks_to_write.last() {
199+ ancestor_integrated_until = last_hunk.before.end;
200+ }
201+ }
202+ ResolveWith::Union => {
203+ let (hunks_front_and_back, num_hunks_front) =
204+ zealously_contract_hunks(&mut filled_hunks, &mut intersecting, input, &current_tokens);
205+
206+ let (our_hunks, their_hunks) = match filled_hunks_side {
207+ Side::Current => (&filled_hunks, &intersecting),
208+ Side::Other => (&intersecting, &filled_hunks),
209+ Side::Ancestor => {
210+ unreachable!("initial hunks are never ancestors")
211+ }
212+ };
213+ let (front_hunks, back_hunks) = hunks_front_and_back.split_at(num_hunks_front);
214+ let first_hunk = front_hunks
215+ .first()
216+ .or(our_hunks.first())
217+ .expect("at least one hunk to write");
218+ write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out);
219+ write_hunks(front_hunks, input, &current_tokens, out);
220+ assure_ends_with_nl(out, detect_line_ending_or_nl(front_hunks, input, &current_tokens));
221+ write_hunks(our_hunks, input, &current_tokens, out);
222+ assure_ends_with_nl(out, detect_line_ending_or_nl(our_hunks, input, &current_tokens));
223+ write_hunks(their_hunks, input, &current_tokens, out);
224+ if !back_hunks.is_empty() {
225+ assure_ends_with_nl(out, detect_line_ending_or_nl(their_hunks, input, &current_tokens));
226+ }
227+ write_hunks(back_hunks, input, &current_tokens, out);
228+ let last_hunk = back_hunks
229+ .last()
230+ .or(their_hunks.last())
231+ .or(our_hunks.last())
232+ .or(front_hunks.last())
233+ .expect("at least one hunk");
234+ ancestor_integrated_until = last_hunk.before.end;
235+ }
236+ };
237+ }
238+ }
239+ } else {
240+ write_ancestor(input, ancestor_integrated_until, hunk.before.start as usize, out);
241+ ancestor_integrated_until = hunk.before.end;
242+ write_hunks(std::slice::from_ref(&hunk), input, &current_tokens, out);
243+ }
244+ }
245+ write_ancestor(input, ancestor_integrated_until, input.before.len(), out);
246+
247+ resolution
248+}
gix-merge/src/blob/builtin_driver/text/mod.rs+89−0
@@ -0,0 +1,89 @@
1+/// The way the built-in [text driver](crate::blob::BuiltinDriver::Text) will express
2+/// merge conflicts in the resulting file.
3+#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
4+pub enum ConflictStyle {
5+ /// Only show the zealously minified conflicting lines of the local changes and the incoming (other) changes,
6+ /// hiding the base version entirely.
7+ ///
8+ /// ```
9+ /// line1-changed-by-both
10+ /// <<<<<<< local
11+ /// line2-to-be-changed-in-incoming
12+ /// =======
13+ /// line2-changed
14+ /// >>>>>>> incoming
15+ ///```
16+ #[default]
17+ Merge,
18+ /// Show non-minimized hunks of local changes, the base, and the incoming (other) changes.
19+ ///
20+ /// This mode does not hide any information.
21+ /// ```
22+ /// <<<<<<< local
23+ /// line1-changed-by-both
24+ /// line2-to-be-changed-in-incoming
25+ /// ||||||| 9a8d80c
26+ /// line1-to-be-changed-by-both
27+ /// line2-to-be-changed-in-incoming
28+ /// =======
29+ /// line1-changed-by-both
30+ /// line2-changed
31+ /// >>>>>>> incoming
32+ ///```
33+ Diff3,
34+ /// Like [`Diff3](Self::Diff3), but will show *minimized* hunks of local change and the incoming (other) changes,
35+ /// as well as non-minimized hunks of the base.
36+ ///
37+ /// ```
38+ /// line1-changed-by-both
39+ /// <<<<<<< local
40+ /// line2-to-be-changed-in-incoming
41+ /// ||||||| 9a8d80c
42+ /// line1-to-be-changed-by-both
43+ /// line2-to-be-changed-in-incoming
44+ /// =======
45+ /// line2-changed
46+ /// >>>>>>> incoming
47+ /// ```
48+ ZealousDiff3,
49+}
50+
51+/// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text).
52+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
53+pub struct Options {
54+ /// Determine of the diff will be performed.
55+ /// Defaults to [`imara_diff::Algorithm::Myers`].
56+ pub diff_algorithm: imara_diff::Algorithm,
57+ /// How to visualize conflicts in merged files.
58+ pub conflict_style: ConflictStyle,
59+ /// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<`
60+ pub marker_size: usize,
61+ /// Decide what to do to automatically resolve conflicts.
62+ /// If `None`, add conflict markers according to `conflict_style` and `marker_size`.
63+ pub on_conflict: Option<ResolveWith>,
64+}
65+
66+impl Default for Options {
67+ fn default() -> Self {
68+ Options {
69+ conflict_style: Default::default(),
70+ marker_size: 7,
71+ on_conflict: None,
72+ diff_algorithm: imara_diff::Algorithm::Myers,
73+ }
74+ }
75+}
76+
77+/// What to do to resolve a conflict.
78+#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
79+pub enum ResolveWith {
80+ /// Chose our side to resolve a conflict.
81+ Ours,
82+ /// Chose their side to resolve a conflict.
83+ Theirs,
84+ /// Place our and their lines one after another, in any order
85+ Union,
86+}
87+
88+pub(super) mod function;
89+mod utils;
gix-merge/src/blob/builtin_driver/text/utils.rs+474−0
@@ -0,0 +1,474 @@
1+use crate::blob::builtin_driver::text::ConflictStyle;
2+use bstr::{BStr, ByteSlice, ByteVec};
3+use std::iter::Peekable;
4+use std::ops::Range;
5+
6+/// Used only when `diff3` is the conflict style as `zdiff3` automatically reduces hunks into nothing.
7+/// Here we check if all hunks are the same.
8+pub fn hunks_differ_in_diff3(
9+ style: ConflictStyle,
10+ a: &[Hunk],
11+ b: &[Hunk],
12+ input: &imara_diff::intern::InternedInput<&[u8]>,
13+ current_tokens: &[imara_diff::intern::Token],
14+) -> bool {
15+ if style != ConflictStyle::Diff3 {
16+ return true;
17+ }
18+
19+ let tokens_for_hunk =
20+ |hunk: &Hunk| -> &[imara_diff::intern::Token] { tokens_for_side(hunk.side, input, current_tokens) };
21+
22+ a.iter()
23+ .flat_map(tokens_for_hunk)
24+ .ne(b.iter().flat_map(tokens_for_hunk))
25+}
26+
27+pub fn contains_lines(hunks: &[Hunk]) -> bool {
28+ hunks.iter().any(|h| !h.after.is_empty())
29+}
30+
31+/// ## Deviation
32+///
33+/// This implementation definitely isn't the same as in Git, primarily because it seemed impossible
34+/// to understand what's going on there without investing more time than it seemed worth.
35+pub fn detect_line_ending(
36+ hunks: &[Hunk],
37+ input: &mut imara_diff::intern::InternedInput<&[u8]>,
38+ current_tokens: &[imara_diff::intern::Token],
39+) -> Option<&'static BStr> {
40+ fn is_eol_crlf(
41+ hunks: &[Hunk],
42+ input: &mut imara_diff::intern::InternedInput<&[u8]>,
43+ current_tokens: &[imara_diff::intern::Token],
44+ ) -> Option<bool> {
45+ let (range, side) = hunks.iter().rev().find_map(|h| {
46+ (!h.after.is_empty())
47+ .then_some((&h.after, h.side))
48+ .or((!h.before.is_empty()).then_some((&h.before, Side::Ancestor)))
49+ })?;
50+
51+ let tokens = tokens_for_side(side, input, current_tokens);
52+ {
53+ let last_line = tokens
54+ .get(range.end as usize - 1)
55+ .map(|token| &input.interner[*token])?;
56+ if last_line.last() == Some(&b'\n') {
57+ return last_line.get(last_line.len().checked_sub(2)?).map(|c| *c == b'\r');
58+ }
59+ }
60+ let second_to_last_line = tokens
61+ .get(range.end.checked_sub(2)? as usize)
62+ .map(|token| &input.interner[*token])?;
63+ second_to_last_line
64+ .get(second_to_last_line.len().checked_sub(2)?)
65+ .map(|c| *c == b'\r')
66+ }
67+ is_eol_crlf(hunks, input, current_tokens).map(|is_crlf| if is_crlf { b"\r\n".into() } else { b"\n".into() })
68+}
69+
70+pub fn detect_line_ending_or_nl(
71+ hunks: &[Hunk],
72+ input: &mut imara_diff::intern::InternedInput<&[u8]>,
73+ current_tokens: &[imara_diff::intern::Token],
74+) -> &'static BStr {
75+ detect_line_ending(hunks, input, current_tokens).unwrap_or(b"\n".into())
76+}
77+
78+fn tokens_for_side<'a>(
79+ side: Side,
80+ input: &'a imara_diff::intern::InternedInput<&[u8]>,
81+ current_tokens: &'a [imara_diff::intern::Token],
82+) -> &'a [imara_diff::intern::Token] {
83+ match side {
84+ Side::Current => current_tokens,
85+ Side::Other => &input.after,
86+ Side::Ancestor => &input.before,
87+ }
88+}
89+
90+pub fn assure_ends_with_nl(out: &mut Vec<u8>, nl: &BStr) {
91+ if !out.is_empty() && !out.ends_with(b"\n") {
92+ out.push_str(nl);
93+ }
94+}
95+
96+pub fn write_conflict_marker(out: &mut Vec<u8>, marker: u8, label: Option<&BStr>, marker_size: usize, nl: &BStr) {
97+ assure_ends_with_nl(out, nl);
98+ out.extend(std::iter::repeat(marker).take(marker_size));
99+ if let Some(label) = label {
100+ out.push(b' ');
101+ out.extend_from_slice(label);
102+ }
103+ out.push_str(nl);
104+}
105+
106+pub fn write_ancestor(input: &imara_diff::intern::InternedInput<&[u8]>, from: u32, to: usize, out: &mut Vec<u8>) {
107+ if to < from as usize {
108+ return;
109+ }
110+ if let Some(tokens) = input.before.get(from as usize..to) {
111+ write_tokens(&input.interner, tokens, out);
112+ }
113+}
114+
115+/// Look at all hunks in `in_out` and fill in the ancestor in the range of `ancestor_range`.
116+/// This is all based on knowing the ranges are sequences of tokens.
117+pub fn fill_ancestor(Range { start, end }: &Range<u32>, in_out: &mut Vec<Hunk>) {
118+ fn is_nonzero(num: &u32) -> bool {
119+ *num > 0
120+ }
121+ if in_out.is_empty() {
122+ return;
123+ }
124+ let first = &in_out[0];
125+ let mut first_idx = 0;
126+ if let Some(lines_to_add) = first.before.start.checked_sub(*start).filter(is_nonzero) {
127+ in_out.insert(0, ancestor_hunk(*start, lines_to_add));
128+ first_idx += 1;
129+ }
130+
131+ let mut added_hunks = false;
132+ for (idx, next_idx) in (first_idx..in_out.len()).map(|idx| (idx, idx + 1)) {
133+ let Some(next_hunk) = in_out.get(next_idx) else { break };
134+ 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));
137+ added_hunks = true;
138+ }
139+ }
140+ let in_out_len = in_out.len();
141+ if added_hunks {
142+ in_out[first_idx..in_out_len].sort_by_key(|hunk| hunk.before.start);
143+ }
144+
145+ let last = &in_out[in_out_len - 1];
146+ if let Some(lines_to_add) = end.checked_sub(last.before.end).filter(is_nonzero) {
147+ in_out.push(ancestor_hunk(last.before.end, lines_to_add));
148+ }
149+}
150+
151+fn ancestor_hunk(start: u32, num_lines: u32) -> Hunk {
152+ let range = start..start + num_lines;
153+ Hunk {
154+ before: range.clone(),
155+ after: range,
156+ side: Side::Ancestor,
157+ }
158+}
159+
160+/// Reduce the area of `a_hunks` and the hunks in `b_hunks` so that only those lines that are
161+/// actually different remain. Note that we have to compare the resolved values, not only the tokens,
162+/// so `current_tokens` is expected to be known to the `input` (and its `interner`).
163+/// Hunks from all input arrays maybe removed in the process from the front and back, in case they
164+/// are entirely equal to what's in `hunk`. Note also that `a_hunks` and `b_hunks` are treated to be consecutive,
165+/// so [`fill_ancestor()`] must have been called beforehand, and are assumed to covert the same space in the
166+/// ancestor buffer.
167+/// Use `mode` to determine how hunks may be handled.
168+///
169+/// Return a new vector of all the hunks that were removed from front and back, with partial hunks inserted,
170+/// along with the amount of hunks that go front, with the remaining going towards the back.
171+// TODO: refactor so hunks and their associated data can go into an array for easier handling.
172+#[must_use]
173+pub fn zealously_contract_hunks(
174+ a_hunks: &mut Vec<Hunk>,
175+ b_hunks: &mut Vec<Hunk>,
176+ input: &imara_diff::intern::InternedInput<&[u8]>,
177+ current_tokens: &[imara_diff::intern::Token],
178+) -> (Vec<Hunk>, usize) {
179+ let line_content = |token_idx: u32, side: Side| {
180+ let tokens = match side {
181+ Side::Current => current_tokens,
182+ Side::Other => &input.after,
183+ Side::Ancestor => &input.before,
184+ };
185+ &input.interner[tokens[token_idx as usize]]
186+ };
187+ let (mut last_a_hunk_idx, mut last_b_hunk_idx) = (0, 0);
188+ let (mut out, hunks_in_front) = {
189+ let (mut remove_leading_a_hunks_from, mut remove_leading_b_hunks_from) = (None, None);
190+ let (mut a_hunk_token_equal_till, mut b_hunk_token_equal_till) = (None, None);
191+ for ((a_token_idx, a_hunk_idx, a_hunk_side), (b_token_idx, b_hunk_idx, b_hunk_side)) in
192+ iterate_hunks(a_hunks).zip(iterate_hunks(b_hunks))
193+ {
194+ let a_line = line_content(a_token_idx, a_hunk_side).as_bstr();
195+ let b_line = line_content(b_token_idx, b_hunk_side).as_bstr();
196+
197+ if last_a_hunk_idx != a_hunk_idx {
198+ a_hunk_token_equal_till = None;
199+ last_a_hunk_idx = a_hunk_idx;
200+ }
201+ if last_b_hunk_idx != b_hunk_idx {
202+ b_hunk_token_equal_till = None;
203+ last_b_hunk_idx = b_hunk_idx;
204+ }
205+ if a_line == b_line {
206+ (remove_leading_a_hunks_from, remove_leading_b_hunks_from) = (Some(a_hunk_idx), Some(b_hunk_idx));
207+ (a_hunk_token_equal_till, b_hunk_token_equal_till) = (Some(a_token_idx), Some(b_token_idx));
208+ } else {
209+ break;
210+ }
211+ }
212+
213+ let mut out = Vec::with_capacity(remove_leading_a_hunks_from.unwrap_or_else(|| {
214+ if a_hunk_token_equal_till.is_some() {
215+ 1
216+ } else {
217+ 0
218+ }
219+ }));
220+ truncate_hunks_from_from_front(
221+ a_hunks,
222+ remove_leading_a_hunks_from,
223+ a_hunk_token_equal_till,
224+ Some(&mut out),
225+ );
226+ truncate_hunks_from_from_front(b_hunks, remove_leading_b_hunks_from, b_hunk_token_equal_till, None);
227+ let hunks_in_front = out.len();
228+ (out, hunks_in_front)
229+ };
230+
231+ (last_a_hunk_idx, last_b_hunk_idx) = (0, 0);
232+ {
233+ let (mut remove_trailing_a_hunks_from, mut remove_trailing_b_hunks_from) = (None, None);
234+ let (mut a_hunk_token_equal_from, mut b_hunk_token_equal_from) = (None, None);
235+ for ((a_token_idx, a_hunk_idx, a_hunk_side), (b_token_idx, b_hunk_idx, b_hunk_side)) in
236+ iterate_hunks_rev(a_hunks).zip(iterate_hunks_rev(b_hunks))
237+ {
238+ let a_line = line_content(a_token_idx, a_hunk_side).as_bstr();
239+ let b_line = line_content(b_token_idx, b_hunk_side).as_bstr();
240+
241+ if last_a_hunk_idx != a_hunk_idx {
242+ a_hunk_token_equal_from = None;
243+ last_a_hunk_idx = a_hunk_idx;
244+ }
245+ if last_b_hunk_idx != b_hunk_idx {
246+ b_hunk_token_equal_from = None;
247+ last_b_hunk_idx = b_hunk_idx;
248+ }
249+
250+ if a_line == b_line {
251+ (remove_trailing_a_hunks_from, remove_trailing_b_hunks_from) = (Some(a_hunk_idx), Some(b_hunk_idx));
252+ (a_hunk_token_equal_from, b_hunk_token_equal_from) = (Some(a_token_idx), Some(b_token_idx));
253+ } else {
254+ break;
255+ }
256+ }
257+
258+ truncate_hunks_from_from_back(
259+ a_hunks,
260+ remove_trailing_a_hunks_from,
261+ a_hunk_token_equal_from,
262+ Some(&mut out),
263+ );
264+ truncate_hunks_from_from_back(b_hunks, remove_trailing_b_hunks_from, b_hunk_token_equal_from, None);
265+ }
266+
267+ (out, hunks_in_front)
268+}
269+
270+fn range_by_side(hunk: &mut Hunk) -> &mut Range<u32> {
271+ match hunk.side {
272+ Side::Current | Side::Other => &mut hunk.after,
273+ Side::Ancestor => &mut hunk.before,
274+ }
275+}
276+fn truncate_hunks_from_from_front(
277+ hunks: &mut Vec<Hunk>,
278+ hunks_to_remove_until_idx: Option<usize>,
279+ hunk_token_equal_till: Option<u32>,
280+ mut out_hunks: Option<&mut Vec<Hunk>>,
281+) {
282+ let Some(hunks_to_remove_until_idx) = hunks_to_remove_until_idx else {
283+ assert!(hunk_token_equal_till.is_none());
284+ return;
285+ };
286+ let mut last_index_to_remove = Some(hunks_to_remove_until_idx);
287+ let hunk = &mut hunks[hunks_to_remove_until_idx];
288+ let range = range_by_side(hunk);
289+ if let Some(hunk_token_equal_till) = hunk_token_equal_till {
290+ let orig_start = range.start;
291+ let new_start = hunk_token_equal_till + 1;
292+ range.start = new_start;
293+ if Range::<u32>::is_empty(range) {
294+ range.start = orig_start;
295+ } else if let Some(out) = out_hunks.as_deref_mut() {
296+ last_index_to_remove = hunks_to_remove_until_idx.checked_sub(1);
297+ let mut removed_hunk = hunk.clone();
298+ let new_range = range_by_side(&mut removed_hunk);
299+
300+ new_range.start = orig_start;
301+ new_range.end = new_start;
302+
303+ out.push(removed_hunk);
304+ } else {
305+ last_index_to_remove = hunks_to_remove_until_idx.checked_sub(1);
306+ }
307+ }
308+ if let Some(last_index_to_remove) = last_index_to_remove {
309+ let mut current_idx = 0;
310+ hunks.retain(|hunk| {
311+ if current_idx > last_index_to_remove {
312+ true
313+ } else {
314+ current_idx += 1;
315+ if let Some(out) = out_hunks.as_deref_mut() {
316+ out.push(hunk.clone());
317+ }
318+ false
319+ }
320+ });
321+ }
322+}
323+
324+fn truncate_hunks_from_from_back(
325+ hunks: &mut Vec<Hunk>,
326+ remove_trailing_hunks_from_idx: Option<usize>,
327+ hunk_token_equal_from: Option<u32>,
328+ mut out_hunks: Option<&mut Vec<Hunk>>,
329+) {
330+ let Some(mut remove_trailing_hunks_from_idx) = remove_trailing_hunks_from_idx else {
331+ assert!(hunk_token_equal_from.is_none());
332+ return;
333+ };
334+
335+ let hunk = &mut hunks[remove_trailing_hunks_from_idx];
336+ let range = range_by_side(hunk);
337+ if let Some(hunk_token_equal_from) = hunk_token_equal_from {
338+ let orig_end = range.end;
339+ let new_end = hunk_token_equal_from;
340+ range.end = new_end;
341+ if Range::<u32>::is_empty(range) {
342+ range.end = orig_end;
343+ } else if let Some(out) = out_hunks.as_deref_mut() {
344+ remove_trailing_hunks_from_idx += 1;
345+ let mut removed_hunk = hunk.clone();
346+ let new_range = range_by_side(&mut removed_hunk);
347+
348+ new_range.start = new_end;
349+ new_range.end = orig_end;
350+
351+ out.push(removed_hunk);
352+ } else {
353+ remove_trailing_hunks_from_idx += 1;
354+ }
355+ }
356+ if let Some(out) = out_hunks {
357+ out.extend_from_slice(&hunks[remove_trailing_hunks_from_idx..]);
358+ }
359+ hunks.truncate(remove_trailing_hunks_from_idx);
360+}
361+
362+/// Return an iterator over `(token_idx, hunk_idx, hunk_side)` from `hunks`.
363+fn iterate_hunks(hunks: &[Hunk]) -> impl Iterator<Item = (u32, usize, Side)> + '_ {
364+ hunks.iter().enumerate().flat_map(|(hunk_idx, hunk)| {
365+ match hunk.side {
366+ Side::Current | Side::Other => &hunk.after,
367+ Side::Ancestor => &hunk.before,
368+ }
369+ .clone()
370+ .map(move |idx| (idx, hunk_idx, hunk.side))
371+ })
372+}
373+
374+/// Return a reverse iterator over `(token_idx, hunk_idx, hunk_side)` from `hunks`.
375+fn iterate_hunks_rev(hunks: &[Hunk]) -> impl Iterator<Item = (u32, usize, Side)> + '_ {
376+ hunks.iter().enumerate().rev().flat_map(|(hunk_idx, hunk)| {
377+ match hunk.side {
378+ Side::Current | Side::Other => &hunk.after,
379+ Side::Ancestor => &hunk.before,
380+ }
381+ .clone()
382+ .rev()
383+ .map(move |idx| (idx, hunk_idx, hunk.side))
384+ })
385+}
386+
387+pub fn write_hunks(
388+ hunks: &[Hunk],
389+ input: &imara_diff::intern::InternedInput<&[u8]>,
390+ current_tokens: &[imara_diff::intern::Token],
391+ out: &mut Vec<u8>,
392+) {
393+ for hunk in hunks {
394+ let (tokens, range) = match hunk.side {
395+ Side::Current => (current_tokens, &hunk.after),
396+ Side::Other => (input.after.as_slice(), &hunk.after),
397+ Side::Ancestor => (input.before.as_slice(), &hunk.before),
398+ };
399+ write_tokens(&input.interner, &tokens[usize_range(range)], out);
400+ }
401+}
402+
403+fn usize_range(range: &Range<u32>) -> Range<usize> {
404+ range.start as usize..range.end as usize
405+}
406+
407+fn write_tokens(
408+ interner: &imara_diff::intern::Interner<&[u8]>,
409+ tokens: &[imara_diff::intern::Token],
410+ out: &mut Vec<u8>,
411+) {
412+ for token in tokens {
413+ out.extend_from_slice(interner[*token]);
414+ }
415+}
416+
417+/// Find all hunks in `iter` which aren't from the same side as `hunk` and intersect with it.
418+/// Return `true` if `out` is non-empty after the operation, indicating overlapping hunks were found.
419+pub fn take_intersecting(hunk: &Hunk, iter: &mut Peekable<impl Iterator<Item = Hunk>>, out: &mut Vec<Hunk>) -> bool {
420+ out.clear();
421+ while iter
422+ .peek()
423+ .filter(|b_hunk| {
424+ b_hunk.side != hunk.side
425+ && (hunk.before.contains(&b_hunk.before.start)
426+ || (hunk.before.is_empty() && hunk.before.start == b_hunk.before.start))
427+ })
428+ .is_some()
429+ {
430+ out.extend(iter.next());
431+ }
432+ !out.is_empty()
433+}
434+
435+pub fn tokens(input: &[u8]) -> imara_diff::sources::ByteLines<'_, true> {
436+ imara_diff::sources::byte_lines_with_terminator(input)
437+}
438+
439+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
440+pub enum Side {
441+ Current,
442+ Other,
443+ /// A special marker that is just used to be able to mix-in hunks that only point to the ancestor.
444+ /// Only `before` matters then.
445+ Ancestor,
446+}
447+
448+#[derive(Debug, Clone)]
449+pub struct Hunk {
450+ pub before: Range<u32>,
451+ pub after: Range<u32>,
452+ pub side: Side,
453+}
454+
455+pub struct CollectHunks {
456+ pub hunks: Vec<Hunk>,
457+ pub side: Side,
458+}
459+
460+impl imara_diff::Sink for CollectHunks {
461+ type Out = Vec<Hunk>;
462+
463+ fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
464+ self.hunks.push(Hunk {
465+ before,
466+ after,
467+ side: self.side,
468+ });
469+ }
470+
471+ fn finish(self) -> Self::Out {
472+ self.hunks
473+ }
474+}
gix-merge/src/blob/mod.rs+9−5
@@ -11,16 +11,20 @@ pub mod pipeline; · 10 unmodified lines
1111 ///
1212 pub mod platform;
1313
14−/// Identify a merge resolution.
14+/// Define if a merge is conflicted or not.
1515 #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
1616 pub enum Resolution {
1717 /// Everything could be resolved during the merge.
18+ ///
19+ /// Conflicts may have been resolved automatically, depending on the options.
1820 Complete,
19− /// A conflict is still present.
21+ /// A conflict is still present in the form of conflict markers.
22+ ///
23+ /// Note that this won't be the case if conflicts were automatically resolved.
2024 Conflict,
2125 }
2226
23−/// A way to classify a resource suitable for merging.
27+/// A way to classify the side of a resource for merging.
2428 #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
2529 pub enum ResourceKind {
2630 /// Our side of the state.
@@ -31,7 +35,7 @@ pub enum ResourceKind { · 4 unmodified lines
3135 CommonAncestorOrBase,
3236 }
3337
34−/// Define a driver program that merges
38+/// Define a built-in way of performing a three-way merge, including auto-resolution support.
3539 ///
3640 /// Some values are related to diffing, some are related to conversions.
3741 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
@@ -59,7 +63,7 @@ pub enum BuiltinDriver { · 21 unmodified lines
5963 Union,
6064 }
6165
62−/// Define a driver program that merges
66+/// Define a driver program that performs a three-way merge.
6367 ///
6468 /// Some values are related to diffing, some are related to conversions.
6569 #[derive(Default, Debug, Clone, PartialEq, Eq)]
gix-merge/src/blob/platform.rs+1−1
@@ -137,7 +137,7 @@ pub mod merge { · 136 unmodified lines
137137 pub other: ResourceRef<'parent>,
138138 }
139139
140− #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
140+ #[derive(Copy, Clone, Debug, Eq, PartialEq)]
141141 pub struct Options {
142142 /// If `true`, the resources being merged are contained in a virtual ancestor,
143143 /// which is the case when merge bases are merged into one.
gix-merge/tests/fixtures/generated-archives/text-baseline.tar+0−0

Binary file not shown.

gix-merge/tests/fixtures/text-baseline.sh+659−0
@@ -0,0 +1,659 @@
1+#!/usr/bin/env bash
2+set -eu -o pipefail
3+
4+git init
5+rm -Rf .git/hooks
6+
7+function baseline() {
8+ local ours=$DIR/${1:?1: our file}.blob;
9+ local base=$DIR/${2:?2: base file}.blob;
10+ local theirs=$DIR/${3:?3: their file}.blob;
11+ local output=$DIR/${4:?4: the name of the output file}.merged;
12+
13+ shift 4
14+ git merge-file --stdout "$@" "$ours" "$base" "$theirs" > "$output" || true
15+
16+ echo "$ours" "$base" "$theirs" "$output" "$@" >> baseline.cases
17+}
18+
19+mkdir simple
20+(cd simple
21+ echo -e "line1-changed-by-both\nline2-to-be-changed-in-incoming" > ours.blob
22+ echo -e "line1-to-be-changed-by-both\nline2-to-be-changed-in-incoming" > base.blob
23+ echo -e "line1-changed-by-both\nline2-changed" > theirs.blob
24+)
25+
26+# one big change includes multiple smaller ones
27+mkdir multi-change
28+(cd multi-change
29+ cat <<EOF > base.blob
30+0
31+1
32+2
33+3
34+4
35+5
36+6
37+7
38+8
39+9
40+EOF
41+
42+ cat <<EOF > ours.blob
43+0
44+1
45+X
46+X
47+4
48+5
49+Y
50+Y
51+8
52+Z
53+EOF
54+
55+ cat <<EOF > theirs.blob
56+T
57+T
58+T
59+T
60+T
61+T
62+T
63+T
64+T
65+T
66+EOF
67+)
68+
69+# a change with deletion/clearing our file
70+mkdir clear-ours
71+(cd clear-ours
72+ cat <<EOF > base.blob
73+0
74+1
75+2
76+3
77+4
78+5
79+EOF
80+
81+ touch ours.blob
82+
83+ cat <<EOF > theirs.blob
84+T
85+T
86+T
87+T
88+T
89+EOF
90+)
91+
92+# a change with deletion/clearing their file
93+mkdir clear-theirs
94+(cd clear-theirs
95+ cat <<EOF > base.blob
96+0
97+1
98+2
99+3
100+4
101+5
102+EOF
103+
104+ cat <<EOF > ours.blob
105+O
106+O
107+O
108+O
109+O
110+EOF
111+
112+ touch theirs.blob
113+)
114+
115+# differently sized changes
116+mkdir ours-2-lines-theirs-1-line
117+(cd ours-2-lines-theirs-1-line
118+ cat <<EOF > base.blob
119+0
120+1
121+2
122+3
123+4
124+5
125+EOF
126+
127+ cat <<EOF > ours.blob
128+0
129+1
130+X
131+X
132+4
133+5
134+EOF
135+
136+ cat <<EOF > theirs.blob
137+0
138+1
139+Y
140+3
141+4
142+5
143+EOF
144+)
145+
146+# partial match
147+mkdir partial-match
148+(cd partial-match
149+ cat <<EOF > base.blob
150+0
151+1
152+2
153+3
154+4
155+5
156+EOF
157+
158+ cat <<EOF > ours.blob
159+0
160+X1
161+X2
162+X3
163+X4
164+5
165+EOF
166+
167+ cat <<EOF > theirs.blob
168+0
169+X1
170+2
171+X3
172+X4
173+5
174+EOF
175+)
176+
177+# based on 'unique merge base' from 'diff3-conflict-markers'
178+mkdir unique-merge-base-with-insertion
179+(cd unique-merge-base-with-insertion
180+ cat <<EOF > base.blob
181+1
182+2
183+3
184+4
185+5
186+EOF
187+
188+ # no trailing newline
189+ echo -n $'1\n2\n3\n4\n5\n7' > ours.blob
190+ echo -n $'1\n2\n3\n4\n5\nsix' > theirs.blob
191+)
192+
193+mkdir zdiff3-basic
194+(cd zdiff3-basic
195+ cat <<EOF > base.blob
196+1
197+2
198+3
199+4
200+5
201+6
202+7
203+8
204+9
205+EOF
206+
207+ cat <<EOF > ours.blob
208+1
209+2
210+3
211+4
212+A
213+B
214+C
215+D
216+E
217+7
218+8
219+9
220+EOF
221+
222+ cat <<EOF > theirs.blob
223+1
224+2
225+3
226+4
227+A
228+X
229+C
230+Y
231+E
232+7
233+8
234+9
235+EOF
236+)
237+
238+mkdir zdiff3-middlecommon
239+(cd zdiff3-middlecommon
240+ cat <<EOF > base.blob
241+1
242+2
243+3
244+AA
245+4
246+5
247+BB
248+6
249+7
250+8
251+EOF
252+
253+ cat <<EOF > ours.blob
254+1
255+2
256+3
257+CC
258+4
259+5
260+DD
261+6
262+7
263+8
264+EOF
265+
266+ cat <<EOF > theirs.blob
267+1
268+2
269+3
270+EE
271+4
272+5
273+FF
274+6
275+7
276+8
277+EOF
278+)
279+
280+
281+mkdir zdiff3-interesting
282+(cd zdiff3-interesting
283+ cat <<EOF > base.blob
284+1
285+2
286+3
287+4
288+5
289+6
290+7
291+8
292+9
293+EOF
294+
295+ cat <<EOF > ours.blob
296+1
297+2
298+3
299+4
300+A
301+B
302+C
303+D
304+E
305+F
306+G
307+H
308+I
309+J
310+7
311+8
312+9
313+EOF
314+
315+ cat <<EOF > theirs.blob
316+1
317+2
318+3
319+4
320+A
321+B
322+C
323+5
324+6
325+G
326+H
327+I
328+J
329+7
330+8
331+9
332+EOF
333+)
334+
335+mkdir zdiff3-evil
336+(cd zdiff3-evil
337+ cat <<EOF > base.blob
338+1
339+2
340+3
341+4
342+5
343+6
344+7
345+8
346+9
347+EOF
348+
349+ cat <<EOF > ours.blob
350+1
351+2
352+3
353+4
354+X
355+A
356+B
357+C
358+7
359+8
360+9
361+EOF
362+
363+ cat <<EOF > theirs.blob
364+1
365+2
366+3
367+4
368+Y
369+A
370+B
371+C
372+B
373+C
374+7
375+8
376+9
377+EOF
378+)
379+
380+mkdir no-change-add
381+(cd no-change-add
382+
383+ echo -e " line 1\n\n line 2" > base.blob
384+ echo -e " line 1\n\n line in between\n\n line 2\n\n line in between\n\n line 3" > ours.blob
385+ cp ours.blob theirs.blob
386+)
387+
388+mkdir no-change-remove
389+(cd no-change-remove
390+
391+ echo -e " line 1\n\n line in between\n\n line 2\n\n line in between\n\n line 3" > base.blob
392+ echo -e " line 1\n\n line 2" > ours.blob
393+ cp ours.blob theirs.blob
394+)
395+
396+mkdir complex
397+(cd complex
398+ cat <<EOF >base.blob
399+Dominus regit me,
400+et nihil mihi deerit.
401+In loco pascuae ibi me collocavit,
402+super aquam refectionis educavit me;
403+animam meam convertit,
404+deduxit me super semitas jusitiae,
405+propter nomen suum.
406+EOF
407+
408+ cat <<EOF >new1.blob
409+Dominus regit me,
410+et nihil mihi deerit.
411+In loco pascuae ibi me collocavit,
412+super aquam refectionis educavit me;
413+animam meam convertit,
414+deduxit me super semitas jusitiae,
415+propter nomen suum.
416+Nam et si ambulavero in medio umbrae mortis,
417+non timebo mala, quoniam tu mecum es:
418+virga tua et baculus tuus ipsa me consolata sunt.
419+EOF
420+
421+ cat <<EOF >new2.blob
422+Dominus regit me, et nihil mihi deerit.
423+In loco pascuae ibi me collocavit,
424+super aquam refectionis educavit me;
425+animam meam convertit,
426+deduxit me super semitas jusitiae,
427+propter nomen suum.
428+EOF
429+
430+ cat <<EOF >new3.blob
431+DOMINUS regit me,
432+et nihil mihi deerit.
433+In loco pascuae ibi me collocavit,
434+super aquam refectionis educavit me;
435+animam meam convertit,
436+deduxit me super semitas jusitiae,
437+propter nomen suum.
438+EOF
439+
440+ cat <<EOF >new4.blob
441+Dominus regit me, et nihil mihi deerit.
442+In loco pascuae ibi me collocavit,
443+super aquam refectionis educavit me;
444+animam meam convertit,
445+deduxit me super semitas jusitiae,
446+EOF
447+
448+ cat <<EOF >new5.blob
449+Dominus regit me,
450+et nihil mihi deerit.
451+In loco pascuae ibi me collocavit,
452+super aquam refectionis educavit me;
453+animam meam convertit,
454+deduxit me super semitas jusitiae,
455+propter nomen suum.
456+Nam et si ambulavero in medio umbrae mortis,
457+non timebo mala, quoniam TU mecum es:
458+virga tua et baculus tuus ipsa me consolata sunt.
459+EOF
460+
461+ echo -n "propter nomen suum." >>new4.blob
462+
463+ cat <<EOF >base.c
464+int f(int x, int y)
465+{
466+ if (x == 0)
467+ {
468+ return y;
469+ }
470+ return x;
471+}
472+
473+int g(size_t u)
474+{
475+ while (u < 30)
476+ {
477+ u++;
478+ }
479+ return u;
480+}
481+EOF
482+
483+ cat <<EOF >ours.c
484+int g(size_t u)
485+{
486+ while (u < 30)
487+ {
488+ u++;
489+ }
490+ return u;
491+}
492+
493+int h(int x, int y, int z)
494+{
495+ if (z == 0)
496+ {
497+ return x;
498+ }
499+ return y;
500+}
501+EOF
502+
503+ cat <<EOF >theirs.c
504+int f(int x, int y)
505+{
506+ if (x == 0)
507+ {
508+ return y;
509+ }
510+ return x;
511+}
512+
513+int g(size_t u)
514+{
515+ while (u > 34)
516+ {
517+ u--;
518+ }
519+ return u;
520+}
521+EOF
522+
523+ mkdir no-change
524+ (cd no-change
525+ cp ../base.blob .
526+ cp base.blob ours.blob
527+ cp base.blob theirs.blob
528+ )
529+ mkdir no-conflict
530+ (cd no-conflict
531+ cp ../new1.blob ours.blob
532+ cp ../base.blob base.blob
533+ cp ../new2.blob theirs.blob
534+ )
535+ mkdir no-conflict-too
536+ (cd no-conflict-too
537+ cp ../base.blob ours.blob
538+ cp ../base.blob base.blob
539+ cp ../new2.blob theirs.blob
540+ )
541+ mkdir they-changed
542+ (cd they-changed
543+ touch ours.blob base.blob
544+ cp ../new2.blob theirs.blob
545+ )
546+ mkdir missing-LF-at-EOF
547+ (cd missing-LF-at-EOF
548+ cp ../new1.blob ours.blob
549+ cp ../base.blob base.blob
550+ cp ../new4.blob theirs.blob
551+ )
552+ mkdir missing-LF-at-EOF-no-conflict
553+ (cd missing-LF-at-EOF-no-conflict
554+ cp ../new4.blob ours.blob
555+ cp ../new2.blob base.blob
556+ cp ../new3.blob theirs.blob
557+ )
558+ mkdir with-conflicts
559+ (cd with-conflicts
560+ cp ../new1.blob ours.blob
561+ cp ../base.blob base.blob
562+ cp ../new3.blob theirs.blob
563+ )
564+ mkdir with-conflicts-in-removed-tail
565+ (cd with-conflicts-in-removed-tail
566+ cp ../base.blob ours.blob
567+ cp ../new1.blob base.blob
568+ cp ../new5.blob theirs.blob
569+ )
570+ mkdir auto-simplification
571+ (cd auto-simplification
572+ sed -e "s/deerit.\$/deerit;/" -e "s/me;\$/me./" <../new5.blob >ours.blob
573+ cp ../new5.blob base.blob
574+ sed -e "s/deerit.\$/deerit,/" -e "s/me;\$/me,/" <../new5.blob >theirs.blob
575+ )
576+ mkdir auto-simplification2
577+ (cd auto-simplification2
578+ sed -e "s/deerit./&%%%%/" -e "s/locavit,/locavit;/" <../auto-simplification/ours.blob | tr % "\012" >ours.blob
579+ cp ../new5.blob base.blob
580+ sed -e "s/deerit./&%%%%/" -e "s/locavit,/locavit --/" <../auto-simplification/theirs.blob | tr % "\012" >theirs.blob
581+ )
582+ mkdir conflict-without-LF
583+ (cd conflict-without-LF
584+ printf "line1\nline2\nline3" >base.blob
585+ printf "line1\nline2\nline3x" >ours.blob
586+ printf "line1\nline2\nline3y" >theirs.blob
587+ )
588+
589+ mkdir marker-newline-handling-crlf
590+ (cd marker-newline-handling-crlf
591+ printf "1\\r\\n2\\r\\n3" >base.blob
592+ printf "1\\r\\n2\\r\\n4" >ours.blob
593+ printf "1\\r\\n2\\r\\n5" >theirs.blob
594+ )
595+
596+ mkdir marker-newline-handling-lf
597+ (cd marker-newline-handling-lf
598+ printf "1\\r\\n2\\n3" >base.blob
599+ printf "1\\r\\n2\\n4" >ours.blob
600+ printf "1\\r\\n2\\n5" >theirs.blob
601+ )
602+
603+ mkdir marker-newline-handling-lf2
604+ (cd marker-newline-handling-lf2
605+ printf "1\\r\\n2\\r\\n3" >base.blob
606+ printf "1\\r\\n2\\n4" >ours.blob
607+ printf "1\\r\\n2\\n5" >theirs.blob
608+ )
609+
610+ mkdir spurious-c-conflicts
611+ (cd spurious-c-conflicts
612+ cp ../base.c base.blob
613+ cp ../ours.c ours.blob
614+ cp ../theirs.c theirs.blob
615+ )
616+)
617+
618+
619+for dir in simple \
620+ multi-change \
621+ clear-ours \
622+ clear-theirs \
623+ ours-2-lines-theirs-1-line \
624+ partial-match \
625+ unique-merge-base-with-insertion \
626+ zdiff3-basic \
627+ zdiff3-middlecommon \
628+ zdiff3-interesting \
629+ zdiff3-evil \
630+ no-change-add \
631+ no-change-remove \
632+ complex/no-change \
633+ complex/no-conflict \
634+ complex/no-conflict-too \
635+ complex/they-changed \
636+ complex/missing-LF-at-EOF \
637+ complex/missing-LF-at-EOF-no-conflict \
638+ complex/with-conflicts \
639+ complex/with-conflicts-in-removed-tail \
640+ complex/auto-simplification \
641+ complex/auto-simplification2 \
642+ complex/conflict-without-LF \
643+ complex/marker-newline-handling-crlf \
644+ complex/marker-newline-handling-lf \
645+ complex/marker-newline-handling-lf2 \
646+ complex/spurious-c-conflicts; do
647+ DIR=$dir
648+ baseline ours base theirs merge
649+ baseline ours base theirs diff3 --diff3
650+ baseline ours base theirs zdiff3 --zdiff3
651+ baseline ours base theirs merge-ours --ours
652+ baseline ours base theirs merge-theirs --theirs
653+ baseline ours base theirs merge-union --union
654+ (
655+ export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=diff.algorithm GIT_CONFIG_KEY_VALUE=0=histogram
656+ baseline ours base theirs diff3-histogram --diff3
657+ baseline ours base theirs zdiff3-histogram --zdiff3
658+ )
659+done
gix-merge/tests/merge/blob/builtin_driver.rs+214−0
@@ -0,0 +1,214 @@
1+use gix_merge::blob::builtin_driver::binary::{Pick, ResolveWith};
2+use gix_merge::blob::{builtin_driver, Resolution};
3+
4+#[test]
5+fn binary() {
6+ assert_eq!(
7+ builtin_driver::binary(None),
8+ (Pick::Ours, Resolution::Conflict),
9+ "by default it picks ours and marks it as conflict"
10+ );
11+ assert_eq!(
12+ builtin_driver::binary(Some(ResolveWith::Ancestor)),
13+ (Pick::Ancestor, Resolution::Complete),
14+ "Otherwise we can pick anything and it will mark it as complete"
15+ );
16+ assert_eq!(
17+ builtin_driver::binary(Some(ResolveWith::Ours)),
18+ (Pick::Ours, Resolution::Complete)
19+ );
20+ assert_eq!(
21+ builtin_driver::binary(Some(ResolveWith::Theirs)),
22+ (Pick::Theirs, Resolution::Complete)
23+ );
24+}
25+
26+mod text {
27+ use bstr::ByteSlice;
28+ use gix_merge::blob::Resolution;
29+ use pretty_assertions::assert_str_eq;
30+
31+ const DIVERGING: &[&str] = &[
32+ // Somehow, on in zdiff mode, it's different, and I wasn't able to figure out the rule properly.
33+ // Now we prefer ancestor/before newlines and somewhat ignore our hunks. It's probably a minor issue in practice.
34+ // gix: "1\r\n2\n<<<<<<< complex/marker-newline-handling-lf2/ours.blob\n4\r\n||||||| complex/marker-newline-handling-lf2/base.blob\r\n2\r\n3\n=======\n5\n>>>>>>> complex/marker-newline-handling-lf2/theirs.blob\n"
35+ // git: "1\r\n2\n<<<<<<< complex/marker-newline-handling-lf2/ours.blob\n4 \n||||||| complex/marker-newline-handling-lf2/base.blob \n2\r\n3\n=======\n5\n>>>>>>> complex/marker-newline-handling-lf2/theirs.blob\n"
36+ "complex/marker-newline-handling-lf2/zdiff3.merged",
37+ "complex/marker-newline-handling-lf2/zdiff3-histogram.merged",
38+ // This is related to Git seemingly extending a hunk to increase overlap (see diff3)
39+ "zdiff3-interesting/merge.merged",
40+ "zdiff3-interesting/merge-ours.merged",
41+ "zdiff3-interesting/diff3.merged",
42+ "zdiff3-interesting/diff3-histogram.merged",
43+ "zdiff3-interesting/zdiff3.merged",
44+ "zdiff3-interesting/zdiff3-histogram.merged",
45+ "zdiff3-interesting/merge-union.merged",
46+ // Git can extend hunks, similar to above, but the effect is not as noticeable.
47+ // Implementing this would be interesting, to figure out when the hunk processing should apply.
48+ "zdiff3-evil/merge.merged",
49+ "zdiff3-evil/merge-union.merged",
50+ // Git seems to merge to hunks if they are close together to get a less noisy diff.
51+ "zdiff3-middlecommon/merge.merged",
52+ "zdiff3-middlecommon/merge-union.merged",
53+ // Git has special character handling, which does magic to prevent conflicts
54+ "complex/auto-simplification/merge.merged",
55+ "complex/auto-simplification/merge-union.merged",
56+ // Git has special newline handling when diffing,
57+ // which auto-inserts a newline when it was removed, kind of.
58+ "complex/missing-LF-at-EOF/merge.merged",
59+ "complex/missing-LF-at-EOF/diff3.merged",
60+ "complex/missing-LF-at-EOF/diff3-histogram.merged",
61+ "complex/missing-LF-at-EOF/zdiff3.merged",
62+ "complex/missing-LF-at-EOF/zdiff3-histogram.merged",
63+ "complex/missing-LF-at-EOF/merge-ours.merged",
64+ "complex/missing-LF-at-EOF/merge-theirs.merged",
65+ "complex/missing-LF-at-EOF/merge-union.merged",
66+ // Git has different diff-slider-heuristics so diffs can be different.
67+ // See https://github.com/mhagger/diff-slider-tools.
68+ "complex/spurious-c-conflicts/merge.merged",
69+ "complex/spurious-c-conflicts/merge-union.merged",
70+ "complex/spurious-c-conflicts/diff3-histogram.merged",
71+ "complex/spurious-c-conflicts/zdiff3-histogram.merged",
72+ ];
73+
74+ // TODO: fix all of these eventually
75+ fn is_case_diverging(case: &baseline::Expectation) -> bool {
76+ DIVERGING.iter().any(|name| case.name == *name)
77+ }
78+
79+ #[test]
80+ fn run_baseline() -> crate::Result {
81+ 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.ours,
93+ Some(case.ours_marker.as_str().as_ref()),
94+ &case.base,
95+ Some(case.base_marker.as_str().as_ref()),
96+ &case.theirs,
97+ Some(case.theirs_marker.as_str().as_ref()),
98+ case.options,
99+ );
100+ if is_case_diverging(&case) {
101+ num_diverging += 1;
102+ } else {
103+ let expected_resolution = if case.expected.contains_str("<<<<<<<") {
104+ Resolution::Conflict
105+ } else {
106+ Resolution::Complete
107+ };
108+ assert_eq!(out.as_bstr(), case.expected);
109+ assert_str_eq!(
110+ out.as_bstr().to_str_lossy(),
111+ case.expected.to_str_lossy(),
112+ "{}: output mismatch\n{}",
113+ case.name,
114+ out.as_bstr()
115+ );
116+ assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name,);
117+ }
118+ }
119+
120+ assert_eq!(
121+ num_diverging,
122+ DIVERGING.len(),
123+ "Number of expected diverging cases must match the actual one - probably the implementation improved"
124+ );
125+ assert_eq!(
126+ (num_diverging as f32 / num_cases as f32) * 100.0,
127+ 12.053572,
128+ "Just to show the percentage of skipped tests - this should get better"
129+ );
130+ Ok(())
131+ }
132+
133+ mod baseline {
134+ use bstr::BString;
135+ use gix_merge::blob::builtin_driver::text::{ConflictStyle, ResolveWith};
136+ use std::path::Path;
137+
138+ #[derive(Debug)]
139+ pub struct Expectation {
140+ pub ours: BString,
141+ pub ours_marker: String,
142+ pub theirs: BString,
143+ pub theirs_marker: String,
144+ pub base: BString,
145+ pub base_marker: String,
146+ pub name: BString,
147+ pub expected: BString,
148+ pub options: gix_merge::blob::builtin_driver::text::Options,
149+ }
150+
151+ pub struct Expectations<'a> {
152+ root: &'a Path,
153+ lines: std::str::Lines<'a>,
154+ }
155+
156+ impl<'a> Expectations<'a> {
157+ pub fn new(root: &'a Path, cases: &'a str) -> Self {
158+ Expectations {
159+ root,
160+ lines: cases.lines(),
161+ }
162+ }
163+ }
164+
165+ impl Iterator for Expectations<'_> {
166+ type Item = Expectation;
167+
168+ fn next(&mut self) -> Option<Self::Item> {
169+ let line = self.lines.next()?;
170+ let mut words = line.split(' ');
171+ let (Some(ours), Some(base), Some(theirs), Some(output)) =
172+ (words.next(), words.next(), words.next(), words.next())
173+ else {
174+ panic!("need at least the input and output")
175+ };
176+
177+ let read = |rela_path: &str| read_blob(self.root, rela_path);
178+
179+ let mut options = gix_merge::blob::builtin_driver::text::Options::default();
180+ for arg in words {
181+ match arg {
182+ "--diff3" => options.conflict_style = ConflictStyle::Diff3,
183+ "--zdiff3" => options.conflict_style = ConflictStyle::ZealousDiff3,
184+ "--ours" => options.on_conflict = Some(ResolveWith::Ours),
185+ "--theirs" => options.on_conflict = Some(ResolveWith::Theirs),
186+ "--union" => options.on_conflict = Some(ResolveWith::Union),
187+ _ => panic!("Unknown argument to parse into options: '{arg}'"),
188+ }
189+ }
190+ if output.contains("histogram") {
191+ options.diff_algorithm = imara_diff::Algorithm::Histogram;
192+ }
193+
194+ Some(Expectation {
195+ ours: read(ours),
196+ ours_marker: ours.into(),
197+ theirs: read(theirs),
198+ theirs_marker: theirs.into(),
199+ base: read(base),
200+ base_marker: base.into(),
201+ expected: read(output),
202+ name: output.into(),
203+ options,
204+ })
205+ }
206+ }
207+
208+ fn read_blob(root: &Path, rela_path: &str) -> BString {
209+ std::fs::read(root.join(rela_path))
210+ .unwrap_or_else(|_| panic!("Failed to read '{rela_path}' in '{}'", root.display()))
211+ .into()
212+ }
213+ }
214+}
gix-merge/tests/merge/blob/mod.rs+1−0
@@ -0,0 +1 @@
1+mod builtin_driver;
gix-merge/tests/merge/main.rs+4−0
@@ -0,0 +1,4 @@
1+#[cfg(feature = "blob")]
2+mod blob;
3+
4+pub use gix_testtools::Result;