Skip to main content

githops_core/
cache.rs

1use anyhow::Result;
2use sha2::{Digest, Sha256};
3use std::path::{Path, PathBuf};
4
5// ---------------------------------------------------------------------------
6// Input discovery
7// ---------------------------------------------------------------------------
8
9/// Expand glob patterns relative to `root` and return sorted, deduplicated
10/// file paths.  Directories and broken globs are silently skipped.
11pub fn expand_globs(patterns: &[String], root: &Path) -> Vec<PathBuf> {
12    let mut paths = Vec::new();
13    for pattern in patterns {
14        let full = root.join(pattern).to_string_lossy().into_owned();
15        if let Ok(entries) = glob::glob(&full) {
16            for entry in entries.flatten() {
17                if entry.is_file() {
18                    paths.push(entry);
19                }
20            }
21        }
22    }
23    paths.sort();
24    paths.dedup();
25    paths
26}
27
28/// Read the contents of each path.  Files that cannot be read are silently
29/// omitted (the cache will miss on the next run, triggering a re-execution).
30pub fn read_inputs(paths: &[PathBuf]) -> Vec<(PathBuf, Vec<u8>)> {
31    paths
32        .iter()
33        .filter_map(|p| std::fs::read(p).ok().map(|c| (p.clone(), c)))
34        .collect()
35}
36
37// ---------------------------------------------------------------------------
38// Key computation
39// ---------------------------------------------------------------------------
40
41/// Compute a deterministic SHA-256 cache key from:
42/// * the command's `run` script
43/// * any extra `key` strings declared in the command cache config
44/// * the path and content of every input file (in sorted path order)
45pub fn compute_key(
46    run: &str,
47    extra_keys: &[String],
48    input_files: &[(PathBuf, Vec<u8>)],
49) -> String {
50    let mut hasher = Sha256::new();
51
52    hasher.update(run.as_bytes());
53
54    for k in extra_keys {
55        hasher.update(b"\x00key\x00");
56        hasher.update(k.as_bytes());
57    }
58
59    for (path, content) in input_files {
60        hasher.update(b"\x00file\x00");
61        hasher.update(path.to_string_lossy().as_bytes());
62        hasher.update(b"\x00");
63        hasher.update(content);
64    }
65
66    format!("{:x}", hasher.finalize())
67}
68
69// ---------------------------------------------------------------------------
70// Cache store (marker-file strategy)
71// ---------------------------------------------------------------------------
72
73/// Returns `true` when a cache entry exists for `key`, meaning the command's
74/// last run with these exact inputs succeeded.
75pub fn is_hit(key: &str, cache_dir: &Path) -> bool {
76    cache_dir.join(format!("{}.ok", key)).exists()
77}
78
79/// Persist a successful run in the cache so future invocations can skip it.
80pub fn record_hit(key: &str, cache_dir: &Path) -> Result<()> {
81    std::fs::create_dir_all(cache_dir)?;
82    // Write a marker file whose name encodes the key.
83    std::fs::write(cache_dir.join(format!("{}.ok", key)), b"")?;
84    Ok(())
85}