gitstack 5.3.0

Git history viewer with insights - Author stats, file heatmap, code ownership
Documentation
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;

use anyhow::Result;
use chrono::Local;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ActionUsageEntry {
    pub count: u64,
    pub last_used_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MetricsData {
    pub quick_actions: BTreeMap<String, ActionUsageEntry>,
}

fn metrics_path() -> Option<PathBuf> {
    dirs::cache_dir().map(|p| p.join("gitstack").join("metrics-v1.json"))
}

fn config_path() -> Option<PathBuf> {
    dirs::config_dir().map(|p| p.join("gitstack").join("config.toml"))
}

pub fn quick_action_usage_enabled() -> bool {
    let Some(path) = config_path() else {
        return false;
    };
    let Ok(content) = fs::read_to_string(path) else {
        return false;
    };
    let Ok(value) = content.parse::<toml::Value>() else {
        return false;
    };
    value
        .get("metrics")
        .and_then(|v| v.get("quick_action_usage"))
        .and_then(|v| v.as_bool())
        .unwrap_or(false)
}

pub fn load_metrics() -> MetricsData {
    let Some(path) = metrics_path() else {
        return MetricsData::default();
    };
    let Ok(content) = fs::read_to_string(path) else {
        return MetricsData::default();
    };
    serde_json::from_str(&content).unwrap_or_default()
}

pub fn save_metrics(data: &MetricsData) -> Result<()> {
    let Some(path) = metrics_path() else {
        return Ok(());
    };
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, serde_json::to_string_pretty(data)?)?;
    Ok(())
}

pub fn record_quick_action_usage(action_id: &str) -> Result<()> {
    if !quick_action_usage_enabled() {
        return Ok(());
    }

    let mut metrics = load_metrics();
    let entry = metrics
        .quick_actions
        .entry(action_id.to_string())
        .or_default();
    entry.count += 1;
    entry.last_used_at = Local::now().format("%Y-%m-%dT%H:%M:%S%z").to_string();
    save_metrics(&metrics)
}

pub fn reset_metrics() -> Result<()> {
    let Some(path) = metrics_path() else {
        return Ok(());
    };
    if path.exists() {
        fs::remove_file(path)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_load_metrics_defaults() {
        let data = load_metrics();
        assert!(data.quick_actions.len() <= 1000);
    }
}