1use crate::Result;
2use crate::core::traits::*;
3use crate::core::{git::*, output::*};
4use std::collections::HashMap;
5
6pub struct AnalysisCommands;
8
9impl AnalysisCommands {
10 pub fn summary(since: Option<String>) -> Result<String> {
12 SummaryCommand::new(since).execute()
13 }
14
15 pub fn graph(colored: bool) -> Result<String> {
17 if colored {
18 ColorGraphCommand::new().execute()
19 } else {
20 GraphCommand::new().execute()
21 }
22 }
23
24 pub fn contributors(since: Option<String>) -> Result<String> {
26 ContributorsCommand::new(since).execute()
27 }
28
29 pub fn technical_debt() -> Result<String> {
31 TechnicalDebtCommand::new().execute()
32 }
33
34 pub fn large_files(threshold_mb: Option<f64>, limit: Option<usize>) -> Result<String> {
36 LargeFilesCommand::new(threshold_mb, limit).execute()
37 }
38
39 pub fn since(time_spec: String) -> Result<String> {
41 SinceCommand::new(time_spec).execute()
42 }
43}
44
45pub struct SummaryCommand {
47 since: Option<String>,
48}
49
50impl SummaryCommand {
51 pub fn new(since: Option<String>) -> Self {
52 Self { since }
53 }
54
55 fn get_commit_stats(&self) -> Result<CommitStats> {
56 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
57 let args = if self.since.is_some() {
58 vec!["rev-list", "--count", "--since", since_arg, "HEAD"]
59 } else {
60 vec!["rev-list", "--count", "HEAD"]
61 };
62
63 let count_output = GitOperations::run(&args)?;
64 let total_commits: u32 = count_output.trim().parse().unwrap_or(0);
65
66 Ok(CommitStats {
67 total_commits,
68 period: since_arg.to_string(),
69 })
70 }
71
72 fn get_author_stats(&self) -> Result<Vec<AuthorStats>> {
73 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
74 let args = vec!["shortlog", "-sn", "--since", since_arg];
75
76 let output = GitOperations::run(&args)?;
77 let mut authors = Vec::new();
78
79 for line in output.lines() {
80 if let Some((count_str, name)) = line.trim().split_once('\t') {
81 if let Ok(count) = count_str.trim().parse::<u32>() {
82 authors.push(AuthorStats {
83 name: name.to_string(),
84 commits: count,
85 });
86 }
87 }
88 }
89
90 Ok(authors)
91 }
92
93 fn get_file_stats(&self) -> Result<FileStats> {
94 let output = GitOperations::run(&["ls-files"])?;
95 let total_files = output.lines().count();
96
97 let mut total_lines = 0;
99 if let Ok(wc_output) = GitOperations::run(&["ls-files", "-z"]) {
100 total_lines = wc_output.split('\0').count();
102 }
103
104 Ok(FileStats {
105 total_files,
106 _total_lines: total_lines,
107 })
108 }
109}
110
111impl Command for SummaryCommand {
112 fn execute(&self) -> Result<String> {
113 let mut output = BufferedOutput::new();
114
115 output.add_line("📊 Repository Summary".to_string());
116 output.add_line("=".repeat(50));
117
118 if let Ok(repo_path) = GitOperations::repo_root() {
120 let repo_name = std::path::Path::new(&repo_path)
121 .file_name()
122 .map(|s| s.to_string_lossy().to_string())
123 .unwrap_or_else(|| "Unknown".to_string());
124 output.add_line(format!("🗂️ Repository: {}", Format::bold(&repo_name)));
125 }
126
127 let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
129 output.add_line(format!(
130 "📍 Current branch: {}",
131 Format::bold(¤t_branch)
132 ));
133
134 if let Some(upstream_branch) = upstream {
135 if ahead > 0 || behind > 0 {
136 output.add_line(format!(
137 "🔗 Upstream: {upstream_branch} ({ahead} ahead, {behind} behind)"
138 ));
139 } else {
140 output.add_line(format!("🔗 Upstream: {upstream_branch} (up to date)"));
141 }
142 }
143
144 match self.get_commit_stats() {
146 Ok(stats) => {
147 output.add_line(format!(
148 "📈 Commits ({}): {}",
149 stats.period, stats.total_commits
150 ));
151 }
152 Err(_) => {
153 output.add_line("📈 Commits: Unable to retrieve".to_string());
154 }
155 }
156
157 match self.get_author_stats() {
159 Ok(authors) => {
160 if !authors.is_empty() {
161 output.add_line(format!(
162 "👥 Top contributors ({}): ",
163 self.since.as_deref().unwrap_or("all time")
164 ));
165 for (i, author) in authors.iter().take(5).enumerate() {
166 let prefix = match i {
167 0 => "🥇",
168 1 => "🥈",
169 2 => "🥉",
170 _ => "👤",
171 };
172 output.add_line(format!(
173 " {} {} ({} commits)",
174 prefix, author.name, author.commits
175 ));
176 }
177 }
178 }
179 Err(_) => {
180 output.add_line("👥 Contributors: Unable to retrieve".to_string());
181 }
182 }
183
184 match self.get_file_stats() {
186 Ok(stats) => {
187 output.add_line(format!("📁 Files: {} total", stats.total_files));
188 }
189 Err(_) => {
190 output.add_line("📁 Files: Unable to retrieve".to_string());
191 }
192 }
193
194 Ok(output.content())
195 }
196
197 fn name(&self) -> &'static str {
198 "summary"
199 }
200
201 fn description(&self) -> &'static str {
202 "Generate a summary of repository activity"
203 }
204}
205
206impl GitCommand for SummaryCommand {}
207
208pub struct ColorGraphCommand;
210
211impl Default for ColorGraphCommand {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl ColorGraphCommand {
218 pub fn new() -> Self {
219 Self
220 }
221}
222
223impl Command for ColorGraphCommand {
224 fn execute(&self) -> Result<String> {
225 GitOperations::run(&[
226 "log",
227 "--graph",
228 "--pretty=format:%C(auto)%h%d %s %C(black)%C(bold)%cr",
229 "--abbrev-commit",
230 "--all",
231 "-20", ])
233 }
234
235 fn name(&self) -> &'static str {
236 "color-graph"
237 }
238
239 fn description(&self) -> &'static str {
240 "Show a colored commit graph"
241 }
242}
243
244impl GitCommand for ColorGraphCommand {}
245
246pub struct GraphCommand;
248
249impl Default for GraphCommand {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255impl GraphCommand {
256 pub fn new() -> Self {
257 Self
258 }
259}
260
261impl Command for GraphCommand {
262 fn execute(&self) -> Result<String> {
263 GitOperations::run(&["log", "--graph", "--oneline", "--all", "-20"])
264 }
265
266 fn name(&self) -> &'static str {
267 "graph"
268 }
269
270 fn description(&self) -> &'static str {
271 "Show a simple commit graph"
272 }
273}
274
275impl GitCommand for GraphCommand {}
276
277pub struct ContributorsCommand {
279 since: Option<String>,
280}
281
282impl ContributorsCommand {
283 pub fn new(since: Option<String>) -> Self {
284 Self { since }
285 }
286}
287
288impl Command for ContributorsCommand {
289 fn execute(&self) -> Result<String> {
290 let mut args = vec!["shortlog", "-sn"];
291 if let Some(ref since) = self.since {
292 args.extend_from_slice(&["--since", since]);
293 }
294
295 let output = GitOperations::run(&args)?;
296 let mut result = String::new();
297
298 result.push_str("👥 Contributors:\n");
299 result.push_str(&"=".repeat(30));
300 result.push('\n');
301
302 for (i, line) in output.lines().enumerate() {
303 if let Some((count, name)) = line.trim().split_once('\t') {
304 let prefix = match i {
305 0 => "🥇",
306 1 => "🥈",
307 2 => "🥉",
308 _ => "👤",
309 };
310 result.push_str(&format!("{prefix} {name} ({count} commits)\n"));
311 }
312 }
313
314 Ok(result)
315 }
316
317 fn name(&self) -> &'static str {
318 "contributors"
319 }
320
321 fn description(&self) -> &'static str {
322 "Show contributor statistics"
323 }
324}
325
326impl GitCommand for ContributorsCommand {}
327
328pub struct TechnicalDebtCommand;
330
331impl Default for TechnicalDebtCommand {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337impl TechnicalDebtCommand {
338 pub fn new() -> Self {
339 Self
340 }
341
342 fn analyze_file_churn(&self) -> Result<Vec<FileChurn>> {
343 let output = GitOperations::run(&[
344 "log",
345 "--name-only",
346 "--pretty=format:",
347 "--since=3 months ago",
348 ])?;
349
350 let mut file_changes: HashMap<String, u32> = HashMap::new();
351
352 for line in output.lines() {
353 let line = line.trim();
354 if !line.is_empty() && !line.starts_with("commit") {
355 *file_changes.entry(line.to_string()).or_insert(0) += 1;
356 }
357 }
358
359 let mut churns: Vec<FileChurn> = file_changes
360 .into_iter()
361 .map(|(file, changes)| FileChurn { file, changes })
362 .collect();
363
364 churns.sort_by(|a, b| b.changes.cmp(&a.changes));
365 churns.truncate(10); Ok(churns)
368 }
369
370 fn find_large_files(&self) -> Result<Vec<LargeFile>> {
371 let output = GitOperations::run(&["ls-files"])?;
373 let mut large_files = Vec::new();
374
375 for file in output.lines() {
376 if let Ok(metadata) = std::fs::metadata(file) {
377 let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
378 if size_mb > 1.0 {
379 large_files.push(LargeFile {
381 path: file.to_string(),
382 size_mb,
383 });
384 }
385 }
386 }
387
388 large_files.sort_by(|a, b| b.size_mb.partial_cmp(&a.size_mb).unwrap());
389 large_files.truncate(10);
390
391 Ok(large_files)
392 }
393}
394
395impl Command for TechnicalDebtCommand {
396 fn execute(&self) -> Result<String> {
397 let mut output = BufferedOutput::new();
398
399 output.add_line("🔧 Technical Debt Analysis".to_string());
400 output.add_line("=".repeat(50));
401
402 match self.analyze_file_churn() {
404 Ok(churns) if !churns.is_empty() => {
405 output.add_line("📊 Most frequently changed files (last 3 months):".to_string());
406 for churn in churns {
407 output.add_line(format!(" 📁 {} ({} changes)", churn.file, churn.changes));
408 }
409 output.add_line("".to_string());
410 }
411 _ => {
412 output.add_line("📊 File churn: No data available".to_string());
413 }
414 }
415
416 match self.find_large_files() {
418 Ok(large_files) if !large_files.is_empty() => {
419 output.add_line("📦 Large files (>1MB):".to_string());
420 for file in large_files {
421 output.add_line(format!(" 🗃️ {} ({:.2} MB)", file.path, file.size_mb));
422 }
423 }
424 _ => {
425 output.add_line("📦 Large files: None found".to_string());
426 }
427 }
428
429 Ok(output.content())
430 }
431
432 fn name(&self) -> &'static str {
433 "technical-debt"
434 }
435
436 fn description(&self) -> &'static str {
437 "Analyze technical debt indicators"
438 }
439}
440
441impl GitCommand for TechnicalDebtCommand {}
442
443pub struct LargeFilesCommand {
445 threshold_mb: Option<f64>,
446 limit: Option<usize>,
447}
448
449impl LargeFilesCommand {
450 pub fn new(threshold_mb: Option<f64>, limit: Option<usize>) -> Self {
451 Self {
452 threshold_mb,
453 limit,
454 }
455 }
456}
457
458impl Command for LargeFilesCommand {
459 fn execute(&self) -> Result<String> {
460 let threshold = self.threshold_mb.unwrap_or(1.0);
461 let limit = self.limit.unwrap_or(10);
462
463 let output = GitOperations::run(&["ls-files"])?;
464 let mut large_files = Vec::new();
465
466 for file in output.lines() {
467 if let Ok(metadata) = std::fs::metadata(file) {
468 let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
469 if size_mb >= threshold {
470 large_files.push(LargeFile {
471 path: file.to_string(),
472 size_mb,
473 });
474 }
475 }
476 }
477
478 large_files.sort_by(|a, b| b.size_mb.partial_cmp(&a.size_mb).unwrap());
479 large_files.truncate(limit);
480
481 if large_files.is_empty() {
482 return Ok(format!("No files larger than {threshold:.1}MB found"));
483 }
484
485 let mut result = format!("📦 Files larger than {threshold:.1}MB:\n");
486 result.push_str(&"=".repeat(40));
487 result.push('\n');
488
489 for file in large_files {
490 result.push_str(&format!("🗃️ {} ({:.2} MB)\n", file.path, file.size_mb));
491 }
492
493 Ok(result)
494 }
495
496 fn name(&self) -> &'static str {
497 "large-files"
498 }
499
500 fn description(&self) -> &'static str {
501 "Find large files in the repository"
502 }
503}
504
505impl GitCommand for LargeFilesCommand {}
506
507pub struct SinceCommand {
509 time_spec: String,
510}
511
512impl SinceCommand {
513 pub fn new(time_spec: String) -> Self {
514 Self { time_spec }
515 }
516}
517
518impl Command for SinceCommand {
519 fn execute(&self) -> Result<String> {
520 let output = GitOperations::run(&["log", "--oneline", "--since", &self.time_spec])?;
521
522 if output.trim().is_empty() {
523 return Ok(format!("No commits found since '{}'", self.time_spec));
524 }
525
526 let mut result = format!("📅 Commits since '{}':\n", self.time_spec);
527 result.push_str(&"=".repeat(40));
528 result.push('\n');
529
530 for line in output.lines() {
531 result.push_str(&format!("• {line}\n"));
532 }
533
534 Ok(result)
535 }
536
537 fn name(&self) -> &'static str {
538 "since"
539 }
540
541 fn description(&self) -> &'static str {
542 "Show commits since a specific time"
543 }
544}
545
546impl GitCommand for SinceCommand {}
547
548#[derive(Debug)]
550struct CommitStats {
551 total_commits: u32,
552 period: String,
553}
554
555#[derive(Debug)]
556struct AuthorStats {
557 name: String,
558 commits: u32,
559}
560
561#[derive(Debug)]
562struct FileStats {
563 total_files: usize,
564 _total_lines: usize,
565}
566
567#[derive(Debug)]
568struct FileChurn {
569 file: String,
570 changes: u32,
571}
572
573#[derive(Debug)]
574struct LargeFile {
575 path: String,
576 size_mb: f64,
577}