codetether_agent/
worktree.rs1use anyhow::{anyhow, Result};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::sync::Mutex;
9
10#[derive(Debug, Clone)]
12pub struct WorktreeInfo {
13 pub name: String,
15 pub path: PathBuf,
17 pub branch: String,
19 pub active: bool,
21}
22
23#[derive(Debug)]
25pub struct WorktreeManager {
26 base_dir: PathBuf,
28 worktrees: Mutex<Vec<WorktreeInfo>>,
30}
31
32#[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 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 pub async fn create(&self, name: &str) -> Result<WorktreeInfo> {
54 let worktree_path = self.base_dir.join(name);
55
56 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 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 pub async fn list(&self) -> Vec<WorktreeInfo> {
81 self.worktrees.lock().await.clone()
82 }
83
84 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 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 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 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 pub async fn complete_merge(&self, name: &str, _commit_msg: &str) -> Result<MergeResult> {
120 self.merge(name).await
121 }
122
123 pub async fn abort_merge(&self, _name: &str) -> Result<()> {
125 Ok(())
126 }
127
128 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 pub fn inject_workspace_stub(&self, _worktree_path: &Path) -> Result<()> {
146 Ok(())
148 }
149}
150
151impl Default for WorktreeManager {
152 fn default() -> Self {
153 Self::new("/tmp/codetether-worktrees")
154 }
155}