working on translating cli options to usable args.

This commit is contained in:
Gabe Venberg 2024-02-11 12:24:46 -06:00
parent d113289fd2
commit 0293018f85
7 changed files with 150 additions and 56 deletions

17
Cargo.lock generated
View file

@ -275,6 +275,7 @@ dependencies = [
"git2", "git2",
"iana-time-zone", "iana-time-zone",
"itertools", "itertools",
"nom",
"once_cell", "once_cell",
"proptest", "proptest",
"rand", "rand",
@ -439,6 +440,22 @@ version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.17" version = "0.2.17"

View file

@ -15,6 +15,7 @@ clap = { version = "4.4.18", features = ["derive"] }
git2 = "0.18.2" git2 = "0.18.2"
iana-time-zone = "0.1.60" iana-time-zone = "0.1.60"
itertools = "0.12.1" itertools = "0.12.1"
nom = "7.1.3"
once_cell = "1.19.0" once_cell = "1.19.0"
proptest = "1.4.0" proptest = "1.4.0"
rand = {version="0.8.5", features=["small_rng"]} rand = {version="0.8.5", features=["small_rng"]}

View file

@ -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 clap::Parser;
use git2::Commit;
use git2::Oid;
use git2::Repository;
use git2::Sort;
use crate::time::DateTimeRange;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(author, version, about)] #[command(author, version, about)]
@ -19,7 +29,78 @@ pub(crate) struct Cli {
pub(crate) last_commit: Option<String>, pub(crate) last_commit: Option<String>,
/// Timezone to place commits in, will default to local tz. Must be a Tz identifier as listed /// 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 <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>. Defaults to local tz
#[arg(short, long)] #[arg(short, long)]
pub(crate) timezone: Option<chrono_tz::Tz> pub(crate) timezone: Option<chrono_tz::Tz>,
}
pub(crate) struct ProgramOptions {
pub(crate) start_time: DateTime<Tz>,
pub(crate) end_time: DateTime<Tz>,
pub(crate) allowed_times: Vec<DateTimeRange<Tz>>,
pub(crate) first_commit: Oid,
pub(crate) last_commit: Oid,
}
impl TryFrom<Cli> for ProgramOptions {
type Error = GitErrors;
fn try_from(cli: Cli) -> Result<Self, Self::Error> {
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<Tz, String> {
match iana_time_zone::get_timezone() {
Ok(tz_string) => tz_string.parse(),
Err(e) => Err(e.to_string()),
}
} }

View file

@ -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,
}

View file

