1use std::path::{Path, PathBuf};
16use std::process::Command;
17use std::sync::atomic::AtomicBool;
18
19use rtb_credentials::CredentialRef;
20
21use super::{auth, Repo, RepoError};
22
23#[derive(Debug, Default, Clone)]
28#[non_exhaustive]
29pub struct CloneOptions {
30 pub credential: Option<CredentialRef>,
34}
35
36impl CloneOptions {
37 #[must_use]
41 pub fn with_credential(mut self, cref: CredentialRef) -> Self {
42 self.credential = Some(cref);
43 self
44 }
45}
46
47impl Repo {
48 pub async fn clone(
63 url: &str,
64 dst: impl AsRef<Path>,
65 opts: CloneOptions,
66 ) -> Result<Self, RepoError> {
67 let url = url.to_string();
68 let dst: PathBuf = dst.as_ref().to_path_buf();
69
70 let token = match opts.credential.as_ref() {
71 Some(cref) => Some(auth::resolve_default(cref).await?),
72 None => None,
73 };
74
75 let dst_for_task = dst.clone();
76 let url_for_task = url.clone();
77 tokio::task::spawn_blocking(move || {
78 token.map_or_else(
79 || run_clone_anonymous(&url_for_task, &dst_for_task),
80 |secret| run_clone_with_auth(&url_for_task, &dst_for_task, &auth::expose(&secret)),
81 )
82 })
83 .await
84 .map_err(|join| RepoError::CloneFailed {
85 url: url.clone(),
86 cause: format!("spawn_blocking join: {join}"),
87 })?
88 }
89}
90
91fn run_clone_anonymous(url: &str, dst: &Path) -> Result<Repo, RepoError> {
92 let mut prepare = gix::prepare_clone(url, dst).map_err(|e| RepoError::CloneFailed {
93 url: url.to_string(),
94 cause: format!("prepare: {e}"),
95 })?;
96
97 let interrupt = AtomicBool::new(false);
98 let (mut prepare_checkout, _fetch_outcome) =
99 prepare.fetch_then_checkout(gix::progress::Discard, &interrupt).map_err(|e| {
100 RepoError::CloneFailed { url: url.to_string(), cause: format!("fetch: {e}") }
101 })?;
102
103 let (repository, _checkout_outcome) =
104 prepare_checkout.main_worktree(gix::progress::Discard, &interrupt).map_err(|e| {
105 RepoError::CloneFailed { url: url.to_string(), cause: format!("checkout: {e}") }
106 })?;
107
108 Ok(Repo::from_thread_safe(repository.into_sync(), dst.to_path_buf()))
109}
110
111fn run_clone_with_auth(url: &str, dst: &Path, token: &str) -> Result<Repo, RepoError> {
112 let out = Command::new("git")
113 .arg("-c")
114 .arg(format!("credential.helper={}", auth::CREDENTIAL_HELPER))
115 .arg("clone")
116 .arg("--")
117 .arg(url)
118 .arg(dst)
119 .env(auth::TOKEN_ENV, token)
120 .env("GIT_TERMINAL_PROMPT", "0")
123 .output()
124 .map_err(|e| RepoError::CloneFailed {
125 url: url.to_string(),
126 cause: format!("spawn `git clone`: {e}"),
127 })?;
128 if !out.status.success() {
129 return Err(RepoError::CloneFailed {
130 url: url.to_string(),
131 cause: format!("git clone: {}", String::from_utf8_lossy(&out.stderr).trim()),
132 });
133 }
134 let repo = gix::open(dst).map_err(|e| RepoError::CloneFailed {
136 url: url.to_string(),
137 cause: format!("open post-clone: {e}"),
138 })?;
139 Ok(Repo::from_thread_safe(repo.into_sync(), dst.to_path_buf()))
140}