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    /// Show what would be pushed/pulled
30    pub fn what(target: Option<String>) -> Result<String> {
31        WhatCommand::new(target).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 format_branch_info(
59        current: &str,
60        upstream: Option<&str>,
61        ahead: u32,
62        behind: u32,
63    ) -> String {
64        let mut info = format!("šŸ“ Current branch: {}", Format::bold(current));
65
66        if let Some(upstream_branch) = upstream {
67            info.push_str(&format!("\nšŸ”— Upstream: {upstream_branch}"));
68
69            if ahead > 0 || behind > 0 {
70                let mut status_parts = Vec::new();
71                if ahead > 0 {
72                    status_parts.push(format!("{ahead} ahead"));
73                }
74                if behind > 0 {
75                    status_parts.push(format!("{behind} behind"));
76                }
77                info.push_str(&format!("\nšŸ“Š Status: {}", status_parts.join(", ")));
78            } else {
79                info.push_str("\nāœ… Status: Up to date");
80            }
81        } else {
82            info.push_str("\nāŒ No upstream configured");
83        }
84
85        info
86    }
87}
88
89impl Command for InfoCommand {
90    fn execute(&self) -> Result<String> {
91        let mut output = BufferedOutput::new();
92
93        // Repository info
94        let repo_name = match GitOperations::repo_root() {
95            Ok(path) => std::path::Path::new(&path)
96                .file_name()
97                .map(|s| s.to_string_lossy().to_string())
98                .unwrap_or_else(|| "Unknown".to_string()),
99            Err(_) => return Err(GitXError::GitCommand("Not in a git repository".to_string())),
100        };
101
102        output.add_line(format!("šŸ—‚ļø  Repository: {}", Format::bold(&repo_name)));
103
104        // Branch information
105        let (current, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
106        output.add_line(Self::format_branch_info(
107            &current,
108            upstream.as_deref(),
109            ahead,
110            behind,
111        ));
112
113        // Working directory status
114        if GitOperations::is_working_directory_clean()? {
115            output.add_line("āœ… Working directory: Clean".to_string());
116        } else {
117            output.add_line("āš ļø  Working directory: Has changes".to_string());
118        }
119
120        // Staged files
121        let staged = GitOperations::staged_files()?;
122        if staged.is_empty() {
123            output.add_line("šŸ“‹ Staged files: None".to_string());
124        } else {
125            output.add_line(format!("šŸ“‹ Staged files: {} file(s)", staged.len()));
126            if self.show_detailed {
127                for file in staged {
128                    output.add_line(format!("   • {file}"));
129                }
130            }
131        }
132
133        // Recent branches
134        if self.show_detailed {
135            match GitOperations::recent_branches(Some(5)) {
136                Ok(recent) if !recent.is_empty() => {
137                    output.add_line("\nšŸ•’ Recent branches:".to_string());
138                    for (i, branch) in recent.iter().enumerate() {
139                        let prefix = if i == 0 { "🌟" } else { "šŸ“" };
140                        output.add_line(format!("   {prefix} {branch}"));
141                    }
142                }
143                _ => {}
144            }
145        }
146
147        Ok(output.content())
148    }
149
150    fn name(&self) -> &'static str {
151        "info"
152    }
153
154    fn description(&self) -> &'static str {
155        "Show repository information and status"
156    }
157}
158
159impl GitCommand for InfoCommand {}
160
161/// Command to check repository health
162pub struct HealthCommand;
163
164impl Default for HealthCommand {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl HealthCommand {
171    pub fn new() -> Self {
172        Self
173    }
174
175    fn check_git_config() -> Vec<String> {
176        let mut issues = Vec::new();
177
178        // Check user name and email
179        if GitOperations::run(&["config", "user.name"]).is_err() {
180            issues.push("āŒ Git user.name not configured".to_string());
181        }
182        if GitOperations::run(&["config", "user.email"]).is_err() {
183            issues.push("āŒ Git user.email not configured".to_string());
184        }
185
186        issues
187    }
188
189    fn check_remotes() -> Vec<String> {
190        let mut issues = Vec::new();
191
192        match RemoteOperations::list() {
193            Ok(remotes) => {
194                if remotes.is_empty() {
195                    issues.push("āš ļø  No remotes configured".to_string());
196                }
197            }
198            Err(_) => {
199                issues.push("āŒ Could not check remotes".to_string());
200            }
201        }
202
203        issues
204    }
205
206    fn check_branches() -> Vec<String> {
207        let mut issues = Vec::new();
208
209        // Check for very old branches
210        match GitOperations::local_branches() {
211            Ok(branches) => {
212                if branches.len() > 20 {
213                    issues.push(format!(
214                        "āš ļø  Many local branches ({}) - consider cleaning up",
215                        branches.len()
216                    ));
217                }
218            }
219            Err(_) => {
220                issues.push("āŒ Could not check branches".to_string());
221            }
222        }
223
224        issues
225    }
226}
227
228impl Command for HealthCommand {
229    fn execute(&self) -> Result<String> {
230        let mut output = BufferedOutput::new();
231        output.add_line("šŸ„ Repository Health Check".to_string());
232        output.add_line("=".repeat(30));
233
234        let mut all_issues = Vec::new();
235
236        // Check git configuration
237        let config_issues = Self::check_git_config();
238        if config_issues.is_empty() {
239            output.add_line("āœ… Git configuration: OK".to_string());
240        } else {
241            output.add_line("āŒ Git configuration: Issues found".to_string());
242            all_issues.extend(config_issues);
243        }
244
245        // Check remotes
246        let remote_issues = Self::check_remotes();
247        if remote_issues.is_empty() {
248            output.add_line("āœ… Remotes: OK".to_string());
249        } else {
250            output.add_line("āš ļø  Remotes: Issues found".to_string());
251            all_issues.extend(remote_issues);
252        }
253
254        // Check branches
255        let branch_issues = Self::check_branches();
256        if branch_issues.is_empty() {
257            output.add_line("āœ… Branches: OK".to_string());
258        } else {
259            output.add_line("āš ļø  Branches: Issues found".to_string());
260            all_issues.extend(branch_issues);
261        }
262
263        // Summary
264        if all_issues.is_empty() {
265            output.add_line("\nšŸŽ‰ Repository is healthy!".to_string());
266        } else {
267            output.add_line(format!("\nšŸ”§ Found {} issue(s):", all_issues.len()));
268            for issue in all_issues {
269                output.add_line(format!("   {issue}"));
270            }
271        }
272
273        Ok(output.content())
274    }
275
276    fn name(&self) -> &'static str {
277        "health"
278    }
279
280    fn description(&self) -> &'static str {
281        "Check repository health and configuration"
282    }
283}
284
285impl GitCommand for HealthCommand {}
286
287/// Sync strategies
288#[derive(Debug, Clone)]
289pub enum SyncStrategy {
290    Merge,
291    Rebase,
292    Auto,
293}
294
295/// Command to sync with upstream
296pub struct SyncCommand {
297    strategy: SyncStrategy,
298}
299
300impl SyncCommand {
301    pub fn new(strategy: SyncStrategy) -> Self {
302        Self { strategy }
303    }
304}
305
306impl Command for SyncCommand {
307    fn execute(&self) -> Result<String> {
308        // Fetch latest changes
309        RemoteOperations::fetch(None)?;
310
311        let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
312
313        let upstream_branch = upstream.ok_or_else(|| {
314            GitXError::GitCommand(format!(
315                "No upstream configured for branch '{current_branch}'"
316            ))
317        })?;
318
319        if behind == 0 {
320            return Ok("āœ… Already up to date with upstream".to_string());
321        }
322
323        let strategy_name = match self.strategy {
324            SyncStrategy::Merge => "merge",
325            SyncStrategy::Rebase => "rebase",
326            SyncStrategy::Auto => {
327                // Auto-choose: rebase if no local commits, merge otherwise
328                if ahead == 0 { "merge" } else { "rebase" }
329            }
330        };
331
332        // Perform sync
333        match strategy_name {
334            "merge" => {
335                GitOperations::run_status(&["merge", &upstream_branch])?;
336                Ok(format!("āœ… Merged {behind} commits from {upstream_branch}"))
337            }
338            "rebase" => {
339                GitOperations::run_status(&["rebase", &upstream_branch])?;
340                Ok(format!("āœ… Rebased {ahead} commits onto {upstream_branch}"))
341            }
342            _ => unreachable!(),
343        }
344    }
345
346    fn name(&self) -> &'static str {
347        "sync"
348    }
349
350    fn description(&self) -> &'static str {
351        "Sync current branch with upstream"
352    }
353}
354
355impl GitCommand for SyncCommand {}
356
357/// Upstream actions
358#[derive(Debug, Clone)]
359pub enum UpstreamAction {
360    Set { remote: String, branch: String },
361    Status,
362    SyncAll,
363}
364
365/// Command to manage upstream configuration
366pub struct UpstreamCommand {
367    action: UpstreamAction,
368}
369
370impl UpstreamCommand {
371    pub fn new(action: UpstreamAction) -> Self {
372        Self { action }
373    }
374}
375
376impl Command for UpstreamCommand {
377    fn execute(&self) -> Result<String> {
378        match &self.action {
379            UpstreamAction::Set { remote, branch } => {
380                RemoteOperations::set_upstream(remote, branch)?;
381                Ok(format!("āœ… Set upstream to {remote}/{branch}"))
382            }
383            UpstreamAction::Status => {
384                let branches = GitOperations::local_branches()?;
385                let mut output = BufferedOutput::new();
386                output.add_line("šŸ”— Upstream Status:".to_string());
387
388                for branch in branches {
389                    // Switch to each branch and check upstream
390                    // This is a simplified version - in practice you'd want to parse git config
391                    output.add_line(format!("šŸ“ {branch}: (checking...)"));
392                }
393
394                Ok(output.content())
395            }
396            UpstreamAction::SyncAll => {
397                let current_branch = GitOperations::current_branch()?;
398                let branches = GitOperations::local_branches()?;
399                let mut synced = 0;
400
401                for branch in branches {
402                    if branch == current_branch {
403                        continue; // Skip current branch
404                    }
405
406                    // Try to sync each branch (simplified)
407                    if BranchOperations::switch(&branch).is_ok()
408                        && SyncCommand::new(SyncStrategy::Auto).execute().is_ok()
409                    {
410                        synced += 1;
411                    }
412                }
413
414                // Return to original branch
415                BranchOperations::switch(&current_branch)?;
416
417                Ok(format!("āœ… Synced {synced} branches"))
418            }
419        }
420    }
421
422    fn name(&self) -> &'static str {
423        "upstream"
424    }
425
426    fn description(&self) -> &'static str {
427        "Manage upstream branch configuration"
428    }
429}
430
431impl GitCommand for UpstreamCommand {}
432
433/// Command to show what would be pushed/pulled
434pub struct WhatCommand {
435    target: Option<String>,
436}
437
438impl WhatCommand {
439    pub fn new(target: Option<String>) -> Self {
440        Self { target }
441    }
442}
443
444impl Command for WhatCommand {
445    fn execute(&self) -> Result<String> {
446        let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
447        let mut output = BufferedOutput::new();
448
449        let target_ref = self
450            .target
451            .as_deref()
452            .unwrap_or_else(|| upstream.as_deref().unwrap_or("origin/main"));
453
454        output.add_line(format!("šŸ” Comparing {current_branch} with {target_ref}"));
455        output.add_line("=".repeat(50));
456
457        // Show commits that would be pushed
458        if ahead > 0 {
459            output.add_line(format!("šŸ“¤ {ahead} commit(s) to push:"));
460            match GitOperations::run(&["log", "--oneline", &format!("{target_ref}..HEAD")]) {
461                Ok(commits) => {
462                    for line in commits.lines() {
463                        output.add_line(format!("  • {line}"));
464                    }
465                }
466                Err(_) => {
467                    output.add_line("  (Could not retrieve commit details)".to_string());
468                }
469            }
470        } else {
471            output.add_line("šŸ“¤ No commits to push".to_string());
472        }
473
474        // Show commits that would be pulled
475        if behind > 0 {
476            output.add_line(format!("\nšŸ“„ {behind} commit(s) to pull:"));
477            match GitOperations::run(&["log", "--oneline", &format!("HEAD..{target_ref}")]) {
478                Ok(commits) => {
479                    for line in commits.lines() {
480                        output.add_line(format!("  • {line}"));
481                    }
482                }
483                Err(_) => {
484                    output.add_line("  (Could not retrieve commit details)".to_string());
485                }
486            }
487        } else {
488            output.add_line("\nšŸ“„ No commits to pull".to_string());
489        }
490
491        Ok(output.content())
492    }
493
494    fn name(&self) -> &'static str {
495        "what"
496    }
497
498    fn description(&self) -> &'static str {
499        "Show what would be pushed or pulled"
500    }
501}
502
503impl GitCommand for WhatCommand {}