Skip to main content

fluidattacks_core_git/clone/
reason.rs

1//! Running a clone and saying what went wrong when it does.
2
3use anyhow::{Context, Result};
4use tokio::process::Command;
5
6const REDACTED: &str = "[REDACTED]";
7
8/// Run a clone, and fail with what git printed rather than with its exit code.
9///
10/// Anything in `secrets` is taken out of that message.
11pub async fn run(command: &mut Command, transport: &str, secrets: &[&str]) -> Result<()> {
12    let output = command
13        .output()
14        .await
15        .with_context(|| format!("running git clone ({transport})"))?;
16
17    if output.status.success() {
18        return Ok(());
19    }
20
21    let said = String::from_utf8_lossy(&output.stderr);
22    let said = hide(said.trim(), secrets);
23
24    if said.is_empty() {
25        anyhow::bail!(
26            "git clone ({transport}) failed with status {}",
27            output.status
28        );
29    }
30
31    anyhow::bail!("{said}");
32}
33
34// git leaves the credential out of its own messages, but a server answers what
35// it wants and git repeats it under `remote:`.
36#[must_use]
37pub fn hide(said: &str, secrets: &[&str]) -> String {
38    secrets
39        .iter()
40        .filter(|secret| !secret.is_empty())
41        .fold(said.to_owned(), |text, secret| {
42            text.replace(secret, REDACTED)
43        })
44}
45
46#[cfg(test)]
47mod tests {
48    use tokio::process::Command;
49
50    use super::run;
51
52    fn shell(script: &str) -> Command {
53        let mut command = Command::new("sh");
54        command.args(["-c", script]);
55
56        command
57    }
58
59    #[tokio::test]
60    async fn a_clone_that_worked_says_nothing() {
61        assert!(run(&mut shell("exit 0"), "TEST", &[]).await.is_ok());
62    }
63
64    #[tokio::test]
65    async fn a_clone_that_failed_carries_what_it_printed() {
66        let outcome = run(
67            &mut shell("echo 'fatal: no such repository' >&2; exit 128"),
68            "TEST",
69            &[],
70        )
71        .await;
72
73        assert_eq!(
74            outcome.unwrap_err().to_string(),
75            "fatal: no such repository"
76        );
77    }
78
79    #[tokio::test]
80    async fn a_clone_that_printed_nothing_carries_its_status() {
81        let outcome = run(&mut shell("exit 3"), "TEST", &[]).await;
82
83        assert!(outcome.unwrap_err().to_string().contains("(TEST)"));
84    }
85
86    #[tokio::test]
87    async fn a_secret_the_server_answered_is_taken_out() {
88        let outcome = run(
89            &mut shell("echo 'remote: rejected s3cr3t' >&2; exit 128"),
90            "TEST",
91            &["s3cr3t"],
92        )
93        .await;
94
95        assert_eq!(
96            outcome.unwrap_err().to_string(),
97            "remote: rejected [REDACTED]"
98        );
99    }
100
101    #[tokio::test]
102    async fn a_credential_that_is_empty_leaves_the_message_alone() {
103        let outcome = run(
104            &mut shell("echo 'fatal: no such repository' >&2; exit 128"),
105            "TEST",
106            &[""],
107        )
108        .await;
109
110        assert_eq!(
111            outcome.unwrap_err().to_string(),
112            "fatal: no such repository"
113        );
114    }
115}