1use crate::Result;
2use crate::core::git::AsyncGitOperations;
3use crate::core::traits::*;
4use crate::core::{git::*, output::*};
5use chrono::{NaiveDate, Utc};
6use std::collections::{BTreeMap, HashMap};
7
8pub struct AnalysisCommands;
10
11impl AnalysisCommands {
12 pub fn summary(since: Option<String>) -> Result<String> {
14 SummaryCommand::new(since).execute()
15 }
16
17 pub fn graph(colored: bool) -> Result<String> {
19 if colored {
20 ColorGraphCommand::new().execute()
21 } else {
22 GraphCommand::new().execute()
23 }
24 }
25
26 pub fn contributors(since: Option<String>) -> Result<String> {
28 ContributorsCommand::new(since).execute()
29 }
30
31 pub fn technical_debt() -> Result<String> {
33 TechnicalDebtCommand::new().execute()
34 }
35
36 pub fn large_files(threshold_mb: Option<f64>, limit: Option<usize>) -> Result<String> {
38 LargeFilesCommand::new(threshold_mb, limit).execute()
39 }
40
41 pub fn since(time_spec: String) -> Result<String> {
43 SinceCommand::new(time_spec).execute()
44 }
45
46 pub fn what(target: Option<String>) -> Result<String> {
48 WhatCommand::new(target).execute()
49 }
50}
51
52pub struct SummaryCommand {
54 since: Option<String>,
55}
56
57impl SummaryCommand {
58 pub fn new(since: Option<String>) -> Self {
59 Self { since }
60 }
61
62 fn get_commit_stats(&self) -> Result<CommitStats> {
63 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
64 let args = if self.since.is_some() {
65 vec!["rev-list", "--count", "--since", since_arg, "HEAD"]
66 } else {
67 vec!["rev-list", "--count", "HEAD"]
68 };
69
70 let count_output = GitOperations::run(&args)?;
71 let total_commits: u32 = count_output.trim().parse().unwrap_or(0);
72
73 Ok(CommitStats {
74 total_commits,
75 period: since_arg.to_string(),
76 })
77 }
78
79 fn get_detailed_commit_summary(&self) -> Result<String> {
80 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
81 let git_log_output = GitOperations::run(&[
82 "log",
83 "--since",
84 since_arg,
85 "--pretty=format:%h|%ad|%s|%an|%cr",
86 "--date=short",
87 ])?;
88
89 if git_log_output.trim().is_empty() {
90 return Ok(format!("š
No commits found since {since_arg}"));
91 }
92
93 let grouped = self.parse_git_log_output(&git_log_output);
94 Ok(self.format_commit_summary(since_arg, &grouped))
95 }
96
97 fn parse_git_log_output(&self, stdout: &str) -> BTreeMap<NaiveDate, Vec<String>> {
98 let mut grouped: BTreeMap<NaiveDate, Vec<String>> = BTreeMap::new();
99
100 for line in stdout.lines() {
101 if let Some((date, formatted_commit)) = self.parse_commit_line(line) {
102 grouped.entry(date).or_default().push(formatted_commit);
103 }
104 }
105
106 grouped
107 }
108
109 fn parse_commit_line(&self, line: &str) -> Option<(NaiveDate, String)> {
110 let parts: Vec<&str> = line.splitn(5, '|').collect();
111 if parts.len() != 5 {
112 return None;
113 }
114
115 let date = self.parse_commit_date(parts[1])?;
116 let message = parts[2];
117 let entry = format!(" - {} {}", self.get_commit_emoji(message), message.trim());
118 let author = parts[3];
119 let time = parts[4];
120 let meta = format!("(by {author}, {time})");
121 Some((date, format!("{entry} {meta}")))
122 }
123
124 fn parse_commit_date(&self, date_str: &str) -> Option<NaiveDate> {
125 NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
126 .ok()
127 .or_else(|| Some(Utc::now().date_naive()))
128 }
129
130 fn format_commit_summary(
131 &self,
132 since: &str,
133 grouped: &BTreeMap<NaiveDate, Vec<String>>,
134 ) -> String {
135 let mut result = format!("š
Commit Summary since {since}:\n");
136 result.push_str(&"=".repeat(50));
137 result.push('\n');
138
139 for (date, commits) in grouped.iter().rev() {
140 result.push_str(&format!("\nš {date}\n"));
141 for commit in commits {
142 result.push_str(commit);
143 result.push('\n');
144 }
145 }
146
147 result
148 }
149
150 fn get_commit_emoji(&self, message: &str) -> &'static str {
151 let msg_bytes = message.as_bytes();
153 if msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"fix"))
154 || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"bug"))
155 {
156 "š"
157 } else if msg_bytes
158 .windows(4)
159 .any(|w| w.eq_ignore_ascii_case(b"feat"))
160 || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"add"))
161 {
162 "āØ"
163 } else if msg_bytes
164 .windows(6)
165 .any(|w| w.eq_ignore_ascii_case(b"remove"))
166 || msg_bytes
167 .windows(6)
168 .any(|w| w.eq_ignore_ascii_case(b"delete"))
169 {
170 "š„"
171 } else if msg_bytes
172 .windows(8)
173 .any(|w| w.eq_ignore_ascii_case(b"refactor"))
174 {
175 "š "
176 } else {
177 "š¹"
178 }
179 }
180
181 fn get_author_stats(&self) -> Result<Vec<AuthorStats>> {
182 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
183 let args = vec!["shortlog", "-sn", "--since", since_arg];
184
185 let output = GitOperations::run(&args)?;
186 let mut authors = Vec::new();
187
188 for line in output.lines() {
189 if let Some((count_str, name)) = line.trim().split_once('\t') {
190 if let Ok(count) = count_str.trim().parse::<u32>() {
191 authors.push(AuthorStats {
192 name: name.to_string(),
193 commits: count,
194 });
195 }
196 }
197 }
198
199 Ok(authors)
200 }
201
202 fn get_file_stats(&self) -> Result<FileStats> {
203 let output = GitOperations::run(&["ls-files"])?;
204 let total_files = output.lines().count();
205
206 let mut total_lines = 0;
208 if let Ok(wc_output) = GitOperations::run(&["ls-files", "-z"]) {
209 total_lines = wc_output.split('\0').count();
211 }
212
213 Ok(FileStats {
214 total_files,
215 _total_lines: total_lines,
216 })
217 }
218}
219
220impl Command for SummaryCommand {
221 fn execute(&self) -> Result<String> {
222 if self.since.is_some() {
224 return self.get_detailed_commit_summary();
225 }
226
227 let mut output = BufferedOutput::new();
229
230 output.add_line("š Repository Summary".to_string());
231 output.add_line("=".repeat(50));
232
233 if let Ok(repo_path) = GitOperations::repo_root() {
235 let repo_name = std::path::Path::new(&repo_path)
236 .file_name()
237 .map(|s| s.to_string_lossy().to_string())
238 .unwrap_or_else(|| "Unknown".to_string());
239 output.add_line(format!("šļø Repository: {}", Format::bold(&repo_name)));
240 }
241
242 let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
244 output.add_line(format!(
245 "š Current branch: {}",
246 Format::bold(¤t_branch)
247 ));
248
249 if let Some(upstream_branch) = upstream {
250 if ahead > 0 || behind > 0 {
251 output.add_line(format!(
252 "š Upstream: {upstream_branch} ({ahead} ahead, {behind} behind)"
253 ));
254 } else {
255 output.add_line(format!("š Upstream: {upstream_branch} (up to date)"));
256 }
257 }
258
259 match self.get_commit_stats() {
261 Ok(stats) => {
262 output.add_line(format!(
263 "š Commits ({}): {}",
264 stats.period, stats.total_commits
265 ));
266 }
267 Err(_) => {
268 output.add_line("š Commits: Unable to retrieve".to_string());
269 }
270 }
271
272 match self.get_author_stats() {
274 Ok(authors) => {
275 if !authors.is_empty() {
276 output.add_line(format!(
277 "š„ Top contributors ({}): ",
278 self.since.as_deref().unwrap_or("all time")
279 ));
280 for (i, author) in authors.iter().take(5).enumerate() {
281 let prefix = match i {
282 0 => "š„",
283 1 => "š„",
284 2 => "š„",
285 _ => "š¤",
286 };
287 output.add_line(format!(
288 " {} {} ({} commits)",
289 prefix, author.name, author.commits
290 ));
291 }
292 }
293 }
294 Err(_) => {
295 output.add_line("š„ Contributors: Unable to retrieve".to_string());
296 }
297 }
298
299 match self.get_file_stats() {
301 Ok(stats) => {
302 output.add_line(format!("š Files: {} total", stats.total_files));
303 }
304 Err(_) => {
305 output.add_line("š Files: Unable to retrieve".to_string());
306 }
307 }
308
309 Ok(output.content())
310 }
311
312 fn name(&self) -> &'static str {
313 "summary"
314 }
315
316 fn description(&self) -> &'static str {
317 "Generate a summary of repository activity"
318 }
319}
320
321impl GitCommand for SummaryCommand {}
322
323pub struct AsyncSummaryCommand {
325 since: Option<String>,
326}
327
328impl AsyncSummaryCommand {
329 pub fn new(since: Option<String>) -> Self {
330 Self { since }
331 }
332
333 pub async fn execute_parallel(&self) -> Result<String> {
334 if self.since.is_some() {
336 return self.get_detailed_commit_summary_async().await;
337 }
338
339 let (
341 repo_root_result,
342 branch_info_result,
343 commit_stats_result,
344 author_stats_result,
345 file_stats_result,
346 ) = tokio::try_join!(
347 AsyncGitOperations::repo_root(),
348 AsyncGitOperations::branch_info_parallel(),
349 self.get_commit_stats_async(),
350 self.get_author_stats_async(),
351 self.get_file_stats_async(),
352 )?;
353
354 let mut output = BufferedOutput::new();
355
356 output.add_line("š Repository Summary".to_string());
357 output.add_line("=".repeat(50));
358
359 let repo_name = std::path::Path::new(&repo_root_result)
361 .file_name()
362 .map(|s| s.to_string_lossy().to_string())
363 .unwrap_or_else(|| "Unknown".to_string());
364 output.add_line(format!("šļø Repository: {}", Format::bold(&repo_name)));
365
366 let (current_branch, upstream, ahead, behind) = branch_info_result;
368 output.add_line(format!(
369 "š Current branch: {}",
370 Format::bold(¤t_branch)
371 ));
372
373 if let Some(upstream_branch) = upstream {
374 if ahead > 0 || behind > 0 {
375 output.add_line(format!(
376 "š Upstream: {upstream_branch} ({ahead} ahead, {behind} behind)"
377 ));
378 } else {
379 output.add_line(format!("š Upstream: {upstream_branch} (up to date)"));
380 }
381 }
382
383 output.add_line(format!(
385 "š Commits ({}): {}",
386 commit_stats_result.period, commit_stats_result.total_commits
387 ));
388
389 if !author_stats_result.is_empty() {
391 output.add_line(format!(
392 "š„ Top contributors ({}): ",
393 self.since.as_deref().unwrap_or("all time")
394 ));
395 for (i, author) in author_stats_result.iter().take(5).enumerate() {
396 let prefix = match i {
397 0 => "š„",
398 1 => "š„",
399 2 => "š„",
400 _ => "š¤",
401 };
402 output.add_line(format!(
403 " {} {} ({} commits)",
404 prefix, author.name, author.commits
405 ));
406 }
407 }
408
409 output.add_line(format!("š Files: {} total", file_stats_result.total_files));
411
412 Ok(output.content())
413 }
414
415 async fn get_detailed_commit_summary_async(&self) -> Result<String> {
416 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
417 let git_log_output = AsyncGitOperations::run(&[
418 "log",
419 "--since",
420 since_arg,
421 "--pretty=format:%h|%ad|%s|%an|%cr",
422 "--date=short",
423 ])
424 .await?;
425
426 if git_log_output.trim().is_empty() {
427 return Ok(format!("š
No commits found since {since_arg}"));
428 }
429
430 let grouped = self.parse_git_log_output(&git_log_output);
431 Ok(self.format_commit_summary(since_arg, &grouped))
432 }
433
434 async fn get_commit_stats_async(&self) -> Result<CommitStats> {
435 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
436 let args = if self.since.is_some() {
437 vec!["rev-list", "--count", "--since", since_arg, "HEAD"]
438 } else {
439 vec!["rev-list", "--count", "HEAD"]
440 };
441
442 let count_output = AsyncGitOperations::run(&args).await?;
443 let total_commits: u32 = count_output.trim().parse().unwrap_or(0);
444
445 Ok(CommitStats {
446 total_commits,
447 period: since_arg.to_string(),
448 })
449 }
450
451 async fn get_author_stats_async(&self) -> Result<Vec<AuthorStats>> {
452 let since_arg = self.since.as_deref().unwrap_or("1 month ago");
453 let args = vec!["shortlog", "-sn", "--since", since_arg];
454
455 let output = AsyncGitOperations::run(&args).await?;
456 let mut authors = Vec::new();
457
458 for line in output.lines() {
459 if let Some((count_str, name)) = line.trim().split_once('\t') {
460 if let Ok(count) = count_str.trim().parse::<u32>() {
461 authors.push(AuthorStats {
462 name: name.to_string(),
463 commits: count,
464 });
465 }
466 }
467 }
468
469 Ok(authors)
470 }
471
472 async fn get_file_stats_async(&self) -> Result<FileStats> {
473 let (output, wc_output) = tokio::try_join!(
474 AsyncGitOperations::run(&["ls-files"]),
475 AsyncGitOperations::run(&["ls-files", "-z"])
476 )?;
477
478 let total_files = output.lines().count();
479 let total_lines = wc_output.split('\0').count();
480
481 Ok(FileStats {
482 total_files,
483 _total_lines: total_lines,
484 })
485 }
486
487 fn parse_git_log_output(
489 &self,
490 stdout: &str,
491 ) -> std::collections::BTreeMap<chrono::NaiveDate, Vec<String>> {
492 use std::collections::BTreeMap;
493 let mut grouped: BTreeMap<chrono::NaiveDate, Vec<String>> = BTreeMap::new();
494
495 for line in stdout.lines() {
496 if let Some((date, formatted_commit)) = self.parse_commit_line(line) {
497 grouped.entry(date).or_default().push(formatted_commit);
498 }
499 }
500
501 grouped
502 }
503
504 fn parse_commit_line(&self, line: &str) -> Option<(chrono::NaiveDate, String)> {
505 let parts: Vec<&str> = line.splitn(5, '|').collect();
506 if parts.len() != 5 {
507 return None;
508 }
509
510 let date = self.parse_commit_date(parts[1])?;
511 let message = parts[2];
512 let entry = format!(" - {} {}", self.get_commit_emoji(message), message.trim());
513 let author = parts[3];
514 let time = parts[4];
515 let meta = format!("(by {author}, {time})");
516 Some((date, format!("{entry} {meta}")))
517 }
518
519 fn parse_commit_date(&self, date_str: &str) -> Option<chrono::NaiveDate> {
520 use chrono::{NaiveDate, Utc};
521 NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
522 .ok()
523 .or_else(|| Some(Utc::now().date_naive()))
524 }
525
526 fn format_commit_summary(
527 &self,
528 since: &str,
529 grouped: &std::collections::BTreeMap<chrono::NaiveDate, Vec<String>>,
530 ) -> String {
531 let mut result = format!("š
Commit Summary since {since}:\n");
532 result.push_str(&"=".repeat(50));
533 result.push('\n');
534
535 for (date, commits) in grouped.iter().rev() {
536 result.push_str(&format!("\nš {date}\n"));
537 for commit in commits {
538 result.push_str(commit);
539 result.push('\n');
540 }
541 }
542
543 result
544 }
545
546 fn get_commit_emoji(&self, message: &str) -> &'static str {
547 let msg_bytes = message.as_bytes();
549 if msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"fix"))
550 || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"bug"))
551 {
552 "š"
553 } else if msg_bytes
554 .windows(4)
555 .any(|w| w.eq_ignore_ascii_case(b"feat"))
556 || msg_bytes.windows(3).any(|w| w.eq_ignore_ascii_case(b"add"))
557 {
558 "āØ"
559 } else if msg_bytes
560 .windows(6)
561 .any(|w| w.eq_ignore_ascii_case(b"remove"))
562 || msg_bytes
563 .windows(6)
564 .any(|w| w.eq_ignore_ascii_case(b"delete"))
565 {
566 "š„"
567 } else if msg_bytes
568 .windows(8)
569 .any(|w| w.eq_ignore_ascii_case(b"refactor"))
570 {
571 "š "
572 } else {
573 "š¹"
574 }
575 }
576}
577
578pub struct ColorGraphCommand;
580
581impl Default for ColorGraphCommand {
582 fn default() -> Self {
583 Self::new()
584 }
585}
586
587impl ColorGraphCommand {
588 pub fn new() -> Self {
589 Self
590 }
591}
592
593impl Command for ColorGraphCommand {
594 fn execute(&self) -> Result<String> {
595 GitOperations::run(&[
596 "log",
597 "--graph",
598 "--pretty=format:%C(auto)%h%d %s %C(black)%C(bold)%cr",
599 "--abbrev-commit",
600 "--all",
601 "-20", ])
603 }
604
605 fn name(&self) -> &'static str {
606 "color-graph"
607 }
608
609 fn description(&self) -> &'static str {
610 "Show a colored commit graph"
611 }
612}
613
614impl GitCommand for ColorGraphCommand {}
615
616pub struct GraphCommand;
618
619impl Default for GraphCommand {
620 fn default() -> Self {
621 Self::new()
622 }
623}
624
625impl GraphCommand {
626 pub fn new() -> Self {
627 Self
628 }
629}
630
631impl Command for GraphCommand {
632 fn execute(&self) -> Result<String> {
633 GitOperations::run(&["log", "--graph", "--oneline", "--all", "-20"])
634 }
635
636 fn name(&self) -> &'static str {
637 "graph"
638 }
639
640 fn description(&self) -> &'static str {
641 "Show a simple commit graph"
642 }
643}
644
645impl GitCommand for GraphCommand {}
646
647pub struct ContributorsCommand {
649 since: Option<String>,
650}
651
652impl ContributorsCommand {
653 pub fn new(since: Option<String>) -> Self {
654 Self { since }
655 }
656
657 fn get_detailed_contributors(&self) -> Result<Vec<ContributorStats>> {
658 let args = if let Some(ref since) = self.since {
659 vec![
660 "log",
661 "--all",
662 "--format=%ae|%an|%ad",
663 "--date=short",
664 "--since",
665 since,
666 ]
667 } else {
668 vec!["log", "--all", "--format=%ae|%an|%ad", "--date=short"]
669 };
670
671 let output = GitOperations::run(&args)?;
672
673 if output.trim().is_empty() {
674 return Ok(Vec::new());
675 }
676
677 let mut contributors: HashMap<String, ContributorStats> = HashMap::new();
678
679 for line in output.lines() {
680 let parts: Vec<&str> = line.splitn(3, '|').collect();
681 if parts.len() != 3 {
682 continue;
683 }
684
685 let email = parts[0].trim().to_string();
686 let name = parts[1].trim().to_string();
687 let date = parts[2].trim().to_string();
688
689 contributors
690 .entry(email.clone())
691 .and_modify(|stats| {
692 stats.commit_count += 1;
693 if date < stats.first_commit {
694 stats.first_commit = date.clone();
695 }
696 if date > stats.last_commit {
697 stats.last_commit = date.clone();
698 }
699 })
700 .or_insert(ContributorStats {
701 name: name.clone(),
702 email: email.clone(),
703 commit_count: 1,
704 first_commit: date.clone(),
705 last_commit: date,
706 });
707 }
708
709 let mut sorted_contributors: Vec<ContributorStats> = contributors.into_values().collect();
710 sorted_contributors.sort_by(|a, b| b.commit_count.cmp(&a.commit_count));
711
712 Ok(sorted_contributors)
713 }
714}
715
716impl Command for ContributorsCommand {
717 fn execute(&self) -> Result<String> {
718 let contributors = self.get_detailed_contributors()?;
719
720 if contributors.is_empty() {
721 return Ok("š No contributors found in this repository".to_string());
722 }
723
724 let total_commits: usize = contributors.iter().map(|c| c.commit_count).sum();
725 let mut result = String::new();
726
727 let time_period = self.since.as_deref().unwrap_or("all time");
728 result.push_str(&format!(
729 "š Repository Contributors ({total_commits} total commits, {time_period}):\n"
730 ));
731 result.push_str(&"=".repeat(60));
732 result.push('\n');
733
734 for (index, contributor) in contributors.iter().enumerate() {
735 let rank_icon = match index {
736 0 => "š„",
737 1 => "š„",
738 2 => "š„",
739 _ => "š¤",
740 };
741
742 let percentage = (contributor.commit_count as f64 / total_commits as f64) * 100.0;
743
744 result.push_str(&format!(
745 "{} {} {} commits ({:.1}%)\n",
746 rank_icon, contributor.name, contributor.commit_count, percentage
747 ));
748
749 result.push_str(&format!(
750 " š§ {} | š
{} to {}\n",
751 contributor.email, contributor.first_commit, contributor.last_commit
752 ));
753
754 if index < contributors.len() - 1 {
755 result.push('\n');
756 }
757 }
758
759 Ok(result)
760 }
761
762 fn name(&self) -> &'static str {
763 "contributors"
764 }
765
766 fn description(&self) -> &'static str {
767 "Show repository contributors and their commit statistics"
768 }
769}
770
771impl GitCommand for ContributorsCommand {}
772
773pub struct ParallelContributorsCommand {
775 since: Option<String>,
776}
777
778impl ParallelContributorsCommand {
779 pub fn new(since: Option<String>) -> Self {
780 Self { since }
781 }
782
783 pub fn execute_parallel(&self) -> Result<String> {
784 use rayon::prelude::*;
785 use std::collections::HashMap;
786
787 let args = if let Some(ref since) = self.since {
788 vec![
789 "log",
790 "--all",
791 "--format=%ae|%an|%ad",
792 "--date=short",
793 "--since",
794 since,
795 ]
796 } else {
797 vec!["log", "--all", "--format=%ae|%an|%ad", "--date=short"]
798 };
799
800 let output = GitOperations::run(&args)?;
801
802 if output.trim().is_empty() {
803 return Ok("No commits found".to_string());
804 }
805
806 let lines: Vec<&str> = output.lines().collect();
808
809 let contributors: HashMap<String, ContributorStats> = lines
811 .par_iter()
812 .filter_map(|&line| {
813 let parts: Vec<&str> = line.split('|').collect();
814 if parts.len() == 3 {
815 let email = parts[0].trim().to_string();
816 let name = parts[1].trim().to_string();
817 let date = parts[2].trim().to_string();
818
819 Some((
820 email.clone(),
821 ContributorStats {
822 name,
823 email,
824 commit_count: 1,
825 first_commit: date.clone(),
826 last_commit: date,
827 },
828 ))
829 } else {
830 None
831 }
832 })
833 .fold(
834 HashMap::new,
835 |mut acc: HashMap<String, ContributorStats>, (email, stats)| {
836 acc.entry(email)
837 .and_modify(|existing| {
838 existing.commit_count += 1;
839 if stats.first_commit < existing.first_commit {
840 existing.first_commit = stats.first_commit.clone();
841 }
842 if stats.last_commit > existing.last_commit {
843 existing.last_commit = stats.last_commit.clone();
844 }
845 })
846 .or_insert(stats);
847 acc
848 },
849 )
850 .reduce(HashMap::new, |mut acc, map| {
851 for (email, stats) in map {
852 acc.entry(email)
853 .and_modify(|existing| {
854 existing.commit_count += stats.commit_count;
855 if stats.first_commit < existing.first_commit {
856 existing.first_commit = stats.first_commit.clone();
857 }
858 if stats.last_commit > existing.last_commit {
859 existing.last_commit = stats.last_commit.clone();
860 }
861 })
862 .or_insert(stats);
863 }
864 acc
865 });
866
867 let mut sorted_contributors: Vec<_> = contributors.into_values().collect();
869 sorted_contributors.sort_by(|a, b| b.commit_count.cmp(&a.commit_count));
870
871 let mut output = BufferedOutput::new();
873 let period = self.since.as_deref().unwrap_or("all time");
874 output.add_line(format!("š„ Contributors ({period})"));
875 output.add_line("=".repeat(50));
876
877 for (i, contributor) in sorted_contributors.iter().take(20).enumerate() {
878 let rank = match i {
879 0 => "š„",
880 1 => "š„",
881 2 => "š„",
882 _ => "š¤",
883 };
884
885 output.add_line(format!(
886 "{} {} {} commits",
887 rank, contributor.name, contributor.commit_count
888 ));
889
890 output.add_line(format!(
891 " š§ {} | š
{} to {}",
892 contributor.email, contributor.first_commit, contributor.last_commit
893 ));
894 }
895
896 Ok(output.content())
897 }
898}
899
900pub struct TechnicalDebtCommand;
902
903impl Default for TechnicalDebtCommand {
904 fn default() -> Self {
905 Self::new()
906 }
907}
908
909impl TechnicalDebtCommand {
910 pub fn new() -> Self {
911 Self
912 }
913
914 fn analyze_file_churn(&self) -> Result<Vec<FileChurn>> {
915 let output = GitOperations::run(&[
916 "log",
917 "--name-only",
918 "--pretty=format:",
919 "--since=3 months ago",
920 ])?;
921
922 let mut file_changes: HashMap<String, u32> = HashMap::new();
923
924 for line in output.lines() {
925 let line = line.trim();
926 if !line.is_empty() && !line.starts_with("commit") {
927 *file_changes.entry(line.to_string()).or_insert(0) += 1;
928 }
929 }
930
931 let mut churns: Vec<FileChurn> = file_changes
932 .into_iter()
933 .map(|(file, changes)| FileChurn { file, changes })
934 .collect();
935
936 churns.sort_by(|a, b| b.changes.cmp(&a.changes));
937 churns.truncate(10); Ok(churns)
940 }
941
942 fn find_large_files(&self) -> Result<Vec<LargeFile>> {
943 let output = GitOperations::run(&["ls-files"])?;
945 let mut large_files = Vec::new();
946
947 for file in output.lines() {
948 if let Ok(metadata) = std::fs::metadata(file) {
949 let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
950 if size_mb > 1.0 {
951 large_files.push(LargeFile {
953 path: file.to_string(),
954 size_mb,
955 });
956 }
957 }
958 }
959
960 large_files.sort_by(|a, b| b.size_mb.partial_cmp(&a.size_mb).unwrap());
961 large_files.truncate(10);
962
963 Ok(large_files)
964 }
965}
966
967impl Command for TechnicalDebtCommand {
968 fn execute(&self) -> Result<String> {
969 let mut output = BufferedOutput::new();
970
971 output.add_line("š§ Technical Debt Analysis".to_string());
972 output.add_line("=".repeat(50));
973
974 match self.analyze_file_churn() {
976 Ok(churns) if !churns.is_empty() => {
977 output.add_line("š Most frequently changed files (last 3 months):".to_string());
978 for churn in churns {
979 output.add_line(format!(" š {} ({} changes)", churn.file, churn.changes));
980 }
981 output.add_line("".to_string());
982 }
983 _ => {
984 output.add_line("š File churn: No data available".to_string());
985 }
986 }
987
988 match self.find_large_files() {
990 Ok(large_files) if !large_files.is_empty() => {
991 output.add_line("š¦ Large files (>1MB):".to_string());
992 for file in large_files {
993 output.add_line(format!(" šļø {} ({:.2} MB)", file.path, file.size_mb));
994 }
995 }
996 _ => {
997 output.add_line("š¦ Large files: None found".to_string());
998 }
999 }
1000
1001 Ok(output.content())
1002 }
1003
1004 fn name(&self) -> &'static str {
1005 "technical-debt"
1006 }
1007
1008 fn description(&self) -> &'static str {
1009 "Analyze technical debt indicators"
1010 }
1011}
1012
1013impl GitCommand for TechnicalDebtCommand {}
1014
1015pub struct ParallelTechnicalDebtCommand;
1017
1018impl Default for ParallelTechnicalDebtCommand {
1019 fn default() -> Self {
1020 Self::new()
1021 }
1022}
1023
1024impl ParallelTechnicalDebtCommand {
1025 pub fn new() -> Self {
1026 Self
1027 }
1028
1029 pub fn execute_parallel(&self) -> Result<String> {
1030 let ((file_churn_result, large_files_result), old_files_result) = rayon::join(
1032 || {
1033 rayon::join(
1034 || self.analyze_file_churn_parallel(),
1035 || self.analyze_large_files_parallel(),
1036 )
1037 },
1038 || self.analyze_old_files_parallel(),
1039 );
1040
1041 let file_churn = file_churn_result?;
1042 let large_files = large_files_result?;
1043 let old_files = old_files_result?;
1044
1045 let mut output = BufferedOutput::new();
1046
1047 output.add_line("š§ Technical Debt Analysis".to_string());
1048 output.add_line("=".repeat(40));
1049
1050 if !file_churn.is_empty() {
1052 output.add_line("\nš High-churn files (frequently modified):".to_string());
1053 for churn in file_churn.iter().take(10) {
1054 output.add_line(format!(" š {} ({} changes)", churn.file, churn.changes));
1055 }
1056 }
1057
1058 if !large_files.is_empty() {
1060 output.add_line("\nš¦ Large files:".to_string());
1061 for file in large_files.iter().take(10) {
1062 output.add_line(format!(" š {} ({:.1} MB)", file.path, file.size_mb));
1063 }
1064 }
1065
1066 if !old_files.is_empty() {
1068 output.add_line("\nā° Potentially stale files (not modified recently):".to_string());
1069 for file in old_files.iter().take(10) {
1070 output.add_line(format!(" š
{file}"));
1071 }
1072 }
1073
1074 if file_churn.is_empty() && large_files.is_empty() && old_files.is_empty() {
1075 output.add_line("ā
No significant technical debt detected".to_string());
1076 }
1077
1078 Ok(output.content())
1079 }
1080
1081 fn analyze_file_churn_parallel(&self) -> Result<Vec<FileChurn>> {
1082 use rayon::prelude::*;
1083 use std::collections::HashMap;
1084
1085 let output = GitOperations::run(&[
1086 "log",
1087 "--name-only",
1088 "--pretty=format:",
1089 "--since=3 months ago",
1090 ])?;
1091
1092 let lines: Vec<&str> = output.lines().collect();
1093
1094 let file_changes: HashMap<String, u32> = lines
1096 .par_iter()
1097 .filter_map(|&line| {
1098 let line = line.trim();
1099 if !line.is_empty() && !line.starts_with("commit") {
1100 Some((line.to_string(), 1u32))
1101 } else {
1102 None
1103 }
1104 })
1105 .fold(
1106 HashMap::new,
1107 |mut acc: HashMap<String, u32>, (file, count)| {
1108 *acc.entry(file).or_insert(0) += count;
1109 acc
1110 },
1111 )
1112 .reduce(HashMap::new, |mut acc, map| {
1113 for (file, count) in map {
1114 *acc.entry(file).or_insert(0) += count;
1115 }
1116 acc
1117 });
1118
1119 let mut churns: Vec<FileChurn> = file_changes
1120 .into_iter()
1121 .map(|(file, changes)| FileChurn { file, changes })
1122 .collect();
1123
1124 churns.sort_by(|a, b| b.changes.cmp(&a.changes));
1125 churns.retain(|churn| churn.changes > 5); Ok(churns)
1128 }
1129
1130 fn analyze_large_files_parallel(&self) -> Result<Vec<LargeFile>> {
1131 use rayon::prelude::*;
1132
1133 let output = GitOperations::run(&["ls-files"])?;
1134 let files: Vec<&str> = output.lines().collect();
1135
1136 let large_files: Vec<LargeFile> = files
1137 .par_iter()
1138 .filter_map(|&file| {
1139 if let Ok(metadata) = std::fs::metadata(file) {
1140 let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
1141 if size_mb >= 0.5 {
1142 Some(LargeFile {
1144 path: file.to_string(),
1145 size_mb,
1146 })
1147 } else {
1148 None
1149 }
1150 } else {
1151 None
1152 }
1153 })
1154 .collect();
1155
1156 let mut sorted_files = large_files;
1157 sorted_files.sort_by(|a, b| b.size_mb.partial_cmp(&a.size_mb).unwrap());
1158
1159 Ok(sorted_files)
1160 }
1161
1162 fn analyze_old_files_parallel(&self) -> Result<Vec<String>> {
1163 use rayon::prelude::*;
1164
1165 let output = GitOperations::run(&["ls-files"])?;
1166 let files: Vec<&str> = output.lines().collect();
1167
1168 let old_files: Vec<String> = files
1170 .par_iter()
1171 .filter_map(|&file| {
1172 if let Ok(log_output) =
1174 GitOperations::run(&["log", "-1", "--pretty=format:%cr", "--", file])
1175 {
1176 if log_output.contains("months ago") || log_output.contains("year") {
1177 Some(format!("{} (last modified: {})", file, log_output.trim()))
1178 } else {
1179 None
1180 }
1181 } else {
1182 None
1183 }
1184 })
1185 .collect();
1186
1187 Ok(old_files)
1188 }
1189}
1190
1191pub struct LargeFilesCommand {
1193 threshold_mb: Option<f64>,
1194 limit: Option<usize>,
1195}
1196
1197impl LargeFilesCommand {
1198 pub fn new(threshold_mb: Option<f64>, limit: Option<usize>) -> Self {
1199 Self {
1200 threshold_mb,
1201 limit,
1202 }
1203 }
1204}
1205
1206impl Command for LargeFilesCommand {
1207 fn execute(&self) -> Result<String> {
1208 let threshold = self.threshold_mb.unwrap_or(1.0);
1209 let limit = self.limit.unwrap_or(10);
1210
1211 let output = GitOperations::run(&["ls-files"])?;
1212 let mut large_files = Vec::new();
1213
1214 for file in output.lines() {
1215 if let Ok(metadata) = std::fs::metadata(file) {
1216 let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
1217 if size_mb >= threshold {
1218 large_files.push(LargeFile {
1219 path: file.to_string(),
1220 size_mb,
1221 });
1222 }
1223 }
1224 }
1225
1226 large_files.sort_by(|a, b| b.size_mb.partial_cmp(&a.size_mb).unwrap());
1227 large_files.truncate(limit);
1228
1229 if large_files.is_empty() {
1230 return Ok(format!("No files larger than {threshold:.1}MB found"));
1231 }
1232
1233 let mut result = format!("š¦ Files larger than {threshold:.1}MB:\n");
1234 result.push_str(&"=".repeat(40));
1235 result.push('\n');
1236
1237 for file in large_files {
1238 result.push_str(&format!("šļø {} ({:.2} MB)\n", file.path, file.size_mb));
1239 }
1240
1241 Ok(result)
1242 }
1243
1244 fn name(&self) -> &'static str {
1245 "large-files"
1246 }
1247
1248 fn description(&self) -> &'static str {
1249 "Find large files in the repository"
1250 }
1251}
1252
1253impl GitCommand for LargeFilesCommand {}
1254
1255pub struct ParallelLargeFilesCommand {
1257 threshold_mb: Option<f64>,
1258 limit: Option<usize>,
1259}
1260
1261impl ParallelLargeFilesCommand {
1262 pub fn new(threshold_mb: Option<f64>, limit: Option<usize>) -> Self {
1263 Self {
1264 threshold_mb,
1265 limit,
1266 }
1267 }
1268
1269 pub fn execute_parallel(&self) -> Result<String> {
1270 use rayon::prelude::*;
1271 let threshold = self.threshold_mb.unwrap_or(1.0);
1272 let limit = self.limit.unwrap_or(10);
1273
1274 let output = GitOperations::run(&["ls-files"])?;
1275 let files: Vec<&str> = output.lines().collect();
1276
1277 let large_files: Vec<LargeFile> = files
1279 .par_iter()
1280 .filter_map(|&file| {
1281 if let Ok(metadata) = std::fs::metadata(file) {
1282 let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
1283 if size_mb >= threshold {
1284 Some(LargeFile {
1285 path: file.to_string(),
1286 size_mb,
1287 })
1288 } else {
1289 None
1290 }
1291 } else {
1292 None
1293 }
1294 })
1295 .collect();
1296
1297 let mut sorted_files = large_files;
1299 sorted_files.sort_by(|a, b| b.size_mb.partial_cmp(&a.size_mb).unwrap());
1300 sorted_files.truncate(limit);
1301
1302 if sorted_files.is_empty() {
1303 return Ok(format!("No files larger than {threshold:.1}MB found"));
1304 }
1305
1306 let mut result = format!("š¦ Files larger than {threshold:.1}MB:\n");
1307 result.push_str(&"=".repeat(40));
1308 result.push('\n');
1309
1310 for file in sorted_files {
1311 result.push_str(&format!("šļø {} ({:.2} MB)\n", file.path, file.size_mb));
1312 }
1313
1314 Ok(result)
1315 }
1316}
1317
1318pub struct SinceCommand {
1320 reference: String,
1321}
1322
1323impl SinceCommand {
1324 pub fn new(reference: String) -> Self {
1325 Self { reference }
1326 }
1327}
1328
1329impl Command for SinceCommand {
1330 fn execute(&self) -> Result<String> {
1331 let log_range = format!("{}..HEAD", self.reference);
1333 if let Ok(output) = GitOperations::run(&["log", &log_range, "--pretty=format:- %h %s"]) {
1334 if !output.trim().is_empty() {
1335 return Ok(format!("š Commits since {}:\n{}", self.reference, output));
1336 } else {
1337 return Ok(format!("ā
No new commits since {}", self.reference));
1338 }
1339 }
1340
1341 let output = GitOperations::run(&["log", "--oneline", "--since", &self.reference])?;
1343
1344 if output.trim().is_empty() {
1345 return Ok(format!("ā
No commits found since '{}'", self.reference));
1346 }
1347
1348 let mut result = format!("š
Commits since '{}':\n", self.reference);
1349 result.push_str(&"=".repeat(50));
1350 result.push('\n');
1351
1352 for line in output.lines() {
1353 result.push_str(&format!("⢠{line}\n"));
1354 }
1355
1356 Ok(result)
1357 }
1358
1359 fn name(&self) -> &'static str {
1360 "since"
1361 }
1362
1363 fn description(&self) -> &'static str {
1364 "Show commits since a reference (e.g., cb676ec, origin/main) or time"
1365 }
1366}
1367
1368impl GitCommand for SinceCommand {}
1369
1370pub struct WhatCommand {
1372 target: Option<String>,
1373}
1374
1375impl WhatCommand {
1376 pub fn new(target: Option<String>) -> Self {
1377 Self { target }
1378 }
1379
1380 fn get_default_target(&self) -> String {
1381 "main".to_string()
1382 }
1383
1384 fn format_branch_comparison(&self, current: &str, target: &str) -> String {
1385 format!(
1386 "š Branch: {} vs {}",
1387 Format::bold(current),
1388 Format::bold(target)
1389 )
1390 }
1391
1392 fn parse_commit_counts(&self, output: &str) -> (String, String) {
1393 let mut counts = output.split_whitespace();
1394 let behind = counts.next().unwrap_or("0").to_string();
1395 let ahead = counts.next().unwrap_or("0").to_string();
1396 (ahead, behind)
1397 }
1398
1399 fn format_commit_counts(&self, ahead: &str, behind: &str) -> (String, String) {
1400 (
1401 format!("š {ahead} commits ahead"),
1402 format!("š {behind} commits behind"),
1403 )
1404 }
1405
1406 fn format_rev_list_range(&self, target: &str, current: &str) -> String {
1407 format!("{target}...{current}")
1408 }
1409
1410 fn git_status_to_symbol(&self, status: &str) -> &'static str {
1411 match status {
1412 "A" => "ā",
1413 "M" => "š",
1414 "D" => "ā",
1415 _ => "ā",
1416 }
1417 }
1418
1419 fn format_diff_line(&self, line: &str) -> Option<String> {
1420 let parts: Vec<&str> = line.split_whitespace().collect();
1421 if parts.len() >= 2 {
1422 let symbol = self.git_status_to_symbol(parts[0]);
1423 Some(format!(" {} {}", symbol, parts[1]))
1424 } else {
1425 None
1426 }
1427 }
1428}
1429
1430impl Command for WhatCommand {
1431 fn execute(&self) -> Result<String> {
1432 let target_branch = self
1433 .target
1434 .clone()
1435 .unwrap_or_else(|| self.get_default_target());
1436
1437 let current_branch = GitOperations::current_branch()?;
1439
1440 let mut output = Vec::new();
1441 output.push(self.format_branch_comparison(¤t_branch, &target_branch));
1442
1443 let rev_list_output = GitOperations::run(&[
1445 "rev-list",
1446 "--left-right",
1447 "--count",
1448 &self.format_rev_list_range(&target_branch, ¤t_branch),
1449 ])?;
1450
1451 let (ahead, behind) = self.parse_commit_counts(&rev_list_output);
1452 let (ahead_msg, behind_msg) = self.format_commit_counts(&ahead, &behind);
1453 output.push(ahead_msg);
1454 output.push(behind_msg);
1455
1456 let diff_output = GitOperations::run(&[
1458 "diff",
1459 "--name-status",
1460 &self.format_rev_list_range(&target_branch, ¤t_branch),
1461 ])?;
1462
1463 if !diff_output.trim().is_empty() {
1464 output.push("š Changes:".to_string());
1465 for line in diff_output.lines() {
1466 if let Some(formatted_line) = self.format_diff_line(line) {
1467 output.push(formatted_line);
1468 }
1469 }
1470 } else {
1471 output.push("ā
No file changes".to_string());
1472 }
1473
1474 Ok(output.join("\n"))
1475 }
1476
1477 fn name(&self) -> &'static str {
1478 "what"
1479 }
1480
1481 fn description(&self) -> &'static str {
1482 "Analyze what changed between current branch and target"
1483 }
1484}
1485
1486impl GitCommand for WhatCommand {}
1487
1488#[derive(Debug)]
1490struct CommitStats {
1491 total_commits: u32,
1492 period: String,
1493}
1494
1495#[derive(Debug)]
1496struct AuthorStats {
1497 name: String,
1498 commits: u32,
1499}
1500
1501#[derive(Debug)]
1502struct FileStats {
1503 total_files: usize,
1504 _total_lines: usize,
1505}
1506
1507#[derive(Debug)]
1508struct FileChurn {
1509 file: String,
1510 changes: u32,
1511}
1512
1513#[derive(Debug)]
1514struct LargeFile {
1515 path: String,
1516 size_mb: f64,
1517}
1518
1519#[derive(Debug, Clone)]
1520struct ContributorStats {
1521 name: String,
1522 email: String,
1523 commit_count: usize,
1524 first_commit: String,
1525 last_commit: String,
1526}