1use std::path::{Path, PathBuf};
6use std::process::Command;
7
8pub type GitResult<T> = Result<T, GitError>;
10
11#[derive(Debug, thiserror::Error)]
13pub enum GitError {
14 #[error("Git command failed: {0}")]
15 CommandFailed(String),
16
17 #[error("Worktree already exists: {0}")]
18 WorktreeExists(PathBuf),
19
20 #[error("Worktree not found: {0}")]
21 WorktreeNotFound(PathBuf),
22
23 #[error("Invalid path: {0}")]
24 InvalidPath(String),
25}
26
27pub struct GitWorktreeOps {
29 repo_path: PathBuf,
30}
31
32impl GitWorktreeOps {
33 pub fn new(repo_path: impl AsRef<Path>) -> Self {
35 Self {
36 repo_path: repo_path.as_ref().to_path_buf(),
37 }
38 }
39
40 pub fn create_worktree(
42 &self,
43 worktree_path: impl AsRef<Path>,
44 branch_name: &str,
45 ) -> GitResult<PathBuf> {
46 let worktree_path = worktree_path.as_ref();
47
48 if worktree_path.exists() {
49 return Err(GitError::WorktreeExists(worktree_path.to_path_buf()));
50 }
51
52 let output = Command::new("git")
53 .arg("worktree")
54 .arg("add")
55 .arg(worktree_path)
56 .arg("-b")
57 .arg(branch_name)
58 .current_dir(&self.repo_path)
59 .output()
60 .map_err(|e| GitError::CommandFailed(e.to_string()))?;
61
62 if !output.status.success() {
63 return Err(GitError::CommandFailed(
64 String::from_utf8_lossy(&output.stderr).to_string(),
65 ));
66 }
67
68 Ok(worktree_path.to_path_buf())
69 }
70
71 pub fn remove_worktree(&self, worktree_path: impl AsRef<Path>) -> GitResult<()> {
73 let worktree_path = worktree_path.as_ref();
74
75 if !worktree_path.exists() {
76 return Err(GitError::WorktreeNotFound(worktree_path.to_path_buf()));
77 }
78
79 let output = Command::new("git")
80 .arg("worktree")
81 .arg("remove")
82 .arg(worktree_path)
83 .arg("--force")
84 .current_dir(&self.repo_path)
85 .output()
86 .map_err(|e| GitError::CommandFailed(e.to_string()))?;
87
88 if !output.status.success() {
89 return Err(GitError::CommandFailed(
90 String::from_utf8_lossy(&output.stderr).to_string(),
91 ));
92 }
93
94 Ok(())
95 }
96
97 pub fn list_worktrees(&self) -> GitResult<Vec<PathBuf>> {
99 let output = Command::new("git")
100 .arg("worktree")
101 .arg("list")
102 .arg("--porcelain")
103 .current_dir(&self.repo_path)
104 .output()
105 .map_err(|e| GitError::CommandFailed(e.to_string()))?;
106
107 if !output.status.success() {
108 return Err(GitError::CommandFailed(
109 String::from_utf8_lossy(&output.stderr).to_string(),
110 ));
111 }
112
113 let stdout = String::from_utf8_lossy(&output.stdout);
114 let paths: Vec<PathBuf> = stdout
115 .lines()
116 .filter(|line| line.starts_with("worktree "))
117 .filter_map(|line| line.strip_prefix("worktree "))
118 .map(PathBuf::from)
119 .collect();
120
121 Ok(paths)
122 }
123
124 pub fn prune_worktrees(&self) -> GitResult<()> {
126 let output = Command::new("git")
127 .arg("worktree")
128 .arg("prune")
129 .current_dir(&self.repo_path)
130 .output()
131 .map_err(|e| GitError::CommandFailed(e.to_string()))?;
132
133 if !output.status.success() {
134 return Err(GitError::CommandFailed(
135 String::from_utf8_lossy(&output.stderr).to_string(),
136 ));
137 }
138
139 Ok(())
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use tempfile::TempDir;
147
148 #[test]
149 fn test_git_worktree_ops_creation() {
150 let temp_dir = TempDir::new().unwrap();
151 let ops = GitWorktreeOps::new(temp_dir.path());
152 assert_eq!(ops.repo_path, temp_dir.path());
153 }
154
155 #[test]
156 fn test_invalid_path_error() {
157 let ops = GitWorktreeOps::new("/nonexistent/path");
158 assert!(ops.repo_path.to_str().is_some());
161 }
162}