specfold_core/workspace/vcs/
passing.rs1use 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
10pub struct Passing {
12 pub commit: String,
13 pub path: String,
14 pub bytes: Vec<u8>,
15}
16
17pub 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
24fn 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
53fn 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
59fn 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
93pub 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}