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
27pub 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
38pub fn format_git_error_message() -> &'static str {
40 "❌ Failed to retrieve commits"
41}
42
43pub fn is_stdout_empty(stdout: &str) -> bool {
45 stdout.trim().is_empty()
46}
47
48pub fn format_no_commits_message(since: &str) -> String {
50 format!("✅ No commits found since {since}")
51}
52
53pub 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
66pub 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
79pub 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
86pub fn format_commit_entry(message: &str) -> String {
88 format!(" - {} {}", get_commit_emoji_public(message), message.trim())
89}
90
91pub fn format_commit_meta(author: &str, time: &str) -> String {
93 format!("(by {author}, {time})")
94}
95
96pub 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
109pub fn format_summary_header(since: &str) -> String {
111 format!("🗞️ Commit summary since {since}:\n")
112}
113
114pub fn format_date_header(date: &NaiveDate) -> String {
116 format!("📅 {date}")
117}
118
119pub fn get_commit_emoji_public(message: &str) -> &'static str {
121 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}