fluidattacks-core-git 0.33.0

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

use anyhow::{Context, Result};
use tokio::process::Command;

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

/// Run a clone, and fail with what git printed rather than with its exit code.
///
/// Anything in `secrets` is taken out of that message.
pub async fn run(command: &mut Command, transport: &str, secrets: &[&str]) -> Result<()> {
    let output = command
        .output()
        .await
        .with_context(|| format!("running git clone ({transport})"))?;

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

    let said = String::from_utf8_lossy(&output.stderr);
    let said = hide(said.trim(), secrets);

    if said.is_empty() {
        anyhow::bail!(
            "git clone ({transport}) failed with status {}",
            output.status
        );
    }

    anyhow::bail!("{said}");
}

// git leaves the credential out of its own messages, but a server answers what
// it wants and git repeats it under `remote:`.
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;

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

        command
    }

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

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

        assert_eq!(
            outcome.unwrap_err().to_string(),
            "fatal: no such repository"
        );
    }

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

        assert!(outcome.unwrap_err().to_string().contains("(TEST)"));
    }

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

        assert_eq!(
            outcome.unwrap_err().to_string(),
            "remote: rejected [REDACTED]"
        );
    }

    #[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"),
            "TEST",
            &[""],
        )
        .await;

        assert_eq!(
            outcome.unwrap_err().to_string(),
            "fatal: no such repository"
        );
    }
}