//! Thin shell wrappers around `git`. We avoid pulling in libgit2 — the daemon //! shells out for `cargo build` and friends anyway, and `git` is always on the //! MakeMachine. use anyhow::{Context, Result}; use std::path::Path; use tokio::process::Command; const POST_RECEIVE: &str = include_str!("../../hooks/post-receive"); pub async fn ensure_bare_repo(path: &Path) -> Result<()> { if !path.join("HEAD").exists() { tokio::fs::create_dir_all(path).await?; let out = Command::new("git") .args(["init", "--bare", "--initial-branch=main"]) .arg(path) .output() .await .context("spawning git init")?; anyhow::ensure!( out.status.success(), "git init --bare failed: {}", String::from_utf8_lossy(&out.stderr), ); } install_hook(path).await?; Ok(()) } async fn install_hook(bare: &Path) -> Result<()> { let hook = bare.join("hooks").join("post-receive"); tokio::fs::create_dir_all(bare.join("hooks")).await?; tokio::fs::write(&hook, POST_RECEIVE).await?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perm = tokio::fs::metadata(&hook).await?.permissions(); perm.set_mode(0o755); tokio::fs::set_permissions(&hook, perm).await?; } Ok(()) } pub async fn resolve_ref(bare: &Path, refname: &str) -> Result { let out = Command::new("git") .arg("--git-dir") .arg(bare) .args(["rev-parse", refname]) .output() .await?; anyhow::ensure!( out.status.success(), "git rev-parse {refname} failed: {}", String::from_utf8_lossy(&out.stderr), ); Ok(String::from_utf8(out.stdout)?.trim().to_string()) } pub async fn checkout_worktree(bare: &Path, sha: &str, dest: &Path) -> Result<()> { if dest.exists() { // Pre-existing worktree (re-trigger of same sha). Idempotent — nothing to do. return Ok(()); } tokio::fs::create_dir_all(dest.parent().unwrap()).await?; let out = Command::new("git") .arg("--git-dir") .arg(bare) .args(["worktree", "add", "--detach"]) .arg(dest) .arg(sha) .output() .await?; anyhow::ensure!( out.status.success(), "git worktree add failed: {}", String::from_utf8_lossy(&out.stderr), ); Ok(()) }