batuta/stack/publish_status/
cache.rs1use anyhow::{anyhow, Result};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::hash::{DefaultHasher, Hash, Hasher};
9use std::path::{Path, PathBuf};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use super::types::CacheEntry;
13
14#[derive(Debug, Default, Serialize, Deserialize)]
20pub struct PublishStatusCache {
21 pub(crate) entries: HashMap<String, CacheEntry>,
23 #[serde(skip)]
25 pub(crate) cache_path: Option<PathBuf>,
26}
27
28impl PublishStatusCache {
29 pub(crate) fn default_cache_path() -> PathBuf {
31 dirs::cache_dir()
32 .unwrap_or_else(|| PathBuf::from("."))
33 .join("batuta")
34 .join("publish-status.json")
35 }
36
37 #[must_use]
39 pub fn load() -> Self {
40 let path = Self::default_cache_path();
41 Self::load_from(&path).unwrap_or_default()
42 }
43
44 pub fn load_from(path: &Path) -> Result<Self> {
46 if !path.exists() {
47 return Ok(Self::default());
48 }
49 let data = std::fs::read_to_string(path)?;
50 let mut cache: Self = serde_json::from_str(&data)?;
51 cache.cache_path = Some(path.to_path_buf());
52 Ok(cache)
53 }
54
55 pub fn save(&self) -> Result<()> {
57 let path = self.cache_path.clone().unwrap_or_else(Self::default_cache_path);
58 if let Some(parent) = path.parent() {
59 std::fs::create_dir_all(parent)?;
60 }
61 let data = serde_json::to_string_pretty(self)?;
62 std::fs::write(&path, data)?;
63 Ok(())
64 }
65
66 #[must_use]
68 pub fn get(&self, name: &str, cache_key: &str) -> Option<&CacheEntry> {
69 self.entries.get(name).filter(|e| e.cache_key == cache_key)
70 }
71
72 pub fn insert(&mut self, name: String, entry: CacheEntry) {
74 self.entries.insert(name, entry);
75 }
76
77 pub fn clear(&mut self) {
79 self.entries.clear();
80 }
81}
82
83pub fn compute_cache_key(repo_path: &Path) -> Result<String> {
90 let cargo_toml = repo_path.join("Cargo.toml");
91
92 if !cargo_toml.exists() {
93 return Err(anyhow!("No Cargo.toml found at {:?}", repo_path));
94 }
95
96 let content = std::fs::read(&cargo_toml)?;
98
99 let mtime = std::fs::metadata(&cargo_toml)?
101 .modified()?
102 .duration_since(UNIX_EPOCH)
103 .unwrap_or_default()
104 .as_secs();
105
106 let head_sha = get_git_head(repo_path).unwrap_or_else(|_| "no-git".to_string());
108
109 let mut hasher = DefaultHasher::new();
111 content.hash(&mut hasher);
112 head_sha.hash(&mut hasher);
113 mtime.hash(&mut hasher);
114
115 Ok(format!("{:016x}", hasher.finish()))
116}
117
118pub(crate) fn get_git_head(repo_path: &Path) -> Result<String> {
120 let output = std::process::Command::new("git")
121 .args(["rev-parse", "--short", "HEAD"])
122 .current_dir(repo_path)
123 .output()?;
124
125 if !output.status.success() {
126 return Err(anyhow!("git rev-parse failed"));
127 }
128
129 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
130}