git_x/commands/
analysis.rs

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
8/// Analysis and reporting commands grouped together
9pub struct AnalysisCommands;
10
11impl AnalysisCommands {
12    /// Generate repository summary
13    pub fn summary(since: Option<String>) -> Result<String> {
14        SummaryCommand::new(since).execute()
15    }
16
17    /// Show repository graph
18    pub fn graph(colored: bool) -> Result<String> {
19        if colored {
20            ColorGraphCommand::new().execute()
21        } else {
22            GraphCommand::new().execute()
23        }
24    }
25
26    /// Show contributors statistics
27    pub fn contributors(since: Option<String>) -> Result<String> {
28        ContributorsCommand::new(since).execute()
29    }
30
31    /// Analyze technical debt
32    pub fn technical_debt() -> Result<String> {
33        TechnicalDebtCommand::new().execute()
34    }
35
36    /// Find large files
37    pub fn large_files(threshold_mb: Option<f64>, limit: Option<usize>) -> Result<String> {
38        LargeFilesCommand::new(threshold_mb, limit).execute()
39    }
40
41    /// Show commits since a certain time
42    pub fn since(time_spec: String) -> Result<String> {
43        SinceCommand::new(time_spec).execute()
44    }
45
46    /// Analyze what changed between branches
47    pub fn what(target: Option<String>) -> Result<String> {
48        WhatCommand::new(target).execute()
49    }
50}
51
52/// Command to generate repository summary
53pub 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        // Use case-insensitive matching without allocation
152        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        // Get lines of code (rough estimate)
207        let mut total_lines = 0;
208        if let Ok(wc_output) = GitOperations::run(&["ls-files", "-z"]) {
209            // This is a simplified version - in practice you'd want better file type detection
210            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 a specific since parameter is provided, show detailed commit summary
223        if self.since.is_some() {
224            return self.get_detailed_commit_summary();
225        }
226
227        // Otherwise show repository summary
228        let mut output = BufferedOutput::new();
229
230        output.add_line("šŸ“Š Repository Summary".to_string());
231        output.add_line("=".repeat(50));
232
233        // Repository name
234        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        // Current branch info
243        let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
244        output.add_line(format!(
245            "šŸ“ Current branch: {}",
246            Format::bold(&current_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        // Commit statistics
260        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        // Author statistics
273        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        // File statistics
300        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
323/// Async parallel version of SummaryCommand
324pub 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 a specific since parameter is provided, show detailed commit summary
335        if self.since.is_some() {
336            return self.get_detailed_commit_summary_async().await;
337        }
338
339        // Execute all operations in parallel
340        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        // Repository name
360        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        // Current branch info
367        let (current_branch, upstream, ahead, behind) = branch_info_result;
368        output.add_line(format!(
369            "šŸ“ Current branch: {}",
370            Format::bold(&current_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        // Commit statistics
384        output.add_line(format!(
385            "šŸ“ˆ Commits ({}): {}",
386            commit_stats_result.period, commit_stats_result.total_commits
387        ));
388
389        // Author statistics
390        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        // File statistics
410        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    // Reuse existing helper methods
488    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        // Use case-insensitive matching without allocation
548        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
578/// Command to show colored commit graph
579pub 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", // Limit to recent commits for better performance
602        ])
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
616/// Command to show simple commit graph
617pub 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
647/// Command to show contributors
648pub 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
773/// Parallel version of ContributorsCommand using multi-threading
774pub 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        // Split lines and process in parallel
807        let lines: Vec<&str> = output.lines().collect();
808
809        // Use parallel processing for line parsing and aggregation
810        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        // Sort by commit count
868        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        // Format output
872        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
900/// Command to analyze technical debt
901pub 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); // Top 10 most changed files
938
939        Ok(churns)
940    }
941
942    fn find_large_files(&self) -> Result<Vec<LargeFile>> {
943        // This is a simplified version - you'd want to use git-sizer or similar tools
944        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                    // Files larger than 1MB
952                    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        // File churn analysis
975        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        // Large files analysis
989        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
1015/// Parallel version of TechnicalDebtCommand using multi-threading
1016pub 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        // Run multiple analysis types in parallel
1031        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        // File churn analysis
1051        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        // Large files analysis
1059        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        // Old files analysis
1067        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        // Parallel counting of file changes
1095        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); // Only show files with significant churn
1126
1127        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                        // Files larger than 500KB
1143                        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        // Get files not modified in last 6 months
1169        let old_files: Vec<String> = files
1170            .par_iter()
1171            .filter_map(|&file| {
1172                // Check last modification in git
1173                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
1191/// Command to find large files
1192pub 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
1255/// Parallel version of LargeFilesCommand using multi-threading
1256pub 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        // Process files in parallel using rayon
1278        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        // Sort and limit results
1298        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
1318/// Command to show commits since a certain time or reference
1319pub 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        // First try as a git reference (commit hash, branch, tag)
1332        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        // If that fails, try as a time specification
1342        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
1370/// Command to analyze what changed between branches
1371pub 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        // Get current branch name
1438        let current_branch = GitOperations::current_branch()?;
1439
1440        let mut output = Vec::new();
1441        output.push(self.format_branch_comparison(&current_branch, &target_branch));
1442
1443        // Get ahead/behind commit counts
1444        let rev_list_output = GitOperations::run(&[
1445            "rev-list",
1446            "--left-right",
1447            "--count",
1448            &self.format_rev_list_range(&target_branch, &current_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        // Get diff summary
1457        let diff_output = GitOperations::run(&[
1458            "diff",
1459            "--name-status",
1460            &self.format_rev_list_range(&target_branch, &current_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// Supporting data structures
1489#[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}