Skip to main content

rtb_vcs/git/
fetch.rs

1//! `Repo::fetch` — pull refs and objects from a remote.
2//!
3//! v0.5 commits 5 + 5b. Anonymous fetch (no credential on
4//! `FetchOptions`) and authenticated fetch share a single shell-out
5//! path: the auth case adds `-c credential.helper=...` and an env
6//! var; the anonymous case omits both.
7
8use std::path::Path;
9use std::process::Command;
10
11use rtb_credentials::CredentialRef;
12
13use super::{auth, Repo, RepoError};
14
15/// Options for [`Repo::fetch`].
16///
17/// `#[non_exhaustive]` — construct via [`FetchOptions::default()`]
18/// and the builder methods.
19#[derive(Debug, Default, Clone)]
20#[non_exhaustive]
21pub struct FetchOptions {
22    /// Credential reference used to authenticate the fetch (HTTPS
23    /// only). `None` for anonymous fetches.
24    pub credential: Option<CredentialRef>,
25}
26
27impl FetchOptions {
28    /// Attach a credential for HTTPS authentication. The reference
29    /// is resolved at fetch time via `rtb-credentials`.
30    #[must_use]
31    pub fn with_credential(mut self, cref: CredentialRef) -> Self {
32        self.credential = Some(cref);
33        self
34    }
35}
36
37impl Repo {
38    /// Fetch from `remote`, updating remote-tracking refs but not
39    /// touching the working tree or local branches.
40    ///
41    /// # Errors
42    ///
43    /// - [`RepoError::FetchFailed`] — `git fetch` returned non-zero.
44    /// - [`RepoError::Auth`] — `opts.credential` could not be
45    ///   resolved.
46    pub async fn fetch(&self, remote: &str, opts: FetchOptions) -> Result<(), RepoError> {
47        let workdir = self.path().to_path_buf();
48        let remote = remote.to_string();
49
50        let token = match opts.credential.as_ref() {
51            Some(cref) => Some(auth::resolve_default(cref).await?),
52            None => None,
53        };
54
55        tokio::task::spawn_blocking(move || {
56            let token_str = token.as_ref().map(auth::expose);
57            run_fetch(&workdir, &remote, token_str.as_deref())
58        })
59        .await
60        .map_err(|join| RepoError::FetchFailed {
61            remote: String::new(),
62            cause: format!("spawn_blocking join: {join}"),
63        })?
64    }
65}
66
67fn run_fetch(workdir: &Path, remote: &str, token: Option<&str>) -> Result<(), RepoError> {
68    let mut cmd = Command::new("git");
69    if token.is_some() {
70        cmd.arg("-c").arg(format!("credential.helper={}", auth::CREDENTIAL_HELPER));
71    }
72    cmd.arg("fetch").arg(remote).current_dir(workdir).env("GIT_TERMINAL_PROMPT", "0");
73    if let Some(t) = token {
74        cmd.env(auth::TOKEN_ENV, t);
75    }
76    let out = cmd.output().map_err(|e| RepoError::FetchFailed {
77        remote: remote.to_string(),
78        cause: format!("spawn `git fetch`: {e}"),
79    })?;
80    if !out.status.success() {
81        return Err(RepoError::FetchFailed {
82            remote: remote.to_string(),
83            cause: format!("git fetch: {}", String::from_utf8_lossy(&out.stderr).trim()),
84        });
85    }
86    Ok(())
87}