Skip to content

Commit

Permalink
Implement text and binary merge algorithms, also with baseline te…
Browse files Browse the repository at this point in the history
…sts for correctness.
  • Loading branch information
Byron committed Sep 17, 2024
1 parent b96d11f commit 99874b2
Show file tree
Hide file tree
Showing 9 changed files with 841 additions and 22 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions gix-merge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur

document-features = { version = "0.2.0", optional = true }

[dev-dependencies]
gix-testtools = { path = "../tests/tools" }
pretty_assertions = "1.4.0"

[package.metadata.docs.rs]
all-features = true
features = ["document-features"]
519 changes: 498 additions & 21 deletions gix-merge/src/blob/builtin_driver.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gix-merge/src/blob/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pub mod merge {
pub other: ResourceRef<'parent>,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Options {
/// If `true`, the resources being merged are contained in a virtual ancestor,
/// which is the case when merge bases are merged into one.
Expand Down
Binary file not shown.
185 changes: 185 additions & 0 deletions gix-merge/tests/fixtures/text-baseline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
set -eu -o pipefail

git init
rm -Rf .git/hooks

function baseline() {
local ours=$DIR/${1:?1: our file}.blob;
local base=$DIR/${2:?2: base file}.blob;
local theirs=$DIR/${3:?3: their file}.blob;
local output=$DIR/${4:?4: the name of the output file}.merged;

shift 4
git merge-file --stdout "$@" "$ours" "$base" "$theirs" > "$output" || true

echo "$ours" "$base" "$theirs" "$output" "$@" >> baseline.cases
}

mkdir simple
(cd simple
echo -e "line1-changed-by-both\nline2-to-be-changed-in-incoming" > ours.blob
echo -e "line1-to-be-changed-by-both\nline2-to-be-changed-in-incoming" > base.blob
echo -e "line1-changed-by-both\nline2-changed" > theirs.blob
)

# one big change includes multiple smaller ones
mkdir multi-change
(cd multi-change
cat <<EOF > base.blob
0
1
2
3
4
5
6
7
8
9
EOF

cat <<EOF > ours.blob
0
1
X
X
4
5
Y
Y
8
Z
EOF

cat <<EOF > theirs.blob
T
T
T
T
T
T
T
T
T
T
EOF
)

# a change with deletion/clearing our file
mkdir clear-ours
(cd clear-ours
cat <<EOF > base.blob
0
1
2
3
4
5
EOF

touch ours.blob

cat <<EOF > theirs.blob
T
T
T
T
T
EOF
)

# a change with deletion/clearing their file
mkdir clear-theirs
(cd clear-theirs
cat <<EOF > base.blob
0
1
2
3
4
5
EOF

cat <<EOF > ours.blob
O
O
O
O
O
EOF

touch theirs.blob
)

# differently sized changes
mkdir ours-2-lines-theirs-1-line
(cd ours-2-lines-theirs-1-line
cat <<EOF > base.blob
0
1
2
3
4
5
EOF

cat <<EOF > ours.blob
0
1
X
X
4
5
EOF

cat <<EOF > theirs.blob
0
1
Y
3
4
5
EOF
)

# partial match
mkdir partial-match
(cd partial-match
cat <<EOF > base.blob
0
1
2
3
4
5
EOF

cat <<EOF > ours.blob
0
X
X
X
X
5
EOF

cat <<EOF > theirs.blob
0
X
2
X
X
5
EOF
)

for dir in simple multi-change clear-ours clear-theirs ours-2-lines-theirs-1-line partial-match; do
DIR=$dir
baseline ours base theirs merge
baseline ours base theirs diff3 --diff3
baseline ours base theirs zdiff3 --zdiff3
baseline ours base theirs merge-ours --ours
baseline ours base theirs merge-theirs --theirs
baseline ours base theirs merge-union --union
done
146 changes: 146 additions & 0 deletions gix-merge/tests/merge/blob/builtin_driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use gix_merge::blob::builtin_driver::binary::{Pick, ResolveWith};
use gix_merge::blob::{builtin_driver, Resolution};

#[test]
fn binary() {
assert_eq!(
builtin_driver::binary(None),
(Pick::Ours, Resolution::Conflict),
"by default it picks ours and marks it as conflict"
);
assert_eq!(
builtin_driver::binary(Some(ResolveWith::Ancestor)),
(Pick::Ancestor, Resolution::Complete),
"Otherwise we can pick anything and it will mark it as complete"
);
assert_eq!(
builtin_driver::binary(Some(ResolveWith::Ours)),
(Pick::Ours, Resolution::Complete)
);
assert_eq!(
builtin_driver::binary(Some(ResolveWith::Theirs)),
(Pick::Theirs, Resolution::Complete)
);
}

mod text {
use bstr::ByteSlice;
use gix_merge::blob::builtin_driver::text::ResolveWith;
use gix_merge::blob::Resolution;
use pretty_assertions::assert_str_eq;

#[test]
fn run_baseline() -> crate::Result {
let root = gix_testtools::scripted_fixture_read_only("text-baseline.sh")?;
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
let mut out = Vec::new();
let mut tokens = Vec::new();
for case in baseline::Expectations::new(&root, &cases)
// TODO: remove filter
.filter(|case| {
matches!(
case.options.on_conflict,
Some(ResolveWith::Union | ResolveWith::Ours | ResolveWith::Theirs)
)
})
{
let mut input = imara_diff::intern::InternedInput::default();
dbg!(&case.name, case.options);
let actual = gix_merge::blob::builtin_driver::text(
&mut out,
&mut input,
&mut tokens,
&case.ours,
&case.base,
&case.theirs,
case.options,
);
let expected_resolution = if case.expected.contains_str("<<<<<<<") {
Resolution::Conflict
} else {
Resolution::Complete
};
assert_eq!(actual, expected_resolution, "{}: resolution mismatch", case.name);
assert_str_eq!(
out.as_bstr().to_str_lossy(),
case.expected.to_str_lossy(),
"{}: output mismatch",
case.name
);
}
Ok(())
}

mod baseline {
use bstr::BString;
use gix_merge::blob::builtin_driver::text::{ConflictStyle, ResolveWith};
use std::path::Path;

#[derive(Debug)]
pub struct Expectation {
pub ours: BString,
pub theirs: BString,
pub base: BString,
pub name: BString,
pub expected: BString,
pub options: gix_merge::blob::builtin_driver::text::Options,
}

pub struct Expectations<'a> {
root: &'a Path,
lines: std::str::Lines<'a>,
}

impl<'a> Expectations<'a> {
pub fn new(root: &'a Path, cases: &'a str) -> Self {
Expectations {
root,
lines: cases.lines(),
}
}
}

impl Iterator for Expectations<'_> {
type Item = Expectation;

fn next(&mut self) -> Option<Self::Item> {
let line = self.lines.next()?;
let mut words = line.split(' ');
let (Some(ours), Some(base), Some(theirs), Some(output)) =
(words.next(), words.next(), words.next(), words.next())
else {
panic!("need at least the input and output")
};

let read = |rela_path: &str| read_blob(&self.root, rela_path);

let mut options = gix_merge::blob::builtin_driver::text::Options::default();
for arg in words {
match arg {
"--diff3" => options.conflict_style = ConflictStyle::Diff3,
"--zdiff3" => options.conflict_style = ConflictStyle::ZealousDiff3,
"--ours" => options.on_conflict = Some(ResolveWith::Ours),
"--theirs" => options.on_conflict = Some(ResolveWith::Theirs),
"--union" => options.on_conflict = Some(ResolveWith::Union),
_ => panic!("Unknown argument to parse into options: '{arg}'"),
}
}

Some(Expectation {
ours: read(ours),
theirs: read(theirs),
base: read(base),
expected: read(output),
name: output.into(),
options,
})
}
}

fn read_blob(root: &Path, rela_path: &str) -> BString {
std::fs::read(root.join(rela_path))
.unwrap_or_else(|_| panic!("Failed to read '{rela_path}' in '{}'", root.display()))
.into()
}
}
}
1 change: 1 addition & 0 deletions gix-merge/tests/merge/blob/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod builtin_driver;
4 changes: 4 additions & 0 deletions gix-merge/tests/merge/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#[cfg(feature = "blob")]
mod blob;

pub use gix_testtools::Result;

0 comments on commit 99874b2

Please sign in to comment.