1use std::collections::hash_map::DefaultHasher;
2use std::fs;
3use std::hash::{Hash, Hasher};
4use std::path::PathBuf;
5
6use anyhow::Result;
7use git2::Repository;
8use serde::{Deserialize, Serialize};
9
10use crate::insights::ReviewPack;
11
12const INDEX_VERSION: u8 = 1;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15struct ReviewPackCacheEntry {
16 version: u8,
17 repo_id: String,
18 head: String,
19 selected_hash: Option<String>,
20 pack: ReviewPack,
21}
22
23fn repo_id() -> Option<String> {
24 let repo = Repository::discover(".").ok()?;
25 let root = repo
26 .workdir()
27 .or_else(|| repo.path().parent())
28 .unwrap_or(repo.path());
29 let mut hasher = DefaultHasher::new();
30 root.to_string_lossy().hash(&mut hasher);
31 Some(format!("{:016x}", hasher.finish()))
32}
33
34fn current_head() -> Option<String> {
35 let repo = Repository::discover(".").ok()?;
36 let oid = repo.head().ok()?.target()?;
37 Some(oid.to_string())
38}
39
40fn cache_dir() -> Option<PathBuf> {
41 dirs::cache_dir().map(|p| p.join("gitstack").join("index-v1"))
42}
43
44fn review_pack_cache_path(selected_hash: Option<&str>) -> Option<PathBuf> {
45 let rid = repo_id()?;
46 let suffix = selected_hash.unwrap_or("none");
47 let file_name = format!("review-pack-{}-{}.json", rid, suffix);
48 Some(cache_dir()?.join(file_name))
49}
50
51pub fn load_review_pack(selected_hash: Option<&str>) -> Option<ReviewPack> {
52 let path = review_pack_cache_path(selected_hash)?;
53 let content = fs::read_to_string(path).ok()?;
54 let entry: ReviewPackCacheEntry = serde_json::from_str(&content).ok()?;
55 if entry.version != INDEX_VERSION {
56 return None;
57 }
58 if entry.repo_id != repo_id()? {
59 return None;
60 }
61 if entry.head != current_head()? {
62 return None;
63 }
64 if entry.selected_hash.as_deref() != selected_hash {
65 return None;
66 }
67 Some(entry.pack)
68}
69
70pub fn save_review_pack(selected_hash: Option<&str>, pack: &ReviewPack) -> Result<()> {
71 let Some(path) = review_pack_cache_path(selected_hash) else {
72 return Ok(());
73 };
74
75 if let Some(parent) = path.parent() {
76 fs::create_dir_all(parent)?;
77 }
78
79 let Some(repo_id) = repo_id() else {
80 return Ok(());
81 };
82 let Some(head) = current_head() else {
83 return Ok(());
84 };
85
86 let entry = ReviewPackCacheEntry {
87 version: INDEX_VERSION,
88 repo_id,
89 head,
90 selected_hash: selected_hash.map(str::to_string),
91 pack: pack.clone(),
92 };
93 let json = serde_json::to_string_pretty(&entry)?;
94 fs::write(path, json)?;
95 Ok(())
96}