git_workty/commands/
clean.rs1use crate::config::Config;
2use crate::git::GitRepo;
3use crate::status::is_worktree_dirty;
4use crate::ui::{print_info, print_success, print_warning};
5use crate::worktree::{list_worktrees, Worktree};
6use anyhow::{Context, Result};
7use dialoguer::Confirm;
8use is_terminal::IsTerminal;
9use std::process::Command;
10
11pub struct CleanOptions {
12 pub merged: bool,
13 pub dry_run: bool,
14 pub yes: bool,
15}
16
17pub fn execute(repo: &GitRepo, opts: CleanOptions) -> Result<()> {
18 let config = Config::load(repo)?;
19 let worktrees = list_worktrees(repo)?;
20 let current_path = std::env::current_dir().unwrap_or_default();
21
22 let candidates: Vec<&Worktree> = worktrees
23 .iter()
24 .filter(|wt| {
25 if wt.path == current_path {
26 return false;
27 }
28
29 if wt.is_main_worktree(repo) {
30 return false;
31 }
32
33 if wt.detached {
34 return false;
35 }
36
37 if let Some(branch) = &wt.branch_short {
38 if branch == &config.base {
39 return false;
40 }
41 }
42
43 if opts.merged {
44 if let Some(branch) = &wt.branch_short {
45 matches!(repo.is_merged(branch, &config.base), Ok(true))
46 } else {
47 false
48 }
49 } else {
50 true
51 }
52 })
53 .collect();
54
55 if candidates.is_empty() {
56 print_info("No worktrees to clean up.");
57 return Ok(());
58 }
59
60 println!("Worktrees to remove:");
61 for wt in &candidates {
62 let dirty = if is_worktree_dirty(wt) {
63 " (dirty)"
64 } else {
65 ""
66 };
67 println!(" - {}{}", wt.name(), dirty);
68 }
69
70 if opts.dry_run {
71 print_info("Dry run - no worktrees removed.");
72 return Ok(());
73 }
74
75 let dirty_count = candidates.iter().filter(|wt| is_worktree_dirty(wt)).count();
76 if dirty_count > 0 {
77 print_warning(&format!(
78 "{} worktree(s) have uncommitted changes and will be skipped.",
79 dirty_count
80 ));
81 }
82
83 let clean_candidates: Vec<&&Worktree> = candidates
84 .iter()
85 .filter(|wt| !is_worktree_dirty(wt))
86 .collect();
87
88 if clean_candidates.is_empty() {
89 print_info("All candidate worktrees have uncommitted changes. Nothing to remove.");
90 return Ok(());
91 }
92
93 if !opts.yes && std::io::stdin().is_terminal() {
94 let confirm = Confirm::new()
95 .with_prompt(format!("Remove {} worktree(s)?", clean_candidates.len()))
96 .default(false)
97 .interact()?;
98
99 if !confirm {
100 eprintln!("Aborted.");
101 return Ok(());
102 }
103 } else if !opts.yes {
104 print_warning("Non-interactive mode requires --yes flag for destructive operations.");
105 std::process::exit(1);
106 }
107
108 let mut removed = 0;
109 for wt in clean_candidates {
110 let output = Command::new("git")
111 .current_dir(&repo.root)
112 .args(["worktree", "remove", wt.path.to_str().unwrap()])
113 .output()
114 .context("Failed to remove worktree")?;
115
116 if output.status.success() {
117 print_success(&format!("Removed worktree '{}'", wt.name()));
118 removed += 1;
119 } else {
120 let stderr = String::from_utf8_lossy(&output.stderr);
121 print_warning(&format!(
122 "Failed to remove '{}': {}",
123 wt.name(),
124 stderr.trim()
125 ));
126 }
127 }
128
129 print_info(&format!("Cleaned up {} worktree(s).", removed));
130
131 Ok(())
132}