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 15, 2024
1 parent b96d11f commit a04e415
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 20 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"]
61 changes: 42 additions & 19 deletions gix-merge/src/blob/builtin_driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ impl BuiltinDriver {

///
pub mod binary {
use crate::blob::Resolution;

/// What to do when having to pick a side to resolve a conflict.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ResolveWith {
Expand All @@ -47,23 +45,32 @@ pub mod binary {
Theirs,
}

/// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic.
///
/// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`.
pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) {
match on_conflict {
None => (Pick::Ours, Resolution::Conflict),
Some(ResolveWith::Ours) => (Pick::Ours, Resolution::Complete),
Some(ResolveWith::Theirs) => (Pick::Theirs, Resolution::Complete),
Some(ResolveWith::Ancestor) => (Pick::Ancestor, Resolution::Complete),
pub(super) mod function {
use crate::blob::builtin_driver::binary::{Pick, ResolveWith};
use crate::blob::Resolution;

/// As this algorithm doesn't look at the actual data, it returns a choice solely based on logic.
///
/// It always results in a conflict with `current` being picked unless `on_conflict` is not `None`.
pub fn merge(on_conflict: Option<ResolveWith>) -> (Pick, Resolution) {
match on_conflict {
None => (Pick::Ours, Resolution::Conflict),
Some(resolve) => (
match resolve {
ResolveWith::Ours => Pick::Ours,
ResolveWith::Theirs => Pick::Theirs,
ResolveWith::Ancestor => Pick::Ancestor,
},
Resolution::Complete,
),
}
}
}
}
pub use binary::function::merge as binary;

///
pub mod text {
use crate::blob::Resolution;

/// The way the built-in [text driver](crate::blob::BuiltinDriver::Text) will express
/// merge conflicts in the resulting file.
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
Expand Down Expand Up @@ -115,8 +122,11 @@ pub mod text {
}

/// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text).
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Options {
/// Determine of the diff will be performed.
/// Defaults to [`imara_diff::Algorithm::Myers`].
pub diff_algorithm: imara_diff::Algorithm,
/// How to visualize conflicts in merged files.
pub conflict_style: ConflictStyle,
/// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<`
Expand All @@ -132,6 +142,7 @@ pub mod text {
conflict_style: Default::default(),
marker_size: 7,
on_conflict: None,
diff_algorithm: imara_diff::Algorithm::Myers,
}
}
}
Expand All @@ -147,10 +158,22 @@ pub mod text {
Union,
}

/// Merge `current` and `other` with `ancestor` as base according to `opts`.
///
/// Place the merged result in `out` and return the resolution.
pub fn merge(_out: &mut Vec<u8>, _current: &[u8], _ancestor: &[u8], _other: &[u8], _opts: Options) -> Resolution {
todo!("text merge");
pub(super) mod function {
use crate::blob::builtin_driver::text::Options;
use crate::blob::Resolution;

/// Merge `current` and `other` with `ancestor` as base according to `opts`.
///
/// Place the merged result in `out` and return the resolution.
pub fn merge(
_out: &mut Vec<u8>,
_current: &[u8],
_ancestor: &[u8],
_other: &[u8],
_opts: Options,
) -> Resolution {
todo!("text merge");
}
}
}
pub use text::function::merge as text;
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.
31 changes: 31 additions & 0 deletions gix-merge/tests/fixtures/text-baseline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -eu -o pipefail


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
}

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

DIR=simple
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
122 changes: 122 additions & 0 deletions gix-merge/tests/merge/blob/builtin_driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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::Resolution;
use pretty_assertions::assert_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();
for case in baseline::Expectations::new(&root, &cases) {
let actual =
gix_merge::blob::builtin_driver::text(&mut out, &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_eq!(out.as_bstr(), case.expected, "{}: 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 a04e415

Please sign in to comment.