1use autoflow_data::{AutoFlowError, Result};
2use git2::{Repository, BranchType};
3use std::path::{Path, PathBuf};
4use std::fs;
5
6pub struct WorktreeManager {
8 repo: Repository,
9}
10
11impl WorktreeManager {
12 pub fn new<P: AsRef<Path>>(repo_path: P) -> Result<Self> {
14 let repo = Repository::open(repo_path)?;
15 Ok(Self { repo })
16 }
17
18 pub fn create_worktree(&self, sprint_id: u32, branch_name: &str) -> Result<WorktreeInfo> {
20 let repo_path = self.repo.path().parent()
22 .ok_or_else(|| AutoFlowError::ValidationError("Invalid repository path".to_string()))?;
23
24 let worktree_name = format!("sprint-{}", sprint_id);
26 let worktree_path = repo_path.join("..").join(&worktree_name);
27
28 if worktree_path.exists() {
30 return Err(AutoFlowError::ValidationError(
31 format!("Worktree already exists: {}", worktree_path.display())
32 ));
33 }
34
35 let head = self.repo.head()?;
37 let base_branch = head.shorthand()
38 .ok_or_else(|| AutoFlowError::ValidationError("Could not determine current branch".to_string()))?;
39
40 tracing::info!("Creating worktree {} from branch {}", worktree_name, base_branch);
41
42 let status = std::process::Command::new("git")
45 .args(&["worktree", "add", "-b", branch_name, worktree_path.to_str().unwrap(), "HEAD"])
46 .current_dir(repo_path)
47 .status()?;
48
49 if !status.success() {
50 return Err(AutoFlowError::ValidationError(
51 "Failed to create git worktree".to_string()
52 ));
53 }
54
55 let base_port = 3000;
57 let port = base_port + (sprint_id * 10);
58
59 Ok(WorktreeInfo {
60 name: worktree_name,
61 path: worktree_path,
62 branch: branch_name.to_string(),
63 port,
64 created_at: chrono::Utc::now(),
65 })
66 }
67
68 pub fn list_worktrees(&self) -> Result<Vec<WorktreeInfo>> {
70 let repo_path = self.repo.path().parent()
71 .ok_or_else(|| AutoFlowError::ValidationError("Invalid repository path".to_string()))?;
72
73 let output = std::process::Command::new("git")
75 .args(&["worktree", "list", "--porcelain"])
76 .current_dir(repo_path)
77 .output()?;
78
79 if !output.status.success() {
80 return Err(AutoFlowError::ValidationError(
81 "Failed to list git worktrees".to_string()
82 ));
83 }
84
85 let stdout = String::from_utf8_lossy(&output.stdout);
86 let mut worktrees = Vec::new();
87 let mut current_worktree: Option<(PathBuf, String)> = None;
88
89 for line in stdout.lines() {
90 if line.starts_with("worktree ") {
91 let path = PathBuf::from(line.trim_start_matches("worktree "));
92 current_worktree = Some((path, String::new()));
93 } else if line.starts_with("branch ") {
94 if let Some((path, _)) = &mut current_worktree {
95 let branch = line.trim_start_matches("branch refs/heads/").to_string();
96
97 let name = path.file_name()
99 .and_then(|n| n.to_str())
100 .unwrap_or("unknown")
101 .to_string();
102
103 let sprint_id = if name.starts_with("sprint-") {
104 name.trim_start_matches("sprint-")
105 .parse::<u32>()
106 .ok()
107 } else {
108 None
109 };
110
111 let port = sprint_id.map(|id| 3000 + (id * 10)).unwrap_or(3000);
112
113 worktrees.push(WorktreeInfo {
114 name,
115 path: path.clone(),
116 branch,
117 port,
118 created_at: chrono::Utc::now(), });
120
121 current_worktree = None;
122 }
123 }
124 }
125
126 Ok(worktrees)
127 }
128
129 pub fn delete_worktree(&self, worktree_name: &str) -> Result<()> {
131 let repo_path = self.repo.path().parent()
132 .ok_or_else(|| AutoFlowError::ValidationError("Invalid repository path".to_string()))?;
133
134 tracing::info!("Deleting worktree {}", worktree_name);
135
136 let worktree_path = repo_path.join("..").join(worktree_name);
138
139 if !worktree_path.exists() {
140 return Err(AutoFlowError::ValidationError(
141 format!("Worktree does not exist: {}", worktree_name)
142 ));
143 }
144
145 let status = std::process::Command::new("git")
147 .args(&["worktree", "remove", worktree_path.to_str().unwrap(), "--force"])
148 .current_dir(repo_path)
149 .status()?;
150
151 if !status.success() {
152 return Err(AutoFlowError::ValidationError(
153 "Failed to remove git worktree".to_string()
154 ));
155 }
156
157 Ok(())
158 }
159
160 pub fn merge_worktree(&self, branch_name: &str) -> Result<()> {
162 tracing::info!("Merging branch {} to main", branch_name);
163
164 let main_branch = self.repo.find_branch("main", BranchType::Local)
166 .or_else(|_| self.repo.find_branch("master", BranchType::Local))?;
167
168 let main_ref = main_branch.get().name()
169 .ok_or_else(|| AutoFlowError::ValidationError("Invalid main branch".to_string()))?;
170
171 self.repo.set_head(main_ref)?;
172
173 self.repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
175
176 let branch = self.repo.find_branch(branch_name, BranchType::Local)?;
178 let branch_commit = branch.get().peel_to_commit()?;
179
180 let mut index = self.repo.merge_commits(
181 &self.repo.head()?.peel_to_commit()?,
182 &branch_commit,
183 None
184 )?;
185
186 if index.has_conflicts() {
187 return Err(AutoFlowError::MergeConflict {
188 branch: branch_name.to_string(),
189 });
190 }
191
192 let tree_oid = index.write_tree_to(&self.repo)?;
194 let tree = self.repo.find_tree(tree_oid)?;
195 let signature = self.repo.signature()?;
196 let main_commit = self.repo.head()?.peel_to_commit()?;
197
198 self.repo.commit(
199 Some("HEAD"),
200 &signature,
201 &signature,
202 &format!("Merge sprint branch {}", branch_name),
203 &tree,
204 &[&main_commit, &branch_commit],
205 )?;
206
207 tracing::info!("Successfully merged {} to main", branch_name);
208
209 Ok(())
210 }
211
212 pub fn prune_worktrees(&self) -> Result<Vec<String>> {
214 let repo_path = self.repo.path().parent()
215 .ok_or_else(|| AutoFlowError::ValidationError("Invalid repository path".to_string()))?;
216
217 let status = std::process::Command::new("git")
219 .args(&["worktree", "prune"])
220 .current_dir(repo_path)
221 .status()?;
222
223 if !status.success() {
224 return Err(AutoFlowError::ValidationError(
225 "Failed to prune git worktrees".to_string()
226 ));
227 }
228
229 Ok(Vec::new()) }
231
232 pub fn setup_worktree_env(&self, worktree_info: &WorktreeInfo) -> Result<()> {
234 let repo_path = self.repo.path().parent()
236 .ok_or_else(|| AutoFlowError::ValidationError("Invalid repository path".to_string()))?;
237
238 let docker_compose = repo_path.join("docker-compose.yml");
239
240 if docker_compose.exists() {
241 let content = fs::read_to_string(&docker_compose)?;
242
243 let modified = content.replace("3000:", &format!("{}:", worktree_info.port));
245
246 let worktree_docker = worktree_info.path.join("docker-compose.yml");
247 fs::write(worktree_docker, modified)?;
248 }
249
250 let env_file = repo_path.join(".env");
252 if env_file.exists() {
253 let content = fs::read_to_string(&env_file)?;
254
255 let modified = content.replace("PORT=3000", &format!("PORT={}", worktree_info.port));
257
258 let worktree_env = worktree_info.path.join(".env");
259 fs::write(worktree_env, modified)?;
260 }
261
262 Ok(())
263 }
264}
265
266#[derive(Debug, Clone)]
268pub struct WorktreeInfo {
269 pub name: String,
270 pub path: PathBuf,
271 pub branch: String,
272 pub port: u32,
273 pub created_at: chrono::DateTime<chrono::Utc>,
274}
275
276impl WorktreeInfo {
277 pub fn display_path(&self) -> String {
278 self.path.display().to_string()
279 }
280}