Skip to main content

gitstack/
cache.rs

1use std::collections::hash_map::DefaultHasher;
2use std::fs;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use anyhow::Result;
8use git2::Repository;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12struct CacheEnvelope<T> {
13    generated_at_unix: u64,
14    head_hash: String,
15    payload: T,
16}
17
18pub fn clear_analysis_cache() -> Result<()> {
19    let dir = analysis_cache_dir()?;
20    if dir.exists() {
21        fs::remove_dir_all(&dir)?;
22    }
23    Ok(())
24}
25
26pub fn load_or_compute(
27    key: &str,
28    ttl_hours: u64,
29    compute: impl FnOnce() -> Result<String>,
30) -> Result<String> {
31    let repo = Repository::discover(".")?;
32    let head_hash = repo
33        .head()?
34        .target()
35        .map(|oid| oid.to_string())
36        .unwrap_or_else(|| "HEAD".to_string());
37    let path = cache_file_path(&repo, key)?;
38
39    if let Ok(content) = fs::read_to_string(&path) {
40        if let Ok(envelope) = serde_json::from_str::<CacheEnvelope<String>>(&content) {
41            if envelope.head_hash == head_hash && !is_expired(envelope.generated_at_unix, ttl_hours)
42            {
43                return Ok(envelope.payload);
44            }
45        }
46    }
47
48    let payload = compute()?;
49    let envelope = CacheEnvelope {
50        generated_at_unix: now_unix_secs(),
51        head_hash,
52        payload,
53    };
54
55    if let Some(parent) = path.parent() {
56        let _ = fs::create_dir_all(parent);
57    }
58    if let Ok(serialized) = serde_json::to_string(&envelope) {
59        let _ = fs::write(&path, serialized);
60    }
61
62    Ok(envelope.payload)
63}
64
65fn is_expired(generated_at_unix: u64, ttl_hours: u64) -> bool {
66    if ttl_hours == 0 {
67        return true;
68    }
69    let now = now_unix_secs();
70    now.saturating_sub(generated_at_unix) > ttl_hours.saturating_mul(3600)
71}
72
73fn cache_file_path(repo: &Repository, key: &str) -> Result<PathBuf> {
74    Ok(analysis_cache_dir()?
75        .join(repo_cache_key(repo)?)
76        .join(format!("{}.json", key)))
77}
78
79fn analysis_cache_dir() -> Result<PathBuf> {
80    if let Some(base) = dirs::cache_dir() {
81        return Ok(base.join("gitstack").join("analysis"));
82    }
83    Ok(std::env::temp_dir().join("gitstack").join("analysis"))
84}
85
86fn repo_cache_key(repo: &Repository) -> Result<String> {
87    let path = repo
88        .workdir()
89        .or_else(|| repo.path().parent())
90        .unwrap_or_else(|| Path::new("."));
91
92    let mut hasher = DefaultHasher::new();
93    path.to_string_lossy().hash(&mut hasher);
94    Ok(format!("{:x}", hasher.finish()))
95}
96
97fn now_unix_secs() -> u64 {
98    SystemTime::now()
99        .duration_since(UNIX_EPOCH)
100        .unwrap_or_default()
101        .as_secs()
102}