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 use crate::commands::repository::NewBranchCommand;
12 NewBranchCommand::new(name.to_string(), from.map(|s| s.to_string())).execute()
13 }
14
15 pub fn clean_branches(dry_run: bool) -> Result<String> {
17 CleanBranchesCommand::new(dry_run).execute()
18 }
19
20 pub fn switch_recent() -> Result<String> {
22 SwitchRecentCommand::new().execute()
23 }
24
25 pub fn rename_branch(new_name: &str) -> Result<String> {
27 RenameBranchCommand::new(new_name.to_string()).execute()
28 }
29
30 pub fn prune_branches(dry_run: bool) -> Result<String> {
32 PruneBranchesCommand::new(dry_run).execute()
33 }
34
35 pub fn stash_branch(branch_name: &str) -> Result<String> {
37 StashBranchCommand::new(branch_name.to_string()).execute()
38 }
39}
40
41pub 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 != ¤t_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 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
122pub 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 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 != ¤t_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 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 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
242pub 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 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
301pub 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
343pub 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 != ¤t_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 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
439pub 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 BranchOperations::create(&self.branch_name, None)?;
463
464 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}