use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Result;
use tracing::{debug, instrument};
use uv_cache_key::cache_digest;
use uv_git_types::{GitOid, GitReference, GitUrl};
use uv_redacted::DisplaySafeUrl;
use crate::GIT_STORE;
use crate::git::{GitDatabase, GitRemote};
pub struct GitSource {
git: GitUrl,
disable_ssl: bool,
offline: bool,
cache: PathBuf,
reporter: Option<Arc<dyn Reporter>>,
}
impl GitSource {
pub fn new(git: GitUrl, cache: impl Into<PathBuf>, offline: bool) -> Self {
Self {
git,
disable_ssl: false,
offline,
cache: cache.into(),
reporter: None,
}
}
#[must_use]
pub fn dangerous(self) -> Self {
Self {
disable_ssl: true,
..self
}
}
#[must_use]
pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
Self {
reporter: Some(reporter),
..self
}
}
#[instrument(skip(self), fields(repository = %self.git.url(), rev = ?self.git.precise()))]
pub fn fetch(self) -> Result<Fetch> {
let lfs_requested = self.git.lfs().enabled();
let ident = cache_digest(self.git.repository());
let db_path = self.cache.join("db").join(&ident);
let remote = if let Some(credentials) = GIT_STORE.get(self.git.repository()) {
Cow::Owned(credentials.apply(self.git.url().clone()))
} else {
Cow::Borrowed(self.git.url())
};
let (db, actual_rev, maybe_task) = || -> Result<(GitDatabase, GitOid, Option<usize>)> {
let git_remote = GitRemote::new(&remote);
let maybe_db = git_remote.db_at(&db_path).ok();
if let (Some(rev), Some(db)) = (self.git.precise(), &maybe_db) {
if db.contains(rev) && (!lfs_requested || db.contains_lfs_artifacts(rev)) {
debug!("Using existing Git source `{}`", self.git.url());
return Ok((
maybe_db
.unwrap()
.with_lfs_ready(lfs_requested.then_some(true)),
rev,
None,
));
}
}
if let Some(db) = &maybe_db {
if let GitReference::BranchOrTagOrCommit(maybe_commit) = self.git.reference() {
if let Ok(oid) = maybe_commit.parse::<GitOid>() {
if db.contains(oid) && (!lfs_requested || db.contains_lfs_artifacts(oid)) {
debug!("Using existing Git source `{}`", self.git.url());
return Ok((
maybe_db
.unwrap()
.with_lfs_ready(lfs_requested.then_some(true)),
oid,
None,
));
}
}
}
}
debug!("Updating Git source `{}`", self.git.url());
let task = self.reporter.as_ref().map(|reporter| {
reporter.on_checkout_start(git_remote.url(), self.git.reference().as_rev())
});
let (db, actual_rev) = git_remote.checkout(
&db_path,
maybe_db,
self.git.reference(),
self.git.precise(),
self.disable_ssl,
self.offline,
lfs_requested,
)?;
Ok((db, actual_rev, task))
}()?;
let short_id = db.to_short_id(actual_rev)?;
let canonical = self.git.repository().clone().with_lfs(Some(lfs_requested));
let ident = if lfs_requested {
cache_digest(&canonical)
} else {
ident
};
let checkout_path = self
.cache
.join("checkouts")
.join(&ident)
.join(short_id.as_str());
let checkout = db.copy_to(actual_rev, &checkout_path)?;
if let Some(task) = maybe_task {
if let Some(reporter) = self.reporter.as_ref() {
reporter.on_checkout_complete(remote.as_ref(), actual_rev.as_str(), task);
}
}
Ok(Fetch {
git: self.git.with_precise(actual_rev),
path: checkout_path,
lfs_ready: checkout.lfs_ready().unwrap_or(false),
})
}
}
pub struct Fetch {
git: GitUrl,
path: PathBuf,
lfs_ready: bool,
}
impl Fetch {
pub fn git(&self) -> &GitUrl {
&self.git
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn lfs_ready(&self) -> &bool {
&self.lfs_ready
}
pub fn into_git(self) -> GitUrl {
self.git
}
pub fn into_path(self) -> PathBuf {
self.path
}
}
pub trait Reporter: Send + Sync {
fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize);
}