1use crate::core::traits::*;
2use crate::core::{git::*, interactive::Interactive, safety::Safety, validation::Validate};
3use crate::{GitXError, Result};
4
5pub struct BranchCommands;
7
8impl BranchCommands {
9 pub fn new_branch(name: &str, from: Option<&str>) -> Result<String> {
11 NewBranchCommand::new(name.to_string(), from.map(|s| s.to_string())).execute()
12 }
13
14 pub fn clean_branches(dry_run: bool) -> Result<String> {
16 CleanBranchesCommand::new(dry_run).execute()
17 }
18
19 pub fn switch_recent() -> Result<String> {
21 SwitchRecentCommand::new().execute()
22 }
23
24 pub fn rename_branch(new_name: &str) -> Result<String> {
26 RenameBranchCommand::new(new_name.to_string()).execute()
27 }
28
29 pub fn prune_branches(dry_run: bool) -> Result<String> {
31 PruneBranchesCommand::new(dry_run).execute()
32 }
33
34 pub fn stash_branch(branch_name: &str) -> Result<String> {
36 StashBranchCommand::new(branch_name.to_string()).execute()
37 }
38}
39
40pub struct NewBranchCommand {
42 name: String,
43 from: Option<String>,
44}
45
46impl NewBranchCommand {
47 pub fn new(name: String, from: Option<String>) -> Self {
48 Self { name, from }
49 }
50}
51
52impl Command for NewBranchCommand {
53 fn execute(&self) -> Result<String> {
54 Validate::branch_name(&self.name)?;
56
57 if let Some(ref base) = self.from {
58 if !GitOperations::commit_exists(base)? {
59 return Err(GitXError::GitCommand(format!(
60 "Base branch or ref '{base}' does not exist"
61 )));
62 }
63 }
64
65 if BranchOperations::exists(&self.name)? {
67 return Err(GitXError::GitCommand(format!(
68 "Branch '{}' already exists",
69 self.name
70 )));
71 }
72
73 BranchOperations::create(&self.name, self.from.as_deref())?;
75
76 Ok(format!("✅ Created and switched to branch '{}'", self.name))
77 }
78
79 fn name(&self) -> &'static str {
80 "new-branch"
81 }
82
83 fn description(&self) -> &'static str {
84 "Create and switch to a new branch"
85 }
86}
87
88impl GitCommand for NewBranchCommand {}
89
90pub struct CleanBranchesCommand {
92 dry_run: bool,
93}
94
95impl CleanBranchesCommand {
96 pub fn new(dry_run: bool) -> Self {
97 Self { dry_run }
98 }
99
100 fn get_protected_branches() -> Vec<&'static str> {
101 vec!["main", "master", "develop"]
102 }
103
104 fn is_protected_branch(branch: &str) -> bool {
105 Self::get_protected_branches().contains(&branch)
106 }
107}
108
109impl Command for CleanBranchesCommand {
110 fn execute(&self) -> Result<String> {
111 let merged_branches = GitOperations::merged_branches()?;
112 let current_branch = GitOperations::current_branch()?;
113
114 let branches_to_delete: Vec<String> = merged_branches
115 .into_iter()
116 .filter(|branch| branch != ¤t_branch)
117 .filter(|branch| !Self::is_protected_branch(branch))
118 .collect();
119
120 if branches_to_delete.is_empty() {
121 return Ok("No merged branches to delete.".to_string());
122 }
123
124 if self.dry_run {
125 let mut result = format!(
126 "🧪 (dry run) {} branches would be deleted:\n",
127 branches_to_delete.len()
128 );
129 for branch in &branches_to_delete {
130 result.push_str(&format!("(dry run) Would delete: {branch}\n"));
131 }
132 return Ok(result);
133 }
134
135 let details = format!(
137 "This will delete {} merged branches: {}",
138 branches_to_delete.len(),
139 branches_to_delete.join(", ")
140 );
141
142 if !Safety::confirm_destructive_operation("Clean merged branches", &details)? {
143 return Ok("Operation cancelled by user.".to_string());
144 }
145
146 let mut deleted = Vec::new();
147 for branch in branches_to_delete {
148 if BranchOperations::delete(&branch, false).is_ok() {
149 deleted.push(branch);
150 }
151 }
152
153 Ok(format!(
154 "🧹 Deleted {} merged branches:\n{}",
155 deleted.len(),
156 deleted.join("\n")
157 ))
158 }
159
160 fn name(&self) -> &'static str {
161 "clean-branches"
162 }
163
164 fn description(&self) -> &'static str {
165 "Delete merged branches"
166 }
167}
168
169impl GitCommand for CleanBranchesCommand {}
170impl DryRunnable for CleanBranchesCommand {
171 fn execute_dry_run(&self) -> Result<String> {
172 CleanBranchesCommand::new(true).execute()
173 }
174
175 fn is_dry_run(&self) -> bool {
176 self.dry_run
177 }
178}
179
180impl Destructive for CleanBranchesCommand {
181 fn destruction_description(&self) -> String {
182 "This will permanently delete merged branches".to_string()
183 }
184}
185
186pub struct SwitchRecentCommand;
188
189impl Default for SwitchRecentCommand {
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195impl SwitchRecentCommand {
196 pub fn new() -> Self {
197 Self
198 }
199}
200
201impl Command for SwitchRecentCommand {
202 fn execute(&self) -> Result<String> {
203 let branches = GitOperations::recent_branches(Some(10))?;
204
205 if branches.is_empty() {
206 return Err(GitXError::GitCommand(
207 "No recent branches found".to_string(),
208 ));
209 }
210
211 let selected_branch = if Interactive::is_interactive() {
212 Interactive::branch_picker(&branches, Some("Select a recent branch to switch to"))?
213 } else {
214 branches[0].clone()
216 };
217
218 BranchOperations::switch(&selected_branch)?;
219 Ok(format!("Switched to branch '{selected_branch}'"))
220 }
221
222 fn name(&self) -> &'static str {
223 "switch-recent"
224 }
225
226 fn description(&self) -> &'static str {
227 "Switch to a recently used branch"
228 }
229}
230
231impl GitCommand for SwitchRecentCommand {}
232impl crate::core::traits::Interactive for SwitchRecentCommand {
233 fn execute_non_interactive(&self) -> Result<String> {
234 let branches = GitOperations::recent_branches(Some(1))?;
235 if branches.is_empty() {
236 return Err(GitXError::GitCommand(
237 "No recent branches found".to_string(),
238 ));
239 }
240 BranchOperations::switch(&branches[0])?;
241 Ok(format!("Switched to branch '{}'", branches[0]))
242 }
243}
244
245pub struct RenameBranchCommand {
247 new_name: String,
248}
249
250impl RenameBranchCommand {
251 pub fn new(new_name: String) -> Self {
252 Self { new_name }
253 }
254}
255
256impl Command for RenameBranchCommand {
257 fn execute(&self) -> Result<String> {
258 Validate::branch_name(&self.new_name)?;
259
260 let current_branch = GitOperations::current_branch()?;
261
262 if BranchOperations::exists(&self.new_name)? {
263 return Err(GitXError::GitCommand(format!(
264 "Branch '{}' already exists",
265 self.new_name
266 )));
267 }
268
269 BranchOperations::rename(&self.new_name)?;
270 Ok(format!(
271 "✅ Renamed branch '{}' to '{}'",
272 current_branch, self.new_name
273 ))
274 }
275
276 fn name(&self) -> &'static str {
277 "rename-branch"
278 }
279
280 fn description(&self) -> &'static str {
281 "Rename the current branch"
282 }
283}
284
285impl GitCommand for RenameBranchCommand {}
286
287pub struct PruneBranchesCommand {
289 dry_run: bool,
290}
291
292impl PruneBranchesCommand {
293 pub fn new(dry_run: bool) -> Self {
294 Self { dry_run }
295 }
296}
297
298impl Command for PruneBranchesCommand {
299 fn execute(&self) -> Result<String> {
300 if self.dry_run {
301 GitOperations::run(&["remote", "prune", "origin", "--dry-run"])?;
302 Ok("🧪 (dry run) Showed what would be pruned".to_string())
303 } else {
304 GitOperations::run_status(&["remote", "prune", "origin"])?;
305 Ok("🧹 Pruned remote tracking branches".to_string())
306 }
307 }
308
309 fn name(&self) -> &'static str {
310 "prune-branches"
311 }
312
313 fn description(&self) -> &'static str {
314 "Prune remote tracking branches"
315 }
316}
317
318impl GitCommand for PruneBranchesCommand {}
319impl DryRunnable for PruneBranchesCommand {
320 fn execute_dry_run(&self) -> Result<String> {
321 PruneBranchesCommand::new(true).execute()
322 }
323
324 fn is_dry_run(&self) -> bool {
325 self.dry_run
326 }
327}
328
329pub struct StashBranchCommand {
331 branch_name: String,
332}
333
334impl StashBranchCommand {
335 pub fn new(branch_name: String) -> Self {
336 Self { branch_name }
337 }
338}
339
340impl Command for StashBranchCommand {
341 fn execute(&self) -> Result<String> {
342 Validate::branch_name(&self.branch_name)?;
343
344 if BranchOperations::exists(&self.branch_name)? {
345 return Err(GitXError::GitCommand(format!(
346 "Branch '{}' already exists",
347 self.branch_name
348 )));
349 }
350
351 BranchOperations::create(&self.branch_name, None)?;
353
354 GitOperations::run_status(&["reset", "--hard", "HEAD"])?;
356
357 Ok(format!(
358 "✅ Created branch '{}' with current changes and reset working directory",
359 self.branch_name
360 ))
361 }
362
363 fn name(&self) -> &'static str {
364 "stash-branch"
365 }
366
367 fn description(&self) -> &'static str {
368 "Create a branch with current changes and reset working directory"
369 }
370}
371
372impl GitCommand for StashBranchCommand {}
373impl Destructive for StashBranchCommand {
374 fn destruction_description(&self) -> String {
375 "This will reset your working directory to a clean state".to_string()
376 }
377}