diff --git a/Cargo.lock b/Cargo.lock index 3c5a6159d78..954f648095b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1451,6 +1451,7 @@ dependencies = [ "bstr", "document-features", "gix-chunk", + "gix-date 0.5.1", "gix-features 0.30.0", "gix-hash 0.11.2", "gix-testtools", @@ -1856,6 +1857,7 @@ dependencies = [ "bstr", "document-features", "gix-actor 0.21.0", + "gix-date 0.5.1", "gix-testtools", "serde", "thiserror", @@ -1867,6 +1869,7 @@ version = "0.2.1" dependencies = [ "bitflags 2.1.0", "gix-commitgraph", + "gix-date 0.5.1", "gix-hash 0.11.2", "gix-object 0.30.0", "gix-odb", @@ -1908,6 +1911,7 @@ dependencies = [ "btoi", "document-features", "gix-actor 0.21.0", + "gix-date 0.5.1", "gix-features 0.30.0", "gix-hash 0.11.2", "gix-testtools", @@ -1930,6 +1934,7 @@ dependencies = [ "document-features", "filetime", "gix-actor 0.21.0", + "gix-date 0.5.1", "gix-features 0.30.0", "gix-hash 0.11.2", "gix-object 0.30.0", @@ -2066,6 +2071,7 @@ dependencies = [ "futures-io", "futures-lite", "gix-credentials", + "gix-date 0.5.1", "gix-features 0.30.0", "gix-hash 0.11.2", "gix-packetline", @@ -2127,6 +2133,7 @@ version = "0.30.0" dependencies = [ "document-features", "gix-actor 0.21.0", + "gix-date 0.5.1", "gix-features 0.30.0", "gix-fs 0.2.0", "gix-hash 0.11.2", @@ -2149,6 +2156,7 @@ name = "gix-ref-tests" version = "0.0.0" dependencies = [ "gix-actor 0.21.0", + "gix-date 0.5.1", "gix-discover 0.19.0", "gix-features 0.30.0", "gix-fs 0.2.0", @@ -2199,6 +2207,7 @@ name = "gix-revwalk" version = "0.1.0" dependencies = [ "gix-commitgraph", + "gix-date 0.5.1", "gix-hash 0.11.2", "gix-hashtable 0.2.1", "gix-object 0.30.0", @@ -2341,6 +2350,7 @@ name = "gix-traverse" version = "0.27.0" dependencies = [ "gix-commitgraph", + "gix-date 0.5.1", "gix-hash 0.11.2", "gix-hashtable 0.2.1", "gix-object 0.30.0", diff --git a/cargo-smart-release/src/commit/history.rs b/cargo-smart-release/src/commit/history.rs index 3cac1df8773..2e401007fa7 100644 --- a/cargo-smart-release/src/commit/history.rs +++ b/cargo-smart-release/src/commit/history.rs @@ -10,7 +10,7 @@ pub struct Segment<'a> { pub struct Item { pub id: gix::ObjectId, pub message: Message, - pub commit_time: gix::actor::Time, + pub commit_time: gix::date::Time, pub tree_id: gix::ObjectId, pub parent_tree_id: Option, } diff --git a/cargo-smart-release/src/utils.rs b/cargo-smart-release/src/utils.rs index 1d3aca1e296..5b6ab738137 100644 --- a/cargo-smart-release/src/utils.rs +++ b/cargo-smart-release/src/utils.rs @@ -299,8 +299,8 @@ mod tests { } } -pub fn time_to_offset_date_time(time: gix::actor::Time) -> OffsetDateTime { - time::OffsetDateTime::from_unix_timestamp(time.seconds_since_unix_epoch as i64) +pub fn time_to_offset_date_time(time: gix::date::Time) -> OffsetDateTime { + time::OffsetDateTime::from_unix_timestamp(time.seconds as i64) .expect("always valid unix time") - .replace_offset(time::UtcOffset::from_whole_seconds(time.offset_in_seconds).expect("valid offset")) + .replace_offset(time::UtcOffset::from_whole_seconds(time.offset).expect("valid offset")) } diff --git a/gitoxide-core/src/commitgraph/list.rs b/gitoxide-core/src/commitgraph/list.rs new file mode 100644 index 00000000000..5678166aa3b --- /dev/null +++ b/gitoxide-core/src/commitgraph/list.rs @@ -0,0 +1,57 @@ +pub(crate) mod function { + use std::borrow::Cow; + use std::ffi::OsString; + + use anyhow::{bail, Context}; + use gix::prelude::ObjectIdExt; + use gix::traverse::commit::Sorting; + + use crate::OutputFormat; + + pub fn list( + mut repo: gix::Repository, + spec: OsString, + mut out: impl std::io::Write, + format: OutputFormat, + ) -> anyhow::Result<()> { + if format != OutputFormat::Human { + bail!("Only human output is currently supported"); + } + let graph = repo + .commit_graph() + .context("a commitgraph is required, but none was found")?; + repo.object_cache_size_if_unset(4 * 1024 * 1024); + + let spec = gix::path::os_str_into_bstr(&spec)?; + let id = repo + .rev_parse_single(spec) + .context("Only single revisions are currently supported")?; + let commits = id + .object()? + .peel_to_kind(gix::object::Kind::Commit) + .context("Need commitish as starting point")? + .id() + .ancestors() + .sorting(Sorting::ByCommitTimeNewestFirst) + .all()?; + for commit in commits { + let commit = commit?; + writeln!( + out, + "{} {} {} {}", + commit.id().shorten_or_id(), + commit.commit_time.expect("traversal with date"), + commit.parent_ids.len(), + graph.commit_by_id(commit.id).map_or_else( + || Cow::Borrowed(""), + |c| Cow::Owned(format!( + "{} {}", + c.root_tree_id().to_owned().attach(&repo).shorten_or_id(), + c.generation() + )) + ) + )?; + } + Ok(()) + } +} diff --git a/gitoxide-core/src/commitgraph/mod.rs b/gitoxide-core/src/commitgraph/mod.rs index a8118c56ae7..f75e2f7bcf4 100644 --- a/gitoxide-core/src/commitgraph/mod.rs +++ b/gitoxide-core/src/commitgraph/mod.rs @@ -1 +1,5 @@ +pub mod list; +pub use list::function::list; + pub mod verify; +pub use verify::function::verify; diff --git a/gitoxide-core/src/commitgraph/verify.rs b/gitoxide-core/src/commitgraph/verify.rs index aefa581fe8d..4f4b8488083 100644 --- a/gitoxide-core/src/commitgraph/verify.rs +++ b/gitoxide-core/src/commitgraph/verify.rs @@ -1,12 +1,7 @@ -use std::{io, path::Path}; - -use anyhow::{Context as AnyhowContext, Result}; -use gix::commitgraph::Graph; - use crate::OutputFormat; /// A general purpose context for many operations provided here -pub struct Context { +pub struct Context { /// A stream to which to output errors pub err: W2, /// A stream to which to output operation results @@ -14,64 +9,62 @@ pub struct Context { pub output_statistics: Option, } -impl Default for Context, Vec> { - fn default() -> Self { +pub(crate) mod function { + use std::io; + + use crate::commitgraph::verify::Context; + use crate::OutputFormat; + use anyhow::{Context as AnyhowContext, Result}; + + pub fn verify( + repo: gix::Repository, Context { - err: Vec::new(), - out: Vec::new(), - output_statistics: None, - } - } -} + err: _err, + mut out, + output_statistics, + }: Context, + ) -> Result + where + W1: io::Write, + W2: io::Write, + { + let g = repo.commit_graph()?; -pub fn graph_or_file( - path: impl AsRef, - Context { - err: _err, - mut out, - output_statistics, - }: Context, -) -> Result -where - W1: io::Write, - W2: io::Write, -{ - let g = Graph::at(path).with_context(|| "Could not open commit graph")?; + #[allow(clippy::unnecessary_wraps, unknown_lints)] + fn noop_processor(_commit: &gix::commitgraph::file::Commit<'_>) -> std::result::Result<(), std::fmt::Error> { + Ok(()) + } + let stats = g + .verify_integrity(noop_processor) + .with_context(|| "Verification failure")?; - #[allow(clippy::unnecessary_wraps, unknown_lints)] - fn noop_processor(_commit: &gix::commitgraph::file::Commit<'_>) -> std::result::Result<(), std::fmt::Error> { - Ok(()) - } - let stats = g - .verify_integrity(noop_processor) - .with_context(|| "Verification failure")?; + #[cfg_attr(not(feature = "serde"), allow(clippy::single_match))] + match output_statistics { + Some(OutputFormat::Human) => drop(print_human_output(&mut out, &stats)), + #[cfg(feature = "serde")] + Some(OutputFormat::Json) => serde_json::to_writer_pretty(out, &stats)?, + _ => {} + } - #[cfg_attr(not(feature = "serde"), allow(clippy::single_match))] - match output_statistics { - Some(OutputFormat::Human) => drop(print_human_output(&mut out, &stats)), - #[cfg(feature = "serde")] - Some(OutputFormat::Json) => serde_json::to_writer_pretty(out, &stats)?, - _ => {} + Ok(stats) } - Ok(stats) -} + fn print_human_output(out: &mut impl io::Write, stats: &gix::commitgraph::verify::Outcome) -> io::Result<()> { + writeln!(out, "number of commits with the given number of parents")?; + let mut parent_counts: Vec<_> = stats.parent_counts.iter().map(|(a, b)| (*a, *b)).collect(); + parent_counts.sort_by_key(|e| e.0); + for (parent_count, commit_count) in parent_counts.into_iter() { + writeln!(out, "\t{parent_count:>2}: {commit_count}")?; + } + writeln!(out, "\t->: {}", stats.num_commits)?; -fn print_human_output(out: &mut impl io::Write, stats: &gix::commitgraph::verify::Outcome) -> io::Result<()> { - writeln!(out, "number of commits with the given number of parents")?; - let mut parent_counts: Vec<_> = stats.parent_counts.iter().map(|(a, b)| (*a, *b)).collect(); - parent_counts.sort_by_key(|e| e.0); - for (parent_count, commit_count) in parent_counts.into_iter() { - writeln!(out, "\t{parent_count:>2}: {commit_count}")?; - } - writeln!(out, "\t->: {}", stats.num_commits)?; + write!(out, "\nlongest path length between two commits: ")?; + if let Some(n) = stats.longest_path_length { + writeln!(out, "{n}")?; + } else { + writeln!(out, "unknown")?; + } - write!(out, "\nlongest path length between two commits: ")?; - if let Some(n) = stats.longest_path_length { - writeln!(out, "{n}")?; - } else { - writeln!(out, "unknown")?; + Ok(()) } - - Ok(()) } diff --git a/gitoxide-core/src/hours/core.rs b/gitoxide-core/src/hours/core.rs index efb8c4c46d4..0423e10a548 100644 --- a/gitoxide-core/src/hours/core.rs +++ b/gitoxide-core/src/hours/core.rs @@ -24,11 +24,7 @@ pub fn estimate_hours( let hours_for_commits = commits.iter().map(|t| &t.1).rev().tuple_windows().fold( 0_f32, |hours, (cur, next): (&gix::actor::SignatureRef<'_>, &gix::actor::SignatureRef<'_>)| { - let change_in_minutes = (next - .time - .seconds_since_unix_epoch - .saturating_sub(cur.time.seconds_since_unix_epoch)) as f32 - / MINUTES_PER_HOUR; + let change_in_minutes = (next.time.seconds.saturating_sub(cur.time.seconds)) as f32 / MINUTES_PER_HOUR; if change_in_minutes < MAX_COMMIT_DIFFERENCE_IN_MINUTES { hours + change_in_minutes / MINUTES_PER_HOUR } else { diff --git a/gitoxide-core/src/hours/mod.rs b/gitoxide-core/src/hours/mod.rs index e46409e5761..598c92ed772 100644 --- a/gitoxide-core/src/hours/mod.rs +++ b/gitoxide-core/src/hours/mod.rs @@ -95,12 +95,9 @@ where } out.shrink_to_fit(); out.sort_by(|a, b| { - a.1.email.cmp(b.1.email).then( - a.1.time - .seconds_since_unix_epoch - .cmp(&b.1.time.seconds_since_unix_epoch) - .reverse(), - ) + a.1.email + .cmp(b.1.email) + .then(a.1.time.seconds.cmp(&b.1.time.seconds).reverse()) }); Ok(out) }); diff --git a/gitoxide-core/src/repository/revision/list.rs b/gitoxide-core/src/repository/revision/list.rs index b8a7330cdc5..91949503845 100644 --- a/gitoxide-core/src/repository/revision/list.rs +++ b/gitoxide-core/src/repository/revision/list.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use anyhow::{bail, Context}; -use gix::prelude::ObjectIdExt; +use gix::traverse::commit::Sorting; use crate::OutputFormat; @@ -20,14 +20,23 @@ pub fn list( let id = repo .rev_parse_single(spec) .context("Only single revisions are currently supported")?; - let commit_id = id + let commits = id .object()? .peel_to_kind(gix::object::Kind::Commit) .context("Need commitish as starting point")? - .id - .attach(&repo); - for commit in commit_id.ancestors().all()? { - writeln!(out, "{}", commit?.id().to_hex())?; + .id() + .ancestors() + .sorting(Sorting::ByCommitTimeNewestFirst) + .all()?; + for commit in commits { + let commit = commit?; + writeln!( + out, + "{} {} {}", + commit.id().shorten_or_id(), + commit.commit_time.expect("traversal with date"), + commit.parent_ids.len() + )?; } Ok(()) } diff --git a/gix-actor/src/lib.rs b/gix-actor/src/lib.rs index 93d8bdb0fd5..5a4d078be87 100644 --- a/gix-actor/src/lib.rs +++ b/gix-actor/src/lib.rs @@ -9,8 +9,16 @@ #![deny(missing_docs, rust_2018_idioms)] #![forbid(unsafe_code)] +/// The re-exported `bstr` crate. +/// +/// For convenience to allow using `bstr` without adding it to own cargo manifest. +pub use bstr; use bstr::{BStr, BString}; -pub use gix_date::{time::Sign, Time}; +/// The re-exported `gix-date` crate. +/// +/// For convenience to allow using `gix-date` without adding it to own cargo manifest. +pub use gix_date as date; +use gix_date::Time; mod identity; /// diff --git a/gix-actor/src/signature/decode.rs b/gix-actor/src/signature/decode.rs index 6bd7bfed696..e80d42a1322 100644 --- a/gix-actor/src/signature/decode.rs +++ b/gix-actor/src/signature/decode.rs @@ -1,6 +1,8 @@ pub(crate) mod function { use bstr::ByteSlice; use btoi::btoi; + use gix_date::time::Sign; + use gix_date::{OffsetInSeconds, SecondsSinceUnixEpoch, Time}; use nom::{ branch::alt, bytes::complete::{tag, take, take_until, take_while_m_n}, @@ -10,7 +12,7 @@ pub(crate) mod function { IResult, }; - use crate::{IdentityRef, Sign, SignatureRef, Time}; + use crate::{IdentityRef, SignatureRef}; const SPACE: &[u8] = b" "; @@ -25,7 +27,7 @@ pub(crate) mod function { tag(b" "), context("", |i| { terminated(take_until(SPACE), take(1usize))(i).and_then(|(i, v)| { - btoi::(v) + btoi::(v) .map(|v| (i, v)) .map_err(|_| nom::Err::Error(E::from_error_kind(i, nom::error::ErrorKind::MapRes))) }) @@ -33,14 +35,14 @@ pub(crate) mod function { context("+|-", alt((tag(b"-"), tag(b"+")))), context("HH", |i| { take_while_m_n(2usize, 2, is_digit)(i).and_then(|(i, v)| { - btoi::(v) + btoi::(v) .map(|v| (i, v)) .map_err(|_| nom::Err::Error(E::from_error_kind(i, nom::error::ErrorKind::MapRes))) }) }), context("MM", |i| { take_while_m_n(2usize, 2, is_digit)(i).and_then(|(i, v)| { - btoi::(v) + btoi::(v) .map(|v| (i, v)) .map_err(|_| nom::Err::Error(E::from_error_kind(i, nom::error::ErrorKind::MapRes))) }) @@ -58,8 +60,8 @@ pub(crate) mod function { name: identity.name, email: identity.email, time: Time { - seconds_since_unix_epoch: time, - offset_in_seconds: offset, + seconds: time, + offset, sign, }, }, @@ -93,10 +95,12 @@ pub use function::identity; mod tests { mod parse_signature { use bstr::ByteSlice; + use gix_date::time::Sign; + use gix_date::{OffsetInSeconds, SecondsSinceUnixEpoch}; use gix_testtools::to_bstr_err; use nom::IResult; - use crate::{signature, Sign, SignatureRef, Time}; + use crate::{signature, SignatureRef, Time}; fn decode(i: &[u8]) -> IResult<&[u8], SignatureRef<'_>, nom::error::VerboseError<&[u8]>> { signature::decode(i) @@ -105,18 +109,14 @@ mod tests { fn signature( name: &'static str, email: &'static str, - time: u32, + seconds: SecondsSinceUnixEpoch, sign: Sign, - offset: i32, + offset: OffsetInSeconds, ) -> SignatureRef<'static> { SignatureRef { name: name.as_bytes().as_bstr(), email: email.as_bytes().as_bstr(), - time: Time { - seconds_since_unix_epoch: time, - offset_in_seconds: offset, - sign, - }, + time: Time { seconds, offset, sign }, } } diff --git a/gix-actor/tests/signature/mod.rs b/gix-actor/tests/signature/mod.rs index 6bb0da58685..417a76fc342 100644 --- a/gix-actor/tests/signature/mod.rs +++ b/gix-actor/tests/signature/mod.rs @@ -1,6 +1,8 @@ mod write_to { mod invalid { - use gix_actor::{Sign, Signature, Time}; + use gix_actor::Signature; + use gix_date::time::Sign; + use gix_date::Time; #[test] fn name() { @@ -43,8 +45,8 @@ mod write_to { fn default_time() -> Time { Time { - seconds_since_unix_epoch: 0, - offset_in_seconds: 0, + seconds: 0, + offset: 0, sign: Sign::Plus, } } diff --git a/gix-commitgraph/Cargo.toml b/gix-commitgraph/Cargo.toml index 2b827827b62..e6f0fb2add7 100644 --- a/gix-commitgraph/Cargo.toml +++ b/gix-commitgraph/Cargo.toml @@ -31,6 +31,7 @@ document-features = { version = "0.2.0", optional = true } [dev-dependencies] gix-testtools = { path = "../tests/tools" } +gix-date = { path = "../gix-date" } [package.metadata.docs.rs] all-features = true diff --git a/gix-commitgraph/tests/access/mod.rs b/gix-commitgraph/tests/access/mod.rs index 9dc41c5d348..8000cbb4eb0 100644 --- a/gix-commitgraph/tests/access/mod.rs +++ b/gix-commitgraph/tests/access/mod.rs @@ -1,25 +1,78 @@ -use gix_commitgraph::Graph; - -use crate::{check_common, inspect_refs, make_readonly_repo}; +use crate::{check_common, graph_and_expected, graph_and_expected_named}; #[test] -fn single_parent() -> crate::Result { - let repo_dir = make_readonly_repo("single_parent.sh"); - let refs = inspect_refs(&repo_dir, &["parent", "child"]); - let cg = Graph::from_info_dir(repo_dir.join(".git").join("objects").join("info"))?; +fn single_parent() { + let (cg, refs) = graph_and_expected("single_parent.sh", &["parent", "child"]); check_common(&cg, &refs); assert_eq!(cg.commit_at(refs["parent"].pos()).generation(), 1); assert_eq!(cg.commit_at(refs["child"].pos()).generation(), 2); +} + +#[test] +fn single_commit_huge_dates_generation_v2_also_do_not_allow_huge_dates() { + let (cg, refs) = graph_and_expected_named("single_commit_huge_dates.sh", "v2", &["HEAD"]); + let info = &refs["HEAD"]; + let actual = cg.commit_by_id(info.id).expect("present"); + assert_eq!( + actual.committer_timestamp(), + 1, + "overflow happened, can't represent huge dates" + ); + assert_eq!( + info.time.seconds, 68719476737, + "this is the value we would want to see, but it's not possible in V2 either, as that is just about generations" + ); + assert_eq!(actual.generation(), 1, "generations are fine though"); +} - Ok(()) +#[test] +fn single_commit_huge_dates_overflow_v1() { + let (cg, refs) = graph_and_expected_named("single_commit_huge_dates.sh", "v1", &["HEAD"]); + let info = &refs["HEAD"]; + let actual = cg.commit_by_id(info.id).expect("present"); + assert_eq!(actual.committer_timestamp(), 1, "overflow happened"); + assert_eq!( + info.time.seconds, 68719476737, + "this is the value we would want to see, but it's not possible in V1" + ); + assert_eq!(actual.generation(), 1, "generations are fine though"); } #[test] -fn octupus_merges() -> crate::Result { - let repo_dir = make_readonly_repo("octopus_merges.sh"); - let refs = inspect_refs( - &repo_dir, +fn single_commit_future_64bit_dates_work() { + let (cg, refs) = graph_and_expected_named("single_commit_huge_dates.sh", "max-date", &["HEAD"]); + let info = &refs["HEAD"]; + let actual = cg.commit_by_id(info.id).expect("present"); + assert_eq!( + actual.committer_timestamp(), + info.time.seconds, + "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" + ); + assert_eq!(actual.generation(), 1); +} + +#[test] +fn generation_numbers_overflow_is_handled_in_chained_graph() { + let names = ["extra", "old-2", "future-2", "old-1", "future-1"]; + let (cg, mut refs) = graph_and_expected("generation_number_overflow.sh", &names); + for (r, expected) in names + .iter() + .map(|n| refs.remove(n.to_owned()).expect("present")) + .zip((1..=5).rev()) + { + assert_eq!( + cg.commit_by_id(r.id).expect("present").generation(), + expected, + "actually, this test seems to have valid generation numbers from the get-go. How to repro the actual issue?" + ); + } +} + +#[test] +fn octupus_merges() { + let (cg, refs) = graph_and_expected( + "octopus_merges.sh", &[ "root", "parent1", @@ -30,7 +83,6 @@ fn octupus_merges() -> crate::Result { "four_parents", ], ); - let cg = Graph::at(repo_dir.join(".git").join("objects").join("info"))?; check_common(&cg, &refs); assert_eq!(cg.commit_at(refs["root"].pos()).generation(), 1); @@ -40,32 +92,22 @@ fn octupus_merges() -> crate::Result { assert_eq!(cg.commit_at(refs["parent4"].pos()).generation(), 2); assert_eq!(cg.commit_at(refs["three_parents"].pos()).generation(), 3); assert_eq!(cg.commit_at(refs["four_parents"].pos()).generation(), 3); - - Ok(()) } #[test] -fn single_commit() -> crate::Result { - let repo_dir = make_readonly_repo("single_commit.sh"); - let refs = inspect_refs(&repo_dir, &["commit"]); - let cg = gix_commitgraph::at(repo_dir.join(".git").join("objects").join("info"))?; +fn single_commit() { + let (cg, refs) = graph_and_expected("single_commit.sh", &["commit"]); check_common(&cg, &refs); assert_eq!(cg.commit_at(refs["commit"].pos()).generation(), 1); - - Ok(()) } #[test] -fn two_parents() -> crate::Result { - let repo_dir = make_readonly_repo("two_parents.sh"); - let refs = inspect_refs(&repo_dir, &["parent1", "parent2", "child"]); - let cg = Graph::from_info_dir(repo_dir.join(".git").join("objects").join("info"))?; +fn two_parents() { + let (cg, refs) = graph_and_expected("two_parents.sh", &["parent1", "parent2", "child"]); check_common(&cg, &refs); assert_eq!(cg.commit_at(refs["parent1"].pos()).generation(), 1); assert_eq!(cg.commit_at(refs["parent2"].pos()).generation(), 1); assert_eq!(cg.commit_at(refs["child"].pos()).generation(), 2); - - Ok(()) } diff --git a/gix-commitgraph/tests/commitgraph.rs b/gix-commitgraph/tests/commitgraph.rs index f098e1c65ca..e4516c24f1f 100644 --- a/gix-commitgraph/tests/commitgraph.rs +++ b/gix-commitgraph/tests/commitgraph.rs @@ -8,12 +8,13 @@ use std::{ }; use gix_commitgraph::{Graph, Position as GraphPosition}; - -type Result = std::result::Result<(), Box>; +use gix_testtools::scripted_fixture_read_only; mod access; pub fn check_common(cg: &Graph, expected: &HashMap) { + cg.verify_integrity(|_| Ok::<_, std::convert::Infallible>(())) + .expect("graph is valid"); assert_eq!( usize::try_from(cg.num_commits()).expect("an architecture able to hold 32 bits of integer"), expected.len() @@ -39,6 +40,7 @@ pub fn check_common(cg: &Graph, expected: &HashMap std::path::PathBuf { - scripted_fixture_read_only(script_path).expect("script succeeds all the time") +pub fn graph_and_expected( + script_path: &str, + refs: &[&'static str], +) -> (gix_commitgraph::Graph, HashMap) { + graph_and_expected_named(script_path, "", refs) +} + +pub fn graph_and_expected_named( + script_path: &str, + name: &str, + refs: &[&'static str], +) -> (gix_commitgraph::Graph, HashMap) { + let repo_dir = scripted_fixture_read_only(script_path) + .expect("script succeeds all the time") + .join(name); + let expected = inspect_refs(&repo_dir, refs); + let cg = Graph::from_info_dir(repo_dir.join(".git").join("objects").join("info")).expect("graph present and valid"); + (cg, expected) } pub struct RefInfo { id: gix_hash::ObjectId, + pub time: gix_date::Time, parent_ids: Vec, pos: GraphPosition, root_tree_id: gix_hash::ObjectId, @@ -89,13 +107,13 @@ impl RefInfo { } } -pub fn inspect_refs(repo_dir: impl AsRef, refs: &[&'static str]) -> HashMap { +fn inspect_refs(repo_dir: impl AsRef, refs: &[&'static str]) -> HashMap { let output = Command::new("git") .arg("-C") .arg(repo_dir.as_ref()) .arg("show") .arg("--no-patch") - .arg("--pretty=format:%S %H %T %P") + .arg("--pretty=format:%S %H %T %ct %P") .args(refs) .arg("--") .env_remove("GIT_DIR") @@ -111,7 +129,8 @@ pub fn inspect_refs(repo_dir: impl AsRef, refs: &[&'static str]) -> HashMa parts[0].to_string(), gix_hash::ObjectId::from_hex(parts[1].as_bytes()).expect("40 bytes hex"), gix_hash::ObjectId::from_hex(parts[2].as_bytes()).expect("40 bytes hex"), - parts[3..] + gix_date::Time::new(parts[3].parse().expect("valid stamp"), 0), + parts[4..] .iter() .map(|x| gix_hash::ObjectId::from_hex(x.as_bytes()).expect("40 bytes hex")) .collect(), @@ -132,13 +151,14 @@ pub fn inspect_refs(repo_dir: impl AsRef, refs: &[&'static str]) -> HashMa infos .iter() .cloned() - .map(|(name, id, root_tree_id, parent_ids)| { + .map(|(name, id, root_tree_id, time, parent_ids)| { ( name, RefInfo { id, parent_ids, root_tree_id, + time, pos: get_pos(&id), }, ) diff --git a/gix-commitgraph/tests/fixtures/generated-archives/generation_number_overflow.tar.xz b/gix-commitgraph/tests/fixtures/generated-archives/generation_number_overflow.tar.xz new file mode 100644 index 00000000000..53e2d7a5279 --- /dev/null +++ b/gix-commitgraph/tests/fixtures/generated-archives/generation_number_overflow.tar.xz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:266655a2777562f495bfe6a6f9d57bef7d13fe3ff012a9a97402ca7f8801dccf +size 12504 diff --git a/gix-commitgraph/tests/fixtures/generated-archives/octopus_merges.tar.xz b/gix-commitgraph/tests/fixtures/generated-archives/octopus_merges.tar.xz index aae2969247c..44fdd5cfc5c 100644 --- a/gix-commitgraph/tests/fixtures/generated-archives/octopus_merges.tar.xz +++ b/gix-commitgraph/tests/fixtures/generated-archives/octopus_merges.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdb7b214315cabdf81173aed5530c2030d3d4f5f2888ebc194f6d1268fca685a -size 11028 +oid sha256:e52a2e28465e3ac6b64cc7d9dac2486a216a9d99175e4ade52f68ff2602ea108 +size 11104 diff --git a/gix-commitgraph/tests/fixtures/generated-archives/single_commit_huge_dates.tar.xz b/gix-commitgraph/tests/fixtures/generated-archives/single_commit_huge_dates.tar.xz new file mode 100644 index 00000000000..8a57755d8bb --- /dev/null +++ b/gix-commitgraph/tests/fixtures/generated-archives/single_commit_huge_dates.tar.xz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39cd8603f0e58ff9cf05173f2edcd9446128461bb647d8045e6d73c86205b141 +size 10900 diff --git a/gix-commitgraph/tests/fixtures/generated-archives/single_parent_huge_dates.tar.xz b/gix-commitgraph/tests/fixtures/generated-archives/single_parent_huge_dates.tar.xz new file mode 100644 index 00000000000..22190de85b5 --- /dev/null +++ b/gix-commitgraph/tests/fixtures/generated-archives/single_parent_huge_dates.tar.xz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:466157da5dcaae21d46aa5a1aa09e72e0c545eafae1c4513cfa75ad94115062f +size 10128 diff --git a/gix-commitgraph/tests/fixtures/generation_number_overflow.sh b/gix-commitgraph/tests/fixtures/generation_number_overflow.sh new file mode 100644 index 00000000000..2592c92d4c9 --- /dev/null +++ b/gix-commitgraph/tests/fixtures/generation_number_overflow.sh @@ -0,0 +1,47 @@ +#!/bin/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 +} + +tick +function commit() { + local message=${1:?first argument is the commit message} + local date=${2:-} + local file="$message.t" + echo "$1" > "$file" + git add -- "$file" + if [ -n "$date" ]; then + export GIT_COMMITTER_DATE="$date" + else + tick + fi + git commit -m "$message" + git tag "$message" +} + +# adapted from git/t/t5318 'lower layers have overflow chunk' +UNIX_EPOCH_ZERO="@0 +0000" +FUTURE_DATE="@4147483646 +0000" + +git init +git config commitGraph.generationVersion 2 + +commit future-1 "$FUTURE_DATE" +commit old-1 "$UNIX_EPOCH_ZERO" +git commit-graph write --reachable +commit future-2 "$FUTURE_DATE" +commit old-2 "$UNIX_EPOCH_ZERO" +git commit-graph write --reachable --split=no-merge +commit extra +# this makes sure it's actually in chain format. +git commit-graph write --reachable --split=no-merge diff --git a/gix-commitgraph/tests/fixtures/single_commit_huge_dates.sh b/gix-commitgraph/tests/fixtures/single_commit_huge_dates.sh new file mode 100644 index 00000000000..19ffba26770 --- /dev/null +++ b/gix-commitgraph/tests/fixtures/single_commit_huge_dates.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -eu -o pipefail + +function setup_repo() { + local version=${1:?need generation version} + local time=${2:?timestamp seconds since unix epoch} + git init -q + + # one past the max 32bit date git can represent + export GIT_COMMITTER_DATE="@${time} +0000" + git config commitGraph.generationVersion ${version} + + git commit -q --allow-empty -m c1 + + git commit-graph write --no-progress --reachable +} + +(mkdir v1 && cd v1 && setup_repo 1 68719476737) # the year 4000 something (overflows in graph) +(mkdir v2 && cd v2 && setup_repo 2 68719476737) +(mkdir max-date && cd max-date && setup_repo 1 17147483646) # the year 2500ish diff --git a/gix-date/src/lib.rs b/gix-date/src/lib.rs index 736f5e59816..997f9167713 100644 --- a/gix-date/src/lib.rs +++ b/gix-date/src/lib.rs @@ -22,9 +22,14 @@ pub use parse::function::parse; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Time { /// time in seconds since epoch. - pub seconds_since_unix_epoch: u32, + pub seconds: SecondsSinceUnixEpoch, /// time offset in seconds, may be negative to match the `sign` field. - pub offset_in_seconds: i32, + pub offset: OffsetInSeconds, /// the sign of `offset`, used to encode `-0000` which would otherwise loose sign information. pub sign: time::Sign, } + +/// The amount of seconds since unix epoch. +pub type SecondsSinceUnixEpoch = u64; +/// time offset in seconds. +pub type OffsetInSeconds = i32; diff --git a/gix-date/src/parse.rs b/gix-date/src/parse.rs index 7038a80fbd1..fc257834e10 100644 --- a/gix-date/src/parse.rs +++ b/gix-date/src/parse.rs @@ -7,7 +7,7 @@ pub enum Error { RelativeTimeConversion, #[error("Date string can not be parsed")] InvalidDateString { input: String }, - #[error("Dates past 2038 can not be represented.")] + #[error("The heat-death of the universe happens before this date")] InvalidDate(#[from] std::num::TryFromIntError), #[error("Current time is missing but required to handle relative dates.")] MissingCurrentTime, @@ -24,7 +24,7 @@ pub(crate) mod function { format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT}, Sign, }, - Time, + SecondsSinceUnixEpoch, Time, }; #[allow(missing_docs)] @@ -47,7 +47,7 @@ pub(crate) mod function { Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) } else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT) { Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) - } else if let Ok(val) = u32::from_str(input) { + } else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) { // Format::Unix Time::new(val, 0) } else if let Some(val) = parse_raw(input) { @@ -60,7 +60,7 @@ pub(crate) mod function { }) } - fn timestamp(date: OffsetDateTime) -> Result { + fn timestamp(date: OffsetDateTime) -> Result { let timestamp = date.unix_timestamp(); if timestamp < 0 { Err(Error::TooEarly { timestamp }) @@ -71,7 +71,7 @@ pub(crate) mod function { fn parse_raw(input: &str) -> Option