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
12pub 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 let current_branch = GitOperations::current_branch()
41 .map_err(|e| GitXError::GitCommand(format!("Failed to get current branch: {e}")))?;
42
43 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, ¤t_branch, &protected_branches))
50 .collect();
51
52 if branches.is_empty() {
53 println!("✅ No merged branches to prune.");
54 return Ok(());
55 }
56
57 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 let mut output = BufferedOutput::new();
80 let mut error_output = BufferedOutput::new();
81
82 if dry_run {
83 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 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 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}