git_x/commands/
analysis.rs

1use crate::Result;
2use crate::core::traits::*;
3use crate::core::{git::*, output::*};
4use std::collections::HashMap;
5
6/// Analysis and reporting commands grouped together
7pub struct AnalysisCommands;
8
9impl AnalysisCommands {
10    /// Generate repository summary
11    pub fn summary(since: Option<String>) -> Result<String> {
12        SummaryCommand::new(since).execute()
13    }
14
15    /// Show repository graph
16    pub fn graph(colored: bool) -> Result<String> {
17        if colored {
18            ColorGraphCommand::new().execute()
19        } else {
20            GraphCommand::new().execute()
21        }
22    }
23
24    /// Show contributors statistics
25    pub fn contributors(since: Option<String>) -> Result<String> {
26        ContributorsCommand::new(since).execute()
27    }
28
29    /// Analyze technical debt
30    pub fn technical_debt() -> Result<String> {
31        TechnicalDebtCommand::new().execute()
32    }
33
34    /// Find large files
35    pub fn large_files(threshold_mb: Option<f64>, limit: Option<usize>) -> Result<String> {
36        LargeFilesCommand::new(threshold_mb, limit).execute()
37    }
38
39    /// Show commits since a certain time
40    pub fn since(time_spec: String) -> Result<String> {
41        SinceCommand::new(time_spec).execute()
42    }
43}
44
45/// Command to generate repository summary
46pub 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        // Get lines of code (rough estimate)
98        let mut total_lines = 0;
99        if let Ok(wc_output) = GitOperations::run(&["ls-files", "-z"]) {
100            // This is a simplified version - in practice you'd want better file type detection
101            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        // Repository name
119        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        // Current branch info
128        let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
129        output.add_line(format!(
130            "📍 Current branch: {}",
131            Format::bold(&current_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        // Commit statistics
145        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        // Author statistics
158        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        // File statistics
185        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
208/// Command to show colored commit graph
209pub 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", // Limit to recent commits for better performance
232        ])
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
246/// Command to show simple commit graph
247pub 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
277/// Command to show contributors
278pub 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
328/// Command to analyze technical debt
329pub 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); // Top 10 most changed files
366
367        Ok(churns)
368    }
369
370    fn find_large_files(&self) -> Result<Vec<LargeFile>> {
371        // This is a simplified version - you'd want to use git-sizer or similar tools
372        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                    // Files larger than 1MB
380                    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        // File churn analysis
403        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        // Large files analysis
417        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
443/// Command to find large files
444pub 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
507/// Command to show commits since a certain time
508pub 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// Supporting data structures
549#[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}