git_x/
prune_branches.rs

1use crate::GitXError;
2use crate::command::Command;
3use crate::core::git::{BranchOperations, GitOperations};
4use crate::core::output::BufferedOutput;
5use crate::core::safety::Safety;
6
7pub fn run(except: Option<String>, dry_run: bool) -> Result<(), GitXError> {
8    let cmd = PruneBranchesCommand;
9    cmd.execute((except, dry_run))
10}
11
12/// Command implementation for git prune-branches
13pub struct PruneBranchesCommand;
14
15impl Command for PruneBranchesCommand {
16    type Input = (Option<String>, bool);
17    type Output = ();
18
19    fn execute(&self, (except, dry_run): (Option<String>, bool)) -> Result<(), GitXError> {
20        run_prune_branches(except, dry_run)
21    }
22
23    fn name(&self) -> &'static str {
24        "prune-branches"
25    }
26
27    fn description(&self) -> &'static str {
28        "Prune merged branches with optional exceptions"
29    }
30
31    fn is_destructive(&self) -> bool {
32        true
33    }
34}
35
36fn run_prune_branches(except: Option<String>, dry_run: bool) -> Result<(), GitXError> {
37    let protected_branches = get_all_protected_branches(except.as_deref());
38
39    // Step 1: Get current branch
40    let current_branch = GitOperations::current_branch()
41        .map_err(|e| GitXError::GitCommand(format!("Failed to get current branch: {e}")))?;
42
43    // Step 2: Get merged branches
44    let merged_branches = GitOperations::merged_branches()
45        .map_err(|e| GitXError::GitCommand(format!("Failed to get merged branches: {e}")))?;
46
47    let branches: Vec<String> = merged_branches
48        .into_iter()
49        .filter(|b| !is_branch_protected(b, &current_branch, &protected_branches))
50        .collect();
51
52    if branches.is_empty() {
53        println!("✅ No merged branches to prune.");
54        return Ok(());
55    }
56
57    // Step 3: Safety confirmation for destructive operation (skip if dry run)
58    if !dry_run {
59        let details = format!(
60            "This will delete {} merged branches: {}",
61            branches.len(),
62            branches.join(", ")
63        );
64
65        match Safety::confirm_destructive_operation("Prune merged branches", &details) {
66            Ok(confirmed) => {
67                if !confirmed {
68                    println!("Operation cancelled by user.");
69                    return Ok(());
70                }
71            }
72            Err(e) => {
73                return Err(e);
74            }
75        }
76    }
77
78    // Step 4: Delete branches or show what would be deleted
79    let mut output = BufferedOutput::new();
80    let mut error_output = BufferedOutput::new();
81
82    if dry_run {
83        // Dry run: show what would be deleted
84        let count = branches.len();
85        output.add_line(format!("🧪 (dry run) {count} branches would be deleted:"));
86        for branch in branches {
87            output.add_line(branch);
88        }
89    } else {
90        // Actually delete branches
91        for branch in branches {
92            match BranchOperations::delete(&branch, false) {
93                Ok(()) => {
94                    output.add_line(branch);
95                }
96                Err(_) => {
97                    error_output.add_line(branch);
98                }
99            }
100        }
101    }
102
103    // Flush all outputs at once for better performance
104    output.flush();
105    error_output.flush_err();
106
107    Ok(())
108}
109
110const DEFAULT_PROTECTED_BRANCHES: &[&str] = &["main", "master", "develop"];
111
112pub fn get_all_protected_branches(except: Option<&str>) -> Vec<String> {
113    let mut protected: Vec<String> = DEFAULT_PROTECTED_BRANCHES
114        .iter()
115        .map(|&s| s.to_string())
116        .collect();
117
118    if let Some(except_str) = except {
119        let vec: Vec<_> = except_str
120            .split(',')
121            .map(|s| s.trim().to_string())
122            .filter(|s| !s.is_empty())
123            .collect();
124        protected.extend(vec);
125    }
126
127    protected
128}
129
130pub fn is_branch_protected(
131    branch: &str,
132    current_branch: &str,
133    protected_branches: &[String],
134) -> bool {
135    branch == current_branch || protected_branches.iter().any(|pb| pb == branch)
136}