iforgor 0.3.3

The CLI tool for all those commands you forget about
Documentation
use std::{fs, path::Path};

const CURRENT_VERSION: &str = "3";
const VERSION_FILE: &str = ".version";

/// Check the app directory for version compatibility.
/// If old data exists without a version file, back it up and start fresh.
/// Returns Ok(()) when the app dir is ready to use.
pub fn check_migration(app_dir: &Path) -> anyhow::Result<()> {
    let version_path = app_dir.join(VERSION_FILE);

    if version_path.exists() {
        let version = fs::read_to_string(&version_path)?.trim().to_string();
        if version == CURRENT_VERSION {
            return Ok(());
        }
        // Future: handle version upgrades here.
        eprintln!(
            "Warning: unknown config version \"{version}\", expected \"{CURRENT_VERSION}\". \
             Proceeding anyway."
        );
        return Ok(());
    }

    // No version file. Check if old data exists.
    let has_old_data = app_dir.join("registry.toml").exists()
        || app_dir.join("history.toml").exists()
        || app_dir.join("sources.toml").exists();

    if has_old_data {
        // Back up old directory.
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        let backup_dir = app_dir.with_file_name(format!(".iforgor.bak.{timestamp}"));

        fs::rename(app_dir, &backup_dir)?;
        eprintln!(
            "Old config detected. Backed up to: {}",
            backup_dir.display()
        );
        eprintln!("Migrate your source TOML files to .iforgor/ folders in your projects.");
    }

    // Create fresh directory with version file.
    fs::create_dir_all(app_dir)?;
    fs::write(app_dir.join(VERSION_FILE), CURRENT_VERSION)?;

    Ok(())
}

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

    #[test]
    fn fresh_directory_creates_version() {
        let tmp = tempfile::tempdir().unwrap();
        let app_dir = tmp.path().join(".iforgor");
        check_migration(&app_dir).unwrap();
        let version = fs::read_to_string(app_dir.join(VERSION_FILE)).unwrap();
        assert_eq!(version, CURRENT_VERSION);
    }

    #[test]
    fn current_version_is_noop() {
        let tmp = tempfile::tempdir().unwrap();
        let app_dir = tmp.path().join(".iforgor");
        fs::create_dir_all(&app_dir).unwrap();
        fs::write(app_dir.join(VERSION_FILE), CURRENT_VERSION).unwrap();
        fs::write(app_dir.join("some_file.toml"), "keep me").unwrap();

        check_migration(&app_dir).unwrap();

        // File should still be there.
        assert!(app_dir.join("some_file.toml").exists());
    }

    #[test]
    fn old_data_triggers_backup() {
        let tmp = tempfile::tempdir().unwrap();
        let app_dir = tmp.path().join(".iforgor");
        fs::create_dir_all(&app_dir).unwrap();
        fs::write(app_dir.join("registry.toml"), "old data").unwrap();

        check_migration(&app_dir).unwrap();

        // App dir should have fresh version file.
        let version = fs::read_to_string(app_dir.join(VERSION_FILE)).unwrap();
        assert_eq!(version, CURRENT_VERSION);
        // Old registry.toml should be gone (backed up).
        assert!(!app_dir.join("registry.toml").exists());
        // Backup dir should exist.
        let backups: Vec<_> = fs::read_dir(tmp.path())
            .unwrap()
            .filter_map(|e| e.ok())
            .filter(|e| e.file_name().to_string_lossy().starts_with(".iforgor.bak."))
            .collect();
        assert_eq!(backups.len(), 1);
    }

    #[test]
    fn unknown_version_proceeds() {
        let tmp = tempfile::tempdir().unwrap();
        let app_dir = tmp.path().join(".iforgor");
        fs::create_dir_all(&app_dir).unwrap();
        fs::write(app_dir.join(VERSION_FILE), "999").unwrap();

        // Should not error.
        check_migration(&app_dir).unwrap();
    }
}