borg/notes/
regress_git.rs1use std::io::Read;
6use std::path::Path;
7use std::process::{Child, Command, ExitStatus, Stdio};
8use std::time::{Duration, Instant};
9
10use crate::notes::regress_diff::{self as diff, Hunk};
11
12pub struct Git<'a> {
15 repo: &'a Path,
16 call: Duration,
17 deadline: Instant,
18}
19
20impl<'a> Git<'a> {
21 #[must_use]
23 pub fn new(repo: &'a Path, call: Duration, budget: Duration) -> Git<'a> {
24 Git { repo, call, deadline: Instant::now() + budget }
25 }
26
27 #[must_use]
28 pub fn repo(&self) -> &'a Path {
29 self.repo
30 }
31
32 pub(crate) fn run(&self, args: &[&str]) -> Option<String> {
33 let limit = self.call.min(self.deadline.checked_duration_since(Instant::now())?);
34 if limit.is_zero() {
35 return None;
36 }
37 let mut child = Command::new("git")
38 .arg("-C")
39 .arg(self.repo)
40 .args(["-c", "core.quotePath=false"])
41 .args(args)
42 .stdin(Stdio::null())
43 .stdout(Stdio::piped())
44 .stderr(Stdio::null())
45 .spawn()
46 .ok()?;
47 let mut out = child.stdout.take()?;
48 let reader = std::thread::spawn(move || {
50 let mut buf = Vec::new();
51 let _ = out.read_to_end(&mut buf);
52 String::from_utf8_lossy(&buf).into_owned()
53 });
54 let status = wait_for(&mut child, limit)?;
55 let text = reader.join().ok()?;
56 status.success().then_some(text)
57 }
58
59 pub(crate) fn hunks(&self, parent: &str, sha: &str) -> Option<Vec<Hunk>> {
61 let args = ["diff", "-U0", "-w", "--ignore-blank-lines", "-M", "--no-color", "--no-ext-diff", "--src-prefix=a/", "--dst-prefix=b/", parent, sha];
62 self.run(&args).map(|t| diff::hunks(&t))
63 }
64}
65
66fn wait_for(child: &mut Child, limit: Duration) -> Option<ExitStatus> {
68 let (started, mut nap) = (Instant::now(), Duration::from_millis(1));
69 loop {
70 match child.try_wait() {
71 Ok(Some(s)) => return Some(s),
72 Ok(None) if started.elapsed() < limit => {
73 std::thread::sleep(nap);
74 nap = (nap * 2).min(Duration::from_millis(20));
75 }
76 _ => {
77 let _ = child.kill();
78 let _ = child.wait();
79 return None;
80 }
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn a_call_past_its_limit_or_the_deadline_is_none() {
91 let dir = std::env::temp_dir();
92 assert!(Git::new(&dir, Duration::from_secs(5), Duration::from_secs(5)).run(&["--version"]).is_some_and(|v| v.starts_with("git version")));
93 assert!(Git::new(&dir, Duration::from_secs(5), Duration::ZERO).run(&["--version"]).is_none(), "the run's budget is spent");
94 }
95}