Skip to content

Commit 64e0f78

Browse files
committed
Add baseline tests for tree-merges.
That way, Git can indicate what we need to match.
1 parent 1f5d1f7 commit 64e0f78

File tree

10 files changed

+230
-16
lines changed

10 files changed

+230
-16
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-merge/Cargo.toml

+10-12
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,31 @@ workspace = true
1515
doctest = false
1616

1717
[features]
18-
default = ["blob"]
19-
## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation.
20-
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"]
2118
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
2219
serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"]
2320

2421
[dependencies]
2522
gix-hash = { version = "^0.14.2", path = "../gix-hash" }
2623
gix-object = { version = "^0.44.0", path = "../gix-object" }
27-
gix-filter = { version = "^0.13.0", path = "../gix-filter", optional = true }
28-
gix-worktree = { version = "^0.36.0", path = "../gix-worktree", default-features = false, features = ["attributes"], optional = true }
29-
gix-command = { version = "^0.3.9", path = "../gix-command", optional = true }
30-
gix-path = { version = "^0.10.11", path = "../gix-path", optional = true }
31-
gix-fs = { version = "^0.11.3", path = "../gix-fs", optional = true }
32-
gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile", optional = true }
33-
gix-trace = { version = "^0.1.10", path = "../gix-trace", optional = true }
34-
gix-quote = { version = "^0.4.12", path = "../gix-quote", optional = true }
24+
gix-filter = { version = "^0.13.0", path = "../gix-filter" }
25+
gix-worktree = { version = "^0.36.0", path = "../gix-worktree", default-features = false, features = ["attributes"] }
26+
gix-command = { version = "^0.3.9", path = "../gix-command" }
27+
gix-path = { version = "^0.10.11", path = "../gix-path" }
28+
gix-fs = { version = "^0.11.3", path = "../gix-fs" }
29+
gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile" }
30+
gix-trace = { version = "^0.1.10", path = "../gix-trace" }
31+
gix-quote = { version = "^0.4.12", path = "../gix-quote" }
3532

3633
thiserror = "1.0.63"
37-
imara-diff = { version = "0.1.7", optional = true }
34+
imara-diff = { version = "0.1.7" }
3835
bstr = { version = "1.5.0", default-features = false }
3936
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
4037

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

4340
[dev-dependencies]
4441
gix-testtools = { path = "../tests/tools" }
42+
gix-odb = { path = "../gix-odb" }
4543
pretty_assertions = "1.4.0"
4644

4745
[package.metadata.docs.rs]

gix-merge/src/commit.rs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// The error returned by [commit()](crate::commit()).
2+
#[derive(Debug, thiserror::Error)]
3+
#[allow(missing_docs)]
4+
pub enum Error {}
5+
6+
pub(super) mod function {
7+
use crate::commit::Error;
8+
9+
/// Like [`tree()`](crate::tree()), but it takes only two commits to automatically compute the
10+
/// merge-bases among them.
11+
pub fn commit(our_commit: &gix_hash::oid, their_commit: &gix_hash::oid) -> Result<(), Error> {
12+
todo!()
13+
}
14+
}

gix-merge/src/lib.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
#![forbid(unsafe_code)]
33

44
///
5-
#[cfg(feature = "blob")]
65
pub mod blob;
6+
///
7+
pub mod commit;
8+
pub use commit::function::commit;
9+
///
10+
pub mod tree;
11+
pub use tree::function::tree;

