use crate::branch::Branch;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RepoStats {
pub total: usize,
pub local: usize,
pub remote: usize,
pub merged: usize,
pub merged_local: usize,
pub merged_remote: usize,
pub unmerged: usize,
pub unmerged_local: usize,
pub unmerged_remote: usize,
pub stale: usize,
pub stale_local: usize,
pub stale_remote: usize,
pub safe_to_delete: usize,
pub safe_local: usize,
pub safe_remote: usize,
pub age_lt7: usize,
pub age_7_30: usize,
pub age_30_90: usize,
pub age_gt90: usize,
pub threshold_days: u32,
}
pub fn compute_stats(branches: &[Branch], threshold_days: u32) -> RepoStats {
let mut s = RepoStats {
threshold_days,
..RepoStats::default()
};
for branch in branches {
s.total += 1;
if branch.is_remote {
s.remote += 1;
} else {
s.local += 1;
}
if branch.is_merged {
s.merged += 1;
if branch.is_remote {
s.merged_remote += 1;
} else {
s.merged_local += 1;
}
} else {
s.unmerged += 1;
if branch.is_remote {
s.unmerged_remote += 1;
} else {
s.unmerged_local += 1;
}
}
let is_stale = branch.age_days >= threshold_days as i64;
if is_stale {
s.stale += 1;
if branch.is_remote {
s.stale_remote += 1;
} else {
s.stale_local += 1;
}
}
if branch.is_merged && is_stale {
s.safe_to_delete += 1;
if branch.is_remote {
s.safe_remote += 1;
} else {
s.safe_local += 1;
}
}
match branch.age_days {
d if d < 7 => s.age_lt7 += 1,
d if d < 30 => s.age_7_30 += 1,
d if d < 90 => s.age_30_90 += 1,
_ => s.age_gt90 += 1,
}
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn test_branch(name: &str, age_days: i64, is_merged: bool, is_remote: bool) -> Branch {
Branch {
name: name.to_string(),
age_days,
is_merged,
is_remote,
last_commit_sha: "abc123".to_string(),
last_commit_date: Utc::now(),
}
}
#[test]
fn test_empty() {
let stats = compute_stats(&[], 30);
assert_eq!(
stats,
RepoStats {
threshold_days: 30,
..Default::default()
}
);
}
#[test]
fn test_local_remote_split() {
let branches = vec![
test_branch("local1", 10, false, false),
test_branch("local2", 10, false, false),
test_branch("origin/remote1", 10, false, true),
];
let stats = compute_stats(&branches, 30);
assert_eq!(stats.total, 3);
assert_eq!(stats.local, 2);
assert_eq!(stats.remote, 1);
}
#[test]
fn test_merged_unmerged_split() {
let branches = vec![
test_branch("a", 10, true, false),
test_branch("b", 10, true, true),
test_branch("c", 10, false, false),
];
let stats = compute_stats(&branches, 30);
assert_eq!(stats.merged, 2);
assert_eq!(stats.merged_local, 1);
assert_eq!(stats.merged_remote, 1);
assert_eq!(stats.unmerged, 1);
assert_eq!(stats.unmerged_local, 1);
assert_eq!(stats.unmerged_remote, 0);
}
#[test]
fn test_stale_threshold() {
let branches = vec![
test_branch("fresh", 29, false, false),
test_branch("exact", 30, false, false),
test_branch("old", 60, false, true),
];
let stats = compute_stats(&branches, 30);
assert_eq!(stats.stale, 2); assert_eq!(stats.stale_local, 1); assert_eq!(stats.stale_remote, 1); }
#[test]
fn test_safe_to_delete_requires_merged_and_stale() {
let branches = vec![
test_branch("merged-fresh", 10, true, false), test_branch("unmerged-stale", 40, false, false), test_branch("safe-local", 40, true, false), test_branch("safe-remote", 50, true, true), ];
let stats = compute_stats(&branches, 30);
assert_eq!(stats.safe_to_delete, 2);
assert_eq!(stats.safe_local, 1);
assert_eq!(stats.safe_remote, 1);
}
#[test]
fn test_age_buckets() {
let branches = vec![
test_branch("a", 3, false, false), test_branch("b", 6, false, false), test_branch("c", 7, false, false), test_branch("d", 29, false, false), test_branch("e", 30, false, false), test_branch("f", 89, false, false), test_branch("g", 90, false, false), test_branch("h", 200, false, false), ];
let stats = compute_stats(&branches, 30);
assert_eq!(stats.age_lt7, 2);
assert_eq!(stats.age_7_30, 2);
assert_eq!(stats.age_30_90, 2);
assert_eq!(stats.age_gt90, 2);
}
}