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, prefix: &str) -> Result<Self> {
64 let mut entries = Vec::new();
65
66 if !worktrees_dir.exists() {
67 return Ok(Self { entries });
68 }
69
70 let mut dirs: Vec<_> = fs::read_dir(worktrees_dir)?
71 .filter_map(|e| e.ok())
72 .filter(|e| {
73 e.file_type().map(|t| t.is_dir()).unwrap_or(false)
74 && e.file_name()
75 .to_str()
76 .is_some_and(|n| n.starts_with(prefix))
77 })
78 .collect();
79
80 dirs.sort_by_key(|e| e.file_name());
82
83 for dir_entry in dirs {
84 let name = dir_entry.file_name().to_string_lossy().to_string();
85 let path = dir_entry.path();
86 let branch = name.clone();
87 let marker = acquired_dir.join(&name);
88
89 let (status, owner) = if marker.exists() {
90 let owner = fs::read_to_string(&marker)
91 .ok()
92 .map(|s| s.trim().to_string())
93 .filter(|s| !s.is_empty());
94 (WorktreeStatus::Acquired, owner)
95 } else {
96 (WorktreeStatus::Available, None)
97 };
98
99 entries.push(PoolEntry {
100 name,
101 path,
102 branch,
103 status,
104 owner,
105 });
106 }
107
108 Ok(Self { entries })
109 }
110
111 pub fn count_by_status(&self, status: &WorktreeStatus) -> usize {
113 self.entries.iter().filter(|e| e.status == *status).count()
114 }
115
116 pub fn find_available(&self) -> Option<&PoolEntry> {
118 self.entries
119 .iter()
120 .find(|e| e.status == WorktreeStatus::Available)
121 }
122
123 pub fn find_by_name_or_path(&self, identifier: &str) -> Option<&PoolEntry> {
125 self.entries
126 .iter()
127 .find(|e| e.name == identifier || e.path.to_string_lossy() == identifier)
128 }
129
130 pub fn next_name(&self, prefix: &str) -> String {
133 let max = self
134 .entries
135 .iter()
136 .filter_map(|e| e.name.strip_prefix(prefix))
137 .filter_map(|n| n.parse::<u32>().ok())
138 .max()
139 .unwrap_or(0);
140 format!("{prefix}{:03}", max + 1)
141 }
142
143 pub fn next_action(&self) -> PoolNextAction {
145 if self.entries.is_empty() {
146 return PoolNextAction::WarmPool;
147 }
148
149 let available = self.count_by_status(&WorktreeStatus::Available);
150 let acquired = self.count_by_status(&WorktreeStatus::Acquired);
151
152 if available == 0 {
153 return PoolNextAction::Exhausted { acquired };
154 }
155
156 if acquired == 0 {
157 return PoolNextAction::AllIdle { available };
158 }
159
160 PoolNextAction::Ready { available }
161 }
162}