use super::repo::git;
use std::collections::{HashMap, HashSet};
use std::io::{BufRead, BufReader, Read, Write};
use std::path::Path;
use std::process::{Command, Stdio};
pub struct Passing {
pub commit: String,
pub path: String,
pub bytes: Vec<u8>,
}
pub fn range_end(range: &str) -> String {
let end = range.rsplit_once("...").or_else(|| range.rsplit_once("..")).map(|(_, e)| e).unwrap_or("");
if end.is_empty() { "HEAD".into() } else { end.to_string() }
}
fn written(root: &Path, range: &str) -> Vec<(String, String, String)> {
let args = [
"-c",
"core.quotePath=false",
"log",
"--format=%H",
"--raw",
"--no-abbrev",
"--no-renames",
"--diff-filter=ACM",
range,
];
let Some(out) = git(root, &args) else { return vec![] };
let mut commit = String::new();
let mut rows = vec![];
for line in String::from_utf8_lossy(&out).lines() {
if let Some(raw) = line.strip_prefix(':') {
let Some((meta, path)) = raw.split_once('\t') else { continue };
if let Some(blob) = meta.split_whitespace().nth(3) {
rows.push((commit.clone(), path.to_string(), blob.to_string()));
}
} else if !line.is_empty() {
commit = line.trim().to_string();
}
}
rows
}
fn end_blobs(root: &Path, rev: &str) -> HashSet<String> {
let Some(out) = git(root, &["ls-tree", "-r", rev]) else { return HashSet::new() };
String::from_utf8_lossy(&out).lines().filter_map(|l| l.split_whitespace().nth(2).map(str::to_string)).collect()
}
fn cat(root: &Path, ids: &[String]) -> HashMap<String, Vec<u8>> {
let mut map = HashMap::new();
let child = Command::new("git")
.arg("-C")
.arg(root)
.args(["cat-file", "--batch"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn();
let Ok(mut child) = child else { return map };
let (Some(mut stdin), Some(stdout)) = (child.stdin.take(), child.stdout.take()) else { return map };
let input: String = ids.iter().map(|i| format!("{i}\n")).collect();
let writer = std::thread::spawn(move || stdin.write_all(input.as_bytes()));
let mut r = BufReader::new(stdout);
for id in ids {
let mut head = String::new();
if r.read_line(&mut head).unwrap_or(0) == 0 {
break;
}
let Some(size) = head.split_whitespace().nth(2).and_then(|s| s.parse::<usize>().ok()) else { continue };
let mut buf = vec![0; size + 1];
if r.read_exact(&mut buf).is_err() {
break;
}
buf.pop();
map.insert(id.clone(), buf);
}
let _ = writer.join();
let _ = child.wait();
map
}
pub fn passing(root: &Path, range: &str) -> Vec<Passing> {
let rows = written(root, range);
if rows.is_empty() {
return vec![];
}
let at_end = end_blobs(root, &range_end(range));
let mut seen = HashSet::new();
let rows: Vec<_> = rows.into_iter().filter(|(_, _, b)| !at_end.contains(b) && seen.insert(b.clone())).collect();
let ids: Vec<String> = rows.iter().map(|(_, _, b)| b.clone()).collect();
let mut blobs = cat(root, &ids);
rows.into_iter().filter_map(|(commit, path, b)| Some(Passing { commit, path, bytes: blobs.remove(&b)? })).collect()
}