Skip to main content

sh_layer4/worktree_manager/
mod.rs

1//! # Worktree Manager
2//!
3//! Git Worktree 管理系统,提供分支隔离环境。
4
5use 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/// Worktree 状态
15#[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/// Worktree 配置
35#[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/// Worktree 信息
65#[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
106/// Worktree 管理器
107pub struct WorktreeManager {
108    root_path: PathBuf,
109    worktrees_path: PathBuf,
110    worktrees: RwLock<HashMap<String, Worktree>>,
111}
112
113impl WorktreeManager {
114    /// 创建新的 Worktree 管理器
115    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    /// 确保 worktrees 目录存在
127    fn ensure_worktrees_dir(&self) -> Layer4Result<()> {
128        std::fs::create_dir_all(&self.worktrees_path)?;
129        Ok(())
130    }
131
132    /// 创建 Worktree
133    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        // 使用 git worktree add 命令
140        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    /// 列出所有 Worktree
173    pub async fn list(&self) -> Layer4Result<Vec<Worktree>> {
174        Ok(self.worktrees.read().values().cloned().collect())
175    }
176
177    /// 获取 Worktree
178    pub async fn get(&self, id: &str) -> Layer4Result<Option<Worktree>> {
179        Ok(self.worktrees.read().get(id).cloned())
180    }
181
182    /// 按名称获取 Worktree
183    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    /// 删除 Worktree
193    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            // 使用 git worktree remove 命令
198            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    /// 清理无效的 Worktree
221    pub async fn prune(&self) -> Layer4Result<Vec<String>> {
222        let mut removed = Vec::new();
223
224        // 使用 git worktree prune 命令
225        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    /// 同步 Worktree 状态
245    pub async fn sync(&self) -> Layer4Result<()> {
246        // 更新所有 worktree 的状态
247        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                // 更新状态
254                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    /// Worktree 数量
268    pub fn count(&self) -> usize {
269        self.worktrees.read().len()
270    }
271
272    /// 获取根路径
273    pub fn root_path(&self) -> &PathBuf {
274        &self.root_path
275    }
276
277    /// 获取 worktrees 路径
278    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}