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_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 bytes_in(
dir: &std::path::Path,
args: &[&str],
input: Option<&[u8]>,
) -> std::io::Result<Option<Vec<u8>>> {
use std::io::Write;
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(dir)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(if input.is_some() {
Stdio::piped()
} else {
Stdio::null()
});
let mut child = retrying(|| cmd.spawn())?;
let feeder = match (input, child.stdin.take()) {
(Some(bytes), Some(mut stdin)) => {
let bytes = bytes.to_vec();
Some(std::thread::spawn(move || {
let _ = stdin.write_all(&bytes);
}))
}
_ => None,
};
let out = child.wait_with_output()?;
if let Some(f) = feeder {
let _ = f.join();
}
Ok(out.status.success().then_some(out.stdout))
}
pub fn ls_tree_z(dir: &std::path::Path, tree: &str) -> std::io::Result<Option<Vec<Vec<u8>>>> {
let out = bytes_in(
dir,
&[
"ls-tree",
"-r",
"-z",
"--full-tree",
"--name-only",
"--end-of-options",
tree,
],
None,
)?;
Ok(out.map(|b| {
b.split(|c| *c == 0)
.filter(|p| !p.is_empty())
.map(<[u8]>::to_vec)
.collect()
}))
}
pub fn ls_tree_entries(dir: &std::path::Path, tree: &str) -> std::io::Result<Option<Vec<Vec<u8>>>> {
let out = bytes_in(
dir,
&[
"ls-tree",
"-r",
"-z",
"--full-tree",
"--end-of-options",
tree,
],
None,
)?;
Ok(out.map(|b| {
b.split(|c| *c == 0)
.filter(|p| !p.is_empty())
.map(<[u8]>::to_vec)
.collect()
}))
}
pub type Blobs = Vec<(String, Vec<u8>)>;
pub fn cat_file_batch(dir: &std::path::Path, names: &[String]) -> std::io::Result<Option<Blobs>> {
if names.is_empty() {
return Ok(Some(Vec::new()));
}
let mut input = Vec::new();
for n in names {
input.extend_from_slice(n.as_bytes());
input.push(b'\n');
}
let Some(out) = bytes_in(dir, &["cat-file", "--batch"], Some(&input))? else {
return Ok(None);
};
let mut found = Vec::new();
let mut at = 0;
for name in names {
let Some(nl) = out[at..].iter().position(|c| *c == b'\n') else {
break;
};
let header = String::from_utf8_lossy(&out[at..at + nl]).into_owned();
at += nl + 1;
let mut parts = header.rsplitn(3, ' ');
let (Some(size), Some(_kind)) = (parts.next(), parts.next()) else {
continue;
};
let Ok(size) = size.parse::<usize>() else {
continue;
};
if at + size > out.len() {
break;
}
found.push((name.clone(), out[at..at + size].to_vec()));
at += size + 1;
}
Ok(Some(found))
}
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(),
})
}
#[cfg(test)]
mod 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(
std::io::ErrorKind::PermissionDenied
)));
}
}