1use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::error::{GwError, Result};
7use crate::git;
8use crate::output;
9use crate::pool::{Inventory, PoolEntry, PoolLock, WorktreeStatus};
10
11const POOL_META_DIR: &str = "worktree-pool";
13
14fn canonicalize_clean(path: &Path) -> PathBuf {
17 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
18 #[cfg(target_os = "windows")]
19 {
20 let s = canonical.to_string_lossy();
21 if let Some(stripped) = s.strip_prefix(r"\\?\") {
22 return PathBuf::from(stripped);
23 }
24 }
25 canonical
26}
27
28const POOL_WORKTREES_DIR: &str = ".worktrees";
30
31const SETUP_HOOK: &str = ".gw/setup";
33
34fn main_repo_root() -> Result<PathBuf> {
38 let common = git::git_common_dir()?;
39 let common = canonicalize_clean(&common);
42 common
43 .parent()
44 .map(|p| p.to_path_buf())
45 .ok_or_else(|| GwError::Other("Could not determine main repository root".to_string()))
46}
47
48fn pool_dir() -> Result<PathBuf> {
50 let common = git::git_common_dir()?;
51 Ok(common.join(POOL_META_DIR))
52}
53
54fn inventory_path() -> Result<PathBuf> {
56 Ok(pool_dir()?.join("inventory.json"))
57}
58
59fn worktrees_dir() -> Result<PathBuf> {
61 let root = main_repo_root()?;
62 Ok(root.join(POOL_WORKTREES_DIR))
63}
64
65fn now_unix() -> u64 {
67 SystemTime::now()
68 .duration_since(UNIX_EPOCH)
69 .unwrap_or_default()
70 .as_secs()
71}
72
73fn run_setup_hook(repo_root: &Path, worktree_path: &str, verbose: bool) -> Result<()> {
75 let hook = repo_root.join(SETUP_HOOK);
76 if !hook.exists() {
77 return Ok(());
78 }
79
80 if verbose {
81 output::action(&format!("Running setup hook: {}", hook.display()));
82 }
83
84 let status = std::process::Command::new(&hook)
85 .arg(worktree_path)
86 .current_dir(worktree_path)
87 .status()?;
88
89 if !status.success() {
90 return Err(GwError::Other(format!(
91 "Setup hook failed with exit code: {}",
92 status.code().unwrap_or(-1)
93 )));
94 }
95 Ok(())
96}
97
98pub fn warm(count: usize, verbose: bool) -> Result<()> {
100 if !git::is_git_repo() {
101 return Err(GwError::NotAGitRepository);
102 }
103
104 let pool_dir = pool_dir()?;
105 let inv_path = inventory_path()?;
106 let wt_dir = worktrees_dir()?;
107 let repo_root = main_repo_root()?;
108
109 println!();
110 output::info(&format!(
111 "Warming worktree pool to {} available",
112 output::bold(&count.to_string())
113 ));
114
115 let _lock = PoolLock::acquire(&pool_dir)?;
117 let mut inventory = Inventory::load(&inv_path)?;
118
119 let available = inventory.count_by_status(&WorktreeStatus::Available);
120 let acquired = inventory.count_by_status(&WorktreeStatus::Acquired);
121 let total = inventory.worktrees.len();
122 if available >= count {
123 output::success(&format!(
124 "Pool already has {available} available ({acquired} acquired, {total} total), nothing to do"
125 ));
126 return Ok(());
127 }
128
129 let to_create = count - available;
130
131 output::info("Fetching from origin...");
133 git::fetch_prune(verbose)?;
134 output::success("Fetched");
135
136 let default_remote = git::get_default_remote_branch()?;
137
138 std::fs::create_dir_all(&wt_dir)?;
140
141 let mut created = 0;
142 for i in 0..to_create {
143 let name = inventory.next_name();
144 let abs_path = canonicalize_clean(&wt_dir).join(&name);
145 let abs_path_str = abs_path.to_string_lossy().to_string();
146 let branch = format!("pool/{name}");
147
148 output::info(&format!(
149 "[{}/{}] Creating {}...",
150 i + 1,
151 to_create,
152 output::bold(&name)
153 ));
154
155 if let Err(e) = git::worktree_add(&abs_path_str, &branch, &default_remote, verbose) {
157 output::warn(&format!("Failed to create {name}: {e}"));
158 continue;
159 }
160
161 if let Err(e) = run_setup_hook(&repo_root, &abs_path_str, verbose) {
163 output::warn(&format!(
164 "Setup hook failed for {name}: {e}. Removing worktree."
165 ));
166 let _ = git::worktree_remove(&abs_path_str, verbose);
167 let _ = git::force_delete_branch(&branch, verbose);
168 continue;
169 }
170
171 inventory.worktrees.push(PoolEntry {
172 name: name.clone(),
173 path: abs_path_str,
174 branch,
175 status: WorktreeStatus::Available,
176 created_at: now_unix(),
177 acquired_at: None,
178 acquired_by: None,
179 });
180 created += 1;
181
182 output::success(&format!("[{}/{}] Created {}", i + 1, to_create, name));
183 }
184
185 inventory.save(&inv_path)?;
186
187 let total = inventory.worktrees.len();
188 let available = inventory.count_by_status(&WorktreeStatus::Available);
189
190 println!();
191 output::success(&format!(
192 "Pool warmed: {created} created, {available} available, {total} total"
193 ));
194
195 Ok(())
196}
197
198pub fn acquire(_verbose: bool) -> Result<()> {
200 if !git::is_git_repo() {
201 return Err(GwError::NotAGitRepository);
202 }
203
204 let pool_dir = pool_dir()?;
205 let inv_path = inventory_path()?;
206
207 if !inv_path.exists() {
208 return Err(GwError::PoolNotInitialized);
209 }
210
211 let _lock = PoolLock::acquire(&pool_dir)?;
212 let mut inventory = Inventory::load(&inv_path)?;
213
214 let idx = inventory.find_available().ok_or(GwError::PoolExhausted)?;
215
216 let entry = &mut inventory.worktrees[idx];
217 entry.status = WorktreeStatus::Acquired;
218 entry.acquired_at = Some(now_unix());
219 entry.acquired_by = Some(std::process::id());
220
221 let path = entry.path.clone();
222 let name = entry.name.clone();
223
224 inventory.save(&inv_path)?;
225
226 let remaining = inventory.count_by_status(&WorktreeStatus::Available);
228 eprintln!(
229 "\x1b[0;32m\u{2713}\x1b[0m Acquired {} (PID {}, {} remaining)",
230 name,
231 std::process::id(),
232 remaining,
233 );
234
235 println!("{path}");
237
238 Ok(())
239}
240
241pub fn release(identifier: Option<&str>, verbose: bool) -> Result<()> {
243 if !git::is_git_repo() {
244 return Err(GwError::NotAGitRepository);
245 }
246
247 let pool_dir = pool_dir()?;
248 let inv_path = inventory_path()?;
249 let repo_root = main_repo_root()?;
250
251 if !inv_path.exists() {
252 return Err(GwError::PoolNotInitialized);
253 }
254
255 let resolved = match identifier {
257 Some(id) => id.to_string(),
258 None => std::env::current_dir()
259 .map_err(GwError::Io)?
260 .to_string_lossy()
261 .to_string(),
262 };
263
264 println!();
265 output::info(&format!("Releasing worktree: {}", output::bold(&resolved)));
266
267 let _lock = PoolLock::acquire(&pool_dir)?;
268 let mut inventory = Inventory::load(&inv_path)?;
269
270 let idx = inventory
271 .find_by_name_or_path(&resolved)
272 .ok_or_else(|| GwError::PoolWorktreeNotFound(resolved.clone()))?;
273
274 let entry = &inventory.worktrees[idx];
275 let wt_path = entry.path.clone();
276 let name = entry.name.clone();
277
278 let default_remote = git::get_default_remote_branch()?;
279
280 output::info("Fetching from origin...");
282 git::git_run_in_dir(&wt_path, &["fetch", "--prune", "--quiet"], verbose)?;
283 output::success("Fetched");
284
285 output::info("Resetting worktree...");
287 git::git_run_in_dir(&wt_path, &["reset", "--hard", &default_remote], verbose)?;
288 git::git_run_in_dir(&wt_path, &["clean", "-fd"], verbose)?;
289 output::success("Reset to clean state");
290
291 if let Err(e) = run_setup_hook(&repo_root, &wt_path, verbose) {
293 output::warn(&format!("Setup hook failed during release: {e}"));
294 }
295
296 let entry = &mut inventory.worktrees[idx];
297 entry.status = WorktreeStatus::Available;
298 entry.acquired_at = None;
299 entry.acquired_by = None;
300
301 inventory.save(&inv_path)?;
302
303 output::success(&format!("Released {}", output::bold(&name)));
304
305 Ok(())
306}
307
308pub fn status() -> Result<()> {
310 if !git::is_git_repo() {
311 return Err(GwError::NotAGitRepository);
312 }
313
314 let inv_path = inventory_path()?;
315
316 if !inv_path.exists() {
317 return Err(GwError::PoolNotInitialized);
318 }
319
320 let inventory = Inventory::load(&inv_path)?;
322
323 let available = inventory.count_by_status(&WorktreeStatus::Available);
324 let acquired = inventory.count_by_status(&WorktreeStatus::Acquired);
325 let total = inventory.worktrees.len();
326
327 println!();
328 output::info(&format!(
329 "Pool: {} available, {} acquired, {} total",
330 output::bold(&available.to_string()),
331 output::bold(&acquired.to_string()),
332 output::bold(&total.to_string()),
333 ));
334 println!();
335
336 let header = format!("{:<12} {:<12} {:<8} PATH", "NAME", "STATUS", "PID");
338 println!("{header}");
339 println!("{}", "-".repeat(72));
340
341 for entry in &inventory.worktrees {
342 let pid = entry
343 .acquired_by
344 .map(|p| p.to_string())
345 .unwrap_or_else(|| "-".to_string());
346 println!(
347 "{:<12} {:<12} {:<8} {}",
348 entry.name, entry.status, pid, entry.path
349 );
350 }
351
352 println!();
353 Ok(())
354}
355
356pub fn drain(force: bool, verbose: bool) -> Result<()> {
358 if !git::is_git_repo() {
359 return Err(GwError::NotAGitRepository);
360 }
361
362 let pool_dir = pool_dir()?;
363 let inv_path = inventory_path()?;
364 let repo_root = main_repo_root()?;
368 let repo_root_str = repo_root.to_string_lossy().to_string();
369 let wt_dir = worktrees_dir()?;
370
371 if !inv_path.exists() {
372 return Err(GwError::PoolNotInitialized);
373 }
374
375 println!();
376 output::info("Draining worktree pool...");
377
378 let _lock = PoolLock::acquire(&pool_dir)?;
379 let inventory = Inventory::load(&inv_path)?;
380
381 let acquired = inventory.count_by_status(&WorktreeStatus::Acquired);
383 if acquired > 0 && !force {
384 return Err(GwError::PoolHasAcquiredWorktrees(acquired));
385 }
386
387 let total = inventory.worktrees.len();
388
389 for (i, entry) in inventory.worktrees.iter().enumerate() {
390 output::info(&format!(
391 "[{}/{}] Removing {}...",
392 i + 1,
393 total,
394 output::bold(&entry.name)
395 ));
396
397 if let Err(e) = git::git_run_in_dir(
399 &repo_root_str,
400 &["worktree", "remove", "--force", &entry.path],
401 verbose,
402 ) {
403 output::warn(&format!("Failed to remove worktree {}: {e}", entry.name));
404 let _ = std::fs::remove_dir_all(&entry.path);
405 }
406
407 if let Err(e) =
409 git::git_run_in_dir(&repo_root_str, &["branch", "-D", &entry.branch], verbose)
410 {
411 output::warn(&format!("Failed to delete branch {}: {e}", entry.branch));
412 }
413
414 output::success(&format!("[{}/{}] Removed {}", i + 1, total, entry.name));
415 }
416
417 let _ = std::fs::remove_file(&inv_path);
419 let _ = std::fs::remove_file(pool_dir.join("pool.lock"));
420
421 git::git_run_in_dir(&repo_root_str, &["worktree", "prune"], verbose)?;
423
424 if wt_dir.exists() {
426 let _ = std::fs::remove_dir(&wt_dir);
427 }
428 drop(_lock);
429 let _ = std::fs::remove_dir(&pool_dir);
430
431 println!();
432 output::success(&format!("Drained {total} worktree(s) from pool"));
433
434 Ok(())
435}