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",
"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"

View file

@ -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"]}

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 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<String>,
/// 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)]
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 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<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.
pub fn rebase_commits_across_timerange<Tz: chrono::TimeZone>(
repo: &Repository,
start_sha: &str,
end_sha: &str,
ranges: &[TimeRange<Tz>],
start_commit: &Commit,
end_commit: &Commit,
ranges: &[DateTimeRange<Tz>],
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!()

View file

@ -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<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 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<Tz: chrono::TimeZone> {
pub struct DateTimeRange<Tz: chrono::TimeZone> {
pub start: 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
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.
@ -52,7 +67,7 @@ impl<Tz: chrono::TimeZone> TimeRange<Tz> {
/// 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<Tz: chrono::TimeZone>(
ranges: &[TimeRange<Tz>],
ranges: &[DateTimeRange<Tz>],
number: u32,
max_jitter: &Duration,
) -> Vec<DateTime<Tz>> {
@ -60,7 +75,7 @@ pub fn distribute_across_ranges_with_jitter<Tz: chrono::TimeZone>(
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::<chrono::Utc> {
let timerange = DateTimeRange::<chrono::Utc> {
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::<chrono::Utc> {
let timerange = DateTimeRange::<chrono::Utc> {
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::<chrono::Utc> {
let timerange = DateTimeRange::<chrono::Utc> {
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::<chrono::Utc> {
DateTimeRange::<chrono::Utc> {
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::<chrono::Utc> {
DateTimeRange::<chrono::Utc> {
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::<chrono::Utc> {
DateTimeRange::<chrono::Utc> {
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::<chrono::Utc> {
DateTimeRange::<chrono::Utc> {
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(),
},