Initial upload: GitPulse - Developer Productivity Analyzer CLI tool
Some checks failed
CI / test (push) Has been cancelled
CI / release (push) Has been cancelled

This commit is contained in:
2026-02-04 15:45:33 +00:00
parent 59035e0144
commit 615365d7e2

200
src/git/filter.rs Normal file
View File

@@ -0,0 +1,200 @@
use crate::git::Commit;
use anyhow::{anyhow, bail, Result};
use chrono::{DateTime, Duration, Local, NaiveDate, ParseError, Utc};
#[derive(Debug, Clone, Copy)]
pub enum TimePeriod {
Days(u32),
Weeks(u32),
Months(u32),
Years(u32),
Custom(DateTime<Utc>, DateTime<Utc>),
}
impl TimePeriod {
pub fn days(n: u32) -> Self {
Self::Days(n)
}
pub fn weeks(n: u32) -> Self {
Self::Weeks(n)
}
pub fn months(n: u32) -> Self {
Self::Months(n)
}
pub fn years(n: u32) -> Self {
Self::Years(n)
}
pub fn custom(start: DateTime<Utc>, end: DateTime<Utc>) -> Result<Self> {
if start > end {
bail!("Start date must be before end date");
}
Ok(Self::Custom(start, end))
}
pub fn to_range(&self) -> (DateTime<Utc>, DateTime<Utc>) {
let now = Utc::now();
match self {
TimePeriod::Days(n) => (now - Duration::days(*n as i64), now),
TimePeriod::Weeks(n) => (now - Duration::weeks(*n as i64), now),
TimePeriod::Months(n) => (now - Duration::days((*n as i64) * 30), now),
TimePeriod::Years(n) => (now - Duration::days((*n as i64) * 365), now),
TimePeriod::Custom(start, end) => (*start, *end),
}
}
}
impl std::str::FromStr for TimePeriod {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.ends_with('d') {
let n: u32 = s[..s.len() - 1].parse()?;
Ok(TimePeriod::Days(n))
} else if s.ends_with('w') {
let n: u32 = s[..s.len() - 1].parse()?;
Ok(TimePeriod::Weeks(n))
} else if s.ends_with('m') {
let n: u32 = s[..s.len() - 1].parse()?;
Ok(TimePeriod::Months(n))
} else if s.ends_with('y') {
let n: u32 = s[..s.len() - 1].parse()?;
Ok(TimePeriod::Years(n))
} else if s.contains(':') {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() == 2 {
let start = NaiveDate::parse_from_str(parts[0], "%Y-%m-%d")?
.and_hms_opt(0, 0, 0)
.unwrap();
let end = NaiveDate::parse_from_str(parts[1], "%Y-%m-%d")?
.and_hms_opt(23, 59, 59)
.unwrap();
return Ok(TimePeriod::Custom(
DateTime::from_utc(start, Utc),
DateTime::from_utc(end, Utc),
));
}
Err(ParseError)
} else {
let date = NaiveDate::parse_from_str(s, "%Y-%m-%d")?
.and_hms_opt(0, 0, 0)
.unwrap();
let start = DateTime::from_utc(date, Utc);
let end = start + Duration::days(1);
Ok(TimePeriod::Custom(start, end))
}
}
}
pub struct TimeFilter {
since: Option<DateTime<Utc>>,
until: Option<DateTime<Utc>>,
period: Option<TimePeriod>,
}
impl TimeFilter {
pub fn new() -> Self {
Self {
since: None,
until: None,
period: None,
}
}
pub fn since(mut self, date: DateTime<Utc>) -> Self {
self.since = Some(date);
self
}
pub fn until(mut self, date: DateTime<Utc>) -> Self {
self.until = Some(date);
self
}
pub fn period(mut self, period: TimePeriod) -> Self {
self.period = Some(period);
self
}
pub fn days_back(mut self, days: u32) -> Self {
let now = Utc::now();
self.since = Some(now - Duration::days(days as i64));
self.until = Some(now);
self
}
pub fn build(self) -> TimeFilter {
let (since, until) = if let Some(period) = self.period {
let (start, end) = period.to_range();
(Some(start), Some(end))
} else {
(self.since, self.until)
};
TimeFilter {
since,
until,
period: None,
}
}
pub fn contains(&self, commit: &Commit) -> bool {
let time = commit.time();
if let Some(since) = self.since {
if time < since {
return false;
}
}
if let Some(until) = self.until {
if time > until {
return false;
}
}
true
}
}
impl Default for TimeFilter {
fn default() -> Self {
Self::new().days_back(30)
}
}
pub fn parse_date(s: &str) -> Result<DateTime<Utc>> {
let formats = [
"%Y-%m-%d",
"%Y/%m/%d",
"%d-%m-%Y",
"%d/%m/%Y",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M:%S",
];
for fmt in &formats {
if let Ok(dt) = chrono::DateTime::parse_from_str(s, fmt) {
return Ok(dt.with_timezone(&Utc));
}
}
if s.ends_with('d') {
let n: u32 = s[..s.len() - 1].parse()?;
let now = Utc::now();
return Ok(now - Duration::days(n as i64));
} else if s.ends_with('w') {
let n: u32 = s[..s.len() - 1].parse()?;
let now = Utc::now();
return Ok(now - Duration::weeks(n as i64));
} else if s.ends_with('m') {
let n: u32 = s[..s.len() - 1].parse()?;
let now = Utc::now();
return Ok(now - Duration::days((n as i64) * 30));
} else if s.ends_with('y') {
let n: u32 = s[..s.len() - 1].parse()?;
let now = Utc::now();
return Ok(now - Duration::days((n as i64) * 365));
}
Err(anyhow!("Cannot parse date: {}", s))
}