use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::process::{Command, Stdio};
use crate::notes::{self, Excerpt};
fn git(dir: &Path, args: &[&str]) -> Result<String, String> {
git_in(dir, args, None).map(|o| String::from_utf8_lossy(&o).into_owned())
}
pub fn git_in(dir: &Path, args: &[&str], input: Option<String>) -> Result<Vec<u8>, String> {
let mut child = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.stdin(if input.is_some() { Stdio::piped() } else { Stdio::null() })
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("cannot run git: {e}"))?;
let writer = match (input, child.stdin.take()) {
(Some(text), Some(mut stdin)) => Some(std::thread::spawn(move || {
use std::io::Write;
let _ = stdin.write_all(text.as_bytes());
})),
_ => None,
};
let o = child.wait_with_output().map_err(|e| format!("cannot run git: {e}"))?;
if let Some(w) = writer {
let _ = w.join();
}
if !o.status.success() {
return Err(String::from_utf8_lossy(&o.stderr).trim().to_string());
}
Ok(o.stdout)
}
#[must_use]
pub fn visible(tag: &str, here: &str) -> bool {
tag.is_empty() || tag == here
}
pub fn notes_of(repo: &Path, here: &str, shas: &[String]) -> HashMap<String, Excerpt> {
let list = git(repo, &["notes", &format!("--ref={}", notes::NOTES_REF), "list"]).unwrap_or_default();
let wanted: HashSet<&str> = shas.iter().map(String::as_str).collect();
let blobs: Vec<(&str, &str)> = list.lines().filter_map(|l| l.split_once(' ')).filter(|(_, commit)| wanted.contains(commit)).collect();
if blobs.is_empty() {
return HashMap::new();
}
let input: String = blobs.iter().flat_map(|(blob, _)| [*blob, "\n"]).collect();
let Ok(out) = git_in(repo, &["cat-file", "--batch"], Some(input)) else { return HashMap::new() };
let mut found = HashMap::new();
let mut rest = &out[..];
for (_, commit) in blobs {
let Some(nl) = rest.iter().position(|&b| b == b'\n') else { break };
let head = String::from_utf8_lossy(&rest[..nl]).into_owned();
rest = &rest[nl + 1..];
let Some(size) = head.split(' ').nth(2).and_then(|n| n.parse::<usize>().ok()) else { continue };
if rest.len() < size {
break;
}
let text = String::from_utf8_lossy(&rest[..size]).into_owned();
rest = rest.get(size + 1..).unwrap_or_default();
if let Some(e) = Excerpt::from_note(commit, &text).filter(|e| visible(&e.collective, here)) {
found.insert(commit.to_string(), e);
}
}
found
}
pub fn stored_of<S: std::hash::BuildHasher>(store: &Path, here: &str, shas: &HashSet<&str, S>) -> HashMap<String, Excerpt> {
let mut found: HashMap<String, Excerpt> = HashMap::new();
if shas.is_empty() {
return found;
}
let text = std::fs::read_to_string(store.join("reasoning.jsonl")).unwrap_or_default();
for e in text.lines().filter_map(|l| serde_json::from_str::<Excerpt>(l).ok()) {
if e.kind != "reasoning" || !shas.contains(e.commit.as_str()) || !visible(&e.collective, here) {
continue;
}
if found.get(&e.commit).is_none_or(|old| e.ms > old.ms) {
found.insert(e.commit.clone(), e);
}
}
found
}
pub fn reasoning_for(repo: &Path, store: &Path, here: &str, shas: &[String]) -> HashMap<String, Excerpt> {
let mut found = notes_of(repo, here, shas);
let missing: HashSet<&str> = shas.iter().map(String::as_str).filter(|s| !found.contains_key(*s)).collect();
let stored = stored_of(store, here, &missing);
found.extend(stored);
found
}