1use std::path::{Path, PathBuf};
7
8use crate::error::{GwError, Result};
9use crate::git;
10use crate::output;
11use crate::pool::{PoolEntry, PoolLock, PoolNextAction, PoolState, WorktreeStatus};
12
13const POOL_META_DIR: &str = "worktree-pool";
15
16const POOL_WORKTREES_DIR: &str = ".worktrees";
18
19const SETUP_HOOK: &str = ".gw/setup";
21
22const ACQUIRED_DIR: &str = "acquired";
24
25fn canonicalize_clean(path: &Path) -> PathBuf {
28 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
29 #[cfg(target_os = "windows")]
30 {
31 let s = canonical.to_string_lossy();
32 if let Some(stripped) = s.strip_prefix(r"\\?\") {
33 return PathBuf::from(stripped);
34 }
35 }
36 canonical
37}
38
39fn main_repo_root() -> Result<PathBuf> {
41 let common = git::git_common_dir()?;
42 let common = canonicalize_clean(&common);
43 common
44 .parent()
45 .map(|p| p.to_path_buf())
46 .ok_or_else(|| GwError::Other("Could not determine main repository root".to_string()))
47}
48
49fn pool_dir() -> Result<PathBuf> {
51 let common = git::git_common_dir()?;
52 Ok(common.join(POOL_META_DIR))
53}
54
55fn acquired_dir() -> Result<PathBuf> {
57 Ok(pool_dir()?.join(ACQUIRED_DIR))
58}
59
60fn worktrees_dir() -> Result<PathBuf> {
62 let root = main_repo_root()?;
63 Ok(root.join(POOL_WORKTREES_DIR))
64}
65
66fn run_setup_hook(repo_root: &Path, worktree_path: &str, verbose: bool) -> Result<()> {
68 let hook = repo_root.join(SETUP_HOOK);
69 if !hook.exists() {
70 return Ok(());
71 }
72
73 if verbose {
74 output::action(&format!("Running setup hook: {}", hook.display()));
75 }
76
77 let status = std::process::Command::new(&hook)
78 .arg(worktree_path)
79 .current_dir(worktree_path)
80 .status()?;
81
82 if !status.success() {
83 return Err(GwError::Other(format!(
84 "Setup hook failed with exit code: {}",
85 status.code().unwrap_or(-1)
86 )));
87 }
88 Ok(())
89}
90
91fn worktree_current_branch(path: &Path) -> String {
93 let path_str = path.to_string_lossy();
94 std::process::Command::new("git")
95 .args(["rev-parse", "--abbrev-ref", "HEAD"])
96 .current_dir(&*path_str)
97 .output()
98 .ok()
99 .filter(|o| o.status.success())
100 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
101 .unwrap_or_else(|| "???".to_string())
102}
103
104fn current_owner_name() -> String {
106 git::current_dir_name().unwrap_or_else(|_| "unknown".to_string())
107}
108
109pub fn detect_pool_worktree() -> Option<String> {
112 let cwd = std::env::current_dir().ok()?;
113 let wt_dir = worktrees_dir().ok()?;
114
115 if !cwd.starts_with(&wt_dir) {
116 return None;
117 }
118
119 cwd.strip_prefix(&wt_dir)
121 .ok()
122 .and_then(|rel| rel.components().next())
123 .and_then(|c| c.as_os_str().to_str())
124 .filter(|name| name.starts_with("pool-"))
125 .map(String::from)
126}
127
128pub fn pool_release_after_cleanup(pool_name: &str, verbose: bool) -> Result<()> {
131 let acquired_dir = acquired_dir()?;
132 let repo_root = main_repo_root()?;
133 let wt_dir = worktrees_dir()?;
134 let wt_path = wt_dir.join(pool_name);
135 let wt_path_str = wt_path.to_string_lossy().to_string();
136
137 output::info("Releasing pool worktree...");
138
139 git::git_run_in_dir(&wt_path_str, &["clean", "-fd"], verbose)?;
141
142 if let Err(e) = run_setup_hook(&repo_root, &wt_path_str, verbose) {
144 output::warn(&format!(
145 "Setup hook failed during release: {e}. Worktree remains acquired."
146 ));
147 return Ok(());
148 }
149
150 let marker = acquired_dir.join(pool_name);
152 if marker.exists() {
153 std::fs::remove_file(&marker)?;
154 }
155
156 output::success("Pool worktree released and available for reuse");
157 Ok(())
158}
159
160pub fn warm(count: usize, verbose: bool) -> Result<()> {
164 if !git::is_git_repo() {
165 return Err(GwError::NotAGitRepository);
166 }
167
168 let pool_dir = pool_dir()?;
169 let wt_dir = worktrees_dir()?;
170 let acquired_dir = acquired_dir()?;
171 let repo_root = main_repo_root()?;
172
173 println!();
174 output::info(&format!(
175 "Warming worktree pool to {} available",
176 output::bold(&count.to_string())
177 ));
178
179 let _lock = PoolLock::acquire(&pool_dir)?;
181 let mut state = PoolState::scan(&wt_dir, &acquired_dir)?;
182
183 let available = state.count_by_status(&WorktreeStatus::Available);
184 let acquired = state.count_by_status(&WorktreeStatus::Acquired);
185 let total = state.entries.len();
186 if available >= count {
187 output::success(&format!(
188 "Pool already has {available} available ({acquired} acquired, {total} total), nothing to do"
189 ));
190 return Ok(());
191 }
192
193 let to_create = count - available;
194
195 output::info("Fetching from origin...");
197 git::fetch_prune(verbose)?;
198 output::success("Fetched");
199
200 let default_remote = git::get_default_remote_branch()?;
201
202 std::fs::create_dir_all(&wt_dir)?;
204
205 let mut created = 0;
206 for i in 0..to_create {
207 let name = state.next_name();
208 let abs_path = canonicalize_clean(&wt_dir).join(&name);
209 let abs_path_str = abs_path.to_string_lossy().to_string();
210 let branch = name.clone();
212
213 output::info(&format!(
214 "[{}/{}] Creating {}...",
215 i + 1,
216 to_create,
217 output::bold(&name)
218 ));
219
220 if let Err(e) = git::worktree_add(&abs_path_str, &branch, &default_remote, verbose) {
222 output::warn(&format!("Failed to create {name}: {e}"));
223 continue;
224 }
225
226 if let Err(e) = run_setup_hook(&repo_root, &abs_path_str, verbose) {
228 output::warn(&format!(
229 "Setup hook failed for {name}: {e}. Removing worktree."
230 ));
231 let _ = git::worktree_remove(&abs_path_str, verbose);
232 let _ = git::force_delete_branch(&branch, verbose);
233 continue;
234 }
235
236 state.entries.push(PoolEntry {
238 name: name.clone(),
239 path: abs_path,
240 branch,
241 status: WorktreeStatus::Available,
242 owner: None,
243 });
244 created += 1;
245
246 output::success(&format!("[{}/{}] Created {}", i + 1, to_create, name));
247 }
248
249 let final_state = PoolState::scan(&wt_dir, &acquired_dir)?;
251 let total = final_state.entries.len();
252 let available = final_state.count_by_status(&WorktreeStatus::Available);
253
254 println!();
255 output::success(&format!(
256 "Pool warmed: {created} created, {available} available, {total} total"
257 ));
258
259 Ok(())
260}
261
262pub fn acquire(_verbose: bool) -> Result<()> {
264 if !git::is_git_repo() {
265 return Err(GwError::NotAGitRepository);
266 }
267
268 let pool_dir = pool_dir()?;
269 let wt_dir = worktrees_dir()?;
270 let acquired_dir = acquired_dir()?;
271
272 if !wt_dir.exists() {
273 return Err(GwError::PoolNotInitialized);
274 }
275
276 let _lock = PoolLock::acquire(&pool_dir)?;
277
278 std::fs::create_dir_all(&acquired_dir)?;
280
281 let state = PoolState::scan(&wt_dir, &acquired_dir)?;
282
283 if state.entries.is_empty() {
284 return Err(GwError::PoolNotInitialized);
285 }
286
287 let entry = state.find_available().ok_or(GwError::PoolExhausted)?;
288
289 let owner = current_owner_name();
291 std::fs::write(acquired_dir.join(&entry.name), &owner)?;
292
293 let path = entry.path.to_string_lossy().to_string();
294 let name = entry.name.clone();
295
296 let remaining = state.count_by_status(&WorktreeStatus::Available) - 1;
297 eprintln!(
298 "\x1b[0;32m\u{2713}\x1b[0m Acquired {} (owner: {}, {} remaining)",
299 name, owner, remaining,
300 );
301
302 println!("{path}");
304
305 Ok(())
306}
307
308pub fn status(verbose: bool) -> Result<()> {
310 if !git::is_git_repo() {
311 return Err(GwError::NotAGitRepository);
312 }
313
314 let wt_dir = worktrees_dir()?;
315 let acquired_dir = acquired_dir()?;
316
317 if !wt_dir.exists() {
318 return Err(GwError::PoolNotInitialized);
319 }
320
321 let state = PoolState::scan(&wt_dir, &acquired_dir)?;
323
324 if state.entries.is_empty() {
325 return Err(GwError::PoolNotInitialized);
326 }
327
328 let me = current_owner_name();
329 let available = state.count_by_status(&WorktreeStatus::Available);
330 let acquired = state.count_by_status(&WorktreeStatus::Acquired);
331 let mine: Vec<&PoolEntry> = state
332 .entries
333 .iter()
334 .filter(|e| e.owner.as_deref() == Some(&me))
335 .collect();
336 let total = state.entries.len();
337
338 println!();
339 output::info(&format!(
340 "Pool: {} available, {} acquired ({} by you), {} total",
341 output::bold(&available.to_string()),
342 output::bold(&acquired.to_string()),
343 output::bold(&mine.len().to_string()),
344 output::bold(&total.to_string()),
345 ));
346
347 if !mine.is_empty() {
348 println!();
349 let header = format!("{:<12} {:<24}", "NAME", "BRANCH");
350 println!("{header}");
351 println!("{}", "-".repeat(36));
352
353 for entry in &mine {
354 let branch = worktree_current_branch(&entry.path);
355 println!("{:<12} {}", entry.name, branch);
356 }
357 }
358
359 if verbose {
360 println!();
362 output::info("All entries:");
363 println!();
364 let header = format!("{:<12} {:<12} {:<24} OWNER", "NAME", "STATUS", "BRANCH");
365 println!("{header}");
366 println!("{}", "-".repeat(72));
367
368 for entry in &state.entries {
369 let branch = if entry.status == WorktreeStatus::Acquired {
370 worktree_current_branch(&entry.path)
371 } else {
372 entry.branch.clone()
373 };
374 let owner = entry.owner.as_deref().unwrap_or("-");
375 println!(
376 "{:<12} {:<12} {:<24} {}",
377 entry.name, entry.status, branch, owner
378 );
379 }
380 }
381
382 let next = state.next_action();
384 println!();
385 display_pool_next_action(&next);
386
387 println!();
388 Ok(())
389}
390
391fn display_pool_next_action(action: &PoolNextAction) {
392 match action {
393 PoolNextAction::WarmPool => {
394 output::action("Next: warm the pool");
395 println!(" gw worktree pool warm <count>");
396 }
397 PoolNextAction::Ready { available } => {
398 output::action(&format!("Ready: {} worktree(s) available", available));
399 println!(" gw worktree pool acquire");
400 }
401 PoolNextAction::Exhausted { acquired } => {
402 output::action(&format!(
403 "All {} worktree(s) acquired. Warm more or wait.",
404 acquired
405 ));
406 println!(" gw worktree pool warm <count>");
407 }
408 PoolNextAction::AllIdle { available } => {
409 output::action(&format!(
410 "All {} worktree(s) idle. Acquire or drain.",
411 available
412 ));
413 println!(" gw worktree pool acquire");
414 println!(" gw worktree pool drain");
415 }
416 }
417}
418
419pub fn drain(force: bool, verbose: bool) -> Result<()> {
421 if !git::is_git_repo() {
422 return Err(GwError::NotAGitRepository);
423 }
424
425 let pool_dir = pool_dir()?;
426 let wt_dir = worktrees_dir()?;
427 let acquired_dir = acquired_dir()?;
428 let repo_root = main_repo_root()?;
431 let repo_root_str = repo_root.to_string_lossy().to_string();
432
433 if !wt_dir.exists() {
434 return Err(GwError::PoolNotInitialized);
435 }
436
437 println!();
438 output::info("Draining worktree pool...");
439
440 let _lock = PoolLock::acquire(&pool_dir)?;
441 let state = PoolState::scan(&wt_dir, &acquired_dir)?;
442
443 if state.entries.is_empty() {
444 return Err(GwError::PoolNotInitialized);
445 }
446
447 let acquired = state.count_by_status(&WorktreeStatus::Acquired);
449 if acquired > 0 && !force {
450 return Err(GwError::PoolHasAcquiredWorktrees(acquired));
451 }
452
453 let total = state.entries.len();
454
455 for (i, entry) in state.entries.iter().enumerate() {
456 output::info(&format!(
457 "[{}/{}] Removing {}...",
458 i + 1,
459 total,
460 output::bold(&entry.name)
461 ));
462
463 let path_str = entry.path.to_string_lossy().to_string();
464
465 if let Err(e) = git::git_run_in_dir(
467 &repo_root_str,
468 &["worktree", "remove", "--force", &path_str],
469 verbose,
470 ) {
471 output::warn(&format!("Failed to remove worktree {}: {e}", entry.name));
472 let _ = std::fs::remove_dir_all(&entry.path);
473 }
474
475 if let Err(e) =
477 git::git_run_in_dir(&repo_root_str, &["branch", "-D", &entry.branch], verbose)
478 {
479 output::warn(&format!("Failed to delete branch {}: {e}", entry.branch));
480 }
481
482 let marker = acquired_dir.join(&entry.name);
484 let _ = std::fs::remove_file(&marker);
485
486 output::success(&format!("[{}/{}] Removed {}", i + 1, total, entry.name));
487 }
488
489 if acquired_dir.exists() {
491 let _ = std::fs::remove_dir_all(&acquired_dir);
492 }
493 let _ = std::fs::remove_file(pool_dir.join("pool.lock"));
494
495 git::git_run_in_dir(&repo_root_str, &["worktree", "prune"], verbose)?;
497
498 if wt_dir.exists() {
500 let _ = std::fs::remove_dir(&wt_dir);
501 }
502 drop(_lock);
503 let _ = std::fs::remove_dir(&pool_dir);
504
505 println!();
506 output::success(&format!("Drained {total} worktree(s) from pool"));
507
508 Ok(())
509}