1use std::path::{Path, PathBuf};
2use std::process::{Command, Output};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum GitError {
7 #[error("git command failed: {cmd}\n{stderr}")]
8 CommandFailed { cmd: String, stderr: String },
9 #[error("io error: {0}")]
10 Io(#[from] std::io::Error),
11}
12
13pub fn checkout_pr(
14 work_root: &Path,
15 clone_url: &str,
16 pr_number: u64,
17 token: Option<&str>,
18) -> Result<PathBuf, GitError> {
19 std::fs::create_dir_all(work_root)?;
20 let repo_dir = work_root.join(format!("pr-{pr_number}"));
21 if repo_dir.exists() {
22 std::fs::remove_dir_all(&repo_dir)?;
23 }
24
25 let auth_url = inject_token(clone_url, token);
26 run_git(
27 &["clone", "--depth", "50", &auth_url, repo_dir.to_str().unwrap()],
28 None,
29 )?;
30
31 let local_branch = format!("cac-pr-{pr_number}");
32 let fetch_ref = format!("pull/{pr_number}/head:{local_branch}");
33 run_git(&["fetch", "origin", &fetch_ref], Some(&repo_dir))?;
34 run_git(&["checkout", &local_branch], Some(&repo_dir))?;
35 Ok(repo_dir)
36}
37
38pub fn commit_all(repo_dir: &Path, message: &str) -> Result<bool, GitError> {
39 run_git(&["add", "-A"], Some(repo_dir))?;
40 let status = run_git(&["status", "--porcelain"], Some(repo_dir))?;
41 if String::from_utf8_lossy(&status.stdout).trim().is_empty() {
42 return Ok(false);
43 }
44 run_git(&["commit", "-m", message], Some(repo_dir))?;
45 Ok(true)
46}
47
48pub fn push_branch(repo_dir: &Path, branch: &str, token: Option<&str>) -> Result<(), GitError> {
49 let remote = run_git(&["remote", "get-url", "origin"], Some(repo_dir))?;
50 let remote_url = String::from_utf8_lossy(&remote.stdout).trim().to_string();
51 let auth_url = inject_token(&remote_url, token);
52 run_git(&["remote", "set-url", "origin", &auth_url], Some(repo_dir))?;
53 run_git(
54 &["push", "origin", &format!("HEAD:refs/heads/{branch}")],
55 Some(repo_dir),
56 )?;
57 Ok(())
58}
59
60fn run_git(args: &[&str], cwd: Option<&Path>) -> Result<Output, GitError> {
61 let mut cmd = Command::new("git");
62 cmd.args(args);
63 if let Some(cwd) = cwd {
64 cmd.current_dir(cwd);
65 }
66 let output = cmd.output()?;
67 if output.status.success() {
68 Ok(output)
69 } else {
70 Err(GitError::CommandFailed {
71 cmd: format!("git {}", args.join(" ")),
72 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
73 })
74 }
75}
76
77fn inject_token(url: &str, token: Option<&str>) -> String {
78 let Some(token) = token else {
79 return url.to_string();
80 };
81 if !url.starts_with("https://") {
82 return url.to_string();
83 }
84 let rest = url.strip_prefix("https://").unwrap_or(url);
85 if rest.contains('@') {
86 return url.to_string();
87 }
88 if rest.contains("github.com") {
89 return format!("https://x-access-token:{token}@{rest}");
90 }
91 format!("https://{token}@{rest}")
92}