When a revision like `abcd~1` wasn't found locally, the function would fetch and then return `FETCH_HEAD`, which points to the tip of the fetched branch rather than the requested revision expression. Now it re-resolves the original revision after fetching, correctly handling tilde expressions and other git revision syntax. Release Notes: - N/A
115 lines
3.4 KiB
Rust
115 lines
3.4 KiB
Rust
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;
|
|
|
|
if !repo_path.is_dir() {
|
|
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?;
|
|
}
|
|
|
|
// Always fetch to get latest commits
|
|
run_git(&repo_path, &["fetch", "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
|
|
}
|