git_worktree_manager/operations/
global_ops.rs1use console::style;
6
7use crate::console as cwconsole;
8use crate::constants::{
9 format_config_key, home_dir_or_fallback, path_age_days, CONFIG_KEY_INTENDED_BRANCH,
10};
11use crate::error::Result;
12use crate::git;
13use crate::registry;
14
15use super::display::{format_age, get_worktree_status};
16
17struct GlobalWorktreeRow {
19 repo_name: String,
20 worktree_id: String,
21 current_branch: String,
22 status: String,
23 age: String,
24 rel_path: String,
25}
26
27const MIN_GLOBAL_TABLE_WIDTH: usize = 125;
29
30pub fn global_list_worktrees() -> Result<()> {
32 if let Ok(removed) = registry::prune_registry() {
34 if !removed.is_empty() {
35 println!(
36 "{}",
37 style(format!(
38 "Auto-pruned {} stale registry entry(s)",
39 removed.len()
40 ))
41 .dim()
42 );
43 }
44 }
45
46 let repos = registry::get_all_registered_repos();
47
48 if repos.is_empty() {
49 println!(
50 "\n{}\n\
51 Use {} to discover repositories,\n\
52 or run {} in a repository to auto-register it.\n",
53 style("No repositories registered.").yellow(),
54 style("gw -g scan").cyan(),
55 style("gw new").cyan(),
56 );
57 return Ok(());
58 }
59
60 println!("\n{}\n", style("Global Worktree Overview").cyan().bold());
61
62 let mut total_repos = 0usize;
63 let mut status_counts: std::collections::HashMap<String, usize> =
64 std::collections::HashMap::new();
65 let mut rows: Vec<GlobalWorktreeRow> = Vec::new();
66
67 let mut sorted_repos = repos;
68 sorted_repos.sort_by(|a, b| a.0.cmp(&b.0));
69
70 for (name, repo_path) in &sorted_repos {
71 if !repo_path.exists() {
72 println!(
73 "{} {} — {}",
74 style(format!("⚠ {}", name)).yellow(),
75 style(format!("({})", repo_path.display())).dim(),
76 style("repository not found").red(),
77 );
78 continue;
79 }
80
81 let feature_wts = match git::get_feature_worktrees(Some(repo_path)) {
82 Ok(wts) => wts,
83 Err(_) => {
84 println!(
85 "{} {} — {}",
86 style(format!("⚠ {}", name)).yellow(),
87 style(format!("({})", repo_path.display())).dim(),
88 style("failed to read worktrees").red(),
89 );
90 continue;
91 }
92 };
93
94 let mut has_feature = false;
95 for (branch_name, path) in &feature_wts {
96 let status = get_worktree_status(path, repo_path);
97
98 let intended_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch_name);
100 let worktree_id =
101 git::get_config(&intended_key, Some(repo_path)).unwrap_or(branch_name.clone());
102
103 let age = path_age_days(path).map(format_age).unwrap_or_default();
105
106 let rel_path = pathdiff::diff_paths(path, repo_path)
108 .map(|p| p.to_string_lossy().to_string())
109 .unwrap_or_else(|| path.to_string_lossy().to_string());
110
111 *status_counts.entry(status.clone()).or_insert(0) += 1;
112
113 rows.push(GlobalWorktreeRow {
114 repo_name: name.clone(),
115 worktree_id,
116 current_branch: branch_name.clone(),
117 status,
118 age,
119 rel_path,
120 });
121
122 has_feature = true;
123 }
124
125 if has_feature {
126 total_repos += 1;
127 }
128 }
129
130 if rows.is_empty() {
131 println!(
132 "{}\n",
133 style("No repositories with active worktrees found.").yellow()
134 );
135 return Ok(());
136 }
137
138 let term_width = cwconsole::terminal_width();
140 if term_width >= MIN_GLOBAL_TABLE_WIDTH {
141 global_print_table(&rows);
142 } else {
143 global_print_compact(&rows);
144 }
145
146 let total_worktrees = rows.len();
148 let mut summary_parts = Vec::new();
149 for &status_name in &["clean", "modified", "active", "stale"] {
150 if let Some(&count) = status_counts.get(status_name) {
151 if count > 0 {
152 let styled = cwconsole::status_style(status_name)
153 .apply_to(format!("{} {}", count, status_name));
154 summary_parts.push(styled.to_string());
155 }
156 }
157 }
158
159 let summary = if summary_parts.is_empty() {
160 format!("\n{} repo(s), {} worktree(s)", total_repos, total_worktrees)
161 } else {
162 format!(
163 "\n{} repo(s), {} worktree(s) — {}",
164 total_repos,
165 total_worktrees,
166 summary_parts.join(", ")
167 )
168 };
169 println!("{}", summary);
170 println!();
171
172 Ok(())
173}
174
175fn global_print_table(rows: &[GlobalWorktreeRow]) {
176 let max_repo = rows.iter().map(|r| r.repo_name.len()).max().unwrap_or(12);
177 let max_wt = rows.iter().map(|r| r.worktree_id.len()).max().unwrap_or(20);
178 let max_br = rows
179 .iter()
180 .map(|r| r.current_branch.len())
181 .max()
182 .unwrap_or(20);
183
184 let repo_col = max_repo.clamp(12, 25) + 2;
185 let wt_col = max_wt.clamp(20, 35) + 2;
186 let br_col = max_br.clamp(20, 35) + 2;
187
188 println!(
189 "{:<repo_col$} {:<wt_col$} {:<br_col$} {:<10} {:<12} PATH",
190 "REPO",
191 "WORKTREE",
192 "CURRENT BRANCH",
193 "STATUS",
194 "AGE",
195 repo_col = repo_col,
196 wt_col = wt_col,
197 br_col = br_col,
198 );
199 println!("{}", "─".repeat(repo_col + wt_col + br_col + 82));
200
201 for row in rows {
202 let branch_display = if row.worktree_id != row.current_branch {
203 style(format!("{} (⚠️)", row.current_branch))
204 .yellow()
205 .to_string()
206 } else {
207 row.current_branch.clone()
208 };
209
210 let status_styled =
211 cwconsole::status_style(&row.status).apply_to(format!("{:<10}", row.status));
212
213 println!(
214 "{:<repo_col$} {:<wt_col$} {:<br_col$} {} {:<12} {}",
215 row.repo_name,
216 row.worktree_id,
217 branch_display,
218 status_styled,
219 row.age,
220 row.rel_path,
221 repo_col = repo_col,
222 wt_col = wt_col,
223 br_col = br_col,
224 );
225 }
226}
227
228fn global_print_compact(rows: &[GlobalWorktreeRow]) {
229 let mut current_repo = String::new();
230
231 for row in rows {
232 if row.repo_name != current_repo {
233 if !current_repo.is_empty() {
234 println!(); }
236 println!("{}", style(&row.repo_name).bold());
237 current_repo = row.repo_name.clone();
238 }
239
240 let status_styled = cwconsole::status_style(&row.status).apply_to(&row.status);
241 let age_part = if row.age.is_empty() {
242 String::new()
243 } else {
244 format!(" {}", row.age)
245 };
246
247 println!(
248 " {} {}{}",
249 style(&row.worktree_id).bold(),
250 status_styled,
251 age_part,
252 );
253
254 let mut details = Vec::new();
255 if row.worktree_id != row.current_branch {
256 details.push(format!(
257 "branch: {}",
258 style(format!("{} (⚠️)", row.current_branch)).yellow()
259 ));
260 }
261 details.push(format!("path: {}", row.rel_path));
262 println!(" {}", details.join(" · "));
263 }
264}
265
266pub fn global_scan(base_dir: Option<&std::path::Path>) -> Result<()> {
268 let scan_dir = base_dir
269 .map(|p| p.to_path_buf())
270 .unwrap_or_else(home_dir_or_fallback);
271
272 println!(
273 "\n{}\n Directory: {}\n",
274 style("Scanning for repositories...").cyan().bold(),
275 style(scan_dir.display()).blue(),
276 );
277
278 let found = registry::scan_for_repos(base_dir, 5);
279
280 if found.is_empty() {
281 println!(
282 "{}\n",
283 style("No repositories with worktrees found.").yellow()
284 );
285 return Ok(());
286 }
287
288 println!(
289 "{} Found {} repository(s):\n",
290 style("*").green().bold(),
291 found.len()
292 );
293
294 let mut sorted = found;
295 sorted.sort();
296 for repo_path in &sorted {
297 let _ = registry::register_repo(repo_path);
298 println!(
299 " {} {} {}",
300 style("+").green(),
301 repo_path
302 .file_name()
303 .map(|n| n.to_string_lossy().to_string())
304 .unwrap_or_default(),
305 style(format!("({})", repo_path.display())).dim(),
306 );
307 }
308
309 println!(
310 "\n{} Registered {} repository(s)\n\
311 Use {} to see all worktrees.\n",
312 style("*").green().bold(),
313 sorted.len(),
314 style("gw -g list").cyan(),
315 );
316
317 Ok(())
318}
319
320pub fn global_prune() -> Result<()> {
322 println!("\n{}\n", style("Pruning registry...").cyan().bold());
323
324 match registry::prune_registry() {
325 Ok(removed) => {
326 if removed.is_empty() {
327 println!(
328 "{} Registry is clean, nothing to prune.\n",
329 style("*").green().bold()
330 );
331 } else {
332 println!(
333 "{} Removed {} stale entry(s):\n",
334 style("*").green().bold(),
335 removed.len()
336 );
337 for path in &removed {
338 println!(" {} {}", style("-").red(), path);
339 }
340 println!();
341 }
342 Ok(())
343 }
344 Err(e) => Err(e),
345 }
346}