use std::process::{Command, Stdio};
fn retrying<T>(mut attempt: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
let mut delay = std::time::Duration::from_millis(10);
for tries_left in [2u8, 1, 0] {
match attempt() {
Err(e) if tries_left > 0 && transient(&e) => {
std::thread::sleep(delay);
delay *= 3;
}
other => return other,
}
}
unreachable!("the zero-tries arm returns")
}
fn transient(e: &std::io::Error) -> bool {
if matches!(
e.kind(),
std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
) {
return true;
}
matches!(e.raw_os_error(), Some(4 | 11 | 26 | 35))
}
pub fn stdout(args: &[&str]) -> Option<String> {
let mut cmd = Command::new("git");
cmd.args(args).stderr(Stdio::null());
let out = retrying(|| cmd.output()).ok()?;
if !out.status.success() {
return None;
}
Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
pub fn stdout_in(dir: &std::path::Path, args: &[&str]) -> Option<String> {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(dir).args(args).stderr(Stdio::null());
let out = retrying(|| cmd.output()).ok()?;
if !out.status.success() {
return None;
}
Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
pub fn succeeds_in(dir: &std::path::Path, args: &[&str]) -> bool {
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(dir)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
retrying(|| cmd.status())
.map(|s| s.success())
.unwrap_or(false)
}
pub fn stdout_piped(args: &[&str], stdin: &str) -> Option<String> {
use std::io::Write;
let mut cmd = Command::new("git");
cmd.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
let mut child = retrying(|| cmd.spawn()).ok()?;
child.stdin.take()?.write_all(stdin.as_bytes()).ok()?;
let out = child.wait_with_output().ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).into_owned())
}
pub fn stdout_piped_raw(args: &[&str], stdin: &str) -> Option<Vec<u8>> {
use std::io::Write;
let mut cmd = Command::new("git");
cmd.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
let mut child = retrying(|| cmd.spawn()).ok()?;
child.stdin.take()?.write_all(stdin.as_bytes()).ok()?;
let out = child.wait_with_output().ok()?;
out.status.success().then_some(out.stdout)
}
pub fn stdout_piped_in(dir: &std::path::Path, args: &[&str], stdin: &[u8]) -> Option<String> {
use std::io::Write;
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(dir)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
let mut child = retrying(|| cmd.spawn()).ok()?;
child.stdin.take()?.write_all(stdin).ok()?;
let out = child.wait_with_output().ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}
pub fn stdout_raw(args: &[&str]) -> Option<Vec<u8>> {
let mut cmd = Command::new("git");
cmd.args(args).stderr(Stdio::null());
let out = retrying(|| cmd.output()).ok()?;
out.status.success().then_some(out.stdout)
}
pub struct Output {
pub code: i32,
pub stdout: String,
pub stderr: String,
}
pub fn output(args: &[&str]) -> Option<Output> {
let mut cmd = Command::new("git");
cmd.args(args).stdin(Stdio::null());
let out = retrying(|| cmd.output()).ok()?;
Some(Output {
code: out.status.code()?,
stdout: String::from_utf8_lossy(&out.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&out.stderr).trim().to_string(),
})
}
pub enum Probe {
Exit(i32),
TimedOut(u64),
Failed,
}
pub fn probe(args: &[&str], budget_secs: u64) -> Probe {
let mut cmd = Command::new("git");
cmd.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
if budget_secs == 0 {
return match retrying(|| cmd.status()) {
Ok(s) => s.code().map(Probe::Exit).unwrap_or(Probe::Failed),
Err(_) => Probe::Failed,
};
}
let Ok(mut child) = retrying(|| cmd.spawn()) else {
return Probe::Failed;
};
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(budget_secs);
loop {
match child.try_wait() {
Ok(Some(s)) => return s.code().map(Probe::Exit).unwrap_or(Probe::Failed),
Ok(None) => {}
Err(_) => return Probe::Failed,
}
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return Probe::TimedOut(budget_secs);
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
pub fn succeeds(args: &[&str]) -> bool {
let mut cmd = Command::new("git");
cmd.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
retrying(|| cmd.status())
.map(|s| s.success())
.unwrap_or(false)
}
pub fn stdout_paths(args: &[&str]) -> Option<Vec<String>> {
let (first, rest) = args.split_first()?;
let mut argv = Vec::with_capacity(args.len() + 1);
argv.push(*first);
argv.push("-z");
argv.extend_from_slice(rest);
stdout_raw(&argv).map(|raw| split_nul_paths(&raw))
}
pub(crate) fn split_nul_paths(raw: &[u8]) -> Vec<String> {
raw.split(|&b| b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.collect()
}
#[cfg(test)]
mod retry_tests {
use super::*;
#[test]
fn transient_covers_the_fork_pressure_kinds_and_nothing_else() {
for code in [4, 11, 26, 35] {
assert!(
transient(&std::io::Error::from_raw_os_error(code)),
"raw {code} is a loaded-machine hiccup"
);
}
assert!(transient(&std::io::Error::from(
std::io::ErrorKind::Interrupted
)));
assert!(!transient(&std::io::Error::from(
std::io::ErrorKind::NotFound
)));
assert!(!transient(&std::io::Error::from_raw_os_error(13))); }
#[test]
fn retrying_gives_up_after_three_transient_failures() {
let mut calls = 0;
let r: std::io::Result<()> = retrying(|| {
calls += 1;
Err(std::io::Error::from_raw_os_error(11))
});
assert!(r.is_err());
assert_eq!(calls, 3);
}
#[test]
fn a_hard_error_is_not_retried() {
let mut calls = 0;
let r: std::io::Result<()> = retrying(|| {
calls += 1;
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
});
assert!(r.is_err());
assert_eq!(calls, 1);
}
#[test]
fn one_hiccup_then_an_answer_is_an_answer() {
let mut calls = 0;
let r = retrying(|| {
calls += 1;
if calls == 1 {
Err(std::io::Error::from_raw_os_error(4))
} else {
Ok(42)
}
});
assert_eq!(r.unwrap(), 42);
assert_eq!(calls, 2);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_on_nul_and_drops_the_trailing_empty_segment() {
assert_eq!(
split_nul_paths(b"src/main.rs\0Cargo.toml\0"),
vec!["src/main.rs", "Cargo.toml"]
);
}
#[test]
fn empty_input_is_no_paths() {
assert_eq!(split_nul_paths(b""), Vec::<String>::new());
}
#[test]
fn a_non_ascii_path_is_not_reinterpreted_as_its_quoted_form() {
let mut raw = "é.json".as_bytes().to_vec();
raw.push(0);
let got = split_nul_paths(&raw);
assert_eq!(got, vec!["é.json".to_string()]);
assert_ne!(got[0], "\"\\303\\251.json\"", "must not be the quoted form");
}
}