//! The nodes each change touches, kept beside the store (oxygen#91). //! //! Finding the changes that touched a file (`oxy log --path`, the //! `touches()` revset) otherwise reads and decodes every change of the //! history, its lines included. A change is immutable, so its entry never //! goes stale: one missing is read from the store once and added. Like //! Git's changed-path filters it is derived data, and deleting //! `.oxy/index/touched` loses nothing but time. use super::*; use crate::fasthash::FastMap; use crate::ids::NodeId; /// The cache, loaded; [`Touched::save`] writes it back when it grew. pub(crate) struct Touched { nodes: FastMap>, /// Working-copy changes, which a snapshot replaces: kept for this /// command only, so the cache does not fill with dead entries. transient: FastMap>, added: usize, path: PathBuf, } impl Touched { /// The nodes `h` touches, from the cache or else from the store; `None` /// when the change is not stored (beyond a shallow boundary). pub(crate) fn of(&mut self, repo: &Repo, h: &ChangeHash) -> Option<&[NodeId]> { if !self.nodes.contains_key(h) && !self.transient.contains_key(h) { // The skeleton names every node; no text is read. let (c, _) = repo.store.get_change_lazy(h).ok()?; self.keep(*h, &c); } self.nodes .get(h) .or_else(|| self.transient.get(h)) .map(|n| &n[..]) } fn keep(&mut self, h: ChangeHash, c: &Change) { if c.kind == ChangeKind::WorkingCopy { self.transient.insert(h, nodes_of(c)); } else { self.nodes.insert(h, nodes_of(c)); self.added += 1; } } /// Read every one of `hashes` the cache lacks. pub(crate) fn fill(&mut self, repo: &Repo, hashes: &[ChangeHash]) { use rayon::prelude::*; let missing: Vec = hashes .iter() .filter(|h| !self.nodes.contains_key(*h) && !self.transient.contains_key(*h)) .copied() .collect(); // Read a batch at a time, then decode skeletons (no text) on every // core. A change the store lacks is left out, as `of` leaves it. for batch in missing.chunks(8192) { let read: Vec<(ChangeHash, Change)> = repo .store .get_many(batch) .into_par_iter() .zip(batch.par_iter()) .filter_map(|(got, h)| match got { Ok((ObjectType::Change, bytes)) => { Some((*h, Change::decode_skeleton(&bytes, *h).ok()?.0)) } _ => None, }) .collect(); for (h, c) in &read { self.keep(*h, c); } } } /// Whether `h` touches any of `nodes` (false when it is not stored). pub(crate) fn touches(&mut self, repo: &Repo, h: &ChangeHash, nodes: &HashSet) -> bool { self.of(repo, h) .is_some_and(|n| n.iter().any(|n| nodes.contains(n))) } /// Write the cache back if it grew; quietly nothing when it cannot be. pub(crate) fn save(self) { if self.added == 0 { return; } let _t = crate::timing::phase("write: touched nodes"); let mut w = crate::flat::Writer::default(); w.count(self.nodes.len()); for (h, nodes) in &self.nodes { w.object(h); w.count(nodes.len()); for n in nodes.iter() { w.fixed(&n.0); } } // Checked whole on reading: a damaged entry would hide a change. let sum = *blake3::hash(&w.0).as_bytes(); w.fixed(&sum); let _ = fs::create_dir_all(self.path.parent().unwrap_or(&self.path)); let _ = write_cache(&self.path, &w.0); } } /// The nodes a change's tree operations and edits name, each once. fn nodes_of(c: &Change) -> Box<[NodeId]> { let mut nodes: Vec = c .tree_ops .iter() .map(|o| o.node()) .chain(c.edits.iter().map(|e| e.node())) .collect(); nodes.sort(); nodes.dedup(); nodes.into_boxed_slice() } impl Repo { /// The touched-nodes cache: empty when there is none yet or it does /// not read back whole. pub(crate) fn touched(&self) -> Touched { let _t = crate::timing::phase("load: touched nodes"); let path = self.oxy.join("index").join("touched"); let nodes = fs::read(&path) .ok() .and_then(|b| read_touched(&b)) .unwrap_or_default(); Touched { nodes, transient: FastMap::default(), added: 0, path, } } } fn read_touched(bytes: &[u8]) -> Option>> { let (body, sum) = bytes.split_at_checked(bytes.len().checked_sub(32)?)?; if blake3::hash(body).as_bytes() != sum { return None; } let mut r = crate::flat::Reader::new(body); let n = r.count().ok()?; let mut out = FastMap::default(); out.reserve(n); for _ in 0..n { let h = r.object().ok()?; let k = r.count().ok()?; let mut nodes = Vec::with_capacity(k); for _ in 0..k { nodes.push(NodeId(r.fixed::<16>().ok()?)); } out.insert(h, nodes.into_boxed_slice()); } r.at_end().then_some(out) } /// The changes a cache file holds, for tests. #[cfg(test)] pub(crate) fn read_touched_for_test(bytes: &[u8]) -> Option> { read_touched(bytes).map(|m| m.into_keys().collect()) }