autoflow_git/
worktree.rs

1use autoflow_data::{AutoFlowError, Result};
2use git2::{Repository, BranchType};
3use std::path::{Path, PathBuf};
4use std::fs;
5
6/// Git worktree manager
7pub struct WorktreeManager {
8    repo: Repository,
9}
10
11impl WorktreeManager {
12    /// Create a new worktree manager for the current repository
13    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    /// Create a new worktree for a sprint
19    pub fn create_worktree(&self, sprint_id: u32, branch_name: &str) -> Result<WorktreeInfo> {
20        // Get repository path
21        let repo_path = self.repo.path().parent()
22            .ok_or_else(|| AutoFlowError::ValidationError("Invalid repository path".to_string()))?;
23
24        // Create worktree path
25        let worktree_name = format!("sprint-{}", sprint_id);
26        let worktree_path = repo_path.join("..").join(&worktree_name);
27
28        // Check if worktree already exists
29        if worktree_path.exists() {
30            return Err(AutoFlowError::ValidationError(
31                format!("Worktree already exists: {}", worktree_path.display())
32            ));
33        }
34
35        // Get current branch to use as base
36        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        // Create worktree with new branch using git command (git2 doesn't support worktree creation)
43        // The -b flag creates a new branch from HEAD
44        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        // Calculate unique port for this worktree
56        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    /// List all worktrees
69    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        // Use git command to list worktrees
74        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                    // Extract sprint ID from path if it's a sprint worktree
98                    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(), // We don't have actual creation time
119                    });
120
121                    current_worktree = None;
122                }
123            }
124        }
125
126        Ok(worktrees)
127    }
128
129    /// Delete a worktree
130    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        // Get worktree path
137        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        // Remove worktree using git command
146        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    /// Merge a worktree branch back to main
161    pub fn merge_worktree(&self, branch_name: &str) -> Result<()> {
162        tracing::info!("Merging branch {} to main", branch_name);
163
164        // Checkout main branch
165        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        // Checkout working directory
174        self.repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
175
176        // Merge the branch
177        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        // Create merge commit
193        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    /// Prune merged worktrees
213    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        // Prune using git command
218        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()) // Return empty for now
230    }
231
232    /// Setup environment for a worktree
233    pub fn setup_worktree_env(&self, worktree_info: &WorktreeInfo) -> Result<()> {
234        // Copy docker-compose.yml if it exists
235        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            // Replace ports
244            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        // Copy .env if it exists
251        let env_file = repo_path.join(".env");
252        if env_file.exists() {
253            let content = fs::read_to_string(&env_file)?;
254
255            // Modify port in .env
256            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/// Worktree information
267#[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}