From 76d9a428c15b2a29b1437df285e6e26ef486607a Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 15:45:48 +0000 Subject: [PATCH] Initial upload: GitPulse - Developer Productivity Analyzer CLI tool --- src/utils/author.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/utils/author.rs diff --git a/src/utils/author.rs b/src/utils/author.rs new file mode 100644 index 0000000..d13989a --- /dev/null +++ b/src/utils/author.rs @@ -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, +} + +impl AuthorAggregator { + pub fn new() -> Self { + Self { + stats: HashMap::new(), + } + } + + pub fn add_commit( + &mut self, + name: &str, + email: &str, + timestamp: DateTime, + 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() + } +}