fluidattacks-core-git 0.68.0

Fluid Attacks Core Git
Documentation
//! Running a clone and saying what went wrong when it does.

use tokio::process::Command;

use crate::call::not_run;
use crate::errors::GitCallError;

const REDACTED: &str = "[REDACTED]";

/// Run a clone, and fail with what git printed rather than with its exit code.
///
/// A refusal carries git's stderr whole, with anything in `secrets` taken out.
pub async fn run(command: &mut Command, secrets: &[&str]) -> Result<(), GitCallError> {
    let output = command
        .output()
        .await
        .map_err(|source| not_run("clone", source))?;

    if output.status.success() {
        return Ok(());
    }

    Err(GitCallError::Refused {
        command: "clone",
        status: output.status,
        stderr: hide(&String::from_utf8_lossy(&output.stderr), secrets),
    })
}

// git leaves the credential out of its own messages, but a server answers what
// it wants and git repeats it under `remote:`.
#[must_use]
pub fn hide(said: &str, secrets: &[&str]) -> String {
    secrets
        .iter()
        .filter(|secret| !secret.is_empty())
        .fold(said.to_owned(), |text, secret| {
            text.replace(secret, REDACTED)
        })
}

#[cfg(test)]
mod tests {
    use tokio::process::Command;

    use super::run;
    use crate::errors::GitCallError;

    fn shell(script: &str) -> Command {
        let mut command = Command::new("sh");
        command.args(["-c", script]);

        command
    }

    fn refusal(outcome: Result<(), GitCallError>) -> (Option<i32>, String) {
        match outcome {
            Err(GitCallError::Refused { status, stderr, .. }) => (status.code(), stderr),
            other => panic!("{other:?}"),
        }
    }

    #[tokio::test]
    async fn a_clone_that_worked_says_nothing() {
        assert!(run(&mut shell("exit 0"), &[]).await.is_ok());
    }

    #[tokio::test]
    async fn a_clone_that_failed_carries_what_it_printed_whole() {
        let outcome = run(
            &mut shell("echo 'fatal: no such repository' >&2; exit 128"),
            &[],
        )
        .await;

        assert_eq!(
            refusal(outcome),
            (Some(128), "fatal: no such repository\n".to_owned())
        );
    }

    #[tokio::test]
    async fn a_clone_that_printed_nothing_carries_its_status() {
        let outcome = run(&mut shell("exit 3"), &[]).await;

        assert_eq!(refusal(outcome), (Some(3), String::new()));
    }

    #[tokio::test]
    async fn a_secret_the_server_answered_is_taken_out() {
        let outcome = run(
            &mut shell("echo 'remote: rejected s3cr3t' >&2; exit 128"),
            &["s3cr3t"],
        )
        .await;

        assert_eq!(refusal(outcome).1, "remote: rejected [REDACTED]\n");
    }

    #[tokio::test]
    async fn a_credential_that_is_empty_leaves_the_message_alone() {
        let outcome = run(
            &mut shell("echo 'fatal: no such repository' >&2; exit 128"),
            &[""],
        )
        .await;

        assert_eq!(refusal(outcome).1, "fatal: no such repository\n");
    }
}