Skip to content

Commit

Permalink
Add baseline tests for tree-merges.
Browse files Browse the repository at this point in the history
That way, Git can indicate what we need to match.
  • Loading branch information
Byron committed Oct 12, 2024
1 parent 1f5d1f7 commit 64e0f78
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 16 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

22 changes: 10 additions & 12 deletions gix-merge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,31 @@ workspace = true
doctest = false

[features]
default = ["blob"]
## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation.
blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-quote"]
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"]

[dependencies]
gix-hash = { version = "^0.14.2", path = "../gix-hash" }
gix-object = { version = "^0.44.0", path = "../gix-object" }
gix-filter = { version = "^0.13.0", path = "../gix-filter", optional = true }
gix-worktree = { version = "^0.36.0", path = "../gix-worktree", default-features = false, features = ["attributes"], optional = true }
gix-command = { version = "^0.3.9", path = "../gix-command", optional = true }
gix-path = { version = "^0.10.11", path = "../gix-path", optional = true }
gix-fs = { version = "^0.11.3", path = "../gix-fs", optional = true }
gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile", optional = true }
gix-trace = { version = "^0.1.10", path = "../gix-trace", optional = true }
gix-quote = { version = "^0.4.12", path = "../gix-quote", optional = true }
gix-filter = { version = "^0.13.0", path = "../gix-filter" }
gix-worktree = { version = "^0.36.0", path = "../gix-worktree", default-features = false, features = ["attributes"] }
gix-command = { version = "^0.3.9", path = "../gix-command" }
gix-path = { version = "^0.10.11", path = "../gix-path" }
gix-fs = { version = "^0.11.3", path = "../gix-fs" }
gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile" }
gix-trace = { version = "^0.1.10", path = "../gix-trace" }
gix-quote = { version = "^0.4.12", path = "../gix-quote" }

thiserror = "1.0.63"
imara-diff = { version = "0.1.7", optional = true }
imara-diff = { version = "0.1.7" }
bstr = { version = "1.5.0", default-features = false }
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }

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

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

[package.metadata.docs.rs]
Expand Down
14 changes: 14 additions & 0 deletions gix-merge/src/commit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// The error returned by [commit()](crate::commit()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {}

pub(super) mod function {
use crate::commit::Error;

/// Like [`tree()`](crate::tree()), but it takes only two commits to automatically compute the
/// merge-bases among them.
pub fn commit(our_commit: &gix_hash::oid, their_commit: &gix_hash::oid) -> Result<(), Error> {
todo!()
}
}
7 changes: 6 additions & 1 deletion gix-merge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
#![forbid(unsafe_code)]

///
#[cfg(feature = "blob")]
pub mod blob;
///
pub mod commit;
pub use commit::function::commit;
///
pub mod tree;
pub use tree::function::tree;
37 changes: 37 additions & 0 deletions gix-merge/src/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// The error returned by [tree()](crate::tree()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {}

/// The outcome produced by [tree()](crate::tree()).
pub struct Outcome<'a> {
/// The ready-made (but unwritten) tree if `conflicts` is empty, or the best-possible tree when facing `conflicts`.
///
/// The tree may contain blobs with conflict markers, and will be missing directories or files that were conflicting
/// without a resolution strategy.
tree: gix_object::tree::Editor<'a>,
/// The set of conflicts we encountered. Can be empty to indicate there was no conflict.
conflicts: Vec<Conflict>,
}

/// A description of a conflict (i.e. merge issue without an auto-resolution) as seen during a [tree-merge](crate::tree()).
pub struct Conflict;

pub(super) mod function {
use crate::tree::{Error, Outcome};

/// Perform a merge between `our_tree` and `their_tree`, using `base_trees` as merge-base.
/// Note that if `base_trees` is empty, an empty tree is assumed to be the merge base.
/// If there are more than one tree `base_trees`, it will merge them into one with the specialty that binary
/// files will always be `our` side without conflicting. However, any other conflict will be fatal.
///
/// `objects` provides access to trees when diffing them.
pub fn tree<'a>(
base_trees: &[gix_object::Object],
our_tree: &gix_hash::oid,
their_tree: &gix_hash::oid,
objects: &'a dyn gix_object::FindExt,
) -> Result<Outcome<'a>, Error> {
todo!()
}
}
Binary file not shown.
90 changes: 90 additions & 0 deletions gix-merge/tests/fixtures/tree-baseline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -eu -o pipefail

function tick () {
if test -z "${tick+set}"
then
tick=1112911993
else
tick=$(($tick + 60))
fi
GIT_COMMITTER_DATE="$tick -0700"
GIT_AUTHOR_DATE="$tick -0700"
export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
}

function write_lines () {
printf "%s\n" "$@"
}

