Skip to main content

cac_webhook/
git.rs

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}