Skip to main content

rtb_vcs/git/
push.rs

1//! `Repo::push` — push refs to a remote.
2//!
3//! v0.5 commit 7. Anonymous and authenticated pushes share a single
4//! shell-out path mirroring [`super::fetch`]: the auth case adds
5//! `-c credential.helper=...` and an env var; the anonymous case
6//! omits both.
7//!
8//! Spec A4 (the `git2-fallback` opt-in) is obsolete now that all
9//! v0.5 write paths shell out to `git`. The `RepoError::PushUnsupported`
10//! variant stays for backwards compatibility (the enum is
11//! `#[non_exhaustive]`) but is no longer produced.
12
13use std::path::Path;
14use std::process::Command;
15
16use rtb_credentials::CredentialRef;
17
18use super::{auth, Repo, RepoError};
19
20/// Options for [`Repo::push`].
21#[derive(Debug, Default, Clone)]
22#[non_exhaustive]
23pub struct PushOptions {
24    /// Credential reference used to authenticate the push (HTTPS
25    /// only). `None` for anonymous pushes.
26    pub credential: Option<CredentialRef>,
27}
28
29impl PushOptions {
30    /// Attach a credential for HTTPS authentication. The reference
31    /// is resolved at push time via `rtb-credentials`.
32    #[must_use]
33    pub fn with_credential(mut self, cref: CredentialRef) -> Self {
34        self.credential = Some(cref);
35        self
36    }
37}
38
39impl Repo {
40    /// Push `refspec` to `remote`.
41    ///
42    /// # Errors
43    ///
44    /// - [`RepoError::PushFailed`] — `git push` returned non-zero
45    ///   (unknown remote, network failure, non-fast-forward).
46    /// - [`RepoError::Auth`] — `opts.credential` could not be
47    ///   resolved.
48    pub async fn push(
49        &self,
50        remote: &str,
51        refspec: &str,
52        opts: PushOptions,
53    ) -> Result<(), RepoError> {
54        let workdir = self.path().to_path_buf();
55        let remote = remote.to_string();
56        let refspec = refspec.to_string();
57
58        let token = match opts.credential.as_ref() {
59            Some(cref) => Some(auth::resolve_default(cref).await?),
60            None => None,
61        };
62
63        tokio::task::spawn_blocking(move || {
64            let token_str = token.as_ref().map(auth::expose);
65            run_push(&workdir, &remote, &refspec, token_str.as_deref())
66        })
67        .await
68        .map_err(|join| RepoError::PushFailed {
69            remote: String::new(),
70            refspec: String::new(),
71            cause: format!("spawn_blocking join: {join}"),
72        })?
73    }
74}
75
76fn run_push(
77    workdir: &Path,
78    remote: &str,
79    refspec: &str,
80    token: Option<&str>,
81) -> Result<(), RepoError> {
82    let mut cmd = Command::new("git");
83    if token.is_some() {
84        cmd.arg("-c").arg(format!("credential.helper={}", auth::CREDENTIAL_HELPER));
85    }
86    cmd.arg("push").arg(remote).arg(refspec).current_dir(workdir).env("GIT_TERMINAL_PROMPT", "0");
87    if let Some(t) = token {
88        cmd.env(auth::TOKEN_ENV, t);
89    }
90    let out = cmd.output().map_err(|e| RepoError::PushFailed {
91        remote: remote.to_string(),
92        refspec: refspec.to_string(),
93        cause: format!("spawn `git push`: {e}"),
94    })?;
95    if !out.status.success() {
96        return Err(RepoError::PushFailed {
97            remote: remote.to_string(),
98            refspec: refspec.to_string(),
99            cause: format!("git push: {}", String::from_utf8_lossy(&out.stderr).trim()),
100        });
101    }
102    Ok(())
103}