rtb-vcs 0.5.2

Rust Tool Base — release-provider abstractions and backends (GitHub, GitLab, Bitbucket, Gitea, Codeberg, Direct).
Documentation
//! `Repo::fetch` — pull refs and objects from a remote.
//!
//! v0.5 commits 5 + 5b. Anonymous fetch (no credential on
//! `FetchOptions`) and authenticated fetch share a single shell-out
//! path: the auth case adds `-c credential.helper=...` and an env
//! var; the anonymous case omits both.

use std::path::Path;
use std::process::Command;

use rtb_credentials::CredentialRef;

use super::{auth, Repo, RepoError};

/// Options for [`Repo::fetch`].
///
/// `#[non_exhaustive]` — construct via [`FetchOptions::default()`]
/// and the builder methods.
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct FetchOptions {
    /// Credential reference used to authenticate the fetch (HTTPS
    /// only). `None` for anonymous fetches.
    pub credential: Option<CredentialRef>,
}

impl FetchOptions {
    /// Attach a credential for HTTPS authentication. The reference
    /// is resolved at fetch time via `rtb-credentials`.
    #[must_use]
    pub fn with_credential(mut self, cref: CredentialRef) -> Self {
        self.credential = Some(cref);
        self
    }
}

impl Repo {
    /// Fetch from `remote`, updating remote-tracking refs but not
    /// touching the working tree or local branches.
    ///
    /// # Errors
    ///
    /// - [`RepoError::FetchFailed`] — `git fetch` returned non-zero.
    /// - [`RepoError::Auth`] — `opts.credential` could not be
    ///   resolved.
    pub async fn fetch(&self, remote: &str, opts: FetchOptions) -> Result<(), RepoError> {
        let workdir = self.path().to_path_buf();
        let remote = remote.to_string();

        let token = match opts.credential.as_ref() {
            Some(cref) => Some(auth::resolve_default(cref).await?),
            None => None,
        };

        tokio::task::spawn_blocking(move || {
            let token_str = token.as_ref().map(auth::expose);
            run_fetch(&workdir, &remote, token_str.as_deref())
        })
        .await
        .map_err(|join| RepoError::FetchFailed {
            remote: String::new(),
            cause: format!("spawn_blocking join: {join}"),
        })?
    }
}

fn run_fetch(workdir: &Path, remote: &str, token: Option<&str>) -> Result<(), RepoError> {
    let mut cmd = Command::new("git");
    if token.is_some() {
        cmd.arg("-c").arg(format!("credential.helper={}", auth::CREDENTIAL_HELPER));
    }
    cmd.arg("fetch").arg(remote).current_dir(workdir).env("GIT_TERMINAL_PROMPT", "0");
    if let Some(t) = token {
        cmd.env(auth::TOKEN_ENV, t);
    }
    let out = cmd.output().map_err(|e| RepoError::FetchFailed {
        remote: remote.to_string(),
        cause: format!("spawn `git fetch`: {e}"),
    })?;
    if !out.status.success() {
        return Err(RepoError::FetchFailed {
            remote: remote.to_string(),
            cause: format!("git fetch: {}", String::from_utf8_lossy(&out.stderr).trim()),
        });
    }
    Ok(())
}