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}