use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{GwError, Result};
use crate::git;
use crate::output;
use crate::pool::{Inventory, PoolEntry, PoolLock, WorktreeStatus};
const POOL_META_DIR: &str = "worktree-pool";
fn canonicalize_clean(path: &Path) -> PathBuf {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
#[cfg(target_os = "windows")]
{
let s = canonical.to_string_lossy();
if let Some(stripped) = s.strip_prefix(r"\\?\") {
return PathBuf::from(stripped);
}
}
canonical
}
const POOL_WORKTREES_DIR: &str = ".worktrees";
const SETUP_HOOK: &str = ".gw/setup";
fn main_repo_root() -> Result<PathBuf> {
let common = git::git_common_dir()?;
let common = canonicalize_clean(&common);
common
.parent()
.map(|p| p.to_path_buf())
.ok_or_else(|| GwError::Other("Could not determine main repository root".to_string()))
}
fn pool_dir() -> Result<PathBuf> {
let common = git::git_common_dir()?;
Ok(common.join(POOL_META_DIR))
}
fn inventory_path() -> Result<PathBuf> {
Ok(pool_dir()?.join("inventory.json"))
}
fn worktrees_dir() -> Result<PathBuf> {
let root = main_repo_root()?;
Ok(root.join(POOL_WORKTREES_DIR))
}
fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn run_setup_hook(repo_root: &Path, worktree_path: &str, verbose: bool) -> Result<()> {
let hook = repo_root.join(SETUP_HOOK);
if !hook.exists() {
return Ok(());
}
if verbose {
output::action(&format!("Running setup hook: {}", hook.display()));
}
let status = std::process::Command::new(&hook)
.arg(worktree_path)
.current_dir(worktree_path)
.status()?;
if !status.success() {
return Err(GwError::Other(format!(
"Setup hook failed with exit code: {}",
status.code().unwrap_or(-1)
)));
}
Ok(())
}
pub fn warm(count: usize, verbose: bool) -> Result<()> {
if !git::is_git_repo() {
return Err(GwError::NotAGitRepository);
}
let pool_dir = pool_dir()?;
let inv_path = inventory_path()?;
let wt_dir = worktrees_dir()?;
let repo_root = main_repo_root()?;
println!();
output::info(&format!(
"Warming worktree pool to {} available",
output::bold(&count.to_string())
));
let _lock = PoolLock::acquire(&pool_dir)?;
let mut inventory = Inventory::load(&inv_path)?;
let available = inventory.count_by_status(&WorktreeStatus::Available);
let acquired = inventory.count_by_status(&WorktreeStatus::Acquired);
let total = inventory.worktrees.len();
if available >= count {
output::success(&format!(
"Pool already has {available} available ({acquired} acquired, {total} total), nothing to do"
));
return Ok(());
}
let to_create = count - available;
output::info("Fetching from origin...");
git::fetch_prune(verbose)?;
output::success("Fetched");
let default_remote = git::get_default_remote_branch()?;
std::fs::create_dir_all(&wt_dir)?;
let mut created = 0;
for i in 0..to_create {
let name = inventory.next_name();
let abs_path = canonicalize_clean(&wt_dir).join(&name);
let abs_path_str = abs_path.to_string_lossy().to_string();
let branch = format!("pool/{name}");
output::info(&format!(
"[{}/{}] Creating {}...",
i + 1,
to_create,
output::bold(&name)
));
if let Err(e) = git::worktree_add(&abs_path_str, &branch, &default_remote, verbose) {
output::warn(&format!("Failed to create {name}: {e}"));
continue;
}
if let Err(e) = run_setup_hook(&repo_root, &abs_path_str, verbose) {
output::warn(&format!(
"Setup hook failed for {name}: {e}. Removing worktree."
));
let _ = git::worktree_remove(&abs_path_str, verbose);
let _ = git::force_delete_branch(&branch, verbose);
continue;
}
inventory.worktrees.push(PoolEntry {
name: name.clone(),
path: abs_path_str,
branch,
status: WorktreeStatus::Available,
created_at: now_unix(),
acquired_at: None,
acquired_by: None,
});
created += 1;
output::success(&format!("[{}/{}] Created {}", i + 1, to_create, name));
}
inventory.save(&inv_path)?;
let total = inventory.worktrees.len();
let available = inventory.count_by_status(&WorktreeStatus::Available);
println!();
output::success(&format!(
"Pool warmed: {created} created, {available} available, {total} total"
));
Ok(())
}
pub fn acquire(_verbose: bool) -> Result<()> {
if !git::is_git_repo() {
return Err(GwError::NotAGitRepository);
}
let pool_dir = pool_dir()?;
let inv_path = inventory_path()?;
if !inv_path.exists() {
return Err(GwError::PoolNotInitialized);
}
let _lock = PoolLock::acquire(&pool_dir)?;
let mut inventory = Inventory::load(&inv_path)?;
let idx = inventory.find_available().ok_or(GwError::PoolExhausted)?;
let entry = &mut inventory.worktrees[idx];
entry.status = WorktreeStatus::Acquired;
entry.acquired_at = Some(now_unix());
entry.acquired_by = Some(std::process::id());
let path = entry.path.clone();
let name = entry.name.clone();
inventory.save(&inv_path)?;
let remaining = inventory.count_by_status(&WorktreeStatus::Available);
eprintln!(
"\x1b[0;32m\u{2713}\x1b[0m Acquired {} (PID {}, {} remaining)",
name,
std::process::id(),
remaining,
);
println!("{path}");
Ok(())
}
pub fn release(identifier: Option<&str>, verbose: bool) -> Result<()> {
if !git::is_git_repo() {
return Err(GwError::NotAGitRepository);
}
let pool_dir = pool_dir()?;
let inv_path = inventory_path()?;
let repo_root = main_repo_root()?;
if !inv_path.exists() {
return Err(GwError::PoolNotInitialized);
}
let resolved = match identifier {
Some(id) => id.to_string(),
None => std::env::current_dir()
.map_err(GwError::Io)?
.to_string_lossy()
.to_string(),
};
println!();
output::info(&format!("Releasing worktree: {}", output::bold(&resolved)));
let _lock = PoolLock::acquire(&pool_dir)?;
let mut inventory = Inventory::load(&inv_path)?;
let idx = inventory
.find_by_name_or_path(&resolved)
.ok_or_else(|| GwError::PoolWorktreeNotFound(resolved.clone()))?;
let entry = &inventory.worktrees[idx];
let wt_path = entry.path.clone();
let name = entry.name.clone();
let default_remote = git::get_default_remote_branch()?;
output::info("Fetching from origin...");
git::git_run_in_dir(&wt_path, &["fetch", "--prune", "--quiet"], verbose)?;
output::success("Fetched");
output::info("Resetting worktree...");
git::git_run_in_dir(&wt_path, &["reset", "--hard", &default_remote], verbose)?;
git::git_run_in_dir(&wt_path, &["clean", "-fd"], verbose)?;
output::success("Reset to clean state");
if let Err(e) = run_setup_hook(&repo_root, &wt_path, verbose) {
output::warn(&format!("Setup hook failed during release: {e}"));
}
let entry = &mut inventory.worktrees[idx];
entry.status = WorktreeStatus::Available;
entry.acquired_at = None;
entry.acquired_by = None;
inventory.save(&inv_path)?;
output::success(&format!("Released {}", output::bold(&name)));
Ok(())
}
pub fn status() -> Result<()> {
if !git::is_git_repo() {
return Err(GwError::NotAGitRepository);
}
let inv_path = inventory_path()?;
if !inv_path.exists() {
return Err(GwError::PoolNotInitialized);
}
let inventory = Inventory::load(&inv_path)?;
let available = inventory.count_by_status(&WorktreeStatus::Available);
let acquired = inventory.count_by_status(&WorktreeStatus::Acquired);
let total = inventory.worktrees.len();
println!();
output::info(&format!(
"Pool: {} available, {} acquired, {} total",
output::bold(&available.to_string()),
output::bold(&acquired.to_string()),
output::bold(&total.to_string()),
));
println!();
let header = format!("{:<12} {:<12} {:<8} PATH", "NAME", "STATUS", "PID");
println!("{header}");
println!("{}", "-".repeat(72));
for entry in &inventory.worktrees {
let pid = entry
.acquired_by
.map(|p| p.to_string())
.unwrap_or_else(|| "-".to_string());
println!(
"{:<12} {:<12} {:<8} {}",
entry.name, entry.status, pid, entry.path
);
}
println!();
Ok(())
}
pub fn drain(force: bool, verbose: bool) -> Result<()> {
if !git::is_git_repo() {
return Err(GwError::NotAGitRepository);
}
let pool_dir = pool_dir()?;
let inv_path = inventory_path()?;
let repo_root = main_repo_root()?;
let repo_root_str = repo_root.to_string_lossy().to_string();
let wt_dir = worktrees_dir()?;
if !inv_path.exists() {
return Err(GwError::PoolNotInitialized);
}
println!();
output::info("Draining worktree pool...");
let _lock = PoolLock::acquire(&pool_dir)?;
let inventory = Inventory::load(&inv_path)?;
let acquired = inventory.count_by_status(&WorktreeStatus::Acquired);
if acquired > 0 && !force {
return Err(GwError::PoolHasAcquiredWorktrees(acquired));
}
let total = inventory.worktrees.len();
for (i, entry) in inventory.worktrees.iter().enumerate() {
output::info(&format!(
"[{}/{}] Removing {}...",
i + 1,
total,
output::bold(&entry.name)
));
if let Err(e) = git::git_run_in_dir(
&repo_root_str,
&["worktree", "remove", "--force", &entry.path],
verbose,
) {
output::warn(&format!("Failed to remove worktree {}: {e}", entry.name));
let _ = std::fs::remove_dir_all(&entry.path);
}
if let Err(e) =
git::git_run_in_dir(&repo_root_str, &["branch", "-D", &entry.branch], verbose)
{
output::warn(&format!("Failed to delete branch {}: {e}", entry.branch));
}
output::success(&format!("[{}/{}] Removed {}", i + 1, total, entry.name));
}
let _ = std::fs::remove_file(&inv_path);
let _ = std::fs::remove_file(pool_dir.join("pool.lock"));
git::git_run_in_dir(&repo_root_str, &["worktree", "prune"], verbose)?;
if wt_dir.exists() {
let _ = std::fs::remove_dir(&wt_dir);
}
drop(_lock);
let _ = std::fs::remove_dir(&pool_dir);
println!();
output::success(&format!("Drained {total} worktree(s) from pool"));
Ok(())
}