Skip to content

Commit 99874b2

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

File tree

9 files changed

+841
-22
lines changed

9 files changed

+841
-22
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-merge/Cargo.toml

Lines changed: 4 additions & 0 deletions
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

Lines changed: 498 additions & 21 deletions
Large diffs are not rendered by default.

gix-merge/src/blob/platform.rs

Lines changed: 1 addition & 1 deletion
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.
71.5 KB
Binary file not shown.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
X
161+
X
162+
X
163+
X
164+
5
165+
EOF
166+
167+
cat <<EOF > theirs.blob
168+
0
169+
X
170+
2
171+
X
172+
X
173+
5
174+
EOF
175+
)
176+
177+
for dir in simple multi-change clear-ours clear-theirs ours-2-lines-theirs-1-line partial-match; do
178+
DIR=$dir
179+
baseline ours base theirs merge
180+
baseline ours base theirs diff3 --diff3
181+
baseline ours base theirs zdiff3 --zdiff3
182+
baseline ours base theirs merge-ours --ours
183+
baseline ours base theirs merge-theirs --theirs
184+
baseline ours base theirs merge-union --union
185+
done
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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::builtin_driver::text::ResolveWith;
29+
use gix_merge::blob::Resolution;
30+
use pretty_assertions::assert_str_eq;
31+
32+
#[test]
33+
fn run_baseline() -> crate::Result {
34+
let root = gix_testtools::scripted_fixture_read_only("text-baseline.sh")?;
35+
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
36+
let mut out = Vec::new();
37+
let mut tokens = Vec::new();
38+
for case in baseline::Expectations::new(&root, &cases)
39+
// TODO: remove filter
40+
.filter(|case| {
41+
matches!(
42+
case.options.on_conflict,
43+
Some(ResolveWith::Union | ResolveWith::Ours | ResolveWith::Theirs)
44+
)
45+
})
46+
{
47+
let mut input = imara_diff::intern::InternedInput::default();
48+
dbg!(&case.name, case.options);
49+
let actual = gix_merge::blob::builtin_driver::text(
50+
&mut out,
51+
&mut input,
52+
&mut tokens,
53+
&case.ours,
54+
&case.base,
55+
&case.theirs,
56+
case.options,
57+
);
58+
let expected_resolution = if case.expected.contains_str("<<<<<<<") {
59+
Resolution::Conflict
60+
} else {
61+
Resolution::Complete
62+
};
63+
assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name);
64+
assert_str_eq!(
65+
out.as_bstr().to_str_lossy(),
66+
case.expected.to_str_lossy(),
67+
"{}: output mismatch",
68+
case.name
69+
);
70+
}
71+
Ok(())
72+
}
73+
74+
mod baseline {
75+
use bstr::BString;
76+
use gix_merge::blob::builtin_driver::text::{ConflictStyle, ResolveWith};
77+
use std::path::Path;
78+
79+
#[derive(Debug)]
80+
pub struct Expectation {
81+
pub ours: BString,
82+
pub theirs: BString,
83+
pub base: BString,
84+
pub name: BString,
85+
pub expected: BString,
86+
pub options: gix_merge::blob::builtin_driver::text::Options,
87+
}
88+
89+
pub struct Expectations<'a> {
90+
root: &'a Path,
91+
lines: std::str::Lines<'a>,
92+
}
93+
94+
impl<'a> Expectations<'a> {
95+
pub fn new(root: &'a Path, cases: &'a str) -> Self {
96+
Expectations {
97+
root,
98+
lines: cases.lines(),
99+
}
100+
}
101+
}
102+
103+
impl Iterator for Expectations<'_> {
104+
type Item = Expectation;
105+
106+
fn next(&mut self) -> Option<Self::Item> {
107+
let line = self.lines.next()?;
108+
let mut words = line.split(' ');
109+
let (Some(ours), Some(base), Some(theirs), Some(output)) =
110+
(words.next(), words.next(), words.next(), words.next())
111+
else {
112+
panic!("need at least the input and output")
113+
};
114+
115+
let read = |rela_path: &str| read_blob(&self.root, rela_path);
116+
117+
let mut options = gix_merge::blob::builtin_driver::text::Options::default();
118+
for arg in words {
119+
match arg {
120+
"--diff3" => options.conflict_style = ConflictStyle::Diff3,
121+
"--zdiff3" => options.conflict_style = ConflictStyle::ZealousDiff3,
122+
"--ours" => options.on_conflict = Some(ResolveWith::Ours),
123+
"--theirs" => options.on_conflict = Some(ResolveWith::Theirs),
124+
"--union" => options.on_conflict = Some(ResolveWith::Union),
125+
_ => panic!("Unknown argument to parse into options: '{arg}'"),
126+
}
127+
}
128+
129+
Some(Expectation {
130+
ours: read(ours),
131+
theirs: read(theirs),
132+
base: read(base),
133+
expected: read(output),
134+
name: output.into(),
135+
options,
136+
})
137+
}
138+
}
139+
140+
fn read_blob(root: &Path, rela_path: &str) -> BString {
141+
std::fs::read(root.join(rela_path))
142+
.unwrap_or_else(|_| panic!("Failed to read '{rela_path}' in '{}'", root.display()))
143+
.into()
144+
}
145+
}
146+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod builtin_driver;

gix-merge/tests/merge/main.rs

Lines changed: 4 additions & 0 deletions
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)