workerlib/
execute.rs

1//!
2//! Execute internal commands ([tokio::process::Command]) (e.g. `git reset --hard`)
3//!
4
5use std::path::Path;
6pub use tokio::process::Command;
7
8use crate::redact;
9
10/// Error encountered when shelling out to the git binary for some reason
11#[derive(thiserror::Error, Debug)]
12pub enum Error {
13    /// Program failed to open a subprocess.
14    #[error("Unable to run subcommand {command}")]
15    IO {
16        /// Command for subprocess
17        command: String,
18        /// Error from building subprocess
19        source: std::io::Error,
20    },
21
22    /// The subprocess ran to completion but returned a failure code.
23    #[error("Command {command:?} failed with STDERR: {stderr}")]
24    CommandError {
25        /// Executed subprocess
26        command: String,
27        /// Exit status
28        status: std::process::ExitStatus,
29        /// Stderr from executed command
30        stderr: String,
31        /// Stdout from executed command
32        stdout: String,
33    },
34}
35
36impl Error {
37    /// Return the underlying process status code if applicable
38    pub fn status(&self) -> Option<i32> {
39        if let Error::CommandError {
40            command: _,
41            status,
42            stderr: _,
43            stdout: _,
44        } = self
45        {
46            status.code()
47        } else {
48            None
49        }
50    }
51}
52
53/// Wrapper trait to generate commands for Lorry
54pub trait CommandBuilder {
55    /// Build the command for git processes and execute from the current
56    /// directory
57    fn build(&self, current_dir: &Path) -> Command;
58}
59
60#[tracing::instrument(name = "execute", skip_all)]
61/// Build the command and execute it returning its stdout/stderr or an error
62pub async fn execute<T>(builder: &T, current_dir: &Path) -> Result<(String, String), Error>
63where
64    T: CommandBuilder,
65{
66    let mut cmd = builder.build(current_dir);
67    cmd.stdin(std::process::Stdio::null());
68    let child_process_output = cmd.output().await.map_err(|e| Error::IO {
69        command: format!("{cmd:?}"),
70        source: e,
71    })?;
72    let succeeded = child_process_output.status.success();
73    let out = String::from_utf8_lossy(&child_process_output.stdout);
74    let err = String::from_utf8_lossy(&child_process_output.stderr);
75    tracing::debug!(
76        "Command:{:?} \n Exit code:{:?} \n Stdout: {}, Stderr: {} \n",
77        cmd,
78        child_process_output.status.code(),
79        out,
80        err,
81    );
82    if !succeeded {
83        tracing::debug!(
84            "Failed to run {:?}, Status was: {:?}, Stdout was:\n{:?},Stderr was:\n{:?}",
85            cmd,
86            child_process_output.status.code(),
87            out,
88            err
89        );
90        Err(Error::CommandError {
91            status: child_process_output.status,
92            stdout: redact::redact(&out.to_string()),
93            stderr: redact::redact(&err.to_string()),
94            command: redact::redact(&format!("{cmd:?}")),
95        })
96    } else {
97        Ok((out.to_string(), err.to_string()))
98    }
99}