gix-merge/src/tree.rs

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/// The error returned by [tree()](crate::tree()).
2+
#[derive(Debug, thiserror::Error)]
3+
#[allow(missing_docs)]
4+
pub enum Error {}
5+
6+
/// The outcome produced by [tree()](crate::tree()).
7+
pub struct Outcome<'a> {
8+
/// The ready-made (but unwritten) tree if `conflicts` is empty, or the best-possible tree when facing `conflicts`.
9+
///
10+
/// The tree may contain blobs with conflict markers, and will be missing directories or files that were conflicting
11+
/// without a resolution strategy.
12+
tree: gix_object::tree::Editor<'a>,
13+
/// The set of conflicts we encountered. Can be empty to indicate there was no conflict.
14+
conflicts: Vec<Conflict>,
15+
}
16+
17+
/// A description of a conflict (i.e. merge issue without an auto-resolution) as seen during a [tree-merge](crate::tree()).
18+
pub struct Conflict;
19+
20+
pub(super) mod function {
21+
use crate::tree::{Error, Outcome};
22+
23+
/// Perform a merge between `our_tree` and `their_tree`, using `base_trees` as merge-base.
24+
/// Note that if `base_trees` is empty, an empty tree is assumed to be the merge base.
25+
/// If there are more than one tree `base_trees`, it will merge them into one with the specialty that binary
26+
/// files will always be `our` side without conflicting. However, any other conflict will be fatal.
27+
///
28+
/// `objects` provides access to trees when diffing them.
29+
pub fn tree<'a>(
30+
base_trees: &[gix_object::Object],
31+
our_tree: &gix_hash::oid,
32+
their_tree: &gix_hash::oid,
33+
objects: &'a dyn gix_object::FindExt,
34+
) -> Result<Outcome<'a>, Error> {
35+
todo!()
36+
}
37+
}
Binary file not shown.
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env bash
2+
set -eu -o pipefail
3+
4+
function tick () {
5+
if test -z "${tick+set}"
6+
then
7+
tick=1112911993
8+
else
9+
tick=$(($tick + 60))
10+
fi
11+
GIT_COMMITTER_DATE="$tick -0700"
12+
GIT_AUTHOR_DATE="$tick -0700"
13+
export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
14+
}
15+
16+
function write_lines () {
17+
printf "%s\n" "$@"
18+
}
19+
20+
function baseline () (
21+
local dir=${1:?the directory to enter}
22+
local output_name=${2:?the basename of the output of the merge}
23+
local our_committish=${3:?our side from which a commit can be derived}
24+
local their_committish=${4:?Their side from which a commit can be derived}
25+
26+
cd "$dir"
27+
local our_commit_id
28+
local their_commit_id
29+
30+
our_commit_id="$(git rev-parse "$our_committish")"
31+
their_commit_id="$(git rev-parse "$their_committish")"
32+
33+
local merge_info="${output_name}.merge-info"
34+
git merge-tree -z --write-tree "$our_commit_id" "$their_commit_id" > "$merge_info" || :
35+
echo "$dir" "$our_commit_id" "$their_commit_id" "$merge_info" >> ../baseline.cases
36+
)
37+
38+
git init simple
39+
(cd simple
40+
rm -Rf .git/hooks
41+
write_lines 1 2 3 4 5 >numbers
42+
echo hello >greeting
43+
echo foo >whatever
44+
git add numbers greeting whatever
45+
tick
46+
git commit -m initial
47+
48+
git branch side1
49+
git branch side2
50+
git branch side3
51+
git branch side4
52+
53+
git checkout side1
54+
write_lines 1 2 3 4 5 6 >numbers
55+
echo hi >greeting
56+
echo bar >whatever
57+
git add numbers greeting whatever
58+
tick
59+
git commit -m modify-stuff
60+
61+
git checkout side2
62+
write_lines 0 1 2 3 4 5 >numbers
63+
echo yo >greeting
64+
git rm whatever
65+
mkdir whatever
66+
>whatever/empty
67+
git add numbers greeting whatever/empty
68+
tick
69+
git commit -m other-modifications
70+
71+
git checkout side3
72+
git mv numbers sequence
73+
tick
74+
git commit -m rename-numbers
75+
76+
git checkout side4
77+
write_lines 0 1 2 3 4 5 >numbers
78+
echo yo >greeting
79+
git add numbers greeting
80+
tick
81+
git commit -m other-content-modifications
82+
83+
git switch --orphan unrelated
84+
>something-else
85+
git add something-else
86+
tick
87+
git commit -m first-commit
88+
)
89+
90+
baseline simple without-conflict side1 side3

gix-merge/tests/merge/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
extern crate core;
22

3-
#[cfg(feature = "blob")]
43
mod blob;
4+
mod tree;
55

66
pub use gix_testtools::Result;

gix-merge/tests/merge/tree.rs

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#[test]
2+
fn run_baseline() -> crate::Result {
3+
let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;
4+
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
5+
for case in baseline::Expectations::new(&root, &cases) {}
6+
7+
Ok(())
8+
}
9+
10+
mod baseline {
11+
use std::path::Path;
12+
13+
pub struct Conflict;
14+
15+
pub struct Expectation {
16+
pub odb: gix_odb::Handle,
17+
pub our_commit_id: gix_hash::ObjectId,
18+
pub their_commit_id: gix_hash::ObjectId,
19+
pub merge_info: Result<gix_hash::ObjectId, Conflict>,
20+
}
21+
22+
pub struct Expectations<'a> {
23+
root: &'a Path,
24+
lines: std::str::Lines<'a>,
25+
}
26+
27+
impl<'a> Expectations<'a> {
28+
pub fn new(root: &'a Path, cases: &'a str) -> Self {
29+
Expectations {
30+
root,
31+
lines: cases.lines(),
32+
}
33+
}
34+
}
35+
36+
impl Iterator for Expectations<'_> {
37+
type Item = Expectation;
38+
39+
fn next(&mut self) -> Option<Self::Item> {
40+
let line = self.lines.next()?;
41+
let mut tokens = line.split(' ');
42+
let (Some(subdir), Some(our_commit_id), Some(their_commit_id), Some(merge_info_filename)) =
43+
(tokens.next(), tokens.next(), tokens.next(), tokens.next())
44+
else {
45+
unreachable!("invalid line: {line:?}")
46+
};
47+
assert_eq!(tokens.next(), None, "unexpected trailing tokens in line {line:?}");
48+
49+
let subdir = self.root.join(subdir);
50+
let objects = gix_odb::at(subdir.join(".git/objects")).expect("object dir exists");
51+
let our_commit_id = gix_hash::ObjectId::from_hex(our_commit_id.as_bytes()).unwrap();
52+
let their_commit_id = gix_hash::ObjectId::from_hex(their_commit_id.as_bytes()).unwrap();
53+
let merge_info = parse_merge_info(std::fs::read_to_string(subdir.join(merge_info_filename)).unwrap());
54+
Some(Expectation {
55+
odb: objects,
56+
our_commit_id,
57+
their_commit_id,
58+
merge_info,
59+
})
60+
}
61+
}
62+
63+
fn parse_merge_info(content: String) -> Result<gix_hash::ObjectId, Conflict> {
64+
let mut lines = content.split('\0').filter(|t| !t.is_empty());
65+
let tree_id = gix_hash::ObjectId::from_hex(lines.next().unwrap().as_bytes()).unwrap();
66+
assert_eq!(lines.next(), None, "TODO: implement multi-line answer");
67+
Ok(tree_id)
68+
}
69+
}

gix/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ revparse-regex = ["regex", "revision"]
139139
blob-diff = ["gix-diff/blob", "attributes"]
140140

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

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

0 commit comments

Comments
 (0)