function baseline () (
local dir=${1:?the directory to enter}
local output_name=${2:?the basename of the output of the merge}
local our_committish=${3:?our side from which a commit can be derived}
local their_committish=${4:?Their side from which a commit can be derived}

cd "$dir"
local our_commit_id
local their_commit_id

our_commit_id="$(git rev-parse "$our_committish")"
their_commit_id="$(git rev-parse "$their_committish")"

local merge_info="${output_name}.merge-info"
git merge-tree -z --write-tree "$our_commit_id" "$their_commit_id" > "$merge_info" || :
echo "$dir" "$our_commit_id" "$their_commit_id" "$merge_info" >> ../baseline.cases
)

git init simple
(cd simple
rm -Rf .git/hooks
write_lines 1 2 3 4 5 >numbers
echo hello >greeting
echo foo >whatever
git add numbers greeting whatever
tick
git commit -m initial

git branch side1
git branch side2
git branch side3
git branch side4

git checkout side1
write_lines 1 2 3 4 5 6 >numbers
echo hi >greeting
echo bar >whatever
git add numbers greeting whatever
tick
git commit -m modify-stuff

git checkout side2
write_lines 0 1 2 3 4 5 >numbers
echo yo >greeting
git rm whatever
mkdir whatever
>whatever/empty
git add numbers greeting whatever/empty
tick
git commit -m other-modifications

git checkout side3
git mv numbers sequence
tick
git commit -m rename-numbers

git checkout side4
write_lines 0 1 2 3 4 5 >numbers
echo yo >greeting
git add numbers greeting
tick
git commit -m other-content-modifications

git switch --orphan unrelated
>something-else
git add something-else
tick
git commit -m first-commit
)

baseline simple without-conflict side1 side3
2 changes: 1 addition & 1 deletion gix-merge/tests/merge/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extern crate core;

#[cfg(feature = "blob")]
mod blob;
mod tree;

pub use gix_testtools::Result;
69 changes: 69 additions & 0 deletions gix-merge/tests/merge/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#[test]
fn run_baseline() -> crate::Result {
let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
for case in baseline::Expectations::new(&root, &cases) {}

Ok(())
}

mod baseline {
use std::path::Path;

pub struct Conflict;

pub struct Expectation {
pub odb: gix_odb::Handle,
pub our_commit_id: gix_hash::ObjectId,
pub their_commit_id: gix_hash::ObjectId,
pub merge_info: Result<gix_hash::ObjectId, Conflict>,
}

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 tokens = line.split(' ');
let (Some(subdir), Some(our_commit_id), Some(their_commit_id), Some(merge_info_filename)) =
(tokens.next(), tokens.next(), tokens.next(), tokens.next())
else {
unreachable!("invalid line: {line:?}")
};
assert_eq!(tokens.next(), None, "unexpected trailing tokens in line {line:?}");

let subdir = self.root.join(subdir);
let objects = gix_odb::at(subdir.join(".git/objects")).expect("object dir exists");
let our_commit_id = gix_hash::ObjectId::from_hex(our_commit_id.as_bytes()).unwrap();
let their_commit_id = gix_hash::ObjectId::from_hex(their_commit_id.as_bytes()).unwrap();
let merge_info = parse_merge_info(std::fs::read_to_string(subdir.join(merge_info_filename)).unwrap());
Some(Expectation {
odb: objects,
our_commit_id,
their_commit_id,
merge_info,
})
}
}

fn parse_merge_info(content: String) -> Result<gix_hash::ObjectId, Conflict> {
let mut lines = content.split('\0').filter(|t| !t.is_empty());
let tree_id = gix_hash::ObjectId::from_hex(lines.next().unwrap().as_bytes()).unwrap();
assert_eq!(lines.next(), None, "TODO: implement multi-line answer");
Ok(tree_id)
}
}
4 changes: 2 additions & 2 deletions gix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ revparse-regex = ["regex", "revision"]
blob-diff = ["gix-diff/blob", "attributes"]

## Add functions to specifically merge files, using the standard three-way merge that git offers.
blob-merge = ["dep:gix-merge", "gix-merge/blob", "attributes"]
blob-merge = ["dep:gix-merge", "attributes"]

## Make it possible to turn a tree into a stream of bytes, which can be decoded to entries and turned into various other formats.
worktree-stream = ["gix-worktree-stream", "attributes"]
Expand Down Expand Up @@ -341,7 +341,7 @@ gix-path = { version = "^0.10.11", path = "../gix-path" }
gix-url = { version = "^0.27.5", path = "../gix-url" }
gix-traverse = { version = "^0.41.0", path = "../gix-traverse" }
gix-diff = { version = "^0.46.0", path = "../gix-diff", default-features = false }
gix-merge = { version = "^0.0.0", path = "../gix-merge", default-features = false, optional = true }
gix-merge = { version = "^0.0.0", path = "../gix-merge", optional = true }
gix-mailmap = { version = "^0.24.0", path = "../gix-mailmap", optional = true }
gix-features = { version = "^0.38.2", path = "../gix-features", features = [
"progress",
Expand Down

0 comments on commit 64e0f78

Please sign in to comment.