git_adr/cli/
stats.rs

1//! Show ADR statistics.
2
3use anyhow::Result;
4use clap::Args as ClapArgs;
5use colored::Colorize;
6use std::collections::HashMap;
7
8use crate::core::{AdrStatus, ConfigManager, Git, NotesManager};
9
10/// Arguments for the stats command.
11#[derive(ClapArgs, Debug)]
12pub struct Args {
13    /// Output format (text, json).
14    #[arg(long, short, default_value = "text")]
15    pub format: String,
16}
17
18/// Run the stats command.
19///
20/// # Errors
21///
22/// Returns an error if stats fails.
23pub fn run(args: Args) -> Result<()> {
24    let git = Git::new();
25    git.check_repository()?;
26
27    let config = ConfigManager::new(git.clone()).load()?;
28    let notes = NotesManager::new(git, config);
29
30    let adrs = notes.list()?;
31
32    // Calculate statistics
33    let total = adrs.len();
34
35    // Count by status
36    let mut by_status: HashMap<AdrStatus, usize> = HashMap::new();
37    for adr in &adrs {
38        *by_status.entry(adr.frontmatter.status.clone()).or_insert(0) += 1;
39    }
40
41    // Count by tag
42    let mut by_tag: HashMap<String, usize> = HashMap::new();
43    for adr in &adrs {
44        for tag in &adr.frontmatter.tags {
45            *by_tag.entry(tag.clone()).or_insert(0) += 1;
46        }
47    }
48
49    // Find date range
50    let dates: Vec<_> = adrs
51        .iter()
52        .filter_map(|a| a.frontmatter.date.as_ref())
53        .collect();
54    let oldest = dates.iter().min_by_key(|d| d.datetime());
55    let newest = dates.iter().max_by_key(|d| d.datetime());
56
57    if args.format.as_str() == "json" {
58        let stats = serde_json::json!({
59            "total": total,
60            "by_status": by_status.iter()
61                .map(|(k, v)| (k.to_string(), *v))
62                .collect::<HashMap<_, _>>(),
63            "by_tag": by_tag,
64            "date_range": {
65                "oldest": oldest.map(|d| d.datetime().to_rfc3339()),
66                "newest": newest.map(|d| d.datetime().to_rfc3339()),
67            }
68        });
69        println!("{}", serde_json::to_string_pretty(&stats)?);
70    } else {
71        println!("{}", "ADR Statistics".bold().underline());
72        println!();
73        println!("{} {}", "Total ADRs:".bold(), total.to_string().cyan());
74        println!();
75
76        // Status breakdown
77        println!("{}", "By Status:".bold());
78        let statuses = [
79            AdrStatus::Proposed,
80            AdrStatus::Accepted,
81            AdrStatus::Rejected,
82            AdrStatus::Deprecated,
83            AdrStatus::Superseded,
84        ];
85        for status in &statuses {
86            let count = by_status.get(status).unwrap_or(&0);
87            let bar = "█".repeat(*count);
88            println!(
89                "  {:12} {} {}",
90                status.to_string(),
91                count.to_string().cyan(),
92                bar.green()
93            );
94        }
95        println!();
96
97        // Tag breakdown (top 10)
98        if !by_tag.is_empty() {
99            println!("{}", "Top Tags:".bold());
100            let mut tags: Vec<_> = by_tag.into_iter().collect();
101            tags.sort_by(|a, b| b.1.cmp(&a.1));
102            for (tag, count) in tags.iter().take(10) {
103                println!("  {} {}", tag.cyan(), count);
104            }
105            println!();
106        }
107
108        // Date range
109        if let (Some(old), Some(new)) = (oldest, newest) {
110            println!("{}", "Date Range:".bold());
111            println!(
112                "  {} {} to {}",
113                "From:".dimmed(),
114                old.datetime().format("%Y-%m-%d"),
115                new.datetime().format("%Y-%m-%d")
116            );
117        }
118    }
119
120    Ok(())
121}