ref:main
use std::path::{Path, PathBuf};
use std::process::Command;
/// Prepare workspace for a job: clone or fetch, then checkout the target SHA.
/// Returns the workspace directory path.
pub fn prepare(
work_dir: &Path,
repo_clone_url: &str,
commit_sha: &str,
slot: u32,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let (owner, repo) = parse_repo_url(repo_clone_url)?;
// Localize URL for Docker: host.docker.internal → 127.0.0.1
let local_url = repo_clone_url.replace("host.docker.internal", "127.0.0.1");
let workspace = work_dir.join(&owner).join(&repo).join(slot.to_string());
std::fs::create_dir_all(&workspace)?;
let git_dir = workspace.join(".git");
if git_dir.exists() {
// Existing checkout — update remote and fetch
run_git(
&workspace,
&["remote", "set-url", "origin", &local_url],
"set-url",
)?;
run_git(&workspace, &["fetch", "origin", "--prune"], "fetch")?;
} else {
// Fresh clone
run_git_in(
work_dir,
&["clone", &local_url, workspace.to_str().unwrap()],
"clone",
)?;
}
// Checkout target commit — force to discard any local changes
run_git(&workspace, &["checkout", "--force", commit_sha], "checkout")?;
// NOTE: no `git clean -fdx` — preserves build caches (deps, _build, node_modules, target/)
Ok(workspace)
}
/// Remove workspace directory.
pub fn cleanup(workspace: &Path) {
if workspace.exists() {
if let Err(e) = std::fs::remove_dir_all(workspace) {
eprintln!(
"warning: failed to clean workspace {}: {e}",
workspace.display()
);
}
}
}
/// Parse owner/repo from a clone URL.
fn parse_repo_url(url: &str) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
// Handle both https://host/owner/repo.git and git@host:owner/repo.git
let path = if url.contains("://") {
url::Url::parse(url)?
.path()
.trim_start_matches('/')
.to_string()
} else if let Some(colon_idx) = url.find(':') {
url[colon_idx + 1..].to_string()
} else {
url.to_string()
};
let path = path.trim_end_matches(".git");
let parts: Vec<&str> = path.rsplitn(3, '/').collect();
if parts.len() < 2 {
return Err(format!("cannot parse repo URL: {url}").into());
}
Ok((parts[1].to_string(), parts[0].to_string()))
}
fn run_git(
cwd: &Path,
args: &[&str],
label: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let output = Command::new("git").current_dir(cwd).args(args).output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("warning: git {label} failed: {stderr}");
}
Ok(())
}
fn run_git_in(
cwd: &Path,
args: &[&str],
label: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
run_git(cwd, args, label)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_repo_url() {
assert_eq!(
parse_repo_url("https://anvil.fangorn.io/fangorn/myrepo.git").unwrap(),
("fangorn".into(), "myrepo".into())
);
assert_eq!(
parse_repo_url("git@anvil.fangorn.io:fangorn/myrepo.git").unwrap(),
("fangorn".into(), "myrepo".into())
);
assert_eq!(
parse_repo_url("https://anvil.example.com/org/repo").unwrap(),
("org".into(), "repo".into())
);
}
}