use gix::hash::ObjectId;
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use anyhow::Context as _;
use sha2::{Digest as _, Sha256};
use super::cmd::git_info;
use super::fetch::SHALLOW_BRANCHES;
use crate::snapshot::RepoSnapshot;
/// bump when replies change bytes, or stale ones get linked from the prior export.
pub(crate) const REPLY_KEY_VERSION: u32 = 7;
pub(crate) const REPLY_KEYS: &str = "reply-keys";
pub(crate) struct Replies<'a> {
pub(super) smart_dir: &'a Path,
pub(super) prior_smart: Option<&'a Path>,
pub(super) prior_refs: Option<&'a BTreeMap<String, String>>,
pub(super) git: String,
pub(super) prior: HashMap<String, std::path::PathBuf>,
pub(super) keys: std::sync::Mutex<BTreeMap<String, String>>,
}
impl<'a> Replies<'a> {
pub(crate) fn new(
smart_dir: &'a Path,
prior_smart: Option<&'a Path>,
prior_refs: Option<&'a BTreeMap<String, String>>,
) -> anyhow::Result<Self> {
let git = git_info()?.version.clone();
let prior = prior_smart
.and_then(|dir| {
let keys = std::fs::read_to_string(dir.join(REPLY_KEYS)).ok()?;
let prior = keys
.lines()
.filter_map(|line| line.split_once(' '))
.map(|(key, name)| (key.to_string(), dir.join(name)))
.collect();
Some(prior)
})
.unwrap_or_default();
Ok(Self {
smart_dir,
prior_smart,
prior_refs,
git,
prior,
keys: Default::default(),
})
}
pub(crate) fn key(&self, request: &[u8]) -> String {
let mut hasher = Sha256::new();
crate::plan::hash_fields::u32(&mut hasher, REPLY_KEY_VERSION);
crate::plan::hash_fields::text(&mut hasher, b"git", &self.git);
hasher.update(request);
crate::cas::Digest::from_raw(hasher.finalize().into()).hex()
}
pub(crate) fn link(&self, key: &str, path: &Path) -> Option<u64> {
let prior = self.prior.get(key)?;
std::fs::hard_link(prior, path).ok()?;
let map = crate::depth_one::map_path(prior);
if map.exists() {
std::fs::hard_link(&map, crate::depth_one::map_path(path)).ok()?;
}
Some(std::fs::metadata(path).ok()?.len())
}
pub(crate) fn record(&self, key: &str, name: &str) {
let mut keys = self.keys.lock().expect("reply keys");
keys.insert(name.to_string(), key.to_string());
}
pub(crate) fn keys(&self) -> String {
let keys = self.keys.lock().expect("reply keys");
keys.iter()
.map(|(name, key)| format!("{key} {name}\n"))
.collect()
}
pub(crate) fn reply(
&self,
name: &str,
request: &[u8],
make: impl FnOnce(&Path) -> anyhow::Result<u64>,
) -> anyhow::Result<u64> {
let key = self.key(request);
let path = self.smart_dir.join(name);
let len = match self.link(&key, &path) {
Some(len) => len,
None => make(&path).with_context(|| format!("make {}", path.display()))?,
};
self.record(&key, name);
Ok(len)
}
pub(crate) fn prior_depth_one(
&self,
snapshot: &RepoSnapshot,
tip: &ObjectId,
) -> Vec<std::path::PathBuf> {
let (Some(dir), Some(refs)) = (self.prior_smart, self.prior_refs) else {
return Vec::new();
};
let prior_tips = std::fs::read_to_string(dir.join(SHALLOW_BRANCHES)).unwrap_or_default();
let tip = tip.to_string();
let mut like = Vec::new();
let branches = (snapshot.public_refs.iter())
.filter(|r| r.name.starts_with("refs/heads/") && r.raw == tip);
for branch in branches {
let had = refs.get(&branch.name);
let name = match had.and_then(|had| prior_tips.lines().position(|tip| tip == had)) {
Some(n) => format!("shallow-{n}"),
None => "shallow-head".into(),
};
let path = dir.join(name);
if !like.contains(&path) {
like.push(path);
}
}
like
}
pub(crate) fn forget(&self, name: &str) -> anyhow::Result<()> {
std::fs::remove_file(self.smart_dir.join(name))?;
self.keys.lock().expect("reply keys").remove(name);
Ok(())
}
}