@ -1,19 +1,12 @@
#![allow(dead_code)]
use chrono::Duration; use chrono::Duration;
use git2::{Commit, Repository}; use git2::{Commit, Repository};
use itertools::Itertools; use itertools::Itertools;
use thiserror::Error; use crate::err::GitErrors;
use crate::time::{distribute_across_ranges_with_jitter, TimeRange}; use crate::time::{distribute_across_ranges_with_jitter, DateTimeRange};
#[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),
}
// might be able to replace with graph_ahead_behind and graph_decendant_of?
/// find the number of commits between start and end, /// find the number of commits between start and end,
/// where end is a parent commit of start /// where end is a parent commit of start
fn get_no_of_commits(start: &Commit, end: &Commit) -> Result<u32, GitErrors> { fn get_no_of_commits(start: &Commit, end: &Commit) -> Result<u32, GitErrors> {
@ -26,21 +19,11 @@ fn get_no_of_commits(start: &Commit, end: &Commit) -> Result<u32, GitErrors> {
// will need to do an in memory rebase, ammending each commit with a new signature. // will need to do an in memory rebase, ammending each commit with a new signature.
pub fn rebase_commits_across_timerange<Tz: chrono::TimeZone>( pub fn rebase_commits_across_timerange<Tz: chrono::TimeZone>(
repo: &Repository, repo: &Repository,
start_sha: &str, start_commit: &Commit,
end_sha: &str, end_commit: &Commit,
ranges: &[TimeRange<Tz>], ranges: &[DateTimeRange<Tz>],
max_jitter: &Duration, max_jitter: &Duration,
) -> Result<(), GitErrors> { ) -> 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 num_commits = get_no_of_commits(&start_commit, &end_commit)?;
let times = distribute_across_ranges_with_jitter(ranges, num_commits, max_jitter); let times = distribute_across_ranges_with_jitter(ranges, num_commits, max_jitter);
todo!() todo!()

View file

@ -10,24 +10,8 @@ use clap::Parser;
fn main() { fn main() {
let cli = args::Cli::parse(); let cli = args::Cli::parse();
println!("{:#?}", cli); 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!( println!(
"start_time = {:#?}, end_time = {:#?}, tz = {:#?}", "start_time = {:#?}, end_time = {:#?}, tz = {:#?}",
start_time, end_time, tz start_time, end_time, tz
) )
} }
fn get_local_timezone() -> Result<Tz, String> {
match iana_time_zone::get_timezone() {
Ok(tz_string) => tz_string.parse(),
Err(e) => Err(e.to_string()),
}
}

View file

@ -1,17 +1,32 @@
#![allow(dead_code)]
use chrono::{DateTime, Duration}; use chrono::{DateTime, Duration};
use rand::{rngs::SmallRng, Rng, SeedableRng}; use rand::{rngs::SmallRng, Rng, SeedableRng};
/// A range of time represented by a start datetime and an end datetime. /// A range of time represented by a start datetime and an end datetime.
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct TimeRange<Tz: chrono::TimeZone> { pub struct DateTimeRange<Tz: chrono::TimeZone> {
pub start: DateTime<Tz>, pub start: DateTime<Tz>,
pub end: DateTime<Tz>, pub end: DateTime<Tz>,
} }
impl<Tz: chrono::TimeZone> TimeRange<Tz> { impl<Tz: chrono::TimeZone> DateTimeRange<Tz> {
/// calculates the time range that is part of both time ranges, if any
pub fn intersection(
&self,
other: &DateTimeRange<Tz>,
) -> Option<DateTimeRange<Tz>> {
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 /// whether a time instance lies within the time range
pub fn is_in_range<Otz: chrono::TimeZone>(&self, other: &DateTime<Otz>) -> bool { pub fn is_in_range<Otz: chrono::TimeZone>(&self, other: &DateTime<Otz>) -> bool {
(self.start <= *other) && (other <= &self.end) (&self.start <= other) && (other <= &self.end)
} }
/// the time between the start and end of the time range. /// the time between the start and end of the time range.
@ -52,7 +67,7 @@ impl<Tz: chrono::TimeZone> TimeRange<Tz> {
/// Distribute a number of events across a set of time ranges evenly, /// Distribute a number of events across a set of time ranges evenly,
/// applying a specific amount of jitter. /// applying a specific amount of jitter.
pub fn distribute_across_ranges_with_jitter<Tz: chrono::TimeZone>( pub fn distribute_across_ranges_with_jitter<Tz: chrono::TimeZone>(
ranges: &[TimeRange<Tz>], ranges: &[DateTimeRange<Tz>],
number: u32, number: u32,
max_jitter: &Duration, max_jitter: &Duration,
) -> Vec<DateTime<Tz>> { ) -> Vec<DateTime<Tz>> {
@ -60,7 +75,7 @@ pub fn distribute_across_ranges_with_jitter<Tz: chrono::TimeZone>(
let total_duration: Duration = ranges let total_duration: Duration = ranges
.iter() .iter()
.fold(Duration::zero(), |a, r| a + r.duration()); .fold(Duration::zero(), |a, r| a + r.duration());
let compressed_time = TimeRange { let compressed_time = DateTimeRange {
start: ranges[0].start.clone(), start: ranges[0].start.clone(),
end: ranges[0].start.clone() + total_duration, end: ranges[0].start.clone() + total_duration,
}; };
@ -93,7 +108,7 @@ mod tests {
#[test] #[test]
fn test_distribute_evenly() { fn test_distribute_evenly() {
let timerange = TimeRange::<chrono::Utc> { let timerange = DateTimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), 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(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
}; };
@ -111,7 +126,7 @@ mod tests {
#[test] #[test]
fn test_distribute_with_jitter() { fn test_distribute_with_jitter() {
let timerange = TimeRange::<chrono::Utc> { let timerange = DateTimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), 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(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
}; };
@ -128,7 +143,7 @@ mod tests {
#[test] #[test]
fn test_with_extreme_jitter() { fn test_with_extreme_jitter() {
let timerange = TimeRange::<chrono::Utc> { let timerange = DateTimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), 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(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
}; };
@ -142,11 +157,11 @@ mod tests {
#[test] #[test]
fn test_distribute_across_ranges_without_jitter() { fn test_distribute_across_ranges_without_jitter() {
let ranges = vec![ let ranges = vec![
TimeRange::<chrono::Utc> { DateTimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), 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(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
}, },
TimeRange::<chrono::Utc> { DateTimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap(), 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(), end: Utc.with_ymd_and_hms(2024, 1, 6, 0, 0, 0).unwrap(),
}, },
@ -162,11 +177,11 @@ mod tests {
#[test] #[test]
fn test_distribute_across_ranges_with_jitter() { fn test_distribute_across_ranges_with_jitter() {
let ranges = vec![ let ranges = vec![
TimeRange::<chrono::Utc> { DateTimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), 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(), end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
}, },
TimeRange::<chrono::Utc> { DateTimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap(), 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(), end: Utc.with_ymd_and_hms(2024, 1, 6, 0, 0, 0).unwrap(),
}, },