1use std::path::{Path, PathBuf};
9
10use crate::error::{Error, Result};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum VcsKind {
15 Git,
16 Jj,
17}
18
19pub struct WorktreeInfo {
21 pub path: PathBuf,
22 pub branch: Option<String>,
23 pub vcs_kind: VcsKind,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum VcsOverride {
29 Git,
30}
31
32pub trait VcsBackend {
34 fn create_worktree(&self, repo_path: &Path, worktree_path: &Path, name: &str) -> Result<()>;
35 fn remove_worktree(&self, repo_path: &Path, worktree_path: &Path, name: &str) -> Result<()>;
36 fn list_worktrees(&self, repo_path: &Path, worktree_base: &Path) -> Result<Vec<WorktreeInfo>>;
37}
38
39pub fn detect_backend(
45 repo_path: &Path,
46 vcs_override: Option<VcsOverride>,
47) -> Result<Box<dyn VcsBackend>> {
48 if vcs_override == Some(VcsOverride::Git) {
49 return Ok(Box::new(GitBackend));
50 }
51 if repo_path.join(".jj").is_dir() {
52 ensure_jj_installed()?;
53 return Ok(Box::new(JjBackend));
54 }
55 Ok(Box::new(GitBackend))
56}
57
58fn ensure_jj_installed() -> Result<()> {
59 match std::process::Command::new("jj").arg("--version").output() {
60 Ok(output) if output.status.success() => Ok(()),
61 _ => Err(Error::JjNotInstalled),
62 }
63}
64
65struct GitBackend;
66
67impl VcsBackend for GitBackend {
68 fn create_worktree(&self, repo_path: &Path, worktree_path: &Path, name: &str) -> Result<()> {
69 let output = std::process::Command::new("git")
70 .args([
71 "worktree",
72 "add",
73 worktree_path.to_str().unwrap(),
74 "-b",
75 name,
76 ])
77 .current_dir(repo_path)
78 .output()?;
79
80 if !output.status.success() {
81 let stderr = String::from_utf8_lossy(&output.stderr);
82 if stderr.contains("already exists") {
83 let output2 = std::process::Command::new("git")
84 .args(["worktree", "add", worktree_path.to_str().unwrap(), name])
85 .current_dir(repo_path)
86 .output()?;
87
88 if !output2.status.success() {
89 return Err(Error::VcsCommandFailed(
90 String::from_utf8_lossy(&output2.stderr).to_string(),
91 ));
92 }
93 } else {
94 return Err(Error::VcsCommandFailed(stderr.to_string()));
95 }
96 }
97
98 Ok(())
99 }
100
101 fn remove_worktree(&self, repo_path: &Path, worktree_path: &Path, _name: &str) -> Result<()> {
102 let output = std::process::Command::new("git")
103 .args(["worktree", "remove", worktree_path.to_str().unwrap()])
104 .current_dir(repo_path)
105 .output()?;
106
107 if !output.status.success() {
108 return Err(Error::VcsCommandFailed(
109 String::from_utf8_lossy(&output.stderr).to_string(),
110 ));
111 }
112
113 Ok(())
114 }
115
116 fn list_worktrees(&self, repo_path: &Path, _worktree_base: &Path) -> Result<Vec<WorktreeInfo>> {
117 if !repo_path.join(".git/worktrees").exists() {
119 return Ok(Vec::new());
120 }
121
122 let output = std::process::Command::new("git")
123 .args(["worktree", "list", "--porcelain"])
124 .current_dir(repo_path)
125 .output()?;
126
127 if !output.status.success() {
128 return Err(Error::VcsCommandFailed(
129 String::from_utf8_lossy(&output.stderr).to_string(),
130 ));
131 }
132
133 let stdout = String::from_utf8_lossy(&output.stdout);
134 let mut worktrees = Vec::new();
135 let mut current_path: Option<PathBuf> = None;
136 let mut current_branch: Option<String> = None;
137 let mut is_first = true;
138
139 for line in stdout.lines() {
140 if let Some(path_str) = line.strip_prefix("worktree ") {
141 if let Some(path) = current_path.take() {
143 if !is_first {
144 worktrees.push(WorktreeInfo {
145 path,
146 branch: current_branch.take(),
147 vcs_kind: VcsKind::Git,
148 });
149 }
150 is_first = false;
151 }
152 current_path = Some(PathBuf::from(path_str));
153 current_branch = None;
154 } else if let Some(branch_ref) = line.strip_prefix("branch refs/heads/") {
155 current_branch = Some(branch_ref.to_string());
156 }
157 }
158 if let Some(path) = current_path {
160 if !is_first {
161 worktrees.push(WorktreeInfo {
162 path,
163 branch: current_branch,
164 vcs_kind: VcsKind::Git,
165 });
166 }
167 }
168
169 Ok(worktrees)
170 }
171}
172
173struct JjBackend;
174
175impl VcsBackend for JjBackend {
176 fn create_worktree(&self, repo_path: &Path, worktree_path: &Path, name: &str) -> Result<()> {
177 let output = std::process::Command::new("jj")
178 .args([
179 "workspace",
180 "add",
181 "--name",
182 name,
183 worktree_path.to_str().unwrap(),
184 ])
185 .current_dir(repo_path)
186 .output()?;
187
188 if !output.status.success() {
189 return Err(Error::VcsCommandFailed(
190 String::from_utf8_lossy(&output.stderr).to_string(),
191 ));
192 }
193
194 Ok(())
195 }
196
197 fn remove_worktree(&self, repo_path: &Path, worktree_path: &Path, name: &str) -> Result<()> {
198 let output = std::process::Command::new("jj")
199 .args(["workspace", "forget", name])
200 .current_dir(repo_path)
201 .output()?;
202
203 if !output.status.success() {
204 return Err(Error::VcsCommandFailed(
205 String::from_utf8_lossy(&output.stderr).to_string(),
206 ));
207 }
208
209 if worktree_path.exists() {
211 std::fs::remove_dir_all(worktree_path)?;
212 }
213
214 Ok(())
215 }
216
217 fn list_worktrees(&self, repo_path: &Path, worktree_base: &Path) -> Result<Vec<WorktreeInfo>> {
218 let output = std::process::Command::new("jj")
219 .args(["workspace", "list"])
220 .current_dir(repo_path)
221 .output()?;
222
223 if !output.status.success() {
224 return Err(Error::VcsCommandFailed(
225 String::from_utf8_lossy(&output.stderr).to_string(),
226 ));
227 }
228
229 let stdout = String::from_utf8_lossy(&output.stdout);
230 let mut worktrees = Vec::new();
231
232 for line in stdout.lines() {
233 let Some((name, _rest)) = line.split_once(": ") else {
235 continue;
236 };
237
238 if name == "default" {
240 continue;
241 }
242
243 worktrees.push(WorktreeInfo {
244 path: worktree_base.join(name),
245 branch: None,
246 vcs_kind: VcsKind::Jj,
247 });
248 }
249
250 Ok(worktrees)
251 }
252}