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<HashMap<PathBuf, Arc<Mutex<()>>>> = RefCell::new(HashMap::default());} #[must_use]pub async fn lock_repo(path: impl AsRef<Path>) -> 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<String> { 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<PathBuf> { let (owner, name) = parse_repo_url(url)?; Ok(REPOS_DIR.join(&owner).join(&name))} pub async fn ensure_repo_cloned(repo_url: &str) -> Result<PathBuf> { 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<String> { 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}
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<HashMap<PathBuf, Arc<Mutex<()>>>> = RefCell::new(HashMap::default());
}
#[must_use]
pub async fn lock_repo(path: impl AsRef<Path>) -> 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<String> {
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<PathBuf> {
let (owner, name) = parse_repo_url(url)?;
Ok(REPOS_DIR.join(&owner).join(&name))
}
pub async fn ensure_repo_cloned(repo_url: &str) -> Result<PathBuf> {
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<String> {
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
}