git-ward 0.2.0

Proof-before-delete archival for local Git repositories
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;

use crate::assess::Assessment;
use crate::util::ward_home;

#[derive(Serialize, Deserialize, Default)]
pub struct Cache {
    entries: HashMap<String, Entry>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Entry {
    pub git_mtime_secs: u64,
    pub assessment: Assessment,
}

impl Cache {
    pub fn path() -> PathBuf {
        ward_home().join("cache.json")
    }

    pub fn load() -> Self {
        let path = Self::path();
        if !path.exists() {
            return Self::default();
        }
        match fs::read_to_string(&path) {
            Ok(text) => serde_json::from_str(&text).unwrap_or_default(),
            Err(_) => Self::default(),
        }
    }

    pub fn save(&self) -> Result<()> {
        let path = Self::path();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let text = serde_json::to_string(self)?;
        fs::write(&path, text)?;
        Ok(())
    }

    pub fn lookup(&self, repo: &Path) -> Option<&Assessment> {
        let mtime = git_mtime_secs(repo)?;
        let key = repo.display().to_string();
        let entry = self.entries.get(&key)?;
        if entry.git_mtime_secs == mtime {
            Some(&entry.assessment)
        } else {
            None
        }
    }

    pub fn store(&mut self, repo: &Path, assessment: Assessment) {
        let Some(mtime) = git_mtime_secs(repo) else {
            return;
        };
        let key = repo.display().to_string();
        self.entries.insert(
            key,
            Entry {
                git_mtime_secs: mtime,
                assessment,
            },
        );
    }

    pub fn clear() -> Result<()> {
        let path = Self::path();
        if path.exists() {
            fs::remove_file(path)?;
        }
        Ok(())
    }
}

pub fn git_mtime_secs(repo: &Path) -> Option<u64> {
    let meta = fs::metadata(repo.join(".git")).ok()?;
    let mtime = meta.modified().ok()?;
    let secs = mtime.duration_since(UNIX_EPOCH).ok()?.as_secs();
    Some(secs)
}