timefs 0.1.0

Mount a Git repository as a read-only filesystem.
Documentation
//! timefs CLI and library entry points.

pub mod cache;
pub mod cli;
pub mod config;
pub mod errors;
pub mod fs;
pub mod git;

use anyhow::Result;
use clap::Parser;

/// Parse CLI arguments, initialize logging, and dispatch the requested command.
pub fn run() -> Result<()> {
    let cli = cli::Cli::parse();
    run_with_cli(cli)
}

fn run_with_cli(cli: cli::Cli) -> Result<()> {
    init_tracing(cli.verbose);
    let command = config::AppCommand::from_cli(cli)?;

    match command {
        config::AppCommand::Mount(config) => fs::mount(config),
        config::AppCommand::Unmount(config) => fs::unmount(config),
    }
}

fn init_tracing(verbosity: u8) {
    let level = match verbosity {
        0 => tracing::level_filters::LevelFilter::WARN,
        1 => tracing::level_filters::LevelFilter::INFO,
        _ => tracing::level_filters::LevelFilter::DEBUG,
    };

    let _ = tracing_subscriber::fmt()
        .with_max_level(level)
        .with_writer(std::io::stderr)
        .without_time()
        .try_init();
}

#[cfg(test)]
mod tests {
    use super::run_with_cli;
    use crate::cli::Cli;
    use std::fs;
    use std::path::{Path, PathBuf};
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn mount_rejects_missing_repository_path() {
        let mountpoint = create_temp_dir("missing-repo-mountpoint");
        let repo = unique_path("missing-repo");

        let cli = parse_cli([
            "timefs",
            "mount",
            repo.to_string_lossy().as_ref(),
            mountpoint.to_string_lossy().as_ref(),
        ]);

        let error = run_with_cli(cli).expect_err("mount should fail for a missing repo path");
        assert_eq!(
            error.to_string(),
            format!("repository path does not exist: {}", repo.display())
        );

        fs::remove_dir_all(mountpoint).expect("temporary mountpoint cleanup should succeed");
    }

    #[test]
    fn mount_rejects_missing_mountpoint_path() {
        let repo = create_temp_dir("missing-mountpoint-repo");
        let mountpoint = unique_path("missing-mountpoint");

        let cli = parse_cli([
            "timefs",
            "mount",
            repo.to_string_lossy().as_ref(),
            mountpoint.to_string_lossy().as_ref(),
        ]);

        let error = run_with_cli(cli).expect_err("mount should fail for a missing mountpoint path");
        assert_eq!(
            error.to_string(),
            format!("mountpoint path does not exist: {}", mountpoint.display())
        );

        fs::remove_dir_all(repo).expect("temporary repo cleanup should succeed");
    }

    fn parse_cli<I, T>(args: I) -> Cli
    where
        I: IntoIterator<Item = T>,
        T: Into<std::ffi::OsString> + Clone,
    {
        <Cli as clap::Parser>::parse_from(args)
    }

    fn create_temp_dir(prefix: &str) -> PathBuf {
        let path = unique_path(prefix);
        fs::create_dir_all(&path).expect("temporary directory creation should succeed");
        path
    }

    fn unique_path(prefix: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after the Unix epoch")
            .as_nanos();

        std::env::temp_dir().join(format!("timefs-{prefix}-{nanos}"))
    }

    #[allow(dead_code)]
    fn _assert_exists(path: &Path) {
        assert!(path.exists(), "expected path to exist: {}", path.display());
    }
}