sem-cli 0.6.2

Semantic version control CLI. Shows what entities changed (functions, classes, methods) instead of lines.
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use sem_core::parser::differ::DiffResult;

#[derive(Serialize, Deserialize, Default)]
pub struct SemLifetimeStats {
    pub version: u32,
    pub first_run: Option<String>,
    pub last_run: Option<String>,
    pub total_diffs: u64,
    pub total_files_analyzed: u64,
    pub total_entities_analyzed: u64,
    pub total_changes_detected: u64,
    pub noise_filtered: u64,
    pub added_count: u64,
    pub modified_count: u64,
    pub deleted_count: u64,
    pub moved_count: u64,
    pub renamed_count: u64,
    pub reordered_count: u64,
}

fn stats_path() -> Option<PathBuf> {
    let home = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .ok()?;
    Some(PathBuf::from(home).join(".sem").join("stats.json"))
}

fn now_iso() -> String {
    let dur = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    let secs = dur.as_secs();
    let time_of_day = secs % 86400;
    let h = time_of_day / 3600;
    let m = (time_of_day % 3600) / 60;
    let s = time_of_day % 60;

    let mut y = 1970i64;
    let mut remaining = (secs / 86400) as i64;
    loop {
        let days_in_year = if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
            366
        } else {
            365
        };
        if remaining < days_in_year {
            break;
        }
        remaining -= days_in_year;
        y += 1;
    }
    let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
    let month_days = [
        31,
        if leap { 29 } else { 28 },
        31,
        30,
        31,
        30,
        31,
        31,
        30,
        31,
        30,
        31,
    ];
    let mut mon = 0;
    for (i, &md) in month_days.iter().enumerate() {
        if remaining < md as i64 {
            mon = i;
            break;
        }
        remaining -= md as i64;
    }
    let day = remaining + 1;
    format!(
        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
        y,
        mon + 1,
        day,
        h,
        m,
        s
    )
}

impl SemLifetimeStats {
    pub fn load() -> Self {
        let path = match stats_path() {
            Some(p) => p,
            None => return Self::default(),
        };
        fs::read_to_string(&path)
            .ok()
            .and_then(|s| serde_json::from_str(&s).ok())
            .unwrap_or_default()
    }

    pub fn record_diff(mut self, result: &DiffResult) -> Self {
        let now = now_iso();
        if self.first_run.is_none() {
            self.first_run = Some(now.clone());
        }
        self.last_run = Some(now);
        self.version = 1;

        self.total_diffs += 1;
        self.total_files_analyzed += result.file_count as u64;

        let entities_analyzed = result
            .total_entities_before
            .max(result.total_entities_after) as u64;
        self.total_entities_analyzed += entities_analyzed;

        let changes = (result.added_count
            + result.modified_count
            + result.deleted_count
            + result.moved_count
            + result.renamed_count
            + result.reordered_count) as u64;
        self.total_changes_detected += changes;
        self.noise_filtered += entities_analyzed.saturating_sub(changes);

        self.added_count += result.added_count as u64;
        self.modified_count += result.modified_count as u64;
        self.deleted_count += result.deleted_count as u64;
        self.moved_count += result.moved_count as u64;
        self.renamed_count += result.renamed_count as u64;
        self.reordered_count += result.reordered_count as u64;

        self
    }

    pub fn save(self) -> Self {
        if let Some(path) = stats_path() {
            if let Some(parent) = path.parent() {
                let _ = fs::create_dir_all(parent);
            }
            if let Ok(json) = serde_json::to_string_pretty(&self) {
                let _ = fs::write(&path, json);
            }
        }
        self
    }
}