1use crate::command::Command;
2use crate::{GitXError, Result};
3use std::collections::BTreeMap;
4use std::process::Command as StdCommand;
5
6use chrono::{NaiveDate, Utc};
7
8pub fn run(since: String) -> Result<()> {
9 let cmd = SummaryCommand;
10 cmd.execute(since)
11}
12
13pub struct SummaryCommand;
15
16impl Command for SummaryCommand {
17 type Input = String;
18 type Output = ();
19
20 fn execute(&self, since: String) -> Result<()> {
21 run_summary(&since)
22 }
23
24 fn name(&self) -> &'static str {
25 "summary"
26 }
27
28 fn description(&self) -> &'static str {
29 "Show a summary of commits since a given date"
30 }
31}
32
33fn run_summary(since: &str) -> Result<()> {
34 let git_log = StdCommand::new("git")
35 .args(get_git_log_summary_args(since))
36 .output()
37 .map_err(GitXError::Io)?;
38
39 if !git_log.status.success() {
40 let stderr = String::from_utf8_lossy(&git_log.stderr);
41 return Err(GitXError::GitCommand(format!(
42 "git log failed: {}",
43 stderr.trim()
44 )));
45 }
46
47 let stdout = String::from_utf8_lossy(&git_log.stdout);
48 if is_stdout_empty(&stdout) {
49 println!("{since}");
50 return Ok(());
51 }
52
53 let grouped = parse_git_log_output(&stdout);
54 print!("{}", format_commit_summary(since, &grouped));
55 Ok(())
56}
57
58fn get_git_log_summary_args(since: &str) -> Vec<&str> {
59 vec![
60 "log",
61 "--since",
62 since,
63 "--pretty=format:%h|%ad|%s|%an|%cr",
64 "--date=short",
65 ]
66}
67
68fn is_stdout_empty(stdout: &str) -> bool {
69 stdout.trim().is_empty()
70}
71
72pub fn parse_git_log_output(stdout: &str) -> BTreeMap<NaiveDate, Vec<String>> {
73 let mut grouped: BTreeMap<NaiveDate, Vec<String>> = BTreeMap::new();
74
75 for line in stdout.lines() {
76 if let Some((date, formatted_commit)) = parse_commit_line(line) {
77 grouped.entry(date).or_default().push(formatted_commit);
78 }
79 }
80
81 grouped
82}
83
84pub fn parse_commit_line(line: &str) -> Option<(NaiveDate, String)> {
85 let parts: Vec<&str> = line.splitn(5, '|').collect();
86 if parts.len() != 5 {
87 return None;
88 }
89
90 let date = parse_commit_date(parts[1])?;
91 let message = parts[2];
92 let entry = format!(" - {} {}", get_commit_emoji_public(message), message.trim());
93 let author = parts[3];
94 let time = parts[4];
95 let meta = format!("(by {author}, {time})");
96 Some((date, format!("{entry} {meta}")))
97}
98
99pub fn parse_commit_date(date_str: &str) -> Option<NaiveDate> {
100 NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
101 .ok()
102 .or_else(|| Some(Utc::now().date_naive()))
103}
104
105pub fn format_commit_summary(since: &str, grouped: &BTreeMap<NaiveDate, Vec<String>>) -> String {
106 let mut result = since.to_string();
107
108 for (date, commits) in grouped.iter().rev() {
109 result.push_str(&date.to_string());
110 result.push('\n');
111 for commit in commits {
112 result.push_str(commit);
113 result.push('\n');
114 }
115 result.push('\n');
116 }
117
118 result
119}
120
121pub fn get_commit_emoji_public(message: &str) -> &'static str {
122 let msg_bytes = message.as_bytes();
124 if msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"fix"))
125 || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"bug"))
126 {
127 "🐛"
128 } else if msg_bytes
129 .windows(4)
130 .any(|w| w.eq_ignore_ascii_case(b"feat"))
131 || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"add"))
132 {
133 "✨"
134 } else if msg_bytes
135 .windows(6)
136 .any(|w| w.eq_ignore_ascii_case(b"remove"))
137 || msg_bytes
138 .windows(6)
139 .any(|w| w.eq_ignore_ascii_case(b"delete"))
140 {
141 "🔥"
142 } else if msg_bytes
143 .windows(8)
144 .any(|w| w.eq_ignore_ascii_case(b"refactor"))
145 {
146 "🛠"
147 } else {
148 "🔹"
149 }
150}