1use std::path::Path;
12use std::process::Command;
13
14use rtb_credentials::CredentialRef;
15
16use super::{auth, Repo, RepoError};
17
18#[derive(Debug, Default, Clone)]
20#[non_exhaustive]
21pub struct PushOptions {
22 pub credential: Option<CredentialRef>,
25}
26
27impl PushOptions {
28 #[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 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}