Skip to content

Commit aea4301

Browse files
committed
Add first baseline tests for merge-base support
Translate first tests from the Git test suite and set up expectations.
1 parent 155970f commit aea4301

File tree

8 files changed

+255
-3
lines changed

8 files changed

+255
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-revision/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ rust-version = "1.65"
1515
doctest = false
1616

1717
[features]
18-
default = ["describe"]
18+
default = ["describe", "merge_base"]
1919

2020
## `git describe` functionality
2121
describe = ["dep:gix-trace", "dep:gix-hashtable"]
2222

23+
## `git merge-base` functionality
24+
merge_base = ["dep:gix-trace", "dep:bitflags"]
25+
2326
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
2427
serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"]
2528

@@ -29,17 +32,18 @@ gix-object = { version = "^0.44.0", path = "../gix-object" }
2932
gix-date = { version = "^0.9.0", path = "../gix-date" }
3033
gix-hashtable = { version = "^0.5.2", path = "../gix-hashtable", optional = true }
3134
gix-revwalk = { version = "^0.15.0", path = "../gix-revwalk" }
35+
gix-commitgraph = { version = "0.24.3", path = "../gix-commitgraph" }
3236
gix-trace = { version = "^0.1.8", path = "../gix-trace", optional = true }
3337

3438
bstr = { version = "1.3.0", default-features = false, features = ["std"] }
39+
bitflags = { version = "2", optional = true }
3540
thiserror = "1.0.26"
3641
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
3742
document-features = { version = "0.2.1", optional = true }
3843

3944
[dev-dependencies]
4045
gix-odb = { path = "../gix-odb" }
4146
gix-testtools = { path = "../tests/tools" }
42-
gix-commitgraph = { path = "../gix-commitgraph" }
4347

4448
[package.metadata.docs.rs]
4549
all-features = true

gix-revision/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
pub mod describe;
1414
#[cfg(feature = "describe")]
1515
pub use describe::function::describe;
16+
///
17+
#[allow(clippy::empty_docs)]
18+
#[cfg(feature = "merge_base")]
19+
pub mod merge_base;
20+
pub use merge_base::function::merge_base;
1621

1722
///
1823
pub mod spec;

gix-revision/src/merge_base.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
bitflags::bitflags! {
2+
/// The flags used in the graph for finding [merge bases](crate::merge_base()).
3+
#[derive(Debug, Default, Copy, Clone)]
4+
pub struct Flags: u8 {
5+
/// The commit belongs to the graph reachable by the first commit
6+
const COMMIT1 = 1 << 0;
7+
/// The commit belongs to the graph reachable by all other commits.
8+
const COMMIT2 = 1 << 1;
9+
10+
/// Marks the commit as done, it's reachable by both COMMIT1 and COMMIT2.
11+
const STALE = 1 << 2;
12+
/// The commit was already put ontto the results list.
13+
const RESULT = 1 << 3;
14+
}
15+
}
16+
17+
/// The error returned by the [`merge_base()`][function::describe()] function.
18+
#[derive(Debug, thiserror::Error)]
19+
#[allow(missing_docs)]
20+
pub enum Error {
21+
#[error("A commit could not be decoded during traversal")]
22+
Decode(#[from] gix_object::decode::Error),
23+
}
24+
25+
pub(crate) mod function {
26+
use gix_hash::ObjectId;
27+
use std::cmp::Ordering;
28+
29+
use super::Error;
30+
use crate::{merge_base::Flags, Graph, PriorityQueue};
31+
32+
/// Given a commit at `first` id, traverse the commit `graph` and return all possible merge-base between it and `others`,
33+
/// sorted from best to worst. Returns `None` if there is no merge-base as `first` and `others` don't share history.
34+
/// If `others` is empty, `Some(first)` is returned.
35+
///
36+
/// Note that this function doesn't do any work if `first` is contained in `others`, which is when `first` will be returned
37+
/// as only merge-base right away. This is even the case if some commits of `others` are disjoint.
38+
pub fn merge_base<'name>(
39+
first: ObjectId,
40+
others: &[ObjectId],
41+
graph: &mut Graph<'_, Flags>,
42+
) -> Result<Option<Vec<ObjectId>>, Error> {
43+
let _span = gix_trace::coarse!(
44+
"gix_revision::merge_base()",
45+
%first,
46+
%others,
47+
);
48+
if others.is_empty() || others.contains(&first) {
49+
return Ok(Some(vec![first]));
50+
}
51+
52+
graph.insert(first, Flags::COMMIT1);
53+
let mut queue = PriorityQueue::from_iter(Some((GenThenTime::max(), first)));
54+
Ok(None)
55+
}
56+
57+
struct GenThenTime {
58+
/// Note that the special [`GENERATION_NUMBER_INFINITY`](gix_commitgraph::GENERATION_NUMBER_INFINITY) is used to indicate
59+
/// that no commitgraph is avaialble.
60+
generation: gix_revwalk::graph::Generation,
61+
time: gix_date::SecondsSinceUnixEpoch,
62+
}
63+
64+
impl GenThenTime {
65+
fn max() -> Self {
66+
Self {
67+
generation: gix_commitgraph::GENERATION_NUMBER_INFINITY,
68+
time: gix_date::SecondsSinceUnixEpoch::MAX,
69+
}
70+
}
71+
}
72+
73+
impl Eq for GenThenTime {}
74+
75+
impl PartialEq<Self> for GenThenTime {
76+
fn eq(&self, other: &Self) -> bool {
77+
self.cmp(other).is_eq()
78+
}
79+
}
80+
81+
impl PartialOrd<Self> for GenThenTime {
82+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
83+
self.cmp(&other).into()
84+
}
85+
}
86+
87+
impl Ord for GenThenTime {
88+
fn cmp(&self, other: &Self) -> Ordering {
89+
self.generation.cmp(&other.generation).then(self.time.cmp(&other.time))
90+
}
91+
}
92+
}
Binary file not shown.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
set -eu -o pipefail
3+
4+
git init
5+
6+
EMPTY_TREE=$(git mktree </dev/null)
7+
set -x
8+
function mkcommit () {
9+
local OFFSET_SECONDS=$1
10+
local COMMIT_NAME=$2
11+
shift 2
12+
13+
PARENTS=
14+
for P; do
15+
PARENTS="${PARENTS}-p $P "
16+
done
17+
18+
GIT_COMMITTER_DATE="$((400403349 + OFFSET_SECONDS)) +0000"
19+
GIT_AUTHOR_DATE=$GIT_COMMITTER_DATE
20+
export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
21+
22+
commit=$(echo $COMMIT_NAME | git commit-tree $EMPTY_TREE ${PARENTS:-})
23+
24+
git update-ref "refs/tags/$COMMIT_NAME" "$commit"
25+
echo $commit
26+
}
27+
28+
function baseline() {
29+
echo "$@"
30+
echo $(git rev-parse "$@")
31+
git merge-base --all "$@" || :
32+
echo
33+
}
34+
35+
# Merge-bases adapted from Git test suite
36+
# No merge base
37+
mkcommit 0 DA
38+
mkcommit 100 DB
39+
{
40+
echo "just-one-returns-one-in-code"
41+
echo $(git rev-parse DA)
42+
echo $(git rev-parse DA)
43+
echo
44+
baseline DA DB
45+
baseline DA DA DB
46+
} > 1_disjoint.baseline
47+
48+
# E---D---C---B---A
49+
# \"-_ \ \
50+
# \ `---------G \
51+
# \ \
52+
# F----------------H
53+
E=$(mkcommit 5 E)
54+
D=$(mkcommit 4 D $E)
55+
F=$(mkcommit 6 F $E)
56+
C=$(mkcommit 3 C $D)
57+
B=$(mkcommit 2 B $C)
58+
A=$(mkcommit 1 A $B)
59+
G=$(mkcommit 7 G $B $E)
60+
H=$(mkcommit 8 H $A $F)
61+
62+
{
63+
baseline G H
64+
} > 2_a.baseline
65+
66+
git commit-graph write --no-progress --reachable
67+
git repack -adq

gix-revision/tests/merge_base/mod.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
mod baseline {
2+
use bstr::ByteSlice;
3+
use gix_hash::ObjectId;
4+
use gix_revision::merge_base;
5+
use std::ffi::OsStr;
6+
use std::path::{Path, PathBuf};
7+
8+
#[test]
9+
fn validate() -> crate::Result {
10+
let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?;
11+
let mut count = 0;
12+
let odb = gix_odb::at(&root.join(".git/objects"))?;
13+
for baseline_path in expectation_paths(&root)? {
14+
count += 1;
15+
for use_commitgraph in [false, true] {
16+
for expected in parse_expectations(&baseline_path)? {
17+
let cache = use_commitgraph
18+
.then(|| gix_commitgraph::Graph::from_info_dir(&odb.store_ref().path().join("info")).unwrap());
19+
let mut graph = gix_revision::Graph::new(&odb, cache);
20+
21+
let actual = merge_base(expected.first, &expected.others, &mut graph)?;
22+
assert_eq!(
23+
actual,
24+
expected.bases,
25+
"sample {file:?}:{input}",
26+
file = baseline_path.with_extension("").file_name(),
27+
input = expected.plain_input
28+
);
29+
}
30+
}
31+
}
32+
assert_ne!(count, 0, "there must be at least one baseline");
33+
Ok(())
34+
}
35+
36+
/// The expectation as produced by Git itself
37+
#[derive(Debug)]
38+
struct Expectation {
39+
plain_input: String,
40+
first: ObjectId,
41+
others: Vec<ObjectId>,
42+
bases: Option<Vec<ObjectId>>,
43+
}
44+
45+
fn parse_expectations(baseline: &Path) -> std::io::Result<Vec<Expectation>> {
46+
let lines = std::fs::read(baseline)?;
47+
let mut lines = lines.lines();
48+
let mut out = Vec::new();
49+
while let Some(plain_input) = lines.next() {
50+
let plain_input = plain_input.to_str_lossy().into_owned();
51+
let mut input = lines
52+
.next()
53+
.expect("second line is resolved input objects")
54+
.split(|b| *b == b' ');
55+
let first = ObjectId::from_hex(input.next().expect("at least one object")).unwrap();
56+
let others = input.map(|hex_id| ObjectId::from_hex(hex_id).unwrap()).collect();
57+
let bases: Vec<_> = lines
58+
.by_ref()
59+
.take_while(|l| !l.is_empty())
60+
.map(|hex_id| ObjectId::from_hex(hex_id).unwrap())
61+
.collect();
62+
out.push(Expectation {
63+
plain_input,
64+
first,
65+
others,
66+
bases: if bases.is_empty() { None } else { Some(bases) },
67+
})
68+
}
69+
Ok(out)
70+
}
71+
72+
fn expectation_paths(root: &Path) -> std::io::Result<Vec<PathBuf>> {
73+
let mut out: Vec<_> = std::fs::read_dir(root)?
74+
.map(Result::unwrap)
75+
.filter_map(|e| (e.path().extension() == Some(OsStr::new("baseline"))).then(|| e.path()))
76+
.collect();
77+
out.sort();
78+
Ok(out)
79+
}
80+
}

gix-revision/tests/revision.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#[cfg(feature = "describe")]
22
mod describe;
3+
#[cfg(feature = "merge_base")]
4+
mod merge_base;
35
mod spec;
4-
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + 'static>>;
6+
7+
pub use gix_testtools::Result;
58

69
fn hex_to_id(hex: &str) -> gix_hash::ObjectId {
710
gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")

0 commit comments

Comments
 (0)