git_x/
summary.rs

1use std::collections::BTreeMap;
2use std::process::Command;
3
4use chrono::{NaiveDate, Utc};
5
6pub fn run(since: String) {
7    let git_log = Command::new("git")
8        .args(get_git_log_summary_args(&since))
9        .output()
10        .expect("Failed to run git log");
11
12    if !git_log.status.success() {
13        eprintln!("{}", format_git_error_message());
14        return;
15    }
16
17    let stdout = String::from_utf8_lossy(&git_log.stdout);
18    if is_stdout_empty(&stdout) {
19        println!("{}", format_no_commits_message(&since));
20        return;
21    }
22
23    let grouped = parse_git_log_output(&stdout);
24    print_commit_summary(&since, &grouped);
25}
26
27// Helper function to get git log summary args
28pub fn get_git_log_summary_args(since: &str) -> Vec<&str> {
29    vec![
30        "log",
31        "--since",
32        since,
33        "--pretty=format:%h|%ad|%s|%an|%cr",
34        "--date=short",
35    ]
36}
37
38// Helper function to format git error message
39pub fn format_git_error_message() -> &'static str {
40    "❌ Failed to retrieve commits"
41}
42
43// Helper function to check if stdout is empty
44pub fn is_stdout_empty(stdout: &str) -> bool {
45    stdout.trim().is_empty()
46}
47
48// Helper function to format no commits message
49pub fn format_no_commits_message(since: &str) -> String {
50    format!("✅ No commits found since {since}")
51}
52
53// Helper function to parse git log output
54pub fn parse_git_log_output(stdout: &str) -> BTreeMap<NaiveDate, Vec<String>> {
55    let mut grouped: BTreeMap<NaiveDate, Vec<String>> = BTreeMap::new();
56
57    for line in stdout.lines() {
58        if let Some((date, formatted_commit)) = parse_commit_line(line) {
59            grouped.entry(date).or_default().push(formatted_commit);
60        }
61    }
62
63    grouped
64}
65
66// Helper function to parse a single commit line
67pub fn parse_commit_line(line: &str) -> Option<(NaiveDate, String)> {
68    let parts: Vec<&str> = line.splitn(5, '|').collect();
69    if parts.len() != 5 {
70        return None;
71    }
72
73    let date = parse_commit_date(parts[1])?;
74    let entry = format_commit_entry(parts[2]);
75    let meta = format_commit_meta(parts[3], parts[4]);
76    Some((date, format!("{entry} {meta}")))
77}
78
79// Helper function to parse commit date
80pub fn parse_commit_date(date_str: &str) -> Option<NaiveDate> {
81    NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
82        .ok()
83        .or_else(|| Some(Utc::now().date_naive()))
84}
85
86// Helper function to format commit entry
87pub fn format_commit_entry(message: &str) -> String {
88    format!(" - {} {}", get_commit_emoji_public(message), message.trim())
89}
90
91// Helper function to format commit meta
92pub fn format_commit_meta(author: &str, time: &str) -> String {
93    format!("(by {author}, {time})")
94}
95
96// Helper function to print commit summary
97pub fn print_commit_summary(since: &str, grouped: &BTreeMap<NaiveDate, Vec<String>>) {
98    println!("{}", format_summary_header(since));
99
100    for (date, commits) in grouped.iter().rev() {
101        println!("{}", format_date_header(date));
102        for commit in commits {
103            println!("{commit}");
104        }
105        println!();
106    }
107}
108
109// Helper function to format summary header
110pub fn format_summary_header(since: &str) -> String {
111    format!("🗞️ Commit summary since {since}:\n")
112}
113
114// Helper function to format date header
115pub fn format_date_header(date: &NaiveDate) -> String {
116    format!("📅 {date}")
117}
118
119// Helper function to get emoji for commit message (public version for testing)
120pub fn get_commit_emoji_public(message: &str) -> &'static str {
121    // Use case-insensitive matching without allocation
122    let msg_bytes = message.as_bytes();
123    if msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"fix"))
124        || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"bug"))
125    {
126        "🐛"
127    } else if msg_bytes
128        .windows(4)
129        .any(|w| w.eq_ignore_ascii_case(b"feat"))
130        || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"add"))
131    {
132        "✨"
133    } else if msg_bytes
134        .windows(6)
135        .any(|w| w.eq_ignore_ascii_case(b"remove"))
136        || msg_bytes
137            .windows(6)
138            .any(|w| w.eq_ignore_ascii_case(b"delete"))
139    {
140        "🔥"
141    } else if msg_bytes
142        .windows(8)
143        .any(|w| w.eq_ignore_ascii_case(b"refactor"))
144    {
145        "🛠"
146    } else {
147        "🔹"
148    }
149}