Skip to content

Commit 9f30cc2

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

File tree

9 files changed

+1043
-22
lines changed

9 files changed

+1043
-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: 693 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+
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+
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: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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::{ConflictStyle, 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+
for case in baseline::Expectations::new(&root, &cases)
38+
// TODO: remove filter
39+
.filter(|case| {
40+
matches!(
41+
case.options.on_conflict,
42+
Some(ResolveWith::Union | ResolveWith::Ours | ResolveWith::Theirs)
43+
) || case.options.on_conflict.is_none() && matches!(case.options.conflict_style, ConflictStyle::Merge)
44+
})
45+
{
46+
let mut input = imara_diff::intern::InternedInput::default();
47+
dbg!(&case.name, case.options);
48+
let actual = gix_merge::blob::builtin_driver::text(
49+
&mut out,
50+
&mut input,
51+
&case.ours,
52+
Some(case.ours_marker.as_str().as_ref()),
53+
&case.base,
54+
Some(case.base_marker.as_str().as_ref()),
55+
&case.theirs,
56+
Some(case.theirs_marker.as_str().as_ref()),
57+
case.options,
58+
);
59+
let expected_resolution = if case.expected.contains_str("<<<<<<<") {
60+
Resolution::Conflict
61+
} else {
62+
Resolution::Complete
63+
};
64+
assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name);
65+
assert_str_eq!(
66+
out.as_bstr().to_str_lossy(),
67+
case.expected.to_str_lossy(),
68+
"{}: output mismatch",
69+
case.name
70+
);
71+
}
72+
Ok(())
73+
}
74+
75+
mod baseline {
76+
use bstr::BString;
77+
use gix_merge::blob::builtin_driver::text::{ConflictStyle, ResolveWith};
78+
use std::path::Path;
79+
80+
#[derive(Debug)]
81+
pub struct Expectation {
82+
pub ours: BString,
83+
pub ours_marker: String,
84+
pub theirs: BString,
85+
pub theirs_marker: String,
86+
pub base: BString,
87+
pub base_marker: String,
88+
pub name: BString,
89+
pub expected: BString,
90+
pub options: gix_merge::blob::builtin_driver::text::Options,
91+
}
92+
93+
pub struct Expectations<'a> {
94+
root: &'a Path,
95+
lines: std::str::Lines<'a>,
96+
}
97+
98+
impl<'a> Expectations<'a> {
99+
pub fn new(root: &'a Path, cases: &'a str) -> Self {
100+
Expectations {
101+
root,
102+
lines: cases.lines(),
103+
}
104+
}
105+
}
106+
107+
impl Iterator for Expectations<'_> {
108+
type Item = Expectation;
109+
110+
fn next(&mut self) -> Option<Self::Item> {
111+
let line = self.lines.next()?;
112+
let mut words = line.split(' ');
113+
let (Some(ours), Some(base), Some(theirs), Some(output)) =
114+
(words.next(), words.next(), words.next(), words.next())
115+
else {
116+
panic!("need at least the input and output")
117+
};
118+
119+
let read = |rela_path: &str| read_blob(self.root, rela_path);
120+
121+
let mut options = gix_merge::blob::builtin_driver::text::Options::default();
122+
for arg in words {
123+
match arg {
124+
"--diff3" => options.conflict_style = ConflictStyle::Diff3,
125+
"--zdiff3" => options.conflict_style = ConflictStyle::ZealousDiff3,
126+
"--ours" => options.on_conflict = Some(ResolveWith::Ours),
127+
"--theirs" => options.on_conflict = Some(ResolveWith::Theirs),
128+
"--union" => options.on_conflict = Some(ResolveWith::Union),
129+
_ => panic!("Unknown argument to parse into options: '{arg}'"),
130+
}
131+
}
132+
133+
Some(Expectation {
134+
ours: read(ours),
135+
ours_marker: ours.into(),
136+
theirs: read(theirs),
137+
theirs_marker: theirs.into(),
138+
base: read(base),
139+
base_marker: base.into(),
140+
expected: read(output),
141+
name: output.into(),
142+
options,
143+
})
144+
}
145+
}
146+
147+
fn read_blob(root: &Path, rela_path: &str) -> BString {
148+
std::fs::read(root.join(rela_path))
149+
.unwrap_or_else(|_| panic!("Failed to read '{rela_path}' in '{}'", root.display()))
150+
.into()
151+
}
152+
}
153+
}

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)