git_x/commands/
branch.rs

1use crate::core::traits::*;
2use crate::core::{git::*, interactive::Interactive, safety::Safety, validation::Validate};
3use crate::{GitXError, Result};
4
5/// Branch-related commands grouped together
6pub struct BranchCommands;
7
8impl BranchCommands {
9    /// Create a new branch command
10    pub fn new_branch(name: &str, from: Option<&str>) -> Result<String> {
11        use crate::commands::repository::NewBranchCommand;
12        NewBranchCommand::new(name.to_string(), from.map(|s| s.to_string())).execute()
13    }
14
15    /// Clean merged branches command
16    pub fn clean_branches(dry_run: bool) -> Result<String> {
17        CleanBranchesCommand::new(dry_run).execute()
18    }
19
20    /// Switch to recent branch command
21    pub fn switch_recent() -> Result<String> {
22        SwitchRecentCommand::new().execute()
23    }
24
25    /// Rename current branch command
26    pub fn rename_branch(new_name: &str) -> Result<String> {
27        RenameBranchCommand::new(new_name.to_string()).execute()
28    }
29
30    /// Prune remote branches command
31    pub fn prune_branches(dry_run: bool) -> Result<String> {
32        PruneBranchesCommand::new(dry_run).execute()
33    }
34
35    /// Stash current work into a branch
36    pub fn stash_branch(branch_name: &str) -> Result<String> {
37        StashBranchCommand::new(branch_name.to_string()).execute()
38    }
39}
40
41/// Command to clean merged branches
42pub struct CleanBranchesCommand {
43    dry_run: bool,
44}
45
46impl CleanBranchesCommand {
47    pub fn new(dry_run: bool) -> Self {
48        Self { dry_run }
49    }
50
51    fn get_protected_branches() -> Vec<&'static str> {
52        vec!["main", "master", "develop"]
53    }
54
55    fn is_protected_branch(branch: &str) -> bool {
56        Self::get_protected_branches().contains(&branch)
57    }
58}
59
60impl Command for CleanBranchesCommand {
61    fn execute(&self) -> Result<String> {
62        let merged_branches = GitOperations::merged_branches()?;
63        let current_branch = GitOperations::current_branch()?;
64
65        let branches_to_delete: Vec<String> = merged_branches
66            .into_iter()
67            .filter(|branch| branch != &current_branch)
68            .filter(|branch| !Self::is_protected_branch(branch))
69            .collect();
70
71        if branches_to_delete.is_empty() {
72            return Ok("No merged branches to delete.".to_string());
73        }
74
75        if self.dry_run {
76            let mut result = format!(
77                "๐Ÿงช (dry run) {} branches would be deleted:\n",
78                branches_to_delete.len()
79            );
80            for branch in &branches_to_delete {
81                result.push_str(&format!("(dry run) Would delete: {branch}\n"));
82            }
83            return Ok(result);
84        }
85
86        // Confirm deletion
87        let details = format!(
88            "This will delete {} merged branches: {}",
89            branches_to_delete.len(),
90            branches_to_delete.join(", ")
91        );
92
93        if !Safety::confirm_destructive_operation("Clean merged branches", &details)? {
94            return Ok("Operation cancelled by user.".to_string());
95        }
96
97        let mut deleted = Vec::new();
98        for branch in branches_to_delete {
99            if BranchOperations::delete(&branch, false).is_ok() {
100                deleted.push(branch);
101            }
102        }
103
104        Ok(format!(
105            "๐Ÿงน Deleted {} merged branches:\n{}",
106            deleted.len(),
107            deleted.join("\n")
108        ))
109    }
110
111    fn name(&self) -> &'static str {
112        "clean-branches"
113    }
114
115    fn description(&self) -> &'static str {
116        "Delete merged branches"
117    }
118}
119
120impl GitCommand for CleanBranchesCommand {}
121
122/// Async parallel version of CleanBranchesCommand
123pub struct AsyncCleanBranchesCommand {
124    dry_run: bool,
125}
126
127impl AsyncCleanBranchesCommand {
128    pub fn new(dry_run: bool) -> Self {
129        Self { dry_run }
130    }
131
132    pub async fn execute_parallel(&self) -> Result<String> {
133        use crate::core::{git::AsyncGitOperations, safety::Safety};
134
135        // Get merged branches and current branch in parallel
136        let (merged_branches_result, current_branch_result) = tokio::try_join!(
137            AsyncGitOperations::merged_branches(),
138            AsyncGitOperations::current_branch()
139        )?;
140
141        let branches_to_delete: Vec<String> = merged_branches_result
142            .into_iter()
143            .filter(|branch| branch != &current_branch_result)
144            .filter(|branch| !Self::is_protected_branch(branch))
145            .collect();
146
147        if branches_to_delete.is_empty() {
148            return Ok("No merged branches to delete.".to_string());
149        }
150
151        if self.dry_run {
152            let mut result = format!(
153                "๐Ÿงช (dry run) {} branches would be deleted:\n",
154                branches_to_delete.len()
155            );
156            for branch in &branches_to_delete {
157                result.push_str(&format!("(dry run) Would delete: {branch}\n"));
158            }
159            return Ok(result);
160        }
161
162        // Confirm deletion
163        let details = format!(
164            "This will delete {} merged branches: {}",
165            branches_to_delete.len(),
166            branches_to_delete.join(", ")
167        );
168
169        if !Safety::confirm_destructive_operation("Clean merged branches", &details)? {
170            return Ok("Operation cancelled by user.".to_string());
171        }
172
173        // Delete branches in parallel (but carefully)
174        let delete_tasks = branches_to_delete
175            .iter()
176            .map(|branch| self.delete_branch_async(branch.clone()));
177
178        let results = futures::future::join_all(delete_tasks).await;
179
180        let mut deleted = Vec::new();
181        let mut failed = Vec::new();
182
183        for (branch, result) in branches_to_delete.iter().zip(results.iter()) {
184            match result {
185                Ok(true) => deleted.push(branch.clone()),
186                Ok(false) | Err(_) => failed.push(branch.clone()),
187            }
188        }
189
190        let mut result = String::new();
191
192        if !deleted.is_empty() {
193            result.push_str(&format!("โœ… Deleted {} branches:\n", deleted.len()));
194            for branch in deleted {
195                result.push_str(&format!("   ๐Ÿ—‘๏ธ  {branch}\n"));
196            }
197        }
198
199        if !failed.is_empty() {
200            result.push_str(&format!("โŒ Failed to delete {} branches:\n", failed.len()));
201            for branch in failed {
202                result.push_str(&format!("   โš ๏ธ  {branch}\n"));
203            }
204        }
205
206        Ok(result)
207    }
208
209    async fn delete_branch_async(&self, branch: String) -> Result<bool> {
210        use crate::core::git::AsyncGitOperations;
211
212        match AsyncGitOperations::run_status(&["branch", "-d", &branch]).await {
213            Ok(_) => Ok(true),
214            Err(_) => Ok(false),
215        }
216    }
217
218    fn get_protected_branches() -> Vec<&'static str> {
219        vec!["main", "master", "develop"]
220    }
221
222    fn is_protected_branch(branch: &str) -> bool {
223        Self::get_protected_branches().contains(&branch)
224    }
225}
226impl DryRunnable for CleanBranchesCommand {
227    fn execute_dry_run(&self) -> Result<String> {
228        CleanBranchesCommand::new(true).execute()
229    }
230
231    fn is_dry_run(&self) -> bool {
232        self.dry_run
233    }
234}
235
236impl Destructive for CleanBranchesCommand {
237    fn destruction_description(&self) -> String {
238        "This will permanently delete merged branches".to_string()
239    }
240}
241
242/// Command to switch to a recent branch
243pub struct SwitchRecentCommand;
244
245impl Default for SwitchRecentCommand {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251impl SwitchRecentCommand {
252    pub fn new() -> Self {
253        Self
254    }
255}
256
257impl Command for SwitchRecentCommand {
258    fn execute(&self) -> Result<String> {
259        let branches = GitOperations::recent_branches(Some(10))?;
260
261        if branches.is_empty() {
262            return Err(GitXError::GitCommand(
263                "No recent branches found".to_string(),
264            ));
265        }
266
267        let selected_branch = if Interactive::is_interactive() {
268            Interactive::branch_picker(&branches, Some("Select a recent branch to switch to"))?
269        } else {
270            // In non-interactive mode, just switch to the most recent branch
271            branches[0].clone()
272        };
273
274        BranchOperations::switch(&selected_branch)?;
275        Ok(format!("Switched to branch '{selected_branch}'"))
276    }
277
278    fn name(&self) -> &'static str {
279        "switch-recent"
280    }
281
282    fn description(&self) -> &'static str {
283        "Switch to a recently used branch"
284    }
285}
286
287impl GitCommand for SwitchRecentCommand {}
288impl crate::core::traits::Interactive for SwitchRecentCommand {
289    fn execute_non_interactive(&self) -> Result<String> {
290        let branches = GitOperations::recent_branches(Some(1))?;
291        if branches.is_empty() {
292            return Err(GitXError::GitCommand(
293                "No recent branches found".to_string(),
294            ));
295        }
296        BranchOperations::switch(&branches[0])?;
297        Ok(format!("Switched to branch '{}'", branches[0]))
298    }
299}
300
301/// Command to rename current branch
302pub struct RenameBranchCommand {
303    new_name: String,
304}
305
306impl RenameBranchCommand {
307    pub fn new(new_name: String) -> Self {
308        Self { new_name }
309    }
310}
311
312impl Command for RenameBranchCommand {
313    fn execute(&self) -> Result<String> {
314        Validate::branch_name(&self.new_name)?;
315
316        let current_branch = GitOperations::current_branch()?;
317
318        if BranchOperations::exists(&self.new_name)? {
319            return Err(GitXError::GitCommand(format!(
320                "Branch '{}' already exists",
321                self.new_name
322            )));
323        }
324
325        BranchOperations::rename(&self.new_name)?;
326        Ok(format!(
327            "โœ… Renamed branch '{}' to '{}'",
328            current_branch, self.new_name
329        ))
330    }
331
332    fn name(&self) -> &'static str {
333        "rename-branch"
334    }
335
336    fn description(&self) -> &'static str {
337        "Rename the current branch"
338    }
339}
340
341impl GitCommand for RenameBranchCommand {}
342
343/// Command to prune (delete) merged local branches
344pub struct PruneBranchesCommand {
345    dry_run: bool,
346}
347
348impl PruneBranchesCommand {
349    pub fn new(dry_run: bool) -> Self {
350        Self { dry_run }
351    }
352
353    fn get_protected_branches() -> Vec<&'static str> {
354        vec!["main", "master", "develop"]
355    }
356
357    fn is_protected_branch(branch: &str) -> bool {
358        Self::get_protected_branches().contains(&branch)
359    }
360}
361
362impl Command for PruneBranchesCommand {
363    fn execute(&self) -> Result<String> {
364        let merged_branches = GitOperations::merged_branches()?;
365        let current_branch = GitOperations::current_branch()?;
366
367        let branches_to_delete: Vec<String> = merged_branches
368            .into_iter()
369            .filter(|branch| branch != &current_branch)
370            .filter(|branch| !Self::is_protected_branch(branch))
371            .collect();
372
373        if branches_to_delete.is_empty() {
374            return Ok("โœ… No merged branches to prune.".to_string());
375        }
376
377        if self.dry_run {
378            let mut result = format!(
379                "๐Ÿงช (dry run) {} branches would be deleted:\n",
380                branches_to_delete.len()
381            );
382            for branch in &branches_to_delete {
383                result.push_str(&format!("(dry run) Would delete: {branch}\n"));
384            }
385            return Ok(result);
386        }
387
388        // Confirm deletion
389        let details = format!(
390            "This will delete {} merged branches: {}",
391            branches_to_delete.len(),
392            branches_to_delete.join(", ")
393        );
394
395        if !Safety::confirm_destructive_operation("Delete merged branches", &details)? {
396            return Ok("Operation cancelled by user.".to_string());
397        }
398
399        let mut deleted = Vec::new();
400        for branch in branches_to_delete {
401            if BranchOperations::delete(&branch, false).is_ok() {
402                deleted.push(branch);
403            }
404        }
405
406        Ok(format!(
407            "๐Ÿงน Deleted {} merged branches:\n{}",
408            deleted.len(),
409            deleted.join("\n")
410        ))
411    }
412
413    fn name(&self) -> &'static str {
414        "prune-branches"
415    }
416
417    fn description(&self) -> &'static str {
418        "Delete merged local branches (except protected ones)"
419    }
420}
421
422impl GitCommand for PruneBranchesCommand {}
423impl DryRunnable for PruneBranchesCommand {
424    fn execute_dry_run(&self) -> Result<String> {
425        PruneBranchesCommand::new(true).execute()
426    }
427
428    fn is_dry_run(&self) -> bool {
429        self.dry_run
430    }
431}
432
433impl Destructive for PruneBranchesCommand {
434    fn destruction_description(&self) -> String {
435        "This will permanently delete merged branches".to_string()
436    }
437}
438
439/// Command to stash work into a new branch
440pub struct StashBranchCommand {
441    branch_name: String,
442}
443
444impl StashBranchCommand {
445    pub fn new(branch_name: String) -> Self {
446        Self { branch_name }
447    }
448}
449
450impl Command for StashBranchCommand {
451    fn execute(&self) -> Result<String> {
452        Validate::branch_name(&self.branch_name)?;
453
454        if BranchOperations::exists(&self.branch_name)? {
455            return Err(GitXError::GitCommand(format!(
456                "Branch '{}' already exists",
457                self.branch_name
458            )));
459        }
460
461        // Create branch from current state
462        BranchOperations::create(&self.branch_name, None)?;
463
464        // Reset to clean state
465        GitOperations::run_status(&["reset", "--hard", "HEAD"])?;
466
467        Ok(format!(
468            "โœ… Created branch '{}' with current changes and reset working directory",
469            self.branch_name
470        ))
471    }
472
473    fn name(&self) -> &'static str {
474        "stash-branch"
475    }
476
477    fn description(&self) -> &'static str {
478        "Create a branch with current changes and reset working directory"
479    }
480}
481
482impl GitCommand for StashBranchCommand {}
483impl Destructive for StashBranchCommand {
484    fn destruction_description(&self) -> String {
485        "This will reset your working directory to a clean state".to_string()
486    }
487}