use std::{
borrow::Cow,
hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf},
sync::Arc,
};
use crate::LazyClient;
use tracing::instrument;
use crate::{
GitError, GitUrl, Reporter,
credentials::GIT_STORE,
git::{CheckoutOptions, GitRemote, LfsFilter},
resolver::RepositoryReference,
sha::{GitOid, GitSha},
url::RepositoryUrl,
};
pub fn lfs_enabled_from_env(var_name: &str) -> Option<bool> {
let raw = std::env::var(var_name).ok()?;
let value = raw.trim();
if value.is_empty() {
return None;
}
if value == "0"
|| value.eq_ignore_ascii_case("false")
|| value.eq_ignore_ascii_case("no")
|| value.eq_ignore_ascii_case("off")
{
return Some(false);
}
if value == "1"
|| value.eq_ignore_ascii_case("true")
|| value.eq_ignore_ascii_case("yes")
|| value.eq_ignore_ascii_case("on")
{
return Some(true);
}
tracing::warn!("unrecognised value for {var_name}: {raw:?}; treating as enabled");
Some(true)
}
pub struct GitSource {
git: GitUrl,
client: LazyClient,
cache: PathBuf,
reporter: Option<Arc<dyn Reporter>>,
checkout_options: CheckoutOptions,
}
impl GitSource {
pub fn new(git: GitUrl, client: impl Into<LazyClient>, cache: impl Into<PathBuf>) -> Self {
Self {
git,
client: client.into(),
cache: cache.into(),
reporter: None,
checkout_options: CheckoutOptions::default(),
}
}
#[must_use]
pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
Self {
reporter: Some(reporter),
..self
}
}
#[must_use]
pub fn with_checkout_options(self, options: CheckoutOptions) -> Self {
Self {
checkout_options: options,
..self
}
}
#[must_use]
pub fn with_lfs(mut self, lfs: Option<bool>) -> Self {
self.checkout_options.lfs = lfs;
self
}
#[must_use]
pub fn with_lfs_filter(mut self, filter: LfsFilter) -> Self {
self.checkout_options.lfs_filter = filter;
self
}
#[instrument(skip(self), fields(repository = %self.git.repository, rev = self.git.precise.map(tracing::field::display)))]
pub fn fetch(self) -> Result<Fetch, GitError> {
let canonical = RepositoryUrl::new(&self.git.repository);
let ident = cache_digest(&canonical);
let db_path = self.cache.join("db").join(&ident);
let remote = if let Some(credentials) = GIT_STORE.get(&canonical) {
Cow::Owned(credentials.apply(self.git.repository.clone()))
} else {
Cow::Borrowed(&self.git.repository)
};
let remote = GitRemote::new(&remote);
let existing_db = match remote.db_at(&db_path) {
Ok(db) => Some(db),
Err(GitError::InvalidRepository(path)) => {
tracing::warn!(
"Detected corrupted git cache at {} (not a valid git repository), removing and re-cloning",
path.display()
);
None
}
Err(_) => None,
};
let lfs_requested = self.checkout_options.lfs == Some(true);
let (db, actual_rev, task) = match (self.git.precise, existing_db) {
(Some(rev), Some(db))
if db.contains(rev.into())
&& (!lfs_requested
|| db.contains_lfs_artifacts(
rev.into(),
&self.checkout_options.lfs_filter,
)) =>
{
tracing::debug!(
"Using existing Git source `{}` pointed at `{}`",
self.git.repository,
rev
);
let db = db.with_lfs_ready(lfs_requested.then_some(true));
(db, rev, None)
}
(locked_rev, db) => {
tracing::debug!("Updating Git source `{}`", self.git.repository);
let task = self.reporter.as_ref().map(|reporter| {
reporter.on_checkout_start(remote.url(), self.git.reference.as_rev())
});
let (db, actual_rev) = remote.checkout(
&db_path,
db,
&self.git.reference,
locked_rev.map(GitOid::from),
&self.client,
&self.checkout_options,
)?;
(db, GitSha::from(actual_rev), task)
}
};
let short_id = db.to_short_id(actual_rev.into())?;
let checkout_name = if lfs_requested && !self.checkout_options.lfs_filter.is_empty() {
let mut hasher = DefaultHasher::new();
self.checkout_options.lfs_filter.hash(&mut hasher);
format!("{short_id}-lfs-{:x}", hasher.finish())
} else if lfs_requested {
format!("{short_id}-lfs")
} else {
short_id.clone()
};
let checkout_path = self
.cache
.join("checkouts")
.join(&ident)
.join(checkout_name);
tracing::debug!(
"Copying git revision `{}` to path `{}`",
actual_rev,
checkout_path.display()
);
db.copy_to(
actual_rev.into(),
&checkout_path,
&self.git.repository,
&self.checkout_options,
)?;
if let (Some(task), Some(reporter)) = (task, self.reporter.as_ref()) {
reporter.on_checkout_complete(remote.url(), short_id.as_str(), task);
}
tracing::trace!("Finished fetching Git source `{}`", self.git.repository);
Ok(Fetch {
repository: RepositoryReference {
url: canonical,
reference: self.git.reference.clone(),
},
commit: actual_rev,
path: checkout_path,
lfs_ready: db.lfs_ready() == Some(true),
})
}
}
#[derive(Debug, Clone)]
pub struct Fetch {
repository: RepositoryReference,
commit: GitSha,
path: PathBuf,
lfs_ready: bool,
}
impl Fetch {
pub fn repository(&self) -> &RepositoryReference {
&self.repository
}
pub fn commit(&self) -> GitSha {
self.commit
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn into_path(self) -> PathBuf {
self.path
}
pub fn lfs_ready(&self) -> bool {
self.lfs_ready
}
}
pub fn cache_digest(url: &RepositoryUrl) -> String {
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
let hash = hasher.finish();
format!("{hash:x}")
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_LFS_ENV: &str = "RATTLER_GIT_TEST_LFS";
fn with_env<R>(value: Option<&str>, body: impl FnOnce() -> R) -> R {
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
let _g = LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = std::env::var(TEST_LFS_ENV).ok();
match value {
Some(v) => unsafe { std::env::set_var(TEST_LFS_ENV, v) },
None => unsafe { std::env::remove_var(TEST_LFS_ENV) },
}
let out = body();
match previous {
Some(v) => unsafe { std::env::set_var(TEST_LFS_ENV, v) },
None => unsafe { std::env::remove_var(TEST_LFS_ENV) },
}
out
}
#[test]
fn env_unset_is_none() {
with_env(None, || {
assert_eq!(lfs_enabled_from_env(TEST_LFS_ENV), None);
});
}
#[test]
fn env_empty_is_none() {
with_env(Some(""), || {
assert_eq!(lfs_enabled_from_env(TEST_LFS_ENV), None);
});
with_env(Some(" "), || {
assert_eq!(lfs_enabled_from_env(TEST_LFS_ENV), None);
});
}
#[test]
fn env_truthy_is_some_true() {
for v in ["1", "true", "TRUE", "yes", "YES", "on", "ON"] {
with_env(Some(v), || {
assert_eq!(lfs_enabled_from_env(TEST_LFS_ENV), Some(true), "value={v}");
});
}
}
#[test]
fn env_falsy_is_some_false() {
for v in ["0", "false", "FALSE", "no", "NO", "off", "OFF"] {
with_env(Some(v), || {
assert_eq!(lfs_enabled_from_env(TEST_LFS_ENV), Some(false), "value={v}");
});
}
}
}