use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::AtomicBool;
use rtb_credentials::CredentialRef;
use super::{auth, Repo, RepoError};
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct CloneOptions {
pub credential: Option<CredentialRef>,
}
impl CloneOptions {
#[must_use]
pub fn with_credential(mut self, cref: CredentialRef) -> Self {
self.credential = Some(cref);
self
}
}
impl Repo {
pub async fn clone(
url: &str,
dst: impl AsRef<Path>,
opts: CloneOptions,
) -> Result<Self, RepoError> {
let url = url.to_string();
let dst: PathBuf = dst.as_ref().to_path_buf();
let token = match opts.credential.as_ref() {
Some(cref) => Some(auth::resolve_default(cref).await?),
None => None,
};
let dst_for_task = dst.clone();
let url_for_task = url.clone();
tokio::task::spawn_blocking(move || {
token.map_or_else(
|| run_clone_anonymous(&url_for_task, &dst_for_task),
|secret| run_clone_with_auth(&url_for_task, &dst_for_task, &auth::expose(&secret)),
)
})
.await
.map_err(|join| RepoError::CloneFailed {
url: url.clone(),
cause: format!("spawn_blocking join: {join}"),
})?
}
}
fn run_clone_anonymous(url: &str, dst: &Path) -> Result<Repo, RepoError> {
let mut prepare = gix::prepare_clone(url, dst).map_err(|e| RepoError::CloneFailed {
url: url.to_string(),
cause: format!("prepare: {e}"),
})?;
let interrupt = AtomicBool::new(false);
let (mut prepare_checkout, _fetch_outcome) =
prepare.fetch_then_checkout(gix::progress::Discard, &interrupt).map_err(|e| {
RepoError::CloneFailed { url: url.to_string(), cause: format!("fetch: {e}") }
})?;
let (repository, _checkout_outcome) =
prepare_checkout.main_worktree(gix::progress::Discard, &interrupt).map_err(|e| {
RepoError::CloneFailed { url: url.to_string(), cause: format!("checkout: {e}") }
})?;
Ok(Repo::from_thread_safe(repository.into_sync(), dst.to_path_buf()))
}
fn run_clone_with_auth(url: &str, dst: &Path, token: &str) -> Result<Repo, RepoError> {
let out = Command::new("git")
.arg("-c")
.arg(format!("credential.helper={}", auth::CREDENTIAL_HELPER))
.arg("clone")
.arg("--")
.arg(url)
.arg(dst)
.env(auth::TOKEN_ENV, token)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.map_err(|e| RepoError::CloneFailed {
url: url.to_string(),
cause: format!("spawn `git clone`: {e}"),
})?;
if !out.status.success() {
return Err(RepoError::CloneFailed {
url: url.to_string(),
cause: format!("git clone: {}", String::from_utf8_lossy(&out.stderr).trim()),
});
}
let repo = gix::open(dst).map_err(|e| RepoError::CloneFailed {
url: url.to_string(),
cause: format!("open post-clone: {e}"),
})?;
Ok(Repo::from_thread_safe(repo.into_sync(), dst.to_path_buf()))
}