diff --git a/Cargo.lock b/Cargo.lock index bc25918..7ce90cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,7 @@ dependencies = [ "git2", "iana-time-zone", "itertools", + "nom", "once_cell", "proptest", "rand", @@ -439,6 +440,22 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.17" diff --git a/Cargo.toml b/Cargo.toml index aed59ba..230f658 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ clap = { version = "4.4.18", features = ["derive"] } git2 = "0.18.2" iana-time-zone = "0.1.60" itertools = "0.12.1" +nom = "7.1.3" once_cell = "1.19.0" proptest = "1.4.0" rand = {version="0.8.5", features=["small_rng"]} diff --git a/src/args.rs b/src/args.rs index 80d7e5e..a1191c5 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,5 +1,15 @@ -use chrono::{NaiveDateTime, TimeZone}; +#![allow(dead_code)] +use crate::err::GitErrors; +use chrono::prelude::*; +use chrono::NaiveDateTime; +use chrono_tz::Tz; use clap::Parser; +use git2::Commit; +use git2::Oid; +use git2::Repository; +use git2::Sort; + +use crate::time::DateTimeRange; #[derive(Debug, Parser)] #[command(author, version, about)] @@ -19,7 +29,78 @@ pub(crate) struct Cli { pub(crate) last_commit: Option, /// Timezone to place commits in, will default to local tz. Must be a Tz identifier as listed - /// on https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. Defaults to local tz + /// on . Defaults to local tz #[arg(short, long)] - pub(crate) timezone: Option + pub(crate) timezone: Option, +} + +pub(crate) struct ProgramOptions { + pub(crate) start_time: DateTime, + pub(crate) end_time: DateTime, + pub(crate) allowed_times: Vec>, + pub(crate) first_commit: Oid, + pub(crate) last_commit: Oid, +} + +impl TryFrom for ProgramOptions { + type Error = GitErrors; + + fn try_from(cli: Cli) -> Result { + let tz = cli + .timezone + .unwrap_or_else(|| get_local_timezone().expect("Could not get local timezone")); + let start_time = tz.from_local_datetime(&cli.start_time).unwrap(); + let end_time = if let Some(time) = cli.end_time { + tz.from_local_datetime(&time).unwrap() + } else { + Utc::now().with_timezone(&tz) + }; + let repo = if let Ok(repo) = Repository::open_from_env() { + repo + } else { + return Err(GitErrors::NotARepo); + }; + let first_commit = if let Some(sha) = cli.first_commit { + if let Ok(commit) = repo.find_commit_by_prefix(&sha) { + commit + } else { + return Err(GitErrors::NotACommit(sha)); + } + } else { + repo.head() + .expect("could not get head") + .peel_to_commit() + .expect("could not peel head to commit") + } + .id(); + let last_commit = if let Some(sha) = cli.last_commit { + if let Ok(commit) = repo.find_commit_by_prefix(&sha) { + commit.id() + } else { + return Err(GitErrors::NotACommit(sha)); + } + } else { + let mut sorting = Sort::REVERSE; + sorting.insert(Sort::TOPOLOGICAL); + sorting.insert(Sort::TIME); + let mut revwalker = repo.revwalk().unwrap(); + revwalker.set_sorting(sorting).unwrap(); + revwalker.simplify_first_parent().unwrap(); + revwalker.next().unwrap().unwrap() + }; + Ok(ProgramOptions { + start_time, + end_time, + allowed_times: todo!(), + first_commit, + last_commit, + }) + } +} + +fn get_local_timezone() -> Result { + match iana_time_zone::get_timezone() { + Ok(tz_string) => tz_string.parse(), + Err(e) => Err(e.to_string()), + } } diff --git a/src/err.rs b/src/err.rs index 5aed0b5..a80c0c1 100644 --- a/src/err.rs +++ b/src/err.rs @@ -1 +1,14 @@ -use thiserror; +#![allow(dead_code)] +use thiserror::Error; + +#[derive(Debug, Error)] +pub(crate) enum GitErrors { + #[error("end is not a parent of start")] + NotAParent, + + #[error("{0} is not a commit hash in the repo")] + NotACommit(String), + + #[error("Could not find repository in current path")] + NotARepo, +} diff --git a/src/git.rs b/src/git.rs index dcf6179..db0ba62 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,19 +1,12 @@ +#![allow(dead_code)] use chrono::Duration; use git2::{Commit, Repository}; use itertools::Itertools; -use thiserror::Error; +use crate::err::GitErrors; -use crate::time::{distribute_across_ranges_with_jitter, TimeRange}; - -#[derive(Debug, Error)] -pub(crate) enum GitErrors { - #[error("end is not a parent of start")] - NotAParent, - - #[error("{0} is not a commit hash in the repo")] - NotACommit(String), -} +use crate::time::{distribute_across_ranges_with_jitter, DateTimeRange}; +// might be able to replace with graph_ahead_behind and graph_decendant_of? /// find the number of commits between start and end, /// where end is a parent commit of start fn get_no_of_commits(start: &Commit, end: &Commit) -> Result { @@ -26,21 +19,11 @@ fn get_no_of_commits(start: &Commit, end: &Commit) -> Result { // will need to do an in memory rebase, ammending each commit with a new signature. pub fn rebase_commits_across_timerange( repo: &Repository, - start_sha: &str, - end_sha: &str, - ranges: &[TimeRange], + start_commit: &Commit, + end_commit: &Commit, + ranges: &[DateTimeRange], max_jitter: &Duration, ) -> Result<(), GitErrors> { - let start_commit = if let Ok(commit) = repo.find_commit_by_prefix(start_sha) { - commit - } else { - return Err(GitErrors::NotACommit(start_sha.to_string())); - }; - let end_commit = if let Ok(commit) = repo.find_commit_by_prefix(end_sha) { - commit - } else { - return Err(GitErrors::NotACommit(end_sha.to_string())); - }; let num_commits = get_no_of_commits(&start_commit, &end_commit)?; let times = distribute_across_ranges_with_jitter(ranges, num_commits, max_jitter); todo!() diff --git a/src/main.rs b/src/main.rs index ed50bb0..0fdd906 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,24 +10,8 @@ use clap::Parser; fn main() { let cli = args::Cli::parse(); println!("{:#?}", cli); - let tz = cli - .timezone - .unwrap_or_else(|| get_local_timezone().expect("Could not get local timezone")); - let start_time = tz.from_local_datetime(&cli.start_time).unwrap(); - let end_time = if let Some(time) = cli.end_time { - tz.from_local_datetime(&time).unwrap() - } else { - Utc::now().with_timezone(&tz) - }; println!( "start_time = {:#?}, end_time = {:#?}, tz = {:#?}", start_time, end_time, tz ) } - -fn get_local_timezone() -> Result { - match iana_time_zone::get_timezone() { - Ok(tz_string) => tz_string.parse(), - Err(e) => Err(e.to_string()), - } -} diff --git a/src/time.rs b/src/time.rs index 8bda399..7867dc0 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,17 +1,32 @@ +#![allow(dead_code)] use chrono::{DateTime, Duration}; use rand::{rngs::SmallRng, Rng, SeedableRng}; /// A range of time represented by a start datetime and an end datetime. #[derive(Debug, PartialEq, Eq, Clone)] -pub struct TimeRange { +pub struct DateTimeRange { pub start: DateTime, pub end: DateTime, } -impl TimeRange { +impl DateTimeRange { + /// calculates the time range that is part of both time ranges, if any + pub fn intersection( + &self, + other: &DateTimeRange, + ) -> Option> { + if self.is_in_range(&other.start) || self.is_in_range(&other.end) { + Some(DateTimeRange { + start: self.start.max(other.start), + end: self.end.min(other.end), + }) + } else { + None + } + } /// whether a time instance lies within the time range pub fn is_in_range(&self, other: &DateTime) -> bool { - (self.start <= *other) && (other <= &self.end) + (&self.start <= other) && (other <= &self.end) } /// the time between the start and end of the time range. @@ -52,7 +67,7 @@ impl TimeRange { /// Distribute a number of events across a set of time ranges evenly, /// applying a specific amount of jitter. pub fn distribute_across_ranges_with_jitter( - ranges: &[TimeRange], + ranges: &[DateTimeRange], number: u32, max_jitter: &Duration, ) -> Vec> { @@ -60,7 +75,7 @@ pub fn distribute_across_ranges_with_jitter( let total_duration: Duration = ranges .iter() .fold(Duration::zero(), |a, r| a + r.duration()); - let compressed_time = TimeRange { + let compressed_time = DateTimeRange { start: ranges[0].start.clone(), end: ranges[0].start.clone() + total_duration, }; @@ -93,7 +108,7 @@ mod tests { #[test] fn test_distribute_evenly() { - let timerange = TimeRange:: { + let timerange = DateTimeRange:: { start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(), }; @@ -111,7 +126,7 @@ mod tests { #[test] fn test_distribute_with_jitter() { - let timerange = TimeRange:: { + let timerange = DateTimeRange:: { start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(), }; @@ -128,7 +143,7 @@ mod tests { #[test] fn test_with_extreme_jitter() { - let timerange = TimeRange:: { + let timerange = DateTimeRange:: { start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(), }; @@ -142,11 +157,11 @@ mod tests { #[test] fn test_distribute_across_ranges_without_jitter() { let ranges = vec![ - TimeRange:: { + DateTimeRange:: { start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(), }, - TimeRange:: { + DateTimeRange:: { start: Utc.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap(), end: Utc.with_ymd_and_hms(2024, 1, 6, 0, 0, 0).unwrap(), }, @@ -162,11 +177,11 @@ mod tests { #[test] fn test_distribute_across_ranges_with_jitter() { let ranges = vec![ - TimeRange:: { + DateTimeRange:: { start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(), }, - TimeRange:: { + DateTimeRange:: { start: Utc.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap(), end: Utc.with_ymd_and_hms(2024, 1, 6, 0, 0, 0).unwrap(), },