Skip to content

Commit a6678f9

Browse files
committed
Implement text and binary merge algorithms, also with baseline tests for correctness.
1 parent b96d11f commit a6678f9

File tree

9 files changed

+1012
-22
lines changed

9 files changed

+1012
-22
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-merge/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur
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

+648-21
Large diffs are not rendered by default.

gix-merge/src/blob/platform.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ pub mod merge {
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.
Binary file not shown.
+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
for dir in simple \
194+
multi-change \
195+
clear-ours \
196+
clear-theirs \
197+
ours-2-lines-theirs-1-line \
198+
partial-match \
199+
unique-merge-base-with-insertion; do
200+
DIR=$dir
201+
baseline ours base theirs merge
202+
baseline ours base theirs diff3 --diff3
203+
baseline ours base theirs zdiff3 --zdiff3
204+
baseline ours base theirs merge-ours --ours
205+
baseline ours base theirs merge-theirs --theirs
206+
baseline ours base theirs merge-union --union
207+
done
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
#[test]
32+
fn run_baseline() -> crate::Result {
33+
let root = gix_testtools::scripted_fixture_read_only("text-baseline.sh")?;
34+
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
35+
let mut out = Vec::new();
36+
for case in baseline::Expectations::new(&root, &cases) {
37+
let mut input = imara_diff::intern::InternedInput::default();
38+
dbg!(&case.name, case.options);
39+
let actual = gix_merge::blob::builtin_driver::text(
40+
&mut out,
41+
&mut input,
42+
&case.ours,
43+
Some(case.ours_marker.as_str().as_ref()),
44+
&case.base,
45+
Some(case.base_marker.as_str().as_ref()),
46+
&case.theirs,
47+
Some(case.theirs_marker.as_str().as_ref()),
48+
case.options,
49+
);
50+
let expected_resolution = if case.expected.contains_str("<<<<<<<") {
51+
Resolution::Conflict
52+
} else {
53+
Resolution::Complete
54+
};
55+
assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name,);
56+
assert_str_eq!(
57+
out.as_bstr().to_str_lossy(),
58+
case.expected.to_str_lossy(),
59+
"{}: output mismatch\n{}",
60+
case.name,
61+
out.as_bstr()
62+
);
63+
}
64+
Ok(())
65+
}
66+
67+
mod baseline {
68+
use bstr::BString;
69+
use gix_merge::blob::builtin_driver::text::{ConflictStyle, ResolveWith};
70+
use std::path::Path;
71+
72+
#[derive(Debug)]
73+
pub struct Expectation {
74+
pub ours: BString,
75+
pub ours_marker: String,
76+
pub theirs: BString,
77+
pub theirs_marker: String,
78+
pub base: BString,
79+
pub base_marker: String,
80+
pub name: BString,
81+
pub expected: BString,
82+
pub options: gix_merge::blob::builtin_driver::text::Options,
83+
}
84+
85+
pub struct Expectations<'a> {
86+
root: &'a Path,
87+
lines: std::str::Lines<'a>,
88+
}
89+
90+
impl<'a> Expectations<'a> {
91+
pub fn new(root: &'a Path, cases: &'a str) -> Self {
92+
Expectations {
93+
root,
94+
lines: cases.lines(),
95+
}
96+
}
97+
}
98+
99+
impl Iterator for Expectations<'_> {
100+
type Item = Expectation;
101+
102+
fn next(&mut self) -> Option<Self::Item> {
103+
let line = self.lines.next()?;
104+
let mut words = line.split(' ');
105+
let (Some(ours), Some(base), Some(theirs), Some(output)) =
106+
(words.next(), words.next(), words.next(), words.next())
107+
else {
108+
panic!("need at least the input and output")
109+
};
110+
111+
let read = |rela_path: &str| read_blob(self.root, rela_path);
112+
113+
let mut options = gix_merge::blob::builtin_driver::text::Options::default();
114+
for arg in words {
115+
match arg {
116+
"--diff3" => options.conflict_style = ConflictStyle::Diff3,
117+
"--zdiff3" => options.conflict_style = ConflictStyle::ZealousDiff3,
118+
"--ours" => options.on_conflict = Some(ResolveWith::Ours),
119+
"--theirs" => options.on_conflict = Some(ResolveWith::Theirs),
120+
"--union" => options.on_conflict = Some(ResolveWith::Union),
121+
_ => panic!("Unknown argument to parse into options: '{arg}'"),
122+
}
123+
}
124+
125+
Some(Expectation {
126+
ours: read(ours),
127+
ours_marker: ours.into(),
128+
theirs: read(theirs),
129+
theirs_marker: theirs.into(),
130+
base: read(base),
131+
base_marker: base.into(),
132+
expected: read(output),
133+
name: output.into(),
134+
options,
135+
})
136+
}
137+
}
138+
139+
fn read_blob(root: &Path, rela_path: &str) -> BString {
140+
std::fs::read(root.join(rela_path))
141+
.unwrap_or_else(|_| panic!("Failed to read '{rela_path}' in '{}'", root.display()))
142+
.into()
143+
}
144+
}
145+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod builtin_driver;

gix-merge/tests/merge/main.rs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#[cfg(feature = "blob")]
2+
mod blob;
3+
4+
pub use gix_testtools::Result;

0 commit comments

Comments
 (0)