Initial upload: GitPulse - Developer Productivity Analyzer CLI tool
This commit is contained in:
102
src/utils/author.rs
Normal file
102
src/utils/author.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user