Skip to main content

rtb_forge/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`.
10
11use std::path::Path;
12use std::process::Command;
13
14use rtb_credentials::CredentialRef;
15
16use super::{auth, Repo, RepoError};
17
18/// Options for [`Repo::push`].
19#[derive(Debug, Default, Clone)]
20#[non_exhaustive]
21pub struct PushOptions {
22    /// Credential reference used to authenticate the push (HTTPS
23    /// only). `None` for anonymous pushes.
24    pub credential: Option<CredentialRef>,
25}
26
27impl PushOptions {
28    /// Attach a credential for HTTPS authentication. The reference
29    /// is resolved at push 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    /// Push `refspec` to `remote`.
39    ///
40    /// # Errors
41    ///
42    /// - [`RepoError::PushFailed`] — `git push` returned non-zero
43    ///   (unknown remote, network failure, non-fast-forward).
44    /// - [`RepoError::Auth`] — `opts.credential` could not be
45    ///   resolved.
46    pub async fn push(
47        &self,
48        remote: &str,
49        refspec: &str,
50        opts: PushOptions,
51    ) -> Result<(), RepoError> {
52        let workdir = self.path().to_path_buf();
53        let remote = remote.to_string();
54        let refspec = refspec.to_string();
55
56        let token = match opts.credential.as_ref() {
57            Some(cref) => Some(auth::resolve_default(cref).await?),
58            None => None,
59        };
60
61        tokio::task::spawn_blocking(move || {
62            let token_str = token.as_ref().map(auth::expose);
63            run_push(&workdir, &remote, &refspec, token_str.as_deref())
64        })
65        .await
66        .map_err(|join| RepoError::PushFailed {
67            remote: String::new(),
68            refspec: String::new(),
69            cause: format!("spawn_blocking join: {join}"),
70        })?
71    }
72}
73
74fn run_push(
75    workdir: &Path,
76    remote: &str,
77    refspec: &str,
78    token: Option<&str>,
79) -> Result<(), RepoError> {
80    let mut cmd = Command::new("git");
81    if token.is_some() {
82        cmd.arg("-c").arg(format!("credential.helper={}", auth::CREDENTIAL_HELPER));
83    }
84    cmd.arg("push").arg(remote).arg(refspec).current_dir(workdir).env("GIT_TERMINAL_PROMPT", "0");
85    if let Some(t) = token {
86        cmd.env(auth::TOKEN_ENV, t);
87    }
88    let out = cmd.output().map_err(|e| RepoError::PushFailed {
89        remote: remote.to_string(),
90        refspec: refspec.to_string(),
91        cause: format!("spawn `git push`: {e}"),
92    })?;
93    if !out.status.success() {
94        return Err(RepoError::PushFailed {
95            remote: remote.to_string(),
96            refspec: refspec.to_string(),
97            cause: format!("git push: {}", String::from_utf8_lossy(&out.stderr).trim()),
98        });
99    }
100    Ok(())
101}