Skip to main content

fluidattacks_core_git/clone/
reason.rs

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