1use std::process::{Command, Stdio};
4
5fn retrying<T>(mut attempt: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
17 let mut delay = std::time::Duration::from_millis(10);
18 for tries_left in [2u8, 1, 0] {
19 match attempt() {
20 Err(e) if tries_left > 0 && transient(&e) => {
21 std::thread::sleep(delay);
22 delay *= 3;
23 }
24 other => return other,
25 }
26 }
27 unreachable!("the zero-tries arm returns")
28}
29
30fn transient(e: &std::io::Error) -> bool {
35 if matches!(
36 e.kind(),
37 std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
38 ) {
39 return true;
40 }
41 matches!(e.raw_os_error(), Some(4 | 11 | 26 | 35))
42}
43
44pub fn stdout(args: &[&str]) -> Option<String> {
47 let mut cmd = Command::new("git");
48 cmd.args(args).stderr(Stdio::null());
49 let out = retrying(|| cmd.output()).ok()?;
50 if !out.status.success() {
51 return None;
52 }
53 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
54}
55
56pub fn stdout_in(dir: &std::path::Path, args: &[&str]) -> Option<String> {
62 let mut cmd = Command::new("git");
63 cmd.arg("-C").arg(dir).args(args).stderr(Stdio::null());
64 let out = retrying(|| cmd.output()).ok()?;
65 if !out.status.success() {
66 return None;
67 }
68 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
69}
70
71pub fn stdout_piped(args: &[&str], stdin: &str) -> Option<String> {
75 use std::io::Write;
76 let mut cmd = Command::new("git");
77 cmd.args(args)
78 .stdin(Stdio::piped())
79 .stdout(Stdio::piped())
80 .stderr(Stdio::null());
81 let mut child = retrying(|| cmd.spawn()).ok()?;
82 child.stdin.take()?.write_all(stdin.as_bytes()).ok()?;
83 let out = child.wait_with_output().ok()?;
84 out.status
85 .success()
86 .then(|| String::from_utf8_lossy(&out.stdout).into_owned())
87}
88
89pub fn stdout_piped_raw(args: &[&str], stdin: &str) -> Option<Vec<u8>> {
96 use std::io::Write;
97 let mut cmd = Command::new("git");
98 cmd.args(args)
99 .stdin(Stdio::piped())
100 .stdout(Stdio::piped())
101 .stderr(Stdio::null());
102 let mut child = retrying(|| cmd.spawn()).ok()?;
103 child.stdin.take()?.write_all(stdin.as_bytes()).ok()?;
104 let out = child.wait_with_output().ok()?;
105 out.status.success().then_some(out.stdout)
106}
107
108pub fn stdout_piped_in(dir: &std::path::Path, args: &[&str], stdin: &[u8]) -> Option<String> {
115 use std::io::Write;
116 let mut cmd = Command::new("git");
117 cmd.arg("-C")
118 .arg(dir)
119 .args(args)
120 .stdin(Stdio::piped())
121 .stdout(Stdio::piped())
122 .stderr(Stdio::null());
123 let mut child = retrying(|| cmd.spawn()).ok()?;
124 child.stdin.take()?.write_all(stdin).ok()?;
125 let out = child.wait_with_output().ok()?;
126 out.status
127 .success()
128 .then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
129}
130
131pub fn stdout_raw(args: &[&str]) -> Option<Vec<u8>> {
134 let mut cmd = Command::new("git");
135 cmd.args(args).stderr(Stdio::null());
136 let out = retrying(|| cmd.output()).ok()?;
137 out.status.success().then_some(out.stdout)
138}
139
140pub struct Output {
142 pub code: i32,
143 pub stdout: String,
144 pub stderr: String,
145}
146
147pub fn output(args: &[&str]) -> Option<Output> {
157 let mut cmd = Command::new("git");
158 cmd.args(args).stdin(Stdio::null());
159 let out = retrying(|| cmd.output()).ok()?;
160 Some(Output {
161 code: out.status.code()?,
164 stdout: String::from_utf8_lossy(&out.stdout).trim().to_string(),
165 stderr: String::from_utf8_lossy(&out.stderr).trim().to_string(),
166 })
167}
168
169pub fn succeeds(args: &[&str]) -> bool {
171 let mut cmd = Command::new("git");
172 cmd.args(args)
173 .stdin(Stdio::null())
174 .stdout(Stdio::null())
175 .stderr(Stdio::null());
176 retrying(|| cmd.status())
177 .map(|s| s.success())
178 .unwrap_or(false)
179}
180
181pub fn stdout_paths(args: &[&str]) -> Option<Vec<String>> {
194 let (first, rest) = args.split_first()?;
195 let mut argv = Vec::with_capacity(args.len() + 1);
196 argv.push(*first);
197 argv.push("-z");
198 argv.extend_from_slice(rest);
199 stdout_raw(&argv).map(|raw| split_nul_paths(&raw))
200}
201
202pub(crate) fn split_nul_paths(raw: &[u8]) -> Vec<String> {
207 raw.split(|&b| b == 0)
208 .filter(|s| !s.is_empty())
209 .map(|s| String::from_utf8_lossy(s).into_owned())
210 .collect()
211}
212
213#[cfg(test)]
214mod retry_tests {
215 use super::*;
216
217 #[test]
219 fn transient_covers_the_fork_pressure_kinds_and_nothing_else() {
220 for code in [4, 11, 26, 35] {
221 assert!(
222 transient(&std::io::Error::from_raw_os_error(code)),
223 "raw {code} is a loaded-machine hiccup"
224 );
225 }
226 assert!(transient(&std::io::Error::from(
227 std::io::ErrorKind::Interrupted
228 )));
229 assert!(!transient(&std::io::Error::from(
230 std::io::ErrorKind::NotFound
231 )));
232 assert!(!transient(&std::io::Error::from_raw_os_error(13))); }
234
235 #[test]
238 fn retrying_gives_up_after_three_transient_failures() {
239 let mut calls = 0;
240 let r: std::io::Result<()> = retrying(|| {
241 calls += 1;
242 Err(std::io::Error::from_raw_os_error(11))
243 });
244 assert!(r.is_err());
245 assert_eq!(calls, 3);
246 }
247
248 #[test]
251 fn a_hard_error_is_not_retried() {
252 let mut calls = 0;
253 let r: std::io::Result<()> = retrying(|| {
254 calls += 1;
255 Err(std::io::Error::from(std::io::ErrorKind::NotFound))
256 });
257 assert!(r.is_err());
258 assert_eq!(calls, 1);
259 }
260
261 #[test]
263 fn one_hiccup_then_an_answer_is_an_answer() {
264 let mut calls = 0;
265 let r = retrying(|| {
266 calls += 1;
267 if calls == 1 {
268 Err(std::io::Error::from_raw_os_error(4))
269 } else {
270 Ok(42)
271 }
272 });
273 assert_eq!(r.unwrap(), 42);
274 assert_eq!(calls, 2);
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn splits_on_nul_and_drops_the_trailing_empty_segment() {
284 assert_eq!(
285 split_nul_paths(b"src/main.rs\0Cargo.toml\0"),
286 vec!["src/main.rs", "Cargo.toml"]
287 );
288 }
289
290 #[test]
291 fn empty_input_is_no_paths() {
292 assert_eq!(split_nul_paths(b""), Vec::<String>::new());
293 }
294
295 #[test]
302 fn a_non_ascii_path_is_not_reinterpreted_as_its_quoted_form() {
303 let mut raw = "é.json".as_bytes().to_vec();
304 raw.push(0);
305 let got = split_nul_paths(&raw);
306 assert_eq!(got, vec!["é.json".to_string()]);
307 assert_ne!(got[0], "\"\\303\\251.json\"", "must not be the quoted form");
308 }
309}