Skip to main content

codetether_agent/
worktree.rs

1//! Git worktree management for isolated agent execution
2//!
3//! Provides worktree isolation for parallel agent tasks.
4
5use anyhow::{anyhow, Result};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::sync::Mutex;
9
10/// Worktree information
11#[derive(Debug, Clone)]
12pub struct WorktreeInfo {
13    /// Worktree name/identifier
14    pub name: String,
15    /// Path to the worktree
16    pub path: PathBuf,
17    /// Branch name
18    pub branch: String,
19    /// Whether this worktree is active
20    pub active: bool,
21}
22
23/// Worktree manager for creating and managing isolated git worktrees
24#[derive(Debug)]
25pub struct WorktreeManager {
26    /// Base directory for worktrees
27    base_dir: PathBuf,
28    /// Active worktrees
29    worktrees: Mutex<Vec<WorktreeInfo>>,
30}
31
32/// Merge result
33#[derive(Debug, Clone)]
34pub struct MergeResult {
35    pub success: bool,
36    pub aborted: bool,
37    pub conflicts: Vec<String>,
38    pub conflict_diffs: Vec<(String, String)>,
39    pub files_changed: usize,
40    pub summary: String,
41}
42
43impl WorktreeManager {
44    /// Create a new worktree manager
45    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
46        Self {
47            base_dir: base_dir.into(),
48            worktrees: Mutex::new(Vec::new()),
49        }
50    }
51
52    /// Create a new worktree for a task
53    pub async fn create(&self, name: &str) -> Result<WorktreeInfo> {
54        let worktree_path = self.base_dir.join(name);
55        
56        // Create the directory if it doesn't exist
57        tokio::fs::create_dir_all(&worktree_path).await?;
58        
59        let info = WorktreeInfo {
60            name: name.to_string(),
61            path: worktree_path.clone(),
62            branch: format!("codetether/{}", name),
63            active: true,
64        };
65        
66        let mut worktrees = self.worktrees.lock().await;
67        worktrees.push(info.clone());
68        
69        tracing::info!(worktree = %name, path = %worktree_path.display(), "Created worktree");
70        Ok(info)
71    }
72
73    /// Get information about a worktree
74    pub async fn get(&self, name: &str) -> Option<WorktreeInfo> {
75        let worktrees = self.worktrees.lock().await;
76        worktrees.iter().find(|w| w.name == name).cloned()
77    }
78
79    /// List all worktrees
80    pub async fn list(&self) -> Vec<WorktreeInfo> {
81        self.worktrees.lock().await.clone()
82    }
83
84    /// Clean up a specific worktree
85    pub async fn cleanup(&self, name: &str) -> Result<()> {
86        let mut worktrees = self.worktrees.lock().await;
87        if let Some(pos) = worktrees.iter().position(|w| w.name == name) {
88            let info = &worktrees[pos];
89            // Attempt to remove the directory
90            if let Err(e) = tokio::fs::remove_dir_all(&info.path).await {
91                tracing::warn!(worktree = %name, error = %e, "Failed to remove worktree directory");
92            }
93            worktrees.remove(pos);
94            tracing::info!(worktree = %name, "Cleaned up worktree");
95        }
96        Ok(())
97    }
98
99    /// Merge a worktree branch back
100    pub async fn merge(&self, name: &str) -> Result<MergeResult> {
101        let worktrees = self.worktrees.lock().await;
102        if let Some(_info) = worktrees.iter().find(|w| w.name == name) {
103            // Placeholder: In a real implementation, this would perform git merge
104            tracing::info!(worktree = %name, "Merged worktree branch");
105            Ok(MergeResult {
106                success: true,
107                aborted: false,
108                conflicts: vec![],
109                conflict_diffs: vec![],
110                files_changed: 0,
111                summary: "Merged".to_string(),
112            })
113        } else {
114            Err(anyhow!("Worktree not found: {}", name))
115        }
116    }
117
118    /// Complete a merge
119    pub async fn complete_merge(&self, name: &str, _commit_msg: &str) -> Result<MergeResult> {
120        self.merge(name).await
121    }
122
123    /// Abort a merge
124    pub async fn abort_merge(&self, _name: &str) -> Result<()> {
125        Ok(())
126    }
127
128    /// Clean up all worktrees
129    pub async fn cleanup_all(&self) -> Result<usize> {
130        let mut worktrees = self.worktrees.lock().await;
131        let count = worktrees.len();
132        
133        for info in worktrees.iter() {
134            if let Err(e) = tokio::fs::remove_dir_all(&info.path).await {
135                tracing::warn!(worktree = %info.name, error = %e, "Failed to remove worktree directory");
136            }
137        }
138        
139        worktrees.clear();
140        tracing::info!(count, "Cleaned up all worktrees");
141        Ok(count)
142    }
143
144    /// Inject workspace stub for Cargo workspace isolation
145    pub fn inject_workspace_stub(&self, _worktree_path: &Path) -> Result<()> {
146        // Placeholder: In a real implementation, this would prepend [workspace] to Cargo.toml
147        Ok(())
148    }
149}
150
151impl Default for WorktreeManager {
152    fn default() -> Self {
153        Self::new("/tmp/codetether-worktrees")
154    }
155}