Skip to main content

gitversion_rs/
cache.rs

1//! Disk cache for version calculation results.
2//!
3//! Corresponds to `GitVersion.Core/VersionCalculation/Caching`. Caches results
4//! under `<.git>/gitversion_cache/<key>.json`, keyed by a SHA1 hash of the
5//! repository state (refs + HEAD), config file content, and overrideconfig values.
6//! The cache is automatically invalidated when the repository state or config changes.
7
8use crate::git::GitRepo;
9use crate::output::VersionVariables;
10use rust_i18n::t;
11use sha1::{Digest, Sha1};
12use std::path::{Path, PathBuf};
13
14/// Compute cache key: SHA1 hex of (refs snapshot + HEAD + config file content + overrideconfig).
15///
16/// Corresponds to the four components of `GitVersionCacheKeyFactory`
17/// (gitSystemHash, repositorySnapshotHash, configFileHash, overrideConfigHash).
18/// The GitVersion binary version is intentionally **not** included in the key,
19/// so the cache is reused as long as the repository and config are unchanged
20/// (use `--no-cache` during development).
21pub fn compute_key(repo: &GitRepo, config_path: Option<&Path>, overrides: &[String]) -> String {
22    let mut hasher = Sha1::new();
23
24    // 1) refs snapshot (name + target for all branches/tags).
25    for line in repo.refs_snapshot().unwrap_or_default() {
26        hasher.update(line.as_bytes());
27        hasher.update(b"\n");
28    }
29    hasher.update(b"--head--");
30    // 2) HEAD ref name + tip SHA.
31    hasher.update(repo.head_ref_name().as_bytes());
32    if let Ok(head) = repo.head_commit() {
33        hasher.update(head.sha.as_bytes());
34    }
35    hasher.update(b"--config--");
36    // 3) Config file content.
37    if let Some(p) = config_path {
38        if let Ok(content) = std::fs::read_to_string(p) {
39            hasher.update(content.as_bytes());
40        }
41    }
42    hasher.update(b"--override--");
43    // 4) overrideconfig values.
44    for o in overrides {
45        hasher.update(o.as_bytes());
46        hasher.update(b"\n");
47    }
48
49    let digest = hasher.finalize();
50    let mut hex = String::with_capacity(40);
51    for b in digest {
52        hex.push_str(&format!("{b:02x}"));
53    }
54    hex
55}
56
57/// Path to the cache file: `<.git>/gitversion_cache/<key>.json`.
58fn cache_file(repo: &GitRepo, key: &str) -> PathBuf {
59    repo.git_dir()
60        .join("gitversion_cache")
61        .join(format!("{key}.json"))
62}
63
64/// Load variables from cache. Returns None if missing or corrupt (deletes corrupt entries).
65pub fn load(repo: &GitRepo, key: &str) -> Option<VersionVariables> {
66    let path = cache_file(repo, key);
67    if !path.is_file() {
68        log::debug!("cache miss: {}", path.display());
69        return None;
70    }
71    match std::fs::read_to_string(&path)
72        .ok()
73        .and_then(|s| serde_json::from_str(&s).ok())
74    {
75        Some(vars) => {
76            log::debug!("{}", t!("cache.hit", path = path.display()));
77            Some(vars)
78        }
79        None => {
80            log::warn!("{}", t!("cache.corrupt", path = path.display()));
81            let _ = std::fs::remove_file(&path);
82            None
83        }
84    }
85}
86
87/// Write variables to the cache.
88pub fn store(repo: &GitRepo, key: &str, vars: &VersionVariables) {
89    let path = cache_file(repo, key);
90    if let Some(parent) = path.parent() {
91        if std::fs::create_dir_all(parent).is_err() {
92            return;
93        }
94    }
95    match serde_json::to_string_pretty(vars) {
96        Ok(json) => {
97            if let Err(e) = std::fs::write(&path, json) {
98                log::warn!(
99                    "{}",
100                    t!("cache.write_failed", path = path.display(), error = e)
101                );
102            } else {
103                log::debug!("cache write: {}", path.display());
104            }
105        }
106        Err(e) => log::warn!("{}", t!("cache.serialize_failed", error = e)),
107    }
108}