sh_layer4/worktree_manager/
mod.rs1use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use sh_layer3::generate_short_id;
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::process::Command;
11
12use crate::types::Layer4Result;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum WorktreeStatus {
17 Active,
18 Idle,
19 Error,
20 Locked,
21}
22
23impl std::fmt::Display for WorktreeStatus {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 Self::Active => write!(f, "active"),
27 Self::Idle => write!(f, "idle"),
28 Self::Error => write!(f, "error"),
29 Self::Locked => write!(f, "locked"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct WorktreeConfig {
37 pub name: String,
38 pub branch: String,
39 pub base_branch: Option<String>,
40 pub create_branch: bool,
41}
42
43impl WorktreeConfig {
44 pub fn new(name: impl Into<String>, branch: impl Into<String>) -> Self {
45 Self {
46 name: name.into(),
47 branch: branch.into(),
48 base_branch: None,
49 create_branch: true,
50 }
51 }
52
53 pub fn with_base_branch(mut self, base: impl Into<String>) -> Self {
54 self.base_branch = Some(base.into());
55 self
56 }
57
58 pub fn create_branch(mut self, create: bool) -> Self {
59 self.create_branch = create;
60 self
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Worktree {
67 pub id: String,
68 pub name: String,
69 pub path: PathBuf,
70 pub branch: String,
71 pub status: WorktreeStatus,
72 pub created_at: chrono::DateTime<chrono::Utc>,
73 pub last_used: Option<chrono::DateTime<chrono::Utc>>,
74 pub metadata: HashMap<String, String>,
75}
76
77impl Worktree {
78 pub fn new(
79 id: impl Into<String>,
80 name: impl Into<String>,
81 path: PathBuf,
82 branch: impl Into<String>,
83 ) -> Self {
84 Self {
85 id: id.into(),
86 name: name.into(),
87 path,
88 branch: branch.into(),
89 status: WorktreeStatus::Active,
90 created_at: chrono::Utc::now(),
91 last_used: None,
92 metadata: HashMap::new(),
93 }
94 }
95
96 pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
97 self.metadata.insert(key.to_string(), value.to_string());
98 self
99 }
100
101 pub fn touch(&mut self) {
102 self.last_used = Some(chrono::Utc::now());
103 }
104}
105
106pub struct WorktreeManager {
108 root_path: PathBuf,
109 worktrees_path: PathBuf,
110 worktrees: RwLock<HashMap<String, Worktree>>,
111}
112
113impl WorktreeManager {
114 pub fn new(root_path: impl Into<PathBuf>) -> Self {
116 let root = root_path.into();
117 let worktrees_path = root.join(".claude").join("worktrees");
118
119 Self {
120 root_path: root,
121 worktrees_path,
122 worktrees: RwLock::new(HashMap::new()),
123 }
124 }
125
126 fn ensure_worktrees_dir(&self) -> Layer4Result<()> {
128 std::fs::create_dir_all(&self.worktrees_path)?;
129 Ok(())
130 }
131
132 pub async fn create(&self, config: &WorktreeConfig) -> Layer4Result<Worktree> {
134 self.ensure_worktrees_dir()?;
135
136 let id = generate_short_id();
137 let worktree_path = self.worktrees_path.join(&config.name);
138
139 let branch_arg = if config.create_branch {
141 format!("-b {}", config.branch)
142 } else {
143 config.branch.clone()
144 };
145
146 let output = Command::new("git")
147 .args([
148 "worktree",
149 "add",
150 &worktree_path.to_string_lossy(),
151 &branch_arg,
152 ])
153 .current_dir(&self.root_path)
154 .output();
155
156 match output {
157 Ok(o) if o.status.success() => {
158 let worktree =
159 Worktree::new(&id, &config.name, worktree_path.clone(), &config.branch);
160 self.worktrees.write().insert(id.clone(), worktree.clone());
161 tracing::info!("Created worktree: {} at {:?}", config.name, worktree_path);
162 Ok(worktree)
163 }
164 Ok(o) => {
165 let error = String::from_utf8_lossy(&o.stderr);
166 Err(anyhow::anyhow!("Git worktree add failed: {}", error))
167 }
168 Err(e) => Err(anyhow::anyhow!("Failed to execute git: {}", e)),
169 }
170 }
171
172 pub async fn list(&self) -> Layer4Result<Vec<Worktree>> {
174 Ok(self.worktrees.read().values().cloned().collect())
175 }
176
177 pub async fn get(&self, id: &str) -> Layer4Result<Option<Worktree>> {
179 Ok(self.worktrees.read().get(id).cloned())
180 }
181
182 pub async fn get_by_name(&self, name: &str) -> Layer4Result<Option<Worktree>> {
184 Ok(self
185 .worktrees
186 .read()
187 .values()
188 .find(|w| w.name == name)
189 .cloned())
190 }
191
192 pub async fn remove(&self, id: &str) -> Layer4Result<()> {
194 let worktree = self.worktrees.read().get(id).cloned();
195
196 if let Some(wt) = worktree {
197 let output = Command::new("git")
199 .args(["worktree", "remove", "--force", &wt.path.to_string_lossy()])
200 .current_dir(&self.root_path)
201 .output();
202
203 match output {
204 Ok(o) if o.status.success() => {
205 self.worktrees.write().remove(id);
206 tracing::info!("Removed worktree: {}", wt.name);
207 Ok(())
208 }
209 Ok(o) => {
210 let error = String::from_utf8_lossy(&o.stderr);
211 Err(anyhow::anyhow!("Git worktree remove failed: {}", error))
212 }
213 Err(e) => Err(anyhow::anyhow!("Failed to execute git: {}", e)),
214 }
215 } else {
216 Err(anyhow::anyhow!("Worktree not found: {}", id))
217 }
218 }
219
220 pub async fn prune(&self) -> Layer4Result<Vec<String>> {
222 let mut removed = Vec::new();
223
224 let output = Command::new("git")
226 .args(["worktree", "prune", "-v"])
227 .current_dir(&self.root_path)
228 .output();
229
230 if let Ok(o) = output {
231 if o.status.success() {
232 let stdout = String::from_utf8_lossy(&o.stdout);
233 for line in stdout.lines() {
234 if line.contains("Removing") {
235 removed.push(line.to_string());
236 }
237 }
238 }
239 }
240
241 Ok(removed)
242 }
243
244 pub async fn sync(&self) -> Layer4Result<()> {
246 let worktrees = self.worktrees.read().keys().cloned().collect::<Vec<_>>();
248
249 for id in worktrees {
250 if let Some(wt) = self.worktrees.read().get(&id) {
251 let path_exists = wt.path.exists();
252
253 if let Some(w) = self.worktrees.write().get_mut(&id) {
255 w.status = if path_exists {
256 WorktreeStatus::Active
257 } else {
258 WorktreeStatus::Error
259 };
260 }
261 }
262 }
263
264 Ok(())
265 }
266
267 pub fn count(&self) -> usize {
269 self.worktrees.read().len()
270 }
271
272 pub fn root_path(&self) -> &PathBuf {
274 &self.root_path
275 }
276
277 pub fn worktrees_path(&self) -> &PathBuf {
279 &self.worktrees_path
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_worktree_config() {
289 let config = WorktreeConfig::new("feature-1", "feature/test");
290 assert_eq!(config.name, "feature-1");
291 assert_eq!(config.branch, "feature/test");
292 assert!(config.create_branch);
293 }
294
295 #[test]
296 fn test_worktree_creation() {
297 let wt = Worktree::new("abc123", "test", PathBuf::from("/tmp/test"), "main");
298 assert_eq!(wt.id, "abc123");
299 assert_eq!(wt.name, "test");
300 assert_eq!(wt.status, WorktreeStatus::Active);
301 }
302
303 #[test]
304 fn test_worktree_manager_creation() {
305 let manager = WorktreeManager::new("/tmp/test");
306 assert_eq!(manager.count(), 0);
307 }
308
309 #[test]
310 fn test_worktree_status_display() {
311 assert_eq!(format!("{}", WorktreeStatus::Active), "active");
312 assert_eq!(format!("{}", WorktreeStatus::Error), "error");
313 }
314}