git_workflow/pool/
detect.rs1use std::fs;
8use std::path::{Path, PathBuf};
9
10use crate::error::Result;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum WorktreeStatus {
15 Available,
16 Acquired,
17}
18
19impl std::fmt::Display for WorktreeStatus {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 WorktreeStatus::Available => write!(f, "available"),
23 WorktreeStatus::Acquired => write!(f, "acquired"),
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
30pub struct PoolEntry {
31 pub name: String,
32 pub path: PathBuf,
33 pub branch: String,
34 pub status: WorktreeStatus,
35 pub owner: Option<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum PoolNextAction {
41 WarmPool,
43 Ready { available: usize },
45 Exhausted { acquired: usize },
47 AllIdle { available: usize },
49}
50
51#[derive(Debug)]
53pub struct PoolState {
54 pub entries: Vec<PoolEntry>,
55}
56
57impl PoolState {
58 pub fn scan(worktrees_dir: &Path, acquired_dir: &Path) -> Result<Self> {
63 let mut entries = Vec::new();
64
65 if !worktrees_dir.exists() {
66 return Ok(Self { entries });
67 }
68
69 let mut dirs: Vec<_> = fs::read_dir(worktrees_dir)?
70 .filter_map(|e| e.ok())
71 .filter(|e| {
72 e.file_type().map(|t| t.is_dir()).unwrap_or(false)
73 && e.file_name()
74 .to_str()
75 .is_some_and(|n| n.starts_with("pool-"))
76 })
77 .collect();
78
79 dirs.sort_by_key(|e| e.file_name());
81
82 for dir_entry in dirs {
83 let name = dir_entry.file_name().to_string_lossy().to_string();
84 let path = dir_entry.path();
85 let branch = name.clone();
86 let marker = acquired_dir.join(&name);
87
88 let (status, owner) = if marker.exists() {
89 let owner = fs::read_to_string(&marker)
90 .ok()
91 .map(|s| s.trim().to_string())
92 .filter(|s| !s.is_empty());
93 (WorktreeStatus::Acquired, owner)
94 } else {
95 (WorktreeStatus::Available, None)
96 };
97
98 entries.push(PoolEntry {
99 name,
100 path,
101 branch,
102 status,
103 owner,
104 });
105 }
106
107 Ok(Self { entries })
108 }
109
110 pub fn count_by_status(&self, status: &WorktreeStatus) -> usize {
112 self.entries.iter().filter(|e| e.status == *status).count()
113 }
114
115 pub fn find_available(&self) -> Option<&PoolEntry> {
117 self.entries
118 .iter()
119 .find(|e| e.status == WorktreeStatus::Available)
120 }
121
122 pub fn find_by_name_or_path(&self, identifier: &str) -> Option<&PoolEntry> {
124 self.entries
125 .iter()
126 .find(|e| e.name == identifier || e.path.to_string_lossy() == identifier)
127 }
128
129 pub fn next_name(&self) -> String {
131 let max = self
132 .entries
133 .iter()
134 .filter_map(|e| e.name.strip_prefix("pool-"))
135 .filter_map(|n| n.parse::<u32>().ok())
136 .max()
137 .unwrap_or(0);
138 format!("pool-{:03}", max + 1)
139 }
140
141 pub fn next_action(&self) -> PoolNextAction {
143 if self.entries.is_empty() {
144 return PoolNextAction::WarmPool;
145 }
146
147 let available = self.count_by_status(&WorktreeStatus::Available);
148 let acquired = self.count_by_status(&WorktreeStatus::Acquired);
149
150 if available == 0 {
151 return PoolNextAction::Exhausted { acquired };
152 }
153
154 if acquired == 0 {
155 return PoolNextAction::AllIdle { available };
156 }
157
158 PoolNextAction::Ready { available }
159 }
160}