use crate::GitReference;
use snarkvm::algorithms::crypto_hash::sha256;
use leo_errors::Result;
use std::{
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
};
const CHECKOUTS_SUBDIR: &str = "git/checkouts";
const TMP_SUBDIR: &str = "git/tmp";
pub(crate) fn checkouts_root(home: &Path) -> PathBuf {
home.join(CHECKOUTS_SUBDIR)
}
pub(crate) fn checkout_dir(home: &Path, url: &str, commit: &str) -> PathBuf {
let digest = sha256(url.as_bytes());
let url_hash: String = digest.iter().take(8).map(|b| format!("{b:02x}")).collect();
let repo: String = url
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or(url)
.trim_end_matches(".git")
.chars()
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'))
.take(32)
.collect();
let repo = if repo.is_empty() { "repo".to_string() } else { repo };
checkouts_root(home).join(format!("{repo}-{url_hash}")).join(commit)
}
pub fn resolve(
home: &Path,
name: &str,
url: &str,
reference: &GitReference,
locked_commit: Option<&str>,
offline: bool,
) -> Result<(PathBuf, String)> {
if let Some(commit) = locked_commit
&& (offline || !reference.is_mutable())
{
let dir = checkout_dir(home, url, commit);
if dir.is_dir() {
return Ok((dir, commit.to_string()));
}
}
if offline {
return Err(crate::errors::git_offline_unavailable(name, url).into());
}
let tmp = unique_dir(&home.join(TMP_SUBDIR), "clone");
let result = clone_resolve_checkout(home, name, url, reference, locked_commit, &tmp);
let _ = std::fs::remove_dir_all(&tmp);
result
}
fn clone_resolve_checkout(
home: &Path,
name: &str,
url: &str,
reference: &GitReference,
locked_commit: Option<&str>,
tmp: &Path,
) -> Result<(PathBuf, String)> {
let _ = std::fs::remove_dir_all(tmp);
if let Some(parent) = tmp.parent() {
std::fs::create_dir_all(parent).map_err(|e| crate::errors::git_error(name, url, e))?;
}
let parsed = gix::url::parse(url.into()).map_err(|e| crate::errors::git_error(name, url, e))?;
let mut prepare = gix::prepare_clone(parsed, tmp)
.map_err(|e| crate::errors::git_error(name, url, e))?
.configure_connection(|connection| {
#[allow(clippy::result_large_err)] connection.set_credentials(|_action| Ok(None));
Ok(())
});
let (checkout, _) = prepare
.fetch_then_checkout(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
.map_err(|e| crate::errors::git_error(name, url, e))?;
let repo = checkout.persist();
let revspec = match (locked_commit, reference) {
(Some(commit), reference) if !reference.is_mutable() => commit.to_string(),
(_, GitReference::DefaultBranch) => "HEAD".to_string(),
(_, GitReference::Branch(branch)) => format!("origin/{branch}"),
(_, GitReference::Tag(tag)) => format!("refs/tags/{tag}"),
(_, GitReference::Rev(rev)) => rev.clone(),
};
let id = repo
.rev_parse_single(revspec.as_str())
.map_err(|e| crate::errors::git_reference_error(name, url, &revspec, e))?;
let commit = id.detach().to_string();
let dir = checkout_dir(home, url, &commit);
if !dir.is_dir() {
let staging = staging_dir(&dir);
let renamed =
checkout_tree(&repo, id, &staging).and_then(|()| std::fs::rename(&staging, &dir).map_err(Into::into));
let _ = std::fs::remove_dir_all(&staging);
if let Err(e) = renamed
&& !dir.is_dir()
{
return Err(crate::errors::git_error(name, url, e).into());
}
}
Ok((dir, commit))
}
fn checkout_tree(
repo: &gix::Repository,
id: gix::Id<'_>,
dir: &Path,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
let tree = id.object()?.peel_to_tree()?;
let mut index = repo.index_from_tree(&tree.id())?;
let mut opts = repo.checkout_options(gix::worktree::stack::state::attributes::Source::IdMapping)?;
opts.destination_is_initially_empty = true;
let _ = std::fs::remove_dir_all(dir);
std::fs::create_dir_all(dir)?;
gix::worktree::state::checkout(
&mut index,
dir,
repo.objects.clone().into_arc()?,
&gix::progress::Discard,
&gix::progress::Discard,
&gix::interrupt::IS_INTERRUPTED,
opts,
)?;
Ok(())
}
fn unique_dir(parent: &Path, prefix: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
parent.join(format!("{prefix}-{}-{seq}", std::process::id()))
}
fn staging_dir(final_dir: &Path) -> PathBuf {
unique_dir(final_dir.parent().unwrap_or(final_dir), ".staging")
}