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
2053 "gix-object 0.44.0",2053 "gix-object 0.44.0",
2054 "gix-path 0.10.11",2054 "gix-path 0.10.11",
2055 "gix-tempfile 14.0.2",2055 "gix-tempfile 14.0.2",
2056 "gix-testtools",
2056 "gix-trace 0.1.10",2057 "gix-trace 0.1.10",
2057 "gix-worktree 0.36.0",2058 "gix-worktree 0.36.0",
2058 "imara-diff",2059 "imara-diff",
2060 "pretty_assertions",
2059 "serde",2061 "serde",
2060 "thiserror",2062 "thiserror",
2061]2063]
crate-status.md+10−6
@@ -324,19 +324,23 @@ Check out the [performance discussion][gix-diff-performance] as well. · 323 unmodified lines
324 * [x] prepare invocation of external diff program324 * [x] prepare invocation of external diff program
325 - [ ] pass meta-info325 - [ ] pass meta-info
326* [ ] working with hunks of data326* [ ] working with hunks of data
327* [ ] diff-heuristics match Git perfectly
327* [x] API documentation328* [x] API documentation
328 * [ ] Examples329 * [ ] Examples
329 330
331[gix-diff-performance]: https://github.com/Byron/gitoxide/discussions/74
332
330### gix-merge333### gix-merge
331 334
332* [ ] three-way merge analysis of blobs with choice of how to resolve conflicts335* [x] three-way merge analysis of blobs with choice of how to resolve conflicts
333 - [ ] choose how to resolve conflicts on the data-structure336 - [ ] choose how to resolve conflicts on the data-structure
334 - [ ] produce a new blob based on data-structure containing possible resolutions337 - [ ] produce a new blob based on data-structure containing possible resolutions
335 - [ ] `merge` style338 - [x] `merge` style
336 - [ ] `diff3` style339 - [x] `diff3` style
337 - [ ] `zdiff` style340 - [x] `zdiff` style
338 341* [ ] diff-heuristics match Git perfectly
339[gix-diff-performance]: https://github.com/Byron/gitoxide/discussions/74342* [x] API documentation
343 * [ ] Examples
340 344
341### gix-traverse345### gix-traverse
342 346
gix-merge/Cargo.toml+4−0
@@ -39,6 +39,10 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur · 38 unmodified lines
39 39
40document-features = { version = "0.2.0", optional = true }40document-features = { version = "0.2.0", optional = true }
41 41
42[dev-dependencies]
43gix-testtools = { path = "../tests/tools" }
44pretty_assertions = "1.4.0"
45
42[package.metadata.docs.rs]46[package.metadata.docs.rs]
43all-features = true47all-features = true
44features = ["document-features"]48features = ["document-features"]
gix-merge/src/blob/builtin_driver.rs+0−156
@@ -1,156 +0,0 @@
1use crate::blob::BuiltinDriver;
2
3impl 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///
25pub 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///
64pub 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)]
3pub 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)]
14pub enum Pick {
15 /// Chose the ancestor.
16 Ancestor,
17 /// Chose our side.
18 Ours,
19 /// Chose their side.
20 Theirs,
21}
22
23pub(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 @@
1use crate::blob::BuiltinDriver;
2
3impl 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///
25pub mod binary;
26pub use binary::function::merge as binary;
27
28///
29pub mod text;
30pub use text::function::merge as text;
gix-merge/src/blob/builtin_driver/text/function.rs+248−0
@@ -0,0 +1,248 @@
1use 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};
6use crate::blob::builtin_driver::text::{ConflictStyle, Options, ResolveWith};
7use crate::blob::Resolution;
8use 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)]
23pub 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)]
4pub 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)]
53pub 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
66impl 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)]
79pub 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
88pub(super) mod function;
89mod utils;
gix-merge/src/blob/builtin_driver/text/utils.rs+474−0
@@ -0,0 +1,474 @@
1use crate::blob::builtin_driver::text::ConflictStyle;
2use bstr::{BStr, ByteSlice, ByteVec};
3use std::iter::Peekable;
4use 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.
8pub 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
27pub 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.
35pub 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
70pub 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
78fn 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
90pub 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
96pub 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
106pub 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.
117pub 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
151fn 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]
173pub 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
270fn 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}
276fn 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
324fn 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`.
363fn 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`.
375fn 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
387pub 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
403fn usize_range(range: &Range<u32>) -> Range<usize> {
404 range.start as usize..range.end as usize
405}
406
407fn 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.
419pub 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
435pub 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)]
440pub 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)]
449pub struct Hunk {
450 pub before: Range<u32>,
451 pub after: Range<u32>,
452 pub side: Side,
453}
454
455pub struct CollectHunks {
456 pub hunks: Vec<Hunk>,
457 pub side: Side,
458}
459
460impl 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
11///11///
12pub mod platform;12pub mod platform;
13 13
14/// Identify a merge resolution.14/// Define if a merge is conflicted or not.
15#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]15#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
16pub enum Resolution {16pub enum Resolution {
17 /// Everything could be resolved during the merge.17 /// Everything could be resolved during the merge.
18 ///
19 /// Conflicts may have been resolved automatically, depending on the options.
18 Complete,20 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.
20 Conflict,24 Conflict,
21}25}
22 26
23/// A way to classify a resource suitable for merging.27/// A way to classify the side of a resource for merging.
24#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]28#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
25pub enum ResourceKind {29pub enum ResourceKind {
26 /// Our side of the state.30 /// Our side of the state.
@@ -31,7 +35,7 @@ pub enum ResourceKind { · 4 unmodified lines
31 CommonAncestorOrBase,35 CommonAncestorOrBase,
32}36}
33 37
34/// Define a driver program that merges38/// Define a built-in way of performing a three-way merge, including auto-resolution support.
35///39///
36/// Some values are related to diffing, some are related to conversions.40/// Some values are related to diffing, some are related to conversions.
37#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]41#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
@@ -59,7 +63,7 @@ pub enum BuiltinDriver { · 21 unmodified lines
59 Union,63 Union,
60}64}
61 65
62/// Define a driver program that merges66/// Define a driver program that performs a three-way merge.
63///67///
64/// Some values are related to diffing, some are related to conversions.68/// Some values are related to diffing, some are related to conversions.
65#[derive(Default, Debug, Clone, PartialEq, Eq)]69#[derive(Default, Debug, Clone, PartialEq, Eq)]
gix-merge/src/blob/platform.rs+1−1
@@ -137,7 +137,7 @@ pub mod merge { · 136 unmodified lines
137 pub other: ResourceRef<'parent>,137 pub other: ResourceRef<'parent>,
138 }138 }
139 139
140 #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]140 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
141 pub struct Options {141 pub struct Options {
142 /// If `true`, the resources being merged are contained in a virtual ancestor,142 /// If `true`, the resources being merged are contained in a virtual ancestor,
143 /// which is the case when merge bases are merged into one.143 /// 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
2set -eu -o pipefail
3
4git init
5rm -Rf .git/hooks
6
7function 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
19mkdir 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
27mkdir multi-change
28(cd multi-change
29 cat <<EOF > base.blob
300
311
322
333
344
355
366
377
388
399
40EOF
41
42 cat <<EOF > ours.blob
430
441
45X
46X
474
485
49Y
50Y
518
52Z
53EOF
54
55 cat <<EOF > theirs.blob
56T
57T
58T
59T
60T
61T
62T
63T
64T
65T
66EOF
67)
68
69# a change with deletion/clearing our file
70mkdir clear-ours
71(cd clear-ours
72 cat <<EOF > base.blob
730
741
752
763
774
785
79EOF
80
81 touch ours.blob
82
83 cat <<EOF > theirs.blob
84T
85T
86T
87T
88T
89EOF
90)
91
92# a change with deletion/clearing their file
93mkdir clear-theirs
94(cd clear-theirs
95 cat <<EOF > base.blob
960
971
982
993
1004
1015
102EOF
103
104 cat <<EOF > ours.blob
105O
106O
107O
108O
109O
110EOF
111
112 touch theirs.blob
113)
114
115# differently sized changes
116mkdir ours-2-lines-theirs-1-line
117(cd ours-2-lines-theirs-1-line
118 cat <<EOF > base.blob
1190
1201
1212
1223
1234
1245
125EOF
126
127 cat <<EOF > ours.blob
1280
1291
130X
131X
1324
1335
134EOF
135
136 cat <<EOF > theirs.blob
1370
1381
139Y
1403
1414
1425
143EOF
144)
145
146# partial match
147mkdir partial-match
148(cd partial-match
149 cat <<EOF > base.blob
1500
1511
1522
1533
1544
1555
156EOF
157
158 cat <<EOF > ours.blob
1590
160X1
161X2
162X3
163X4
1645
165EOF
166
167 cat <<EOF > theirs.blob
1680
169X1
1702
171X3
172X4
1735
174EOF
175)
176
177# based on 'unique merge base' from 'diff3-conflict-markers'
178mkdir unique-merge-base-with-insertion
179(cd unique-merge-base-with-insertion
180 cat <<EOF > base.blob
1811
1822
1833
1844
1855
186EOF
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
193mkdir zdiff3-basic
194(cd zdiff3-basic
195 cat <<EOF > base.blob
1961
1972
1983
1994
2005
2016
2027
2038
2049
205EOF
206
207 cat <<EOF > ours.blob
2081
2092
2103
2114
212A
213B
214C
215D
216E
2177
2188
2199
220EOF
221
222 cat <<EOF > theirs.blob
2231
2242
2253
2264
227A
228X
229C
230Y
231E
2327
2338
2349
235EOF
236)
237
238mkdir zdiff3-middlecommon
239(cd zdiff3-middlecommon
240 cat <<EOF > base.blob
2411
2422
2433
244AA
2454
2465
247BB
2486
2497
2508
251EOF
252
253 cat <<EOF > ours.blob
2541
2552
2563
257CC
2584
2595
260DD
2616
2627
2638
264EOF
265
266 cat <<EOF > theirs.blob
2671
2682
2693
270EE
2714
2725
273FF
2746
2757
2768
277EOF
278)
279
280
281mkdir zdiff3-interesting
282(cd zdiff3-interesting
283 cat <<EOF > base.blob
2841
2852
2863
2874
2885
2896
2907
2918
2929
293EOF
294
295 cat <<EOF > ours.blob
2961
2972
2983
2994
300A
301B
302C
303D
304E
305F
306G
307H
308I
309J
3107
3118
3129
313EOF
314
315 cat <<EOF > theirs.blob
3161
3172
3183
3194
320A
321B
322C
3235
3246
325G
326H
327I
328J
3297
3308
3319
332EOF
333)
334
335mkdir zdiff3-evil
336(cd zdiff3-evil
337 cat <<EOF > base.blob
3381
3392
3403
3414
3425
3436
3447
3458
3469
347EOF
348
349 cat <<EOF > ours.blob
3501
3512
3523
3534
354X
355A
356B
357C
3587
3598
3609
361EOF
362
363 cat <<EOF > theirs.blob
3641
3652
3663
3674
368Y
369A
370B
371C
372B
373C
3747
3758
3769
377EOF
378)
379
380mkdir 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
388mkdir 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
396mkdir complex
397(cd complex
398 cat <<EOF >base.blob
399Dominus regit me,
400et nihil mihi deerit.
401In loco pascuae ibi me collocavit,
402super aquam refectionis educavit me;
403animam meam convertit,
404deduxit me super semitas jusitiae,
405propter nomen suum.
406EOF
407
408 cat <<EOF >new1.blob
409Dominus regit me,
410et nihil mihi deerit.
411In loco pascuae ibi me collocavit,
412super aquam refectionis educavit me;
413animam meam convertit,
414deduxit me super semitas jusitiae,
415propter nomen suum.
416Nam et si ambulavero in medio umbrae mortis,
417non timebo mala, quoniam tu mecum es:
418virga tua et baculus tuus ipsa me consolata sunt.
419EOF
420
421 cat <<EOF >new2.blob
422Dominus regit me, et nihil mihi deerit.
423In loco pascuae ibi me collocavit,
424super aquam refectionis educavit me;
425animam meam convertit,
426deduxit me super semitas jusitiae,
427propter nomen suum.
428EOF
429
430 cat <<EOF >new3.blob
431DOMINUS regit me,
432et nihil mihi deerit.
433In loco pascuae ibi me collocavit,
434super aquam refectionis educavit me;
435animam meam convertit,
436deduxit me super semitas jusitiae,
437propter nomen suum.
438EOF
439
440 cat <<EOF >new4.blob
441Dominus regit me, et nihil mihi deerit.
442In loco pascuae ibi me collocavit,
443super aquam refectionis educavit me;
444animam meam convertit,
445deduxit me super semitas jusitiae,
446EOF
447
448 cat <<EOF >new5.blob
449Dominus regit me,
450et nihil mihi deerit.
451In loco pascuae ibi me collocavit,
452super aquam refectionis educavit me;
453animam meam convertit,
454deduxit me super semitas jusitiae,
455propter nomen suum.
456Nam et si ambulavero in medio umbrae mortis,
457non timebo mala, quoniam TU mecum es:
458virga tua et baculus tuus ipsa me consolata sunt.
459EOF
460
461 echo -n "propter nomen suum." >>new4.blob
462
463 cat <<EOF >base.c
464int f(int x, int y)
465{
466 if (x == 0)
467 {
468 return y;
469 }
470 return x;
471}
472
473int g(size_t u)
474{
475 while (u < 30)
476 {
477 u++;
478 }
479 return u;
480}
481EOF
482
483 cat <<EOF >ours.c
484int g(size_t u)
485{
486 while (u < 30)
487 {
488 u++;
489 }
490 return u;
491}
492
493int h(int x, int y, int z)
494{
495 if (z == 0)
496 {
497 return x;
498 }
499 return y;
500}
501EOF
502
503 cat <<EOF >theirs.c
504int f(int x, int y)
505{
506 if (x == 0)
507 {
508 return y;
509 }
510 return x;
511}
512
513int g(size_t u)
514{
515 while (u > 34)
516 {
517 u--;
518 }
519 return u;
520}
521EOF
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
619for 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 )
659done
gix-merge/tests/merge/blob/builtin_driver.rs+214−0
@@ -0,0 +1,214 @@
1use gix_merge::blob::builtin_driver::binary::{Pick, ResolveWith};
2use gix_merge::blob::{builtin_driver, Resolution};
3
4#[test]
5fn 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
26mod 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 @@
1mod builtin_driver;
gix-merge/tests/merge/main.rs+4−0
@@ -0,0 +1,4 @@
1#[cfg(feature = "blob")]
2mod blob;
3
4pub use gix_testtools::Result;