From 64e0f78a3ff061837ff647da96110be432cc7228 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 9 Oct 2024 09:35:53 +0200 Subject: [PATCH] Add baseline tests for tree-merges. That way, Git can indicate what we need to match. --- Cargo.lock | 1 + gix-merge/Cargo.toml | 22 ++--- gix-merge/src/commit.rs | 14 +++ gix-merge/src/lib.rs | 7 +- gix-merge/src/tree.rs | 37 +++++++ .../generated-archives/tree-baseline.tar | Bin 0 -> 68096 bytes gix-merge/tests/fixtures/tree-baseline.sh | 90 ++++++++++++++++++ gix-merge/tests/merge/main.rs | 2 +- gix-merge/tests/merge/tree.rs | 69 ++++++++++++++ gix/Cargo.toml | 4 +- 10 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 gix-merge/src/commit.rs create mode 100644 gix-merge/src/tree.rs create mode 100644 gix-merge/tests/fixtures/generated-archives/tree-baseline.tar create mode 100644 gix-merge/tests/fixtures/tree-baseline.sh create mode 100644 gix-merge/tests/merge/tree.rs diff --git a/Cargo.lock b/Cargo.lock index 82ee1a6e7bb..91ecfd76d0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2073,6 +2073,7 @@ dependencies = [ "gix-fs 0.11.3", "gix-hash 0.14.2", "gix-object 0.44.0", + "gix-odb", "gix-path 0.10.11", "gix-quote 0.4.12", "gix-tempfile 14.0.2", diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index 93a5ae5b664..db2fd65d33f 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -15,26 +15,23 @@ 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"] } @@ -42,6 +39,7 @@ 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] diff --git a/gix-merge/src/commit.rs b/gix-merge/src/commit.rs new file mode 100644 index 00000000000..f5ba90316e4 --- /dev/null +++ b/gix-merge/src/commit.rs @@ -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!() + } +} diff --git a/gix-merge/src/lib.rs b/gix-merge/src/lib.rs index 8e608c53ab4..18be9a67604 100644 --- a/gix-merge/src/lib.rs +++ b/gix-merge/src/lib.rs @@ -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; diff --git a/gix-merge/src/tree.rs b/gix-merge/src/tree.rs new file mode 100644 index 00000000000..c9b9ec9c8e0 --- /dev/null +++ b/gix-merge/src/tree.rs @@ -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, +} + +/// 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, Error> { + todo!() + } +} diff --git a/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar b/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar new file mode 100644 index 0000000000000000000000000000000000000000..8fa3c7a890bc7949a440d2360e7ed175ff6de926 GIT binary patch literal 68096 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 literal 0 HcmV?d00001 diff --git a/gix-merge/tests/fixtures/tree-baseline.sh b/gix-merge/tests/fixtures/tree-baseline.sh new file mode 100644 index 00000000000..a0ac2de1260 --- /dev/null +++ b/gix-merge/tests/fixtures/tree-baseline.sh @@ -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 diff --git a/gix-merge/tests/merge/main.rs b/gix-merge/tests/merge/main.rs index 9f7a6989d2c..b3fb94f8b4a 100644 --- a/gix-merge/tests/merge/main.rs +++ b/gix-merge/tests/merge/main.rs @@ -1,6 +1,6 @@ extern crate core; -#[cfg(feature = "blob")] mod blob; +mod tree; pub use gix_testtools::Result; diff --git a/gix-merge/tests/merge/tree.rs b/gix-merge/tests/merge/tree.rs new file mode 100644 index 00000000000..583357f5f48 --- /dev/null +++ b/gix-merge/tests/merge/tree.rs @@ -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, + } + + 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 { + 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 { + 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) + } +} diff --git a/gix/Cargo.toml b/gix/Cargo.toml index cd1a6142f08..5ca2d4e5798 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -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"] @@ -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",