use anyhow::{Context, Result};
use tokio::process::Command;
const REDACTED: &str = "[REDACTED]";
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}");
}
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"
);
}
}