Skip to main content

specfold_core/workspace/vcs/
passing.rs

1//! C231: file versions a range's commits carry that its end no longer has. Each of them is
2//! pushed with the range and stays in history, so a guard over the range must read them too.
3
4use super::repo::git;
5use std::collections::{HashMap, HashSet};
6use std::io::{BufRead, BufReader, Read, Write};
7use std::path::Path;
8use std::process::{Command, Stdio};
9
10/// One version of a file some commit in the range wrote.
11pub struct Passing {
12    pub commit: String,
13    pub path: String,
14    pub bytes: Vec<u8>,
15}
16
17/// The revision a range ends at: the part after `..` or `...`, HEAD when it is empty or the
18/// range is a single revision.
19pub fn range_end(range: &str) -> String {
20    let end = range.rsplit_once("...").or_else(|| range.rsplit_once("..")).map(|(_, e)| e).unwrap_or("");
21    if end.is_empty() { "HEAD".into() } else { end.to_string() }
22}
23
24/// (commit, path, blob) for every file a commit in `range` added or changed, newest first.
25fn written(root: &Path, range: &str) -> Vec<(String, String, String)> {
26    let args = [
27        "-c",
28        "core.quotePath=false",
29        "log",
30        "--format=%H",
31        "--raw",
32        "--no-abbrev",
33        "--no-renames",
34        "--diff-filter=ACM",
35        range,
36    ];
37    let Some(out) = git(root, &args) else { return vec![] };
38    let mut commit = String::new();
39    let mut rows = vec![];
40    for line in String::from_utf8_lossy(&out).lines() {
41        if let Some(raw) = line.strip_prefix(':') {
42            let Some((meta, path)) = raw.split_once('\t') else { continue };
43            if let Some(blob) = meta.split_whitespace().nth(3) {
44                rows.push((commit.clone(), path.to_string(), blob.to_string()));
45            }
46        } else if !line.is_empty() {
47            commit = line.trim().to_string();
48        }
49    }
50    rows
51}
52
53/// Blob ids in the tree at `rev`.
54fn end_blobs(root: &Path, rev: &str) -> HashSet<String> {
55    let Some(out) = git(root, &["ls-tree", "-r", rev]) else { return HashSet::new() };
56    String::from_utf8_lossy(&out).lines().filter_map(|l| l.split_whitespace().nth(2).map(str::to_string)).collect()
57}
58
59/// Reads blobs through one `git cat-file --batch`, so a long range costs one process.
60fn cat(root: &Path, ids: &[String]) -> HashMap<String, Vec<u8>> {
61    let mut map = HashMap::new();
62    let child = Command::new("git")
63        .arg("-C")
64        .arg(root)
65        .args(["cat-file", "--batch"])
66        .stdin(Stdio::piped())
67        .stdout(Stdio::piped())
68        .stderr(Stdio::null())
69        .spawn();
70    let Ok(mut child) = child else { return map };
71    let (Some(mut stdin), Some(stdout)) = (child.stdin.take(), child.stdout.take()) else { return map };
72    let input: String = ids.iter().map(|i| format!("{i}\n")).collect();
73    let writer = std::thread::spawn(move || stdin.write_all(input.as_bytes()));
74    let mut r = BufReader::new(stdout);
75    for id in ids {
76        let mut head = String::new();
77        if r.read_line(&mut head).unwrap_or(0) == 0 {
78            break;
79        }
80        let Some(size) = head.split_whitespace().nth(2).and_then(|s| s.parse::<usize>().ok()) else { continue };
81        let mut buf = vec![0; size + 1];
82        if r.read_exact(&mut buf).is_err() {
83            break;
84        }
85        buf.pop();
86        map.insert(id.clone(), buf);
87    }
88    let _ = writer.join();
89    let _ = child.wait();
90    map
91}
92
93/// Versions written inside `range` that differ from every blob at its end: a value one commit
94/// adds and a later one removes is still pushed. Each distinct blob is read once.
95pub fn passing(root: &Path, range: &str) -> Vec<Passing> {
96    let rows = written(root, range);
97    if rows.is_empty() {
98        return vec![];
99    }
100    let at_end = end_blobs(root, &range_end(range));
101    let mut seen = HashSet::new();
102    let rows: Vec<_> = rows.into_iter().filter(|(_, _, b)| !at_end.contains(b) && seen.insert(b.clone())).collect();
103    let ids: Vec<String> = rows.iter().map(|(_, _, b)| b.clone()).collect();
104    let mut blobs = cat(root, &ids);
105    rows.into_iter().filter_map(|(commit, path, b)| Some(Passing { commit, path, bytes: blobs.remove(&b)? })).collect()
106}