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:48 +00:00
parent 14112c10a4
commit 76d9a428c1

102
src/utils/author.rs Normal file
View File

@@ -0,0 +1,102 @@
use crate::models::AuthorStats;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
pub fn normalize_author_identity(name: &str, email: &str) -> String {
let normalized_email = email.to_lowercase();
let normalized_name = name.trim();
let (first, last) = {
let parts: Vec<&str> = normalized_name.split_whitespace().collect();
if parts.len() >= 2 {
(parts[0], parts[parts.len() - 1])
} else {
(normalized_name, "")
}
};
format!("{} <{}>", first, normalized_email)
}
pub struct AuthorAggregator {
pub stats: HashMap<String, AuthorStats>,
}
impl AuthorAggregator {
pub fn new() -> Self {
Self {
stats: HashMap::new(),
}
}
pub fn add_commit(
&mut self,
name: &str,
email: &str,
timestamp: DateTime<Utc>,
changes: Option<(usize, usize, usize)>,
) {
let identity = normalize_author_identity(name, email);
let entry = self.stats.entry(identity.clone()).or_insert_with(|| AuthorStats {
name: {
let parts: Vec<&str> = name.split_whitespace().collect();
parts.first().unwrap_or(&name).to_string()
},
email: email.to_string(),
commits: 0,
lines_added: 0,
lines_removed: 0,
net_change: 0,
files_changed: 0,
first_commit: timestamp.to_rfc3339(),
last_commit: timestamp.to_rfc3339(),
active_days: 0,
average_commits_per_day: 0.0,
busiest_day: String::new(),
busiest_day_count: 0,
commit_messages: Vec::new(),
});
entry.commits += 1;
if let Some((added, removed, files)) = changes {
entry.lines_added += added;
entry.lines_removed += removed;
entry.net_change = entry.lines_added as i64 - entry.lines_removed as i64;
entry.files_changed += files;
}
if timestamp.to_rfc3339() < entry.first_commit {
entry.first_commit = timestamp.to_rfc3339();
}
if timestamp.to_rfc3339() > entry.last_commit {
entry.last_commit = timestamp.to_rfc3339();
}
}
pub fn finalize(&mut self) {
for stats in self.stats.values_mut() {
let first = DateTime::parse_from_rfc3339(&stats.first_commit)
.unwrap_or_else(|_| DateTime::from_timestamp(0, 0).unwrap().into());
let last = DateTime::parse_from_rfc3339(&stats.last_commit)
.unwrap_or_else(|_| DateTime::from_timestamp(0, 0).unwrap().into());
let days = (last - first).num_days() as f64;
stats.average_commits_per_day = if days > 0.0 {
stats.commits as f64 / days
} else {
stats.commits as f64
};
}
let mut authors: Vec<_> = self.stats.values_mut().collect();
authors.sort_by(|a, b| b.commits.cmp(&a.commits));
}
}
impl Default for AuthorAggregator {
fn default() -> Self {
Self::new()
}
}