1use std::env;
13
14use anyhow::Result;
15use git2::Repository;
16
17use crate::event::GitEvent;
18use crate::export::{
19 coupling_to_json, heatmap_to_json, impact_to_json, log_to_json, stats_to_json,
20};
21use crate::git::{get_commit_files, load_events};
22use crate::stats::{
23 calculate_change_coupling, calculate_file_heatmap, calculate_impact_scores, calculate_stats,
24};
25
26const MAX_LOG_LIMIT: usize = 10000;
28
29const DEFAULT_LOG_LIMIT: usize = 10;
31
32const MAX_EVENTS_FOR_STATS: usize = 2000;
34
35const _: () = {
37 assert!(MAX_LOG_LIMIT <= 10000, "MAX_LOG_LIMIT must be reasonable");
38 assert!(MAX_LOG_LIMIT > 0, "MAX_LOG_LIMIT must be positive");
39 assert!(
40 DEFAULT_LOG_LIMIT <= MAX_LOG_LIMIT,
41 "DEFAULT must not exceed MAX"
42 );
43 assert!(
44 MAX_EVENTS_FOR_STATS <= 10000,
45 "MAX_EVENTS_FOR_STATS must be reasonable"
46 );
47 assert!(
48 MAX_EVENTS_FOR_STATS > 0,
49 "MAX_EVENTS_FOR_STATS must be positive"
50 );
51};
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum CliCommand {
56 Benchmark,
58 Stats,
60 Heatmap,
62 Impact,
64 Coupling,
66 Log { limit: usize },
68 Help,
70 Version,
72}
73
74pub fn parse_cli_args() -> Option<CliCommand> {
83 let args: Vec<String> = env::args().collect();
84
85 if args.len() <= 1 {
87 return None;
88 }
89
90 let mut i = 1;
92 while i < args.len() {
93 match args[i].as_str() {
94 "--benchmark" => return Some(CliCommand::Benchmark),
95 "--stats" => return Some(CliCommand::Stats),
96 "--heatmap" => return Some(CliCommand::Heatmap),
97 "--impact" => return Some(CliCommand::Impact),
98 "--coupling" => return Some(CliCommand::Coupling),
99 "--log" => {
100 let limit = if i + 2 < args.len() && args[i + 1] == "-n" {
102 match args[i + 2].parse::<usize>() {
104 Ok(n) if n > 0 && n <= MAX_LOG_LIMIT => n,
105 Ok(n) if n > MAX_LOG_LIMIT => {
106 eprintln!(
107 "Warning: limit {} exceeds maximum ({}), using maximum",
108 n, MAX_LOG_LIMIT
109 );
110 MAX_LOG_LIMIT
111 }
112 _ => {
113 eprintln!(
114 "Warning: invalid limit value, using default ({})",
115 DEFAULT_LOG_LIMIT
116 );
117 DEFAULT_LOG_LIMIT
118 }
119 }
120 } else {
121 DEFAULT_LOG_LIMIT
122 };
123 return Some(CliCommand::Log { limit });
124 }
125 "--help" | "-h" => return Some(CliCommand::Help),
126 "--version" | "-V" => return Some(CliCommand::Version),
127 _ => {}
128 }
129 i += 1;
130 }
131
132 None
133}
134
135pub fn run_cli_mode(command: CliCommand) -> Result<()> {
137 match command {
138 CliCommand::Benchmark => {
139 Ok(())
142 }
143 CliCommand::Stats => run_stats(),
144 CliCommand::Heatmap => run_heatmap(),
145 CliCommand::Impact => run_impact(),
146 CliCommand::Coupling => run_coupling(),
147 CliCommand::Log { limit } => run_log(limit),
148 CliCommand::Help => run_help(),
149 CliCommand::Version => run_version(),
150 }
151}
152
153fn run_stats() -> Result<()> {
155 let events = load_events_or_error()?;
156 let event_refs: Vec<&GitEvent> = events.iter().collect();
157 let stats = calculate_stats(&event_refs);
158 let json = stats_to_json(&stats)?;
159 println!("{}", json);
160 Ok(())
161}
162
163fn run_heatmap() -> Result<()> {
165 let events = load_events_or_error()?;
166 let event_refs: Vec<&GitEvent> = events.iter().collect();
167 let heatmap = calculate_file_heatmap(&event_refs, |hash| get_commit_files(hash).ok());
168 let json = heatmap_to_json(&heatmap)?;
169 println!("{}", json);
170 Ok(())
171}
172
173fn run_impact() -> Result<()> {
175 let events = load_events_or_error()?;
176 let event_refs: Vec<&GitEvent> = events.iter().collect();
177 let heatmap = calculate_file_heatmap(&event_refs, |hash| get_commit_files(hash).ok());
178 let analysis =
179 calculate_impact_scores(&event_refs, |hash| get_commit_files(hash).ok(), &heatmap);
180 let json = impact_to_json(&analysis)?;
181 println!("{}", json);
182 Ok(())
183}
184
185fn run_coupling() -> Result<()> {
187 let events = load_events_or_error()?;
188 let event_refs: Vec<&GitEvent> = events.iter().collect();
189 let analysis = calculate_change_coupling(
190 &event_refs,
191 |hash| get_commit_files(hash).ok(),
192 5, 0.3, );
195 let json = coupling_to_json(&analysis)?;
196 println!("{}", json);
197 Ok(())
198}
199
200fn run_log(limit: usize) -> Result<()> {
202 let events = load_events_limited(limit)?;
203 let json = log_to_json(&events)?;
204 println!("{}", json);
205 Ok(())
206}
207
208fn run_help() -> Result<()> {
210 let help = format!(
211 r#"gitstack - Git history viewer with insights
212
213USAGE:
214 gitstack [OPTIONS]
215
216OPTIONS:
217 --stats Output author statistics as JSON (stdout)
218 --heatmap Output file heatmap as JSON (stdout)
219 --impact Output Impact Score as JSON (stdout)
220 --coupling Output Change Coupling as JSON (stdout)
221 --log -n N Output latest N commits as JSON (default: {}, max: {})
222 --help, -h Show this help message
223 --version, -V Show version information
224
225Without options, gitstack starts in interactive TUI mode.
226
227EXAMPLES:
228 gitstack --stats | jq .
229 gitstack --heatmap | jq .
230 gitstack --log -n 5 | jq .
231
232For more information, visit: https://github.com/Hiro-Chiba/gitstack"#,
233 DEFAULT_LOG_LIMIT, MAX_LOG_LIMIT
234 );
235
236 println!("{}", help);
237 Ok(())
238}
239
240fn run_version() -> Result<()> {
242 println!("gitstack {}", env!("CARGO_PKG_VERSION"));
243 Ok(())
244}
245
246fn load_events_or_error() -> Result<Vec<GitEvent>> {
253 Repository::discover(".").map_err(|_| {
255 anyhow::anyhow!("Error: Not a git repository (or any of the parent directories)")
256 })?;
257
258 load_events(MAX_EVENTS_FOR_STATS).map_err(|_| {
259 anyhow::anyhow!("Error: Failed to load git history")
261 })
262}
263
264fn load_events_limited(limit: usize) -> Result<Vec<GitEvent>> {
270 Repository::discover(".").map_err(|_| {
272 anyhow::anyhow!("Error: Not a git repository (or any of the parent directories)")
273 })?;
274
275 let safe_limit = limit.min(MAX_LOG_LIMIT);
277
278 load_events(safe_limit).map_err(|_| {
279 anyhow::anyhow!("Error: Failed to load git history")
281 })
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_parse_cli_args_stats() {
290 assert_eq!(CliCommand::Stats, CliCommand::Stats);
292 }
293
294 #[test]
295 fn test_parse_cli_args_help() {
296 assert_eq!(CliCommand::Help, CliCommand::Help);
297 }
298
299 #[test]
300 fn test_parse_cli_args_version() {
301 assert_eq!(CliCommand::Version, CliCommand::Version);
302 }
303
304 #[test]
305 fn test_parse_cli_args_log_default() {
306 let log = CliCommand::Log { limit: 10 };
307 if let CliCommand::Log { limit } = log {
308 assert_eq!(limit, 10);
309 }
310 }
311
312 #[test]
313 fn test_parse_cli_args_log_custom() {
314 let log = CliCommand::Log { limit: 5 };
315 if let CliCommand::Log { limit } = log {
316 assert_eq!(limit, 5);
317 }
318 }
319
320 #[test]
324 fn test_cli_command_log_limit_type_safety() {
325 let log = CliCommand::Log { limit: 0 };
327 if let CliCommand::Log { limit } = log {
328 assert_eq!(limit, 0);
329 }
330 }
331}