1use crate::infra::error::GitError;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7pub fn resolve_git_common_dir(repo_root: Option<&Path>) -> Result<PathBuf, GitError> {
10 let mut cmd = Command::new("git");
11 if let Some(root) = repo_root {
12 cmd.current_dir(root);
13 }
14 cmd.args(["rev-parse", "--git-common-dir"]);
15
16 let output = cmd.output().map_err(GitError::ProcessFailed)?;
17 if !output.status.success() {
18 let stderr = String::from_utf8_lossy(&output.stderr);
19 if stderr.contains("not a git repository") {
20 return Err(GitError::NotAGitRepository);
21 }
22 return Err(GitError::CommandFailed {
23 command: "git rev-parse --git-common-dir".to_string(),
24 message: stderr.trim().to_string(),
25 });
26 }
27
28 let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
29 let path = PathBuf::from(path_str);
30 let resolved = if path.is_absolute() {
31 path
32 } else {
33 let base = repo_root
34 .map(|p| p.to_path_buf())
35 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
36 base.join(path)
37 };
38
39 Ok(resolved.canonicalize().unwrap_or(resolved))
40}
41
42pub fn resolve_toplevel(repo_root: Option<&Path>) -> Result<PathBuf, GitError> {
44 let mut cmd = Command::new("git");
45 if let Some(root) = repo_root {
46 cmd.current_dir(root);
47 }
48 cmd.args(["rev-parse", "--show-toplevel"]);
49
50 let output = cmd.output().map_err(GitError::ProcessFailed)?;
51 if !output.status.success() {
52 let stderr = String::from_utf8_lossy(&output.stderr);
53 if stderr.contains("not a git repository") {
54 return Err(GitError::NotAGitRepository);
55 }
56 return Err(GitError::CommandFailed {
57 command: "git rev-parse --show-toplevel".to_string(),
58 message: stderr.trim().to_string(),
59 });
60 }
61
62 let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
63 let path = PathBuf::from(path_str);
64 Ok(path.canonicalize().unwrap_or(path))
65}
66
67pub fn resolve_repo_name(toplevel: &Path) -> String {
69 toplevel
70 .file_name()
71 .and_then(|n| n.to_str())
72 .unwrap_or("repo")
73 .to_string()
74}
75
76pub fn git_worktree_prune(repo_root: Option<&Path>) -> Result<(), GitError> {
78 let mut cmd = Command::new("git");
79 if let Some(root) = repo_root {
80 cmd.current_dir(root);
81 }
82 cmd.args(["worktree", "prune"]);
83
84 let output = cmd.output().map_err(GitError::ProcessFailed)?;
85 if !output.status.success() {
86 let stderr = String::from_utf8_lossy(&output.stderr);
87 return Err(GitError::CommandFailed {
88 command: "git worktree prune".to_string(),
89 message: stderr.trim().to_string(),
90 });
91 }
92 Ok(())
93}
94
95pub fn git_is_clean(repo_root: &Path) -> Result<bool, GitError> {
97 let mut cmd = Command::new("git");
98 cmd.current_dir(repo_root);
99 cmd.args(["status", "--porcelain"]);
100
101 let output = cmd.output().map_err(GitError::ProcessFailed)?;
102 if !output.status.success() {
103 let stderr = String::from_utf8_lossy(&output.stderr);
104 return Err(GitError::CommandFailed {
105 command: "git status --porcelain".to_string(),
106 message: stderr.trim().to_string(),
107 });
108 }
109
110 Ok(output.stdout.is_empty())
111}
112
113pub fn git_merge(repo_root: &Path, branch: &str) -> Result<(), GitError> {
115 let mut cmd = Command::new("git");
116 cmd.current_dir(repo_root);
117 cmd.args(["merge", branch]);
118
119 let output = cmd.output().map_err(GitError::ProcessFailed)?;
120 if !output.status.success() {
121 let stderr = String::from_utf8_lossy(&output.stderr);
122 return Err(GitError::CommandFailed {
123 command: format!("git merge {}", branch),
124 message: stderr.trim().to_string(),
125 });
126 }
127
128 Ok(())
129}
130
131pub fn git_checkout(repo_root: &Path, branch: &str) -> Result<(), GitError> {
133 let mut cmd = Command::new("git");
134 cmd.current_dir(repo_root);
135 cmd.args(["checkout", branch]);
136
137 let output = cmd.output().map_err(GitError::ProcessFailed)?;
138 if !output.status.success() {
139 let stderr = String::from_utf8_lossy(&output.stderr);
140 return Err(GitError::CommandFailed {
141 command: format!("git checkout {}", branch),
142 message: stderr.trim().to_string(),
143 });
144 }
145
146 Ok(())
147}
148
149pub fn git_head_commit(worktree_path: &Path) -> Result<String, GitError> {
151 let mut cmd = Command::new("git");
152 cmd.current_dir(worktree_path);
153 cmd.args(["rev-parse", "--short", "HEAD"]);
154
155 let output = cmd.output().map_err(GitError::ProcessFailed)?;
156 if !output.status.success() {
157 let stderr = String::from_utf8_lossy(&output.stderr);
158 return Err(GitError::CommandFailed {
159 command: "git rev-parse --short HEAD".to_string(),
160 message: stderr.trim().to_string(),
161 });
162 }
163
164 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
165}
166
167pub fn git_create_annotated_tag(
169 repo_root: &Path,
170 tag: &str,
171 target_ref: &str,
172 message: &str,
173) -> Result<(), GitError> {
174 let mut cmd = Command::new("git");
175 cmd.current_dir(repo_root);
176 cmd.args(["tag", "-a", tag, target_ref, "-m", message]);
177
178 let output = cmd.output().map_err(GitError::ProcessFailed)?;
179 if !output.status.success() {
180 let stderr = String::from_utf8_lossy(&output.stderr);
181 return Err(GitError::CommandFailed {
182 command: format!("git tag -a {} {} -m {}", tag, target_ref, message),
183 message: stderr.trim().to_string(),
184 });
185 }
186
187 Ok(())
188}
189
190pub fn git_branch_exists(repo_root: &Path, branch: &str) -> Result<bool, GitError> {
205 let mut cmd = Command::new("git");
206 cmd.current_dir(repo_root);
207 let ref_spec = if branch.starts_with("refs/heads/") {
208 branch.to_string()
209 } else {
210 format!("refs/heads/{}", branch)
211 };
212 cmd.args(["rev-parse", "--verify", &ref_spec]);
213
214 let output = cmd.output().map_err(GitError::ProcessFailed)?;
215 Ok(output.status.success())
216}
217
218pub fn git_get_remotes(repo_root: &Path) -> Result<Vec<String>, GitError> {
233 let mut cmd = Command::new("git");
234 cmd.current_dir(repo_root);
235 cmd.arg("remote");
236
237 let output = cmd.output().map_err(GitError::ProcessFailed)?;
238 if !output.status.success() {
239 let stderr = String::from_utf8_lossy(&output.stderr);
240 return Err(GitError::CommandFailed {
241 command: "git remote".to_string(),
242 message: stderr.trim().to_string(),
243 });
244 }
245
246 let remotes = String::from_utf8_lossy(&output.stdout)
247 .lines()
248 .map(|s| s.trim().to_string())
249 .filter(|s| !s.is_empty())
250 .collect();
251
252 Ok(remotes)
253}
254
255pub fn git_push_upstream(repo_root: &Path, remote: &str, branch: &str) -> Result<(), GitError> {
271 let mut cmd = Command::new("git");
272 cmd.current_dir(repo_root);
273 cmd.args(["push", "-u", remote, branch]);
274
275 let output = cmd.output().map_err(GitError::ProcessFailed)?;
276 if !output.status.success() {
277 let stderr = String::from_utf8_lossy(&output.stderr);
278 return Err(GitError::CommandFailed {
279 command: format!("git push -u {} {}", remote, branch),
280 message: stderr.trim().to_string(),
281 });
282 }
283
284 Ok(())
285}
286
287pub fn git_remote_branch_exists(
304 repo_root: &Path,
305 remote: &str,
306 branch: &str,
307) -> Result<bool, GitError> {
308 let mut cmd = Command::new("git");
309 cmd.current_dir(repo_root);
310 cmd.args(["ls-remote", "--heads", remote, branch]);
311
312 let output = cmd.output().map_err(GitError::ProcessFailed)?;
313 if !output.status.success() {
314 let stderr = String::from_utf8_lossy(&output.stderr);
315 return Err(GitError::CommandFailed {
316 command: format!("git ls-remote --heads {} {}", remote, branch),
317 message: stderr.trim().to_string(),
318 });
319 }
320
321 let expected_ref = if branch.starts_with("refs/heads/") {
322 branch.to_string()
323 } else {
324 format!("refs/heads/{}", branch)
325 };
326
327 let stdout = String::from_utf8_lossy(&output.stdout);
328 let exists = stdout
329 .lines()
330 .any(|line| line.split_whitespace().nth(1) == Some(&expected_ref));
331
332 Ok(exists)
333}
334
335pub fn git_delete_remote_branch(
351 repo_root: &Path,
352 remote: &str,
353 branch: &str,
354) -> Result<(), GitError> {
355 let mut cmd = Command::new("git");
356 cmd.current_dir(repo_root);
357 let branch_clean = branch.strip_prefix("refs/heads/").unwrap_or(branch);
358 cmd.args(["push", remote, "--delete", branch_clean]);
359
360 let output = cmd.output().map_err(GitError::ProcessFailed)?;
361 if !output.status.success() {
362 let stderr = String::from_utf8_lossy(&output.stderr);
363 return Err(GitError::CommandFailed {
364 command: format!("git push {} --delete {}", remote, branch_clean),
365 message: stderr.trim().to_string(),
366 });
367 }
368
369 Ok(())
370}