Skip to content

Commit c73b792

Browse files
committed
feat: support for 'generation v2' format for overlow correction in generations.
This leads to greater correctness of generation numbers when dealing with certain kinds of commit-graphs.
1 parent 33c799c commit c73b792

10 files changed

+178
-38
lines changed

Cargo.lock

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

gix-commitgraph/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ document-features = { version = "0.2.0", optional = true }
3131

3232
[dev-dependencies]
3333
gix-testtools = { path = "../tests/tools" }
34+
gix-date = { path = "../gix-date" }
3435

3536
[package.metadata.docs.rs]
3637
all-features = true

gix-commitgraph/tests/access/mod.rs

+69-27
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,78 @@
1-
use gix_commitgraph::Graph;
2-
3-
use crate::{check_common, inspect_refs, make_readonly_repo};
1+
use crate::{check_common, graph_and_expected, graph_and_expected_named};
42

53
#[test]
6-
fn single_parent() -> crate::Result {
7-
let repo_dir = make_readonly_repo("single_parent.sh");
8-
let refs = inspect_refs(&repo_dir, &["parent", "child"]);
9-
let cg = Graph::from_info_dir(repo_dir.join(".git").join("objects").join("info"))?;
4+
fn single_parent() {
5+
let (cg, refs) = graph_and_expected("single_parent.sh", &["parent", "child"]);
106
check_common(&cg, &refs);
117

128
assert_eq!(cg.commit_at(refs["parent"].pos()).generation(), 1);
139
assert_eq!(cg.commit_at(refs["child"].pos()).generation(), 2);
10+
}
11+
12+
#[test]
13+
fn single_commit_huge_dates_generation_v2_also_do_not_allow_huge_dates() {
14+
let (cg, refs) = graph_and_expected_named("single_commit_huge_dates.sh", "v2", &["HEAD"]);
15+
let info = &refs["HEAD"];
16+
let actual = cg.commit_by_id(info.id).expect("present");
17+
assert_eq!(
18+
actual.committer_timestamp(),
19+
1,
20+
"overflow happened, can't represent huge dates"
21+
);
22+
assert_eq!(
23+
info.time.seconds, 68719476737,
24+
"this is the value we would want to see, but it's not possible in V2 either, as that is just about generations"
25+
);
26+
assert_eq!(actual.generation(), 1, "generations are fine though");
27+
}
1428

15-
Ok(())
29+
#[test]
30+
fn single_commit_huge_dates_overflow_v1() {
31+
let (cg, refs) = graph_and_expected_named("single_commit_huge_dates.sh", "v1", &["HEAD"]);
32+
let info = &refs["HEAD"];
33+
let actual = cg.commit_by_id(info.id).expect("present");
34+
assert_eq!(actual.committer_timestamp(), 1, "overflow happened");
35+
assert_eq!(
36+
info.time.seconds, 68719476737,
37+
"this is the value we would want to see, but it's not possible in V1"
38+
);
39+
assert_eq!(actual.generation(), 1, "generations are fine though");
1640
}
1741

1842
#[test]
19-
fn octupus_merges() -> crate::Result {
20-
let repo_dir = make_readonly_repo("octopus_merges.sh");
21-
let refs = inspect_refs(
22-
&repo_dir,
43+
fn single_commit_future_64bit_dates_work() {
44+
let (cg, refs) = graph_and_expected_named("single_commit_huge_dates.sh", "max-date", &["HEAD"]);
45+
let info = &refs["HEAD"];
46+
let actual = cg.commit_by_id(info.id).expect("present");
47+
assert_eq!(
48+
actual.committer_timestamp(),
49+
info.time.seconds,
50+
"this is close the the highest representable value in the graph, like year 2500, so we are good for longer than I should care about"
51+
);
52+
assert_eq!(actual.generation(), 1);
53+
}
54+
55+
#[test]
56+
fn generation_numbers_overflow_is_handled() {
57+
let names = ["extra", "old-2", "future-2", "old-1", "future-1"];
58+
let (cg, mut refs) = graph_and_expected("generation_number_overflow.sh", &names);
59+
for (r, expected) in names
60+
.iter()
61+
.map(|n| refs.remove(n.to_owned()).expect("present"))
62+
.zip((1..=5).rev())
63+
{
64+
assert_eq!(
65+
cg.commit_at(r.pos).generation(),
66+
expected,
67+
"actually, this test seems to have valid generation numbers from the get-go. How to repro the actual issue?"
68+
);
69+
}
70+
}
71+
72+
#[test]
73+
fn octupus_merges() {
74+
let (cg, refs) = graph_and_expected(
75+
"octopus_merges.sh",
2376
&[
2477
"root",
2578
"parent1",
@@ -30,7 +83,6 @@ fn octupus_merges() -> crate::Result {
3083
"four_parents",
3184
],
3285
);
33-
let cg = Graph::at(repo_dir.join(".git").join("objects").join("info"))?;
3486
check_common(&cg, &refs);
3587

3688
assert_eq!(cg.commit_at(refs["root"].pos()).generation(), 1);
@@ -40,32 +92,22 @@ fn octupus_merges() -> crate::Result {
4092
assert_eq!(cg.commit_at(refs["parent4"].pos()).generation(), 2);
4193
assert_eq!(cg.commit_at(refs["three_parents"].pos()).generation(), 3);
4294
assert_eq!(cg.commit_at(refs["four_parents"].pos()).generation(), 3);
43-
44-
Ok(())
4595
}
4696

4797
#[test]
48-
fn single_commit() -> crate::Result {
49-
let repo_dir = make_readonly_repo("single_commit.sh");
50-
let refs = inspect_refs(&repo_dir, &["commit"]);
51-
let cg = gix_commitgraph::at(repo_dir.join(".git").join("objects").join("info"))?;
98+
fn single_commit() {
99+
let (cg, refs) = graph_and_expected("single_commit.sh", &["commit"]);
52100
check_common(&cg, &refs);
53101

54102
assert_eq!(cg.commit_at(refs["commit"].pos()).generation(), 1);
55-
56-
Ok(())
57103
}
58104

59105
#[test]
60-
fn two_parents() -> crate::Result {
61-
let repo_dir = make_readonly_repo("two_parents.sh");
62-
let refs = inspect_refs(&repo_dir, &["parent1", "parent2", "child"]);
63-
let cg = Graph::from_info_dir(repo_dir.join(".git").join("objects").join("info"))?;
106+
fn two_parents() {
107+
let (cg, refs) = graph_and_expected("two_parents.sh", &["parent1", "parent2", "child"]);
64108
check_common(&cg, &refs);
65109

66110
assert_eq!(cg.commit_at(refs["parent1"].pos()).generation(), 1);
67111
assert_eq!(cg.commit_at(refs["parent2"].pos()).generation(), 1);
68112
assert_eq!(cg.commit_at(refs["child"].pos()).generation(), 2);
69-
70-
Ok(())
71113
}

gix-commitgraph/tests/commitgraph.rs

+29-9
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ use std::{
88
};
99

1010
use gix_commitgraph::{Graph, Position as GraphPosition};
11-
12-
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
11+
use gix_testtools::scripted_fixture_read_only;
1312

1413
mod access;
1514

1615
pub fn check_common(cg: &Graph, expected: &HashMap<String, RefInfo, impl BuildHasher>) {
16+
cg.verify_integrity(|_| Ok::<_, std::convert::Infallible>(()))
17+
.expect("graph is valid");
1718
assert_eq!(
1819
usize::try_from(cg.num_commits()).expect("an architecture able to hold 32 bits of integer"),
1920
expected.len()
@@ -39,6 +40,7 @@ pub fn check_common(cg: &Graph, expected: &HashMap<String, RefInfo, impl BuildHa
3940

4041
let commit = cg.commit_at(ref_info.pos());
4142
assert_eq!(commit.id(), ref_info.id());
43+
assert_eq!(commit.committer_timestamp(), ref_info.time.seconds);
4244
assert_eq!(commit.root_tree_id(), ref_info.root_tree_id());
4345
assert_eq!(
4446
commit.parent1().expect("failed to access commit's parent1"),
@@ -59,13 +61,29 @@ pub fn check_common(cg: &Graph, expected: &HashMap<String, RefInfo, impl BuildHa
5961
);
6062
}
6163

62-
use gix_testtools::scripted_fixture_read_only;
63-
pub fn make_readonly_repo(script_path: &str) -> std::path::PathBuf {
64-
scripted_fixture_read_only(script_path).expect("script succeeds all the time")
64+
pub fn graph_and_expected(
65+
script_path: &str,
66+
refs: &[&'static str],
67+
) -> (gix_commitgraph::Graph, HashMap<String, RefInfo>) {
68+
graph_and_expected_named(script_path, "", refs)
69+
}
70+
71+
pub fn graph_and_expected_named(
72+
script_path: &str,
73+
name: &str,
74+
refs: &[&'static str],
75+
) -> (gix_commitgraph::Graph, HashMap<String, RefInfo>) {
76+
let repo_dir = scripted_fixture_read_only(script_path)
77+
.expect("script succeeds all the time")
78+
.join(name);
79+
let expected = inspect_refs(&repo_dir, refs);
80+
let cg = Graph::from_info_dir(repo_dir.join(".git").join("objects").join("info")).expect("graph present and valid");
81+
(cg, expected)
6582
}
6683

6784
pub struct RefInfo {
6885
id: gix_hash::ObjectId,
86+
pub time: gix_date::Time,
6987
parent_ids: Vec<gix_hash::ObjectId>,
7088
pos: GraphPosition,
7189
root_tree_id: gix_hash::ObjectId,
@@ -89,13 +107,13 @@ impl RefInfo {
89107
}
90108
}
91109

92-
pub fn inspect_refs(repo_dir: impl AsRef<Path>, refs: &[&'static str]) -> HashMap<String, RefInfo> {
110+
fn inspect_refs(repo_dir: impl AsRef<Path>, refs: &[&'static str]) -> HashMap<String, RefInfo> {
93111
let output = Command::new("git")
94112
.arg("-C")
95113
.arg(repo_dir.as_ref())
96114
.arg("show")
97115
.arg("--no-patch")
98-
.arg("--pretty=format:%S %H %T %P")
116+
.arg("--pretty=format:%S %H %T %ct %P")
99117
.args(refs)
100118
.arg("--")
101119
.env_remove("GIT_DIR")
@@ -111,7 +129,8 @@ pub fn inspect_refs(repo_dir: impl AsRef<Path>, refs: &[&'static str]) -> HashMa
111129
parts[0].to_string(),
112130
gix_hash::ObjectId::from_hex(parts[1].as_bytes()).expect("40 bytes hex"),
113131
gix_hash::ObjectId::from_hex(parts[2].as_bytes()).expect("40 bytes hex"),
114-
parts[3..]
132+
gix_date::Time::new(parts[3].parse().expect("valid stamp"), 0),
133+
parts[4..]
115134
.iter()
116135
.map(|x| gix_hash::ObjectId::from_hex(x.as_bytes()).expect("40 bytes hex"))
117136
.collect(),
@@ -132,13 +151,14 @@ pub fn inspect_refs(repo_dir: impl AsRef<Path>, refs: &[&'static str]) -> HashMa
132151
infos
133152
.iter()
134153
.cloned()
135-
.map(|(name, id, root_tree_id, parent_ids)| {
154+
.map(|(name, id, root_tree_id, time, parent_ids)| {
136155
(
137156
name,
138157
RefInfo {
139158
id,
140159
parent_ids,
141160
root_tree_id,
161+
time,
142162
pos: get_pos(&id),
143163
},
144164
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:0e73018aa525514bba10850e1ddd8f7a9d190cd4a1f9faed5d4ab44cc32b78ad
3+
size 12204
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:fdb7b214315cabdf81173aed5530c2030d3d4f5f2888ebc194f6d1268fca685a
3-
size 11028
2+
oid sha256:e52a2e28465e3ac6b64cc7d9dac2486a216a9d99175e4ade52f68ff2602ea108
3+
size 11104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:39cd8603f0e58ff9cf05173f2edcd9446128461bb647d8045e6d73c86205b141
3+
size 10900
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:466157da5dcaae21d46aa5a1aa09e72e0c545eafae1c4513cfa75ad94115062f
3+
size 10128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/bin/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+
tick
17+
function commit() {
18+
local message=${1:?first argument is the commit message}
19+
local date=${2:-}
20+
local file="$message.t"
21+
echo "$1" > "$file"
22+
git add -- "$file"
23+
if [ -n "$date" ]; then
24+
export GIT_COMMITTER_DATE="$date"
25+
else
26+
tick
27+
fi
28+
git commit -m "$message"
29+
git tag "$message"
30+
}
31+
32+
# adapted from git/t/t5318 'lower layers have overflow chunk'
33+
UNIX_EPOCH_ZERO="@0 +0000"
34+
FUTURE_DATE="@4147483646 +0000"
35+
36+
git init
37+
git config commitGraph.generationVersion 2
38+
39+
commit future-1 "$FUTURE_DATE"
40+
commit old-1 "$UNIX_EPOCH_ZERO"
41+
git commit-graph write --reachable
42+
commit future-2 "$FUTURE_DATE"
43+
commit old-2 "$UNIX_EPOCH_ZERO"
44+
git commit-graph write --reachable --split=no-merge
45+
commit extra
46+
git commit-graph write --reachable --split=no-merge
47+
git commit-graph write --reachable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
set -eu -o pipefail
3+
4+
function setup_repo() {
5+
local version=${1:?need generation version}
6+
local time=${2:?timestamp seconds since unix epoch}
7+
git init -q
8+
9+
# one past the max 32bit date git can represent
10+
export GIT_COMMITTER_DATE="@${time} +0000"
11+
git config commitGraph.generationVersion ${version}
12+
13+
git commit -q --allow-empty -m c1
14+
15+
git commit-graph write --no-progress --reachable
16+
}
17+
18+
(mkdir v1 && cd v1 && setup_repo 1 68719476737) # the year 4000 something (overflows in graph)
19+
(mkdir v2 && cd v2 && setup_repo 2 68719476737)
20+
(mkdir max-date && cd max-date && setup_repo 1 17147483646) # the year 2500ish

0 commit comments

Comments
 (0)