use anyhow::{Context as _, Result}; use collections::HashMap; use futures::lock::{Mutex, OwnedMutexGuard}; use std::{ cell::RefCell, path::{Path, PathBuf}, sync::Arc, }; use crate::paths::REPOS_DIR; thread_local! { static REPO_LOCKS: RefCell>>> = RefCell::new(HashMap::default()); } #[must_use] pub async fn lock_repo(path: impl AsRef) -> OwnedMutexGuard<()> { REPO_LOCKS .with(|cell| { cell.borrow_mut() .entry(path.as_ref().to_path_buf()) .or_default() .clone() }) .lock_owned() .await } pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result { let output = smol::process::Command::new("git") .current_dir(repo_path) .args(args) .output() .await?; anyhow::ensure!( output.status.success(), "`git {}` within `{}` failed with status: {}\nstderr:\n{}\nstdout:\n{}", args.join(" "), repo_path.display(), output.status, String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout), ); Ok(String::from_utf8(output.stdout)?.trim().to_string()) } pub fn parse_repo_url(url: &str) -> Result<(String, String)> { if url.contains('@') { let (_, path) = url.split_once(':').context("expected : in git url")?; let (owner, repo) = path.split_once('/').context("expected / in git url")?; Ok((owner.to_string(), repo.trim_end_matches(".git").to_string())) } else { let parsed = http_client::Url::parse(url)?; let mut segments = parsed.path_segments().context("empty http url")?; let owner = segments.next().context("expected owner")?; let repo = segments.next().context("expected repo")?; Ok((owner.to_string(), repo.trim_end_matches(".git").to_string())) } } pub fn repo_path_for_url(url: &str) -> Result { let (owner, name) = parse_repo_url(url)?; Ok(REPOS_DIR.join(&owner).join(&name)) } pub async fn ensure_repo_cloned(repo_url: &str) -> Result { let repo_path = repo_path_for_url(repo_url)?; let _lock = lock_repo(&repo_path).await; // Validate existing repo has correct origin, otherwise remove and re-init. let mut git_repo_exists = false; if repo_path.is_dir() { if run_git(&repo_path, &["remote", "get-url", "origin"]) .await .map_or(false, |origin| origin.trim() == repo_url) { git_repo_exists = true; } else { std::fs::remove_dir_all(&repo_path).ok(); } } if !git_repo_exists { log::info!("Cloning {} into {:?}", repo_url, repo_path); std::fs::create_dir_all(&repo_path)?; run_git(&repo_path, &["init"]).await?; run_git(&repo_path, &["remote", "add", "origin", repo_url]) .await .ok(); } // Always fetch to get latest commits run_git(&repo_path, &["fetch", "--depth", "1000", "origin"]).await?; // Check if we have a valid HEAD, if not checkout FETCH_HEAD let has_head = run_git(&repo_path, &["rev-parse", "HEAD"]).await.is_ok(); if !has_head { // Use reset to set HEAD without needing a branch run_git(&repo_path, &["reset", "--hard", "FETCH_HEAD"]).await?; } Ok(repo_path) } pub async fn fetch_if_needed(repo_path: &Path, revision: &str) -> Result { let resolved = run_git( repo_path, &["rev-parse", &format!("{}^{{commit}}", revision)], ) .await; if let Ok(sha) = resolved { return Ok(sha); } if run_git(repo_path, &["fetch", "--depth", "1", "origin", revision]) .await .is_err() { run_git(repo_path, &["fetch", "origin"]).await?; } run_git( repo_path, &["rev-parse", &format!("{}^{{commit}}", revision)], ) .await }