Skip to main content

cadvm_core/
index.rs

1//! Working-tree hash cache (`.cadvm/index.json`).
2//!
3//! Hashing every tracked file on each `status` is wasteful on large STEP files.
4//! This cache remembers, per path, the file's size + modification time and the
5//! content hash computed last time. When a file's size and mtime are unchanged,
6//! its hash is reused instead of re-reading and re-hashing the whole file.
7//!
8//! The cache is a transparent optimization: a miss simply re-hashes. It never
9//! affects correctness — a stale entry can only occur if a file is rewritten
10//! with the exact same size *and* mtime, which the filesystem's nanosecond mtime
11//! makes effectively impossible in practice.
12
13use std::collections::{BTreeMap, BTreeSet};
14use std::path::{Path, PathBuf};
15use std::time::UNIX_EPOCH;
16
17use cadvm_store::ObjectId;
18use serde::{Deserialize, Serialize};
19
20use crate::error::{CoreError, Result};
21use crate::repo::Repository;
22use crate::worktree;
23
24const INDEX_VERSION: u32 = 1;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27struct CacheEntry {
28    size: u64,
29    mtime_ns: i64,
30    hash: ObjectId,
31}
32
33/// A size+mtime keyed cache of working-file content hashes.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct HashCache {
36    version: u32,
37    entries: BTreeMap<PathBuf, CacheEntry>,
38}
39
40impl Default for HashCache {
41    fn default() -> Self {
42        HashCache {
43            version: INDEX_VERSION,
44            entries: BTreeMap::new(),
45        }
46    }
47}
48
49fn mtime_ns(meta: &std::fs::Metadata) -> i64 {
50    meta.modified()
51        .ok()
52        .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
53        .map(|d| d.as_nanos() as i64)
54        .unwrap_or(0)
55}
56
57impl HashCache {
58    /// Load the cache, tolerating a missing or legacy/placeholder file (returns
59    /// an empty cache rather than failing).
60    pub fn load(repo: &Repository) -> HashCache {
61        let path = repo.cadvm_dir().join("index.json");
62        match std::fs::read(&path) {
63            Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_default(),
64            Err(_) => HashCache::default(),
65        }
66    }
67
68    /// Persist the cache to `.cadvm/index.json`.
69    pub fn save(&self, repo: &Repository) -> Result<()> {
70        let path = repo.cadvm_dir().join("index.json");
71        let mut bytes = serde_json::to_vec_pretty(self)?;
72        bytes.push(b'\n');
73        std::fs::write(&path, bytes).map_err(|e| CoreError::io(&path, e))
74    }
75
76    /// Content hash of a working file, reusing the cache when size+mtime match
77    /// and otherwise stream-hashing the file and updating the cache.
78    pub fn hash(&mut self, repo: &Repository, rel: &Path) -> Result<ObjectId> {
79        let path = worktree::abs_path(repo, rel);
80        let meta = std::fs::metadata(&path).map_err(|e| CoreError::io(&path, e))?;
81        let (size, mtime) = (meta.len(), mtime_ns(&meta));
82
83        if let Some(e) = self.entries.get(rel) {
84            if e.size == size && e.mtime_ns == mtime {
85                return Ok(e.hash.clone());
86            }
87        }
88
89        let hash = worktree::hash_working_file(repo, rel)?;
90        self.entries.insert(
91            rel.to_path_buf(),
92            CacheEntry {
93                size,
94                mtime_ns: mtime,
95                hash: hash.clone(),
96            },
97        );
98        Ok(hash)
99    }
100
101    /// Record an already-known hash for a file (stats it for size+mtime). Used
102    /// after a snapshot so the next `status` is a cache hit.
103    pub fn record(&mut self, repo: &Repository, rel: &Path, hash: ObjectId) -> Result<()> {
104        let path = worktree::abs_path(repo, rel);
105        let meta = std::fs::metadata(&path).map_err(|e| CoreError::io(&path, e))?;
106        self.entries.insert(
107            rel.to_path_buf(),
108            CacheEntry {
109                size: meta.len(),
110                mtime_ns: mtime_ns(&meta),
111                hash,
112            },
113        );
114        Ok(())
115    }
116
117    /// Drop entries for paths no longer present (keeps the file tidy).
118    pub fn retain(&mut self, keep: &BTreeSet<PathBuf>) {
119        self.entries.retain(|p, _| keep.contains(p));
120    }
121}