git_x/commands/
repository.rs

1use crate::core::traits::*;
2use crate::core::{git::*, output::*};
3use crate::{GitXError, Result};
4
5/// Repository-level commands grouped together
6pub struct RepositoryCommands;
7
8impl RepositoryCommands {
9    /// Show repository information
10    pub fn info() -> Result<String> {
11        InfoCommand::new().execute()
12    }
13
14    /// Show repository health check
15    pub fn health() -> Result<String> {
16        HealthCommand::new().execute()
17    }
18
19    /// Sync with upstream
20    pub fn sync(strategy: SyncStrategy) -> Result<String> {
21        SyncCommand::new(strategy).execute()
22    }
23
24    /// Manage upstream configuration
25    pub fn upstream(action: UpstreamAction) -> Result<String> {
26        UpstreamCommand::new(action).execute()
27    }
28
29    /// Create a new branch
30    pub fn new_branch(branch_name: String, from: Option<String>) -> Result<String> {
31        NewBranchCommand::new(branch_name, from).execute()
32    }
33}
34
35/// Command to show repository information
36pub struct InfoCommand {
37    show_detailed: bool,
38}
39
40impl Default for InfoCommand {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl InfoCommand {
47    pub fn new() -> Self {
48        Self {
49            show_detailed: false,
50        }
51    }
52
53    pub fn with_details(mut self) -> Self {
54        self.show_detailed = true;
55        self
56    }
57
58    fn get_recent_activity_timeline(limit: usize) -> Result<Vec<String>> {
59        let output = GitOperations::run(&[
60            "log",
61            "--oneline",
62            "--decorate",
63            "--graph",
64            "--all",
65            &format!("--max-count={limit}"),
66            "--pretty=format:%C(auto)%h %s %C(dim)(%cr) %C(bold blue)<%an>%C(reset)",
67        ])?;
68
69        let lines: Vec<String> = output.lines().map(|s| s.to_string()).collect();
70        Ok(lines)
71    }
72
73    fn check_github_pr_status() -> Result<Option<String>> {
74        // Try to detect if GitHub CLI is available and check for PR status
75        match std::process::Command::new("gh")
76            .args(["pr", "status", "--json", "currentBranch"])
77            .output()
78        {
79            Ok(output) if output.status.success() => {
80                let stdout = String::from_utf8_lossy(&output.stdout);
81                if stdout.trim().is_empty() || stdout.contains("null") {
82                    Ok(Some("āŒ No open PR for current branch".to_string()))
83                } else {
84                    Ok(Some("āœ… Open PR found for current branch".to_string()))
85                }
86            }
87            _ => Ok(None), // GitHub CLI not available or error
88        }
89    }
90
91    fn get_branch_differences(current_branch: &str) -> Result<Vec<String>> {
92        let mut differences = Vec::new();
93
94        // Check against main/master
95        for main_branch in ["main", "master", "develop"] {
96            if current_branch == main_branch {
97                continue;
98            }
99
100            // Check if this main branch exists
101            if GitOperations::run(&[
102                "rev-parse",
103                "--verify",
104                &format!("refs/heads/{main_branch}"),
105            ])
106            .is_ok()
107            {
108                // Get ahead/behind count
109                if let Ok(output) = GitOperations::run(&[
110                    "rev-list",
111                    "--left-right",
112                    "--count",
113                    &format!("{main_branch}...{current_branch}"),
114                ]) {
115                    let parts: Vec<&str> = output.split_whitespace().collect();
116                    if parts.len() == 2 {
117                        let behind: u32 = parts[0].parse().unwrap_or(0);
118                        let ahead: u32 = parts[1].parse().unwrap_or(0);
119
120                        if ahead > 0 || behind > 0 {
121                            let mut status_parts = Vec::new();
122                            if ahead > 0 {
123                                status_parts.push(format!("{ahead} ahead"));
124                            }
125                            if behind > 0 {
126                                status_parts.push(format!("{behind} behind"));
127                            }
128                            differences.push(format!(
129                                "šŸ“Š vs {}: {}",
130                                main_branch,
131                                status_parts.join(", ")
132                            ));
133                        } else {
134                            differences.push(format!("āœ… vs {main_branch}: Up to date"));
135                        }
136                        break; // Only check the first existing main branch
137                    }
138                }
139            }
140        }
141
142        Ok(differences)
143    }
144
145    fn format_branch_info(
146        current: &str,
147        upstream: Option<&str>,
148        ahead: u32,
149        behind: u32,
150    ) -> String {
151        let mut info = format!("šŸ“ Current branch: {}", Format::bold(current));
152
153        if let Some(upstream_branch) = upstream {
154            info.push_str(&format!("\nšŸ”— Upstream: {upstream_branch}"));
155
156            if ahead > 0 || behind > 0 {
157                let mut status_parts = Vec::new();
158                if ahead > 0 {
159                    status_parts.push(format!("{ahead} ahead"));
160                }
161                if behind > 0 {
162                    status_parts.push(format!("{behind} behind"));
163                }
164                info.push_str(&format!("\nšŸ“Š Status: {}", status_parts.join(", ")));
165            } else {
166                info.push_str("\nāœ… Status: Up to date");
167            }
168        } else {
169            info.push_str("\nāŒ No upstream configured");
170        }
171
172        info
173    }
174}
175
176impl Command for InfoCommand {
177    fn execute(&self) -> Result<String> {
178        let mut output = BufferedOutput::new();
179
180        // Repository info
181        let repo_name = match GitOperations::repo_root() {
182            Ok(path) => std::path::Path::new(&path)
183                .file_name()
184                .map(|s| s.to_string_lossy().to_string())
185                .unwrap_or_else(|| "Unknown".to_string()),
186            Err(_) => return Err(GitXError::GitCommand("Not in a git repository".to_string())),
187        };
188
189        output.add_line(format!("šŸ—‚ļø  Repository: {}", Format::bold(&repo_name)));
190
191        // Branch information
192        let (current, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
193        output.add_line(Self::format_branch_info(
194            &current,
195            upstream.as_deref(),
196            ahead,
197            behind,
198        ));
199
200        // Working directory status
201        let is_clean = GitOperations::is_working_directory_clean()?;
202        if is_clean {
203            output.add_line("āœ… Working directory: Clean".to_string());
204        } else {
205            output.add_line("āš ļø  Working directory: Has changes".to_string());
206        }
207
208        // Staged files
209        let staged = GitOperations::staged_files()?;
210        if staged.is_empty() {
211            output.add_line("šŸ“‹ Staged files: None".to_string());
212        } else {
213            output.add_line(format!("šŸ“‹ Staged files: {} file(s)", staged.len()));
214            if self.show_detailed {
215                for file in staged {
216                    output.add_line(format!("   • {file}"));
217                }
218            }
219        }
220
221        // Recent activity timeline
222        if self.show_detailed {
223            match Self::get_recent_activity_timeline(8) {
224                Ok(timeline) if !timeline.is_empty() => {
225                    output.add_line("\nšŸ“‹ Recent activity:".to_string());
226                    for line in timeline {
227                        output.add_line(format!("   {line}"));
228                    }
229                }
230                _ => {}
231            }
232        }
233
234        // GitHub PR status (if available)
235        if let Ok(Some(pr_status)) = Self::check_github_pr_status() {
236            output.add_line(pr_status);
237        }
238
239        // Branch differences
240        match Self::get_branch_differences(&current) {
241            Ok(differences) if !differences.is_empty() => {
242                for diff in differences {
243                    output.add_line(diff);
244                }
245            }
246            _ => {}
247        }
248        // Recent branches
249        if self.show_detailed {
250            match GitOperations::recent_branches(Some(5)) {
251                Ok(recent) if !recent.is_empty() => {
252                    output.add_line("\nšŸ•’ Recent branches:".to_string());
253                    for (i, branch) in recent.iter().enumerate() {
254                        let prefix = if i == 0 { "🌟" } else { "šŸ“" };
255                        output.add_line(format!("   {prefix} {branch}"));
256                    }
257                }
258                _ => {}
259            }
260        }
261
262        Ok(output.content())
263    }
264
265    fn name(&self) -> &'static str {
266        "info"
267    }
268
269    fn description(&self) -> &'static str {
270        "Show repository information and status"
271    }
272}
273
274impl GitCommand for InfoCommand {}
275
276/// Async parallel version of Info command
277pub struct AsyncInfoCommand {
278    show_detailed: bool,
279}
280
281impl Default for AsyncInfoCommand {
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287impl AsyncInfoCommand {
288    pub fn new() -> Self {
289        Self {
290            show_detailed: false,
291        }
292    }
293
294    pub fn with_details(mut self) -> Self {
295        self.show_detailed = true;
296        self
297    }
298
299    pub async fn execute_parallel(&self) -> Result<String> {
300        let mut output = BufferedOutput::new();
301
302        // Execute all independent operations in parallel
303        let (
304            repo_root_result,
305            branch_info_result,
306            working_dir_result,
307            staged_files_result,
308            recent_activity_result,
309            github_pr_result,
310        ) = tokio::try_join!(
311            AsyncGitOperations::repo_root(),
312            AsyncGitOperations::branch_info_parallel(),
313            AsyncGitOperations::is_working_directory_clean(),
314            AsyncGitOperations::staged_files(),
315            async {
316                if self.show_detailed {
317                    AsyncGitOperations::get_recent_activity_timeline(8).await
318                } else {
319                    Ok(Vec::new())
320                }
321            },
322            AsyncGitOperations::check_github_pr_status(),
323        )?;
324
325        // Repository info
326        let repo_name = std::path::Path::new(&repo_root_result)
327            .file_name()
328            .map(|s| s.to_string_lossy().to_string())
329            .unwrap_or_else(|| "Unknown".to_string());
330
331        output.add_line(format!("šŸ—‚ļø  Repository: {}", Format::bold(&repo_name)));
332
333        // Branch information
334        let (current, upstream, ahead, behind) = branch_info_result;
335        output.add_line(Self::format_branch_info(
336            &current,
337            upstream.as_deref(),
338            ahead,
339            behind,
340        ));
341
342        // Working directory status
343        if working_dir_result {
344            output.add_line("āœ… Working directory: Clean".to_string());
345        } else {
346            output.add_line("āš ļø  Working directory: Has changes".to_string());
347        }
348
349        // Staged files
350        if staged_files_result.is_empty() {
351            output.add_line("šŸ“‹ Staged files: None".to_string());
352        } else {
353            output.add_line(format!(
354                "šŸ“‹ Staged files: {} file(s)",
355                staged_files_result.len()
356            ));
357            if self.show_detailed {
358                for file in staged_files_result {
359                    output.add_line(format!("   • {file}"));
360                }
361            }
362        }
363
364        // Recent activity timeline
365        if self.show_detailed && !recent_activity_result.is_empty() {
366            output.add_line("\nšŸ“‹ Recent activity:".to_string());
367            for line in recent_activity_result {
368                output.add_line(format!("   {line}"));
369            }
370        }
371
372        // GitHub PR status
373        if let Some(pr_status) = github_pr_result {
374            output.add_line(pr_status);
375        }
376
377        // Execute remaining operations in parallel (dependent on current branch)
378        let (branch_diff_result, recent_branches_result) = tokio::try_join!(
379            AsyncGitOperations::get_branch_differences(&current),
380            async {
381                if self.show_detailed {
382                    AsyncGitOperations::recent_branches(Some(5)).await
383                } else {
384                    Ok(Vec::new())
385                }
386            }
387        )?;
388
389        // Branch differences
390        if !branch_diff_result.is_empty() {
391            for diff in branch_diff_result {
392                output.add_line(diff);
393            }
394        }
395
396        // Recent branches
397        if self.show_detailed && !recent_branches_result.is_empty() {
398            output.add_line("\nšŸ•’ Recent branches:".to_string());
399            for (i, branch) in recent_branches_result.iter().enumerate() {
400                let prefix = if i == 0 { "🌟" } else { "šŸ“" };
401                output.add_line(format!("   {prefix} {branch}"));
402            }
403        }
404
405        Ok(output.content())
406    }
407
408    fn format_branch_info(
409        current: &str,
410        upstream: Option<&str>,
411        ahead: u32,
412        behind: u32,
413    ) -> String {
414        let mut info = format!("šŸ“ Current branch: {}", Format::bold(current));
415
416        if let Some(upstream_branch) = upstream {
417            info.push_str(&format!("\nšŸ”— Upstream: {upstream_branch}"));
418
419            if ahead > 0 || behind > 0 {
420                let mut status_parts = Vec::new();
421                if ahead > 0 {
422                    status_parts.push(format!("{ahead} ahead"));
423                }
424                if behind > 0 {
425                    status_parts.push(format!("{behind} behind"));
426                }
427                info.push_str(&format!("\nšŸ“Š Status: {}", status_parts.join(", ")));
428            } else {
429                info.push_str("\nāœ… Status: Up to date");
430            }
431        } else {
432            info.push_str("\nāŒ No upstream configured");
433        }
434
435        info
436    }
437}
438
439/// Command to check repository health
440pub struct HealthCommand;
441
442impl Default for HealthCommand {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448impl HealthCommand {
449    pub fn new() -> Self {
450        Self
451    }
452
453    fn check_git_config() -> Vec<String> {
454        let mut issues = Vec::new();
455
456        // Check username and email
457        if GitOperations::run(&["config", "user.name"]).is_err() {
458            issues.push("āŒ Git user.name not configured".to_string());
459        }
460        if GitOperations::run(&["config", "user.email"]).is_err() {
461            issues.push("āŒ Git user.email not configured".to_string());
462        }
463
464        issues
465    }
466
467    fn check_remotes() -> Vec<String> {
468        let mut issues = Vec::new();
469
470        match RemoteOperations::list() {
471            Ok(remotes) => {
472                if remotes.is_empty() {
473                    issues.push("āš ļø  No remotes configured".to_string());
474                }
475            }
476            Err(_) => {
477                issues.push("āŒ Could not check remotes".to_string());
478            }
479        }
480
481        issues
482    }
483
484    fn check_branches() -> Vec<String> {
485        let mut issues = Vec::new();
486
487        // Check for very old branches
488        match GitOperations::local_branches() {
489            Ok(branches) => {
490                if branches.len() > 20 {
491                    issues.push(format!(
492                        "āš ļø  Many local branches ({}) - consider cleaning up",
493                        branches.len()
494                    ));
495                }
496            }
497            Err(_) => {
498                issues.push("āŒ Could not check branches".to_string());
499            }
500        }
501
502        // Check for stale branches
503        if let Ok(stale_count) = Self::count_stale_branches() {
504            if stale_count > 0 {
505                issues.push(format!(
506                    "āš ļø  {stale_count} potentially stale branches found"
507                ));
508            }
509        }
510
511        issues
512    }
513
514    fn count_stale_branches() -> Result<usize> {
515        let output = GitOperations::run(&[
516            "for-each-ref",
517            "--format=%(refname:short) %(committerdate:relative)",
518            "refs/heads/",
519        ])?;
520
521        let stale_count = output
522            .lines()
523            .filter(|line| line.contains("months ago") || line.contains("year"))
524            .count();
525
526        Ok(stale_count)
527    }
528
529    fn check_working_directory() -> Vec<String> {
530        let mut issues = Vec::new();
531
532        // Check for untracked files
533        if let Ok(output) = GitOperations::run(&["ls-files", "--others", "--exclude-standard"]) {
534            let untracked_count = output
535                .lines()
536                .filter(|line| !line.trim().is_empty())
537                .count();
538            if untracked_count > 5 {
539                issues.push(format!("āš ļø  {untracked_count} untracked files found"));
540            }
541        }
542
543        // Check for uncommitted changes
544        if let Ok(output) = GitOperations::run(&["diff", "--cached", "--name-only"]) {
545            let staged_count = output
546                .lines()
547                .filter(|line| !line.trim().is_empty())
548                .count();
549            if staged_count > 0 {
550                issues.push(format!("ā„¹ļø  {staged_count} files staged for commit"));
551            }
552        }
553
554        issues
555    }
556
557    fn check_repository_size() -> Vec<String> {
558        let mut issues = Vec::new();
559
560        // Use git count-objects for repository size
561        if let Ok(output) = GitOperations::run(&["count-objects", "-vH"]) {
562            for line in output.lines() {
563                if line.starts_with("size-pack") {
564                    if let Some(size_str) = line.split_whitespace().nth(1) {
565                        // Parse size and check if it's concerning
566                        if size_str.ends_with("GiB") || size_str.contains("1024") {
567                            issues.push(format!(
568                                "āš ļø  Repository size: {size_str} (consider cleanup)"
569                            ));
570                        }
571                    }
572                }
573            }
574        }
575
576        issues
577    }
578
579    fn check_security_issues() -> Vec<String> {
580        let mut issues = Vec::new();
581
582        // Check for potential credentials in history
583        if let Ok(output) = GitOperations::run(&[
584            "log",
585            "--all",
586            "--full-history",
587            "--grep=password",
588            "--grep=secret",
589            "--grep=key",
590            "--grep=token",
591            "--grep=credential",
592            "--pretty=format:%h %s",
593            "-i",
594        ]) {
595            let suspicious_commits: Vec<_> =
596                output.lines().filter(|l| !l.trim().is_empty()).collect();
597            if !suspicious_commits.is_empty() {
598                issues.push(format!(
599                    "šŸ”’ {} potentially sensitive commit message(s) found:",
600                    suspicious_commits.len()
601                ));
602                for commit in suspicious_commits.iter().take(5) {
603                    issues.push(format!("     • {commit}"));
604                }
605                if suspicious_commits.len() > 5 {
606                    issues.push(format!(
607                        "     • ...and {} more",
608                        suspicious_commits.len() - 5
609                    ));
610                }
611            }
612        }
613
614        // Check for files with potentially sensitive extensions
615        if let Ok(output) =
616            GitOperations::run(&["ls-files", "*.pem", "*.key", "*.p12", "*.pfx", "*.jks"])
617        {
618            let sensitive_files: Vec<_> = output.lines().filter(|l| !l.trim().is_empty()).collect();
619            if !sensitive_files.is_empty() {
620                issues.push(format!(
621                    "šŸ” {} potentially sensitive file(s) in repository:",
622                    sensitive_files.len()
623                ));
624                for file in sensitive_files.iter().take(10) {
625                    issues.push(format!("     • {file}"));
626                }
627                if sensitive_files.len() > 10 {
628                    issues.push(format!("     • ...and {} more", sensitive_files.len() - 10));
629                }
630            }
631        }
632
633        // Check for .env files that might contain secrets
634        if let Ok(output) = GitOperations::run(&["ls-files", "*.env*"]) {
635            let env_files: Vec<_> = output.lines().filter(|l| !l.trim().is_empty()).collect();
636            if !env_files.is_empty() {
637                issues.push(format!(
638                    "āš ļø  {} environment file(s) found - ensure no secrets are committed:",
639                    env_files.len()
640                ));
641                for file in env_files.iter().take(10) {
642                    issues.push(format!("     • {file}"));
643                }
644                if env_files.len() > 10 {
645                    issues.push(format!("     • ...and {} more", env_files.len() - 10));
646                }
647            }
648        }
649
650        issues
651    }
652
653    fn check_gitignore_effectiveness() -> Vec<String> {
654        let mut issues = Vec::new();
655
656        // Check if .gitignore exists
657        if GitOperations::run(&["ls-files", ".gitignore"]).is_err() {
658            issues.push("šŸ“ No .gitignore file found".to_string());
659            return issues;
660        }
661
662        // Check for common files that should probably be ignored
663        let should_be_ignored = [
664            ("*.log", "log files"),
665            ("*.tmp", "temporary files"),
666            ("*.swp", "swap files"),
667            ("*.bak", "backup files"),
668            (".DS_Store", "macOS system files"),
669            ("Thumbs.db", "Windows system files"),
670            ("node_modules/", "Node.js dependencies"),
671            ("target/", "Rust build artifacts"),
672            (".vscode/", "VS Code settings"),
673            (".idea/", "IntelliJ settings"),
674        ];
675
676        for (pattern, description) in should_be_ignored {
677            if let Ok(output) = GitOperations::run(&["ls-files", pattern]) {
678                let matching_files: Vec<_> =
679                    output.lines().filter(|l| !l.trim().is_empty()).collect();
680                if !matching_files.is_empty() {
681                    issues.push(format!(
682                        "šŸ—‚ļø  {} {} tracked (consider adding to .gitignore):",
683                        matching_files.len(),
684                        description
685                    ));
686                    for file in matching_files.iter().take(5) {
687                        issues.push(format!("     • {file}"));
688                    }
689                    if matching_files.len() > 5 {
690                        issues.push(format!("     • ...and {} more", matching_files.len() - 5));
691                    }
692                }
693            }
694        }
695
696        issues
697    }
698
699    fn check_binary_files() -> Vec<String> {
700        let mut issues = Vec::new();
701
702        // Optimized version: use filesystem calls instead of external commands
703        if let Ok(output) = GitOperations::run(&["ls-files", "-z"]) {
704            let mut binary_count = 0;
705            let mut large_files = Vec::new();
706            let mut files_checked = 0;
707            const MAX_FILES_TO_CHECK: usize = 1000; // Limit for performance
708
709            for file in output.split('\0').take(MAX_FILES_TO_CHECK) {
710                if file.trim().is_empty() {
711                    continue;
712                }
713
714                files_checked += 1;
715
716                // Use filesystem metadata instead of external commands
717                if let Ok(metadata) = std::fs::metadata(file) {
718                    if metadata.is_file() {
719                        let size = metadata.len();
720
721                        // Simple heuristic: consider it binary if it's over 1MB or has certain extensions
722                        let is_likely_binary = size > 1_000_000
723                            || file.ends_with(".exe")
724                            || file.ends_with(".dll")
725                            || file.ends_with(".so")
726                            || file.ends_with(".dylib")
727                            || file.ends_with(".bin")
728                            || file.ends_with(".jpg")
729                            || file.ends_with(".png")
730                            || file.ends_with(".gif")
731                            || file.ends_with(".pdf")
732                            || file.ends_with(".zip");
733
734                        if is_likely_binary {
735                            binary_count += 1;
736
737                            if size > 1_000_000 {
738                                large_files.push((file.to_string(), size));
739                            }
740                        }
741                    }
742                }
743            }
744
745            if binary_count > 10 {
746                issues.push(format!(
747                    "šŸ“¦ {binary_count} likely binary files tracked (consider Git LFS for large files)"
748                ));
749            }
750
751            if !large_files.is_empty() {
752                issues.push(format!(
753                    "šŸ“ {} large file(s) > 1MB found:",
754                    large_files.len()
755                ));
756                for (file, size) in large_files.iter().take(10) {
757                    let size_mb = *size as f64 / 1_000_000.0;
758                    issues.push(format!("     • {file} ({size_mb:.1} MB)"));
759                }
760                if large_files.len() > 10 {
761                    issues.push(format!("     • ...and {} more", large_files.len() - 10));
762                }
763            }
764
765            if files_checked == MAX_FILES_TO_CHECK {
766                issues.push(format!(
767                    "     • Analysis limited to first {MAX_FILES_TO_CHECK} files for performance"
768                ));
769            }
770        }
771
772        issues
773    }
774}
775
776impl Command for HealthCommand {
777    fn execute(&self) -> Result<String> {
778        use indicatif::{ProgressBar, ProgressStyle};
779
780        let mut output = BufferedOutput::new();
781
782        output.add_line("šŸ„ Repository Health Check".to_string());
783        output.add_line("=".repeat(30));
784
785        // Create progress bar - use hidden progress bar in tests/non-interactive environments
786        let pb = if atty::is(atty::Stream::Stderr)
787            && std::env::var("GIT_X_NON_INTERACTIVE").is_err()
788        {
789            let pb = ProgressBar::new(8);
790            pb.set_style(
791                ProgressStyle::default_bar()
792                    .template(
793                        "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
794                    )
795                    .expect("Failed to set progress style")
796                    .progress_chars("#>-"),
797            );
798            pb
799        } else {
800            ProgressBar::hidden()
801        };
802        pb.set_message("Starting health check...");
803
804        let mut all_issues = Vec::new();
805        let mut issue_count = 0;
806
807        // Check git configuration
808        pb.set_message("Checking Git configuration...");
809        let config_issues = Self::check_git_config();
810        if config_issues.is_empty() {
811            output.add_line("āœ… Git configuration: OK".to_string());
812        } else {
813            output.add_line("āŒ Git configuration: Issues found".to_string());
814            all_issues.extend(config_issues);
815            issue_count += 1;
816        }
817        pb.inc(1);
818
819        // Check remotes
820        pb.set_message("Checking remotes...");
821        let remote_issues = Self::check_remotes();
822        if remote_issues.is_empty() {
823            output.add_line("āœ… Remotes: OK".to_string());
824        } else {
825            output.add_line("āš ļø  Remotes: Issues found".to_string());
826            all_issues.extend(remote_issues);
827            issue_count += 1;
828        }
829        pb.inc(1);
830
831        // Check branches
832        pb.set_message("Analyzing branches...");
833        let branch_issues = Self::check_branches();
834        if branch_issues.is_empty() {
835            output.add_line("āœ… Branches: OK".to_string());
836        } else {
837            output.add_line("āš ļø  Branches: Issues found".to_string());
838            all_issues.extend(branch_issues);
839            issue_count += 1;
840        }
841        pb.inc(1);
842
843        // Check working directory
844        pb.set_message("Checking working directory...");
845        let wd_issues = Self::check_working_directory();
846        if wd_issues.is_empty() {
847            output.add_line("āœ… Working directory: Clean".to_string());
848        } else {
849            output.add_line("ā„¹ļø  Working directory: Has notes".to_string());
850            all_issues.extend(wd_issues);
851            issue_count += 1;
852        }
853        pb.inc(1);
854
855        // Check repository size
856        pb.set_message("Analyzing repository size...");
857        let size_issues = Self::check_repository_size();
858        if size_issues.is_empty() {
859            output.add_line("āœ… Repository size: OK".to_string());
860        } else {
861            output.add_line("āš ļø  Repository size: Large".to_string());
862            all_issues.extend(size_issues);
863            issue_count += 1;
864        }
865        pb.inc(1);
866
867        // Check security issues
868        pb.set_message("Scanning for security issues...");
869        let security_issues = Self::check_security_issues();
870        if security_issues.is_empty() {
871            output.add_line("āœ… Security: No obvious issues found".to_string());
872        } else {
873            output.add_line("āš ļø  Security: Potential issues found".to_string());
874            all_issues.extend(security_issues);
875            issue_count += 1;
876        }
877        pb.inc(1);
878
879        // Check .gitignore effectiveness
880        pb.set_message("Validating .gitignore...");
881        let gitignore_issues = Self::check_gitignore_effectiveness();
882        if gitignore_issues.is_empty() {
883            output.add_line("āœ… .gitignore: Looks good".to_string());
884        } else {
885            output.add_line("āš ļø  .gitignore: Suggestions available".to_string());
886            all_issues.extend(gitignore_issues);
887            issue_count += 1;
888        }
889        pb.inc(1);
890
891        // Check binary files
892        pb.set_message("Analyzing binary files...");
893        let binary_issues = Self::check_binary_files();
894        if binary_issues.is_empty() {
895            output.add_line("āœ… Binary files: OK".to_string());
896        } else {
897            output.add_line("āš ļø  Binary files: Review recommended".to_string());
898            all_issues.extend(binary_issues);
899            issue_count += 1;
900        }
901        pb.inc(1);
902
903        // Finish progress bar
904        pb.set_message("Health check complete!");
905        pb.finish_and_clear();
906
907        // Summary
908        if all_issues.is_empty() {
909            output.add_line("\nšŸŽ‰ Repository is healthy!".to_string());
910        } else {
911            output.add_line(format!("\nšŸ”§ Found {issue_count} issue(s):"));
912            for issue in all_issues {
913                output.add_line(format!("   {issue}"));
914            }
915        }
916
917        Ok(output.content())
918    }
919
920    fn name(&self) -> &'static str {
921        "health"
922    }
923
924    fn description(&self) -> &'static str {
925        "Check repository health and configuration"
926    }
927}
928
929impl GitCommand for HealthCommand {}
930
931/// Async parallel version of Health command
932pub struct AsyncHealthCommand;
933
934impl Default for AsyncHealthCommand {
935    fn default() -> Self {
936        Self::new()
937    }
938}
939
940impl AsyncHealthCommand {
941    pub fn new() -> Self {
942        Self
943    }
944
945    pub async fn execute_parallel(&self) -> Result<String> {
946        let mut output = BufferedOutput::new();
947
948        output.add_line("šŸ„ Repository Health Check (Parallel)".to_string());
949        output.add_line("=".repeat(40));
950
951        // Execute all health checks in parallel
952        let (
953            config_issues,
954            remote_issues,
955            branch_issues,
956            wd_issues,
957            size_issues,
958            security_issues,
959            gitignore_issues,
960            binary_issues,
961        ) = tokio::try_join!(
962            tokio::task::spawn_blocking(HealthCommand::check_git_config),
963            tokio::task::spawn_blocking(HealthCommand::check_remotes),
964            tokio::task::spawn_blocking(HealthCommand::check_branches),
965            tokio::task::spawn_blocking(HealthCommand::check_working_directory),
966            tokio::task::spawn_blocking(HealthCommand::check_repository_size),
967            tokio::task::spawn_blocking(HealthCommand::check_security_issues),
968            tokio::task::spawn_blocking(HealthCommand::check_gitignore_effectiveness),
969            tokio::task::spawn_blocking(HealthCommand::check_binary_files),
970        )?;
971
972        let mut all_issues = Vec::new();
973        let mut issue_count = 0;
974
975        // Process results
976        let checks = [
977            (
978                "Git configuration",
979                config_issues,
980                "āœ… Git configuration: OK",
981                "āŒ Git configuration: Issues found",
982            ),
983            (
984                "Remotes",
985                remote_issues,
986                "āœ… Remotes: OK",
987                "āš ļø  Remotes: Issues found",
988            ),
989            (
990                "Branches",
991                branch_issues,
992                "āœ… Branches: OK",
993                "āš ļø  Branches: Issues found",
994            ),
995            (
996                "Working directory",
997                wd_issues,
998                "āœ… Working directory: Clean",
999                "ā„¹ļø  Working directory: Has notes",
1000            ),
1001            (
1002                "Repository size",
1003                size_issues,
1004                "āœ… Repository size: OK",
1005                "āš ļø  Repository size: Large",
1006            ),
1007            (
1008                "Security",
1009                security_issues,
1010                "āœ… Security: No obvious issues found",
1011                "āš ļø  Security: Potential issues found",
1012            ),
1013            (
1014                ".gitignore",
1015                gitignore_issues,
1016                "āœ… .gitignore: Looks good",
1017                "āš ļø  .gitignore: Suggestions available",
1018            ),
1019            (
1020                "Binary files",
1021                binary_issues,
1022                "āœ… Binary files: OK",
1023                "āš ļø  Binary files: Review recommended",
1024            ),
1025        ];
1026
1027        for (_name, issues, ok_msg, issue_msg) in checks {
1028            if issues.is_empty() {
1029                output.add_line(ok_msg.to_string());
1030            } else {
1031                output.add_line(issue_msg.to_string());
1032                all_issues.extend(issues);
1033                issue_count += 1;
1034            }
1035        }
1036
1037        // Summary
1038        if all_issues.is_empty() {
1039            output.add_line("\nšŸŽ‰ Repository is healthy!".to_string());
1040        } else {
1041            output.add_line(format!("\nšŸ”§ Found {issue_count} issue(s):"));
1042            for issue in all_issues {
1043                output.add_line(format!("   {issue}"));
1044            }
1045        }
1046
1047        Ok(output.content())
1048    }
1049}
1050
1051/// Sync strategies
1052#[derive(Debug, Clone)]
1053pub enum SyncStrategy {
1054    Merge,
1055    Rebase,
1056    Auto,
1057}
1058
1059/// Command to sync with upstream
1060pub struct SyncCommand {
1061    strategy: SyncStrategy,
1062}
1063
1064impl SyncCommand {
1065    pub fn new(strategy: SyncStrategy) -> Self {
1066        Self { strategy }
1067    }
1068}
1069
1070impl Command for SyncCommand {
1071    fn execute(&self) -> Result<String> {
1072        // Fetch latest changes
1073        RemoteOperations::fetch(None)?;
1074
1075        let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
1076
1077        let upstream_branch = upstream.ok_or_else(|| {
1078            GitXError::GitCommand(format!(
1079                "No upstream configured for branch '{current_branch}'"
1080            ))
1081        })?;
1082
1083        if behind == 0 {
1084            return Ok("āœ… Already up to date with upstream".to_string());
1085        }
1086
1087        let strategy_name = match self.strategy {
1088            SyncStrategy::Merge => "merge",
1089            SyncStrategy::Rebase => "rebase",
1090            SyncStrategy::Auto => {
1091                // Auto-choose: rebase if no local commits, merge otherwise
1092                if ahead == 0 { "merge" } else { "rebase" }
1093            }
1094        };
1095
1096        // Perform sync
1097        match strategy_name {
1098            "merge" => {
1099                GitOperations::run_status(&["merge", &upstream_branch])?;
1100                Ok(format!("āœ… Merged {behind} commits from {upstream_branch}"))
1101            }
1102            "rebase" => {
1103                GitOperations::run_status(&["rebase", &upstream_branch])?;
1104                Ok(format!("āœ… Rebased {ahead} commits onto {upstream_branch}"))
1105            }
1106            _ => unreachable!(),
1107        }
1108    }
1109
1110    fn name(&self) -> &'static str {
1111        "sync"
1112    }
1113
1114    fn description(&self) -> &'static str {
1115        "Sync current branch with upstream"
1116    }
1117}
1118
1119impl GitCommand for SyncCommand {}
1120
1121/// Upstream actions
1122#[derive(Debug, Clone)]
1123pub enum UpstreamAction {
1124    Set { remote: String, branch: String },
1125    Status,
1126    SyncAll,
1127}
1128
1129/// Command to manage upstream configuration
1130pub struct UpstreamCommand {
1131    action: UpstreamAction,
1132}
1133
1134impl UpstreamCommand {
1135    pub fn new(action: UpstreamAction) -> Self {
1136        Self { action }
1137    }
1138}
1139
1140impl Command for UpstreamCommand {
1141    fn execute(&self) -> Result<String> {
1142        match &self.action {
1143            UpstreamAction::Set { remote, branch } => {
1144                RemoteOperations::set_upstream(remote, branch)?;
1145                Ok(format!("āœ… Set upstream to {remote}/{branch}"))
1146            }
1147            UpstreamAction::Status => {
1148                let branches = GitOperations::local_branches()?;
1149                let mut output = BufferedOutput::new();
1150                output.add_line("šŸ”— Upstream Status:".to_string());
1151
1152                for branch in branches {
1153                    // Switch to each branch and check upstream
1154                    // This is a simplified version - in practice you'd want to parse git config
1155                    output.add_line(format!("šŸ“ {branch}: (checking...)"));
1156                }
1157
1158                Ok(output.content())
1159            }
1160            UpstreamAction::SyncAll => {
1161                let current_branch = GitOperations::current_branch()?;
1162                let branches = GitOperations::local_branches()?;
1163                let mut synced = 0;
1164
1165                for branch in branches {
1166                    if branch == current_branch {
1167                        continue; // Skip current branch
1168                    }
1169
1170                    // Try to sync each branch (simplified)
1171                    if BranchOperations::switch(&branch).is_ok()
1172                        && SyncCommand::new(SyncStrategy::Auto).execute().is_ok()
1173                    {
1174                        synced += 1;
1175                    }
1176                }
1177
1178                // Return to original branch
1179                BranchOperations::switch(&current_branch)?;
1180
1181                Ok(format!("āœ… Synced {synced} branches"))
1182            }
1183        }
1184    }
1185
1186    fn name(&self) -> &'static str {
1187        "upstream"
1188    }
1189
1190    fn description(&self) -> &'static str {
1191        "Manage upstream branch configuration"
1192    }
1193}
1194
1195impl GitCommand for UpstreamCommand {}
1196
1197/// Async parallel version of UpstreamCommand
1198pub struct AsyncUpstreamCommand {
1199    action: UpstreamAction,
1200}
1201
1202impl AsyncUpstreamCommand {
1203    pub fn new(action: UpstreamAction) -> Self {
1204        Self { action }
1205    }
1206
1207    pub async fn execute_parallel(&self) -> Result<String> {
1208        match &self.action {
1209            UpstreamAction::Set { remote, branch } => {
1210                RemoteOperations::set_upstream(remote, branch)?;
1211                Ok(format!("āœ… Set upstream to {remote}/{branch}"))
1212            }
1213            UpstreamAction::Status => self.get_upstream_status_parallel().await,
1214            UpstreamAction::SyncAll => self.sync_all_branches_parallel().await,
1215        }
1216    }
1217
1218    async fn get_upstream_status_parallel(&self) -> Result<String> {
1219        let branches = AsyncGitOperations::local_branches().await?;
1220
1221        // Check all branches in parallel
1222        let branch_checks = branches
1223            .iter()
1224            .map(|branch| self.check_branch_upstream(branch.clone()));
1225
1226        let results = futures::future::join_all(branch_checks).await;
1227
1228        let mut output = BufferedOutput::new();
1229        output.add_line("šŸ”— Upstream Status:".to_string());
1230        output.add_line("=".repeat(30));
1231
1232        for status in results.into_iter().flatten() {
1233            output.add_line(status);
1234        }
1235
1236        Ok(output.content())
1237    }
1238
1239    async fn check_branch_upstream(&self, branch: String) -> Result<String> {
1240        // Switch to branch and check upstream
1241        match AsyncGitOperations::run(&[
1242            "show-ref",
1243            "--verify",
1244            "--quiet",
1245            &format!("refs/heads/{branch}"),
1246        ])
1247        .await
1248        {
1249            Ok(_) => {
1250                // Check if branch has upstream
1251                match AsyncGitOperations::run(&["config", &format!("branch.{branch}.remote")]).await
1252                {
1253                    Ok(remote) => {
1254                        match AsyncGitOperations::run(&[
1255                            "config",
1256                            &format!("branch.{branch}.merge"),
1257                        ])
1258                        .await
1259                        {
1260                            Ok(merge_ref) => {
1261                                let upstream_branch =
1262                                    merge_ref.strip_prefix("refs/heads/").unwrap_or(&merge_ref);
1263                                Ok(format!(
1264                                    "šŸ“ {}: {} -> {}/{}",
1265                                    branch,
1266                                    "āœ…",
1267                                    remote.trim(),
1268                                    upstream_branch
1269                                ))
1270                            }
1271                            Err(_) => Ok(format!("šŸ“ {branch}: āŒ No upstream configured")),
1272                        }
1273                    }
1274                    Err(_) => Ok(format!("šŸ“ {branch}: āŒ No upstream configured")),
1275                }
1276            }
1277            Err(_) => Ok(format!("šŸ“ {branch}: āŒ Branch does not exist")),
1278        }
1279    }
1280
1281    async fn sync_all_branches_parallel(&self) -> Result<String> {
1282        // Get all branches with upstreams
1283        let branches = AsyncGitOperations::local_branches().await?;
1284
1285        let mut output = BufferedOutput::new();
1286        output.add_line("šŸ”„ Syncing all branches with upstreams...".to_string());
1287
1288        // For safety, we'll do this sequentially to avoid Git conflicts
1289        // but we could check status in parallel first
1290        let current_branch = AsyncGitOperations::current_branch().await?;
1291
1292        for branch in branches {
1293            if branch != current_branch {
1294                // Check if branch has upstream
1295                if AsyncGitOperations::run(&["config", &format!("branch.{branch}.remote")])
1296                    .await
1297                    .is_ok()
1298                {
1299                    output.add_line(format!(
1300                        "šŸ“ {branch}: Has upstream (would sync in real implementation)"
1301                    ));
1302                } else {
1303                    output.add_line(format!("šŸ“ {branch}: No upstream"));
1304                }
1305            }
1306        }
1307
1308        Ok(output.content())
1309    }
1310}
1311
1312/// Command to create a new branch
1313pub struct NewBranchCommand {
1314    branch_name: String,
1315    from: Option<String>,
1316}
1317
1318impl NewBranchCommand {
1319    pub fn new(branch_name: String, from: Option<String>) -> Self {
1320        Self { branch_name, from }
1321    }
1322
1323    fn branch_exists(&self, branch_name: &str) -> bool {
1324        GitOperations::run(&[
1325            "show-ref",
1326            "--verify",
1327            "--quiet",
1328            &format!("refs/heads/{branch_name}"),
1329        ])
1330        .is_ok()
1331    }
1332
1333    fn is_valid_ref(&self, ref_name: &str) -> bool {
1334        GitOperations::run(&["rev-parse", "--verify", "--quiet", ref_name]).is_ok()
1335    }
1336}
1337
1338impl Command for NewBranchCommand {
1339    fn execute(&self) -> Result<String> {
1340        // Validate branch name format and safety
1341        crate::commands::stash::utils::validate_branch_name(&self.branch_name)?;
1342
1343        // Check if branch already exists
1344        if self.branch_exists(&self.branch_name) {
1345            return Err(GitXError::GitCommand(format!(
1346                "Branch '{}' already exists",
1347                self.branch_name
1348            )));
1349        }
1350
1351        // Determine base branch
1352        let base_branch = match &self.from {
1353            Some(branch) => {
1354                if !self.branch_exists(branch) && !self.is_valid_ref(branch) {
1355                    return Err(GitXError::GitCommand(format!(
1356                        "Base branch or ref '{branch}' does not exist"
1357                    )));
1358                }
1359                branch.clone()
1360            }
1361            None => GitOperations::current_branch()?,
1362        };
1363
1364        let mut output = Vec::new();
1365        output.push(format!(
1366            "🌿 Creating new branch '{}' from '{}'",
1367            Format::bold(&self.branch_name),
1368            Format::bold(&base_branch)
1369        ));
1370
1371        // Create and switch to the new branch in one atomic operation
1372        GitOperations::run_status(&["checkout", "-b", &self.branch_name, &base_branch])?;
1373
1374        output.push(format!(
1375            "āœ… Successfully created and switched to branch '{}'",
1376            Format::bold(&self.branch_name)
1377        ));
1378
1379        Ok(output.join("\n"))
1380    }
1381
1382    fn name(&self) -> &'static str {
1383        "new-branch"
1384    }
1385
1386    fn description(&self) -> &'static str {
1387        "Create and switch to a new branch"
1388    }
1389}
1390
1391impl GitCommand for NewBranchCommand {}
1392
1393#[cfg(test)]
1394mod tests {
1395    use super::*;
1396    use serial_test::serial;
1397
1398    /// Helper function to strip ANSI escape codes for testing
1399    fn strip_ansi_codes(text: &str) -> String {
1400        // Simple regex-like approach to remove ANSI escape sequences
1401        let mut result = String::new();
1402        let mut chars = text.chars().peekable();
1403
1404        while let Some(ch) = chars.next() {
1405            if ch == '\x1B' {
1406                // Found escape character, skip until 'm'
1407                for next_ch in chars.by_ref() {
1408                    if next_ch == 'm' {
1409                        break;
1410                    }
1411                }
1412            } else {
1413                result.push(ch);
1414            }
1415        }
1416
1417        result
1418    }
1419
1420    #[test]
1421    #[serial]
1422    fn test_ansi_stripping() {
1423        // Test the ANSI stripping helper function
1424        let formatted_text = Format::bold("main");
1425        let clean_text = strip_ansi_codes(&formatted_text);
1426        assert_eq!(clean_text, "main");
1427
1428        // Test with mixed content
1429        let mixed = format!("Branch: {} Status: OK", Format::bold("feature"));
1430        let clean_mixed = strip_ansi_codes(&mixed);
1431        assert_eq!(clean_mixed, "Branch: feature Status: OK");
1432    }
1433
1434    #[test]
1435    #[serial]
1436    fn test_info_command_creation() {
1437        let cmd = InfoCommand::new();
1438        assert!(!cmd.show_detailed);
1439
1440        let detailed_cmd = cmd.with_details();
1441        assert!(detailed_cmd.show_detailed);
1442    }
1443
1444    #[test]
1445    #[serial]
1446    fn test_command_trait_implementations() {
1447        let info_cmd = InfoCommand::new();
1448        assert_eq!(info_cmd.name(), "info");
1449        assert_eq!(
1450            info_cmd.description(),
1451            "Show repository information and status"
1452        );
1453
1454        let health_cmd = HealthCommand::new();
1455        assert_eq!(health_cmd.name(), "health");
1456        assert_eq!(
1457            health_cmd.description(),
1458            "Check repository health and configuration"
1459        );
1460
1461        let sync_cmd = SyncCommand::new(SyncStrategy::Auto);
1462        assert_eq!(sync_cmd.name(), "sync");
1463        assert_eq!(sync_cmd.description(), "Sync current branch with upstream");
1464    }
1465
1466    #[test]
1467    #[serial]
1468    fn test_branch_info_formatting() {
1469        let formatted = InfoCommand::format_branch_info("main", Some("origin/main"), 2, 1);
1470        let clean_text = strip_ansi_codes(&formatted);
1471
1472        assert!(clean_text.contains("Current branch: main"));
1473        assert!(clean_text.contains("Upstream: origin/main"));
1474        assert!(clean_text.contains("2 ahead"));
1475        assert!(clean_text.contains("1 behind"));
1476    }
1477
1478    #[test]
1479    #[serial]
1480    fn test_branch_info_formatting_no_upstream() {
1481        let formatted = InfoCommand::format_branch_info("feature", None, 0, 0);
1482        let clean_text = strip_ansi_codes(&formatted);
1483
1484        assert!(clean_text.contains("Current branch: feature"));
1485        assert!(clean_text.contains("No upstream configured"));
1486    }
1487
1488    #[test]
1489    #[serial]
1490    fn test_branch_info_formatting_up_to_date() {
1491        let formatted = InfoCommand::format_branch_info("main", Some("origin/main"), 0, 0);
1492        let clean_text = strip_ansi_codes(&formatted);
1493
1494        assert!(clean_text.contains("Status: Up to date"));
1495    }
1496
1497    #[test]
1498    #[serial]
1499    fn test_sync_strategy_auto_selection() {
1500        // Test the auto strategy logic
1501        let sync_cmd = SyncCommand::new(SyncStrategy::Auto);
1502        assert_eq!(sync_cmd.name(), "sync");
1503
1504        // Auto strategy should work for any input
1505        let merge_cmd = SyncCommand::new(SyncStrategy::Merge);
1506        let rebase_cmd = SyncCommand::new(SyncStrategy::Rebase);
1507
1508        assert_eq!(merge_cmd.name(), "sync");
1509        assert_eq!(rebase_cmd.name(), "sync");
1510    }
1511}