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}