use std::fs;
use std::path::{Path, PathBuf};
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorktreeStatus {
Available,
Acquired,
}
impl std::fmt::Display for WorktreeStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WorktreeStatus::Available => write!(f, "available"),
WorktreeStatus::Acquired => write!(f, "acquired"),
}
}
}
#[derive(Debug, Clone)]
pub struct PoolEntry {
pub name: String,
pub path: PathBuf,
pub branch: String,
pub status: WorktreeStatus,
pub owner: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PoolNextAction {
WarmPool,
Ready { available: usize },
Exhausted { acquired: usize },
AllIdle { available: usize },
}
#[derive(Debug)]
pub struct PoolState {
pub entries: Vec<PoolEntry>,
}
impl PoolState {
pub fn scan(worktrees_dir: &Path, acquired_dir: &Path, prefix: &str) -> Result<Self> {
let mut entries = Vec::new();
if !worktrees_dir.exists() {
return Ok(Self { entries });
}
let mut dirs: Vec<_> = fs::read_dir(worktrees_dir)?
.filter_map(|e| e.ok())
.filter(|e| {
e.file_type().map(|t| t.is_dir()).unwrap_or(false)
&& e.file_name()
.to_str()
.is_some_and(|n| n.starts_with(prefix))
})
.collect();
dirs.sort_by_key(|e| e.file_name());
for dir_entry in dirs {
let name = dir_entry.file_name().to_string_lossy().to_string();
let path = dir_entry.path();
let branch = name.clone();
let marker = acquired_dir.join(&name);
let (status, owner) = if marker.exists() {
let owner = fs::read_to_string(&marker)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
(WorktreeStatus::Acquired, owner)
} else {
(WorktreeStatus::Available, None)
};
entries.push(PoolEntry {
name,
path,
branch,
status,
owner,
});
}
Ok(Self { entries })
}
pub fn count_by_status(&self, status: &WorktreeStatus) -> usize {
self.entries.iter().filter(|e| e.status == *status).count()
}
pub fn find_available(&self) -> Option<&PoolEntry> {
self.entries
.iter()
.find(|e| e.status == WorktreeStatus::Available)
}
pub fn find_by_name_or_path(&self, identifier: &str) -> Option<&PoolEntry> {
self.entries
.iter()
.find(|e| e.name == identifier || e.path.to_string_lossy() == identifier)
}
pub fn next_name(&self, prefix: &str) -> String {
let max = self
.entries
.iter()
.filter_map(|e| e.name.strip_prefix(prefix))
.filter_map(|n| n.parse::<u32>().ok())
.max()
.unwrap_or(0);
format!("{prefix}{:03}", max + 1)
}
pub fn next_action(&self) -> PoolNextAction {
if self.entries.is_empty() {
return PoolNextAction::WarmPool;
}
let available = self.count_by_status(&WorktreeStatus::Available);
let acquired = self.count_by_status(&WorktreeStatus::Acquired);
if available == 0 {
return PoolNextAction::Exhausted { acquired };
}
if acquired == 0 {
return PoolNextAction::AllIdle { available };
}
PoolNextAction::Ready { available }
}
}