use std::{
borrow::Cow,
hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf},
sync::Arc,
};
use reqwest_middleware::ClientWithMiddleware;
use tracing::instrument;
use crate::{
credentials::GIT_STORE,
git::{CheckoutOptions, GitRemote},
resolver::RepositoryReference,
sha::{GitOid, GitSha},
url::RepositoryUrl,
GitError, GitUrl, Reporter,
};
pub struct GitSource {
git: GitUrl,
client: ClientWithMiddleware,
cache: PathBuf,
reporter: Option<Arc<dyn Reporter>>,
checkout_options: CheckoutOptions,
}
impl GitSource {
pub fn new(
git: GitUrl,
client: impl Into<ClientWithMiddleware>,
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
}
}
#[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 (db, actual_rev, task) = match (self.git.precise, existing_db) {
(Some(rev), Some(db)) if db.contains(rev.into()) => {
tracing::debug!(
"Using existing Git source `{}` pointed at `{}`",
self.git.repository,
rev
);
(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,
)?;
(db, GitSha::from(actual_rev), task)
}
};
let short_id = db.to_short_id(actual_rev.into())?;
let checkout_path = self
.cache
.join("checkouts")
.join(&ident)
.join(short_id.as_str());
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,
})
}
}
#[derive(Debug, Clone)]
pub struct Fetch {
repository: RepositoryReference,
commit: GitSha,
path: PathBuf,
}
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 cache_digest(url: &RepositoryUrl) -> String {
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
let hash = hasher.finish();
format!("{hash:x}")
}