timefs 0.1.0

Mount a Git repository as a read-only filesystem.
Documentation
#[allow(dead_code)]
mod support;

use self::support::{with_mounted_timefs, RepositoryFixture};
use std::fs;
use std::os::unix::ffi::OsStringExt;
use std::thread;
use std::time::Duration;

#[test]
fn ref_snapshot_freezes_moving_references_and_history() {
    let live_fixture = release_fixture("timefs-task10-live");
    let (_value, live_mount) = with_mounted_timefs(live_fixture.path(), &[], &["-f"], |mounted| {
        assert_eq!(
            fs::read(mounted.path().join("now").join("README.md"))
                .expect("live now/README.md should be readable"),
            b"release v1\n"
        );

        advance_release_fixture(&live_fixture);
        thread::sleep(Duration::from_secs(2));

        assert_eq!(
            fs::read(mounted.path().join("now").join("fresh.txt"))
                .expect("live now/fresh.txt should appear after HEAD advances"),
            b"fresh from v2\n"
        );
        assert_eq!(
            fs::read(
                mounted
                    .path()
                    .join("refs")
                    .join("heads")
                    .join("main")
                    .join("fresh.txt"),
            )
            .expect("live refs/heads/main/fresh.txt should appear after HEAD advances"),
            b"fresh from v2\n"
        );
        assert_eq!(
            sorted_dir_entries(&mounted.path().join("history").join("README.md")).len(),
            2
        );
        assert_eq!(
            sorted_dir_entries(&mounted.path().join("history").join("fresh.txt")).len(),
            1
        );
    });
    assert!(
        live_mount.stderr.is_empty(),
        "live release mount emitted unexpected stderr: {}",
        live_mount.stderr
    );

    let snapshot_fixture = release_fixture("timefs-task10-snapshot");
    let before_head = snapshot_fixture.git_rev_parse("HEAD");
    let (_value, snapshot_mount) = with_mounted_timefs(
        snapshot_fixture.path(),
        &[],
        &["-f", "--ref-snapshot"],
        |mounted| {
            assert_eq!(
                fs::read(mounted.path().join("now").join("README.md"))
                    .expect("snapshotted now/README.md should be readable"),
                b"release v1\n"
            );

            advance_release_fixture(&snapshot_fixture);
            let after_head = snapshot_fixture.git_rev_parse("HEAD");
            thread::sleep(Duration::from_secs(2));

            assert_eq!(
                fs::read(mounted.path().join("now").join("README.md"))
                    .expect("snapshotted now/README.md should stay frozen"),
                b"release v1\n"
            );
            assert_eq!(
                fs::read(mounted.path().join("at").join("HEAD").join("README.md"),)
                    .expect("snapshotted at/HEAD/README.md should stay frozen"),
                b"release v1\n"
            );
            assert_eq!(
                fs::metadata(mounted.path().join("now").join("fresh.txt"))
                    .expect_err("snapshotted now/fresh.txt should stay absent")
                    .raw_os_error(),
                Some(libc::ENOENT)
            );
            assert_eq!(
                fs::metadata(
                    mounted
                        .path()
                        .join("refs")
                        .join("heads")
                        .join("main")
                        .join("fresh.txt"),
                )
                .expect_err("snapshotted refs/heads/main/fresh.txt should stay absent")
                .raw_os_error(),
                Some(libc::ENOENT)
            );
            assert_eq!(
                sorted_dir_entries(&mounted.path().join("history").join("README.md")).len(),
                1
            );
            assert_eq!(
                fs::metadata(mounted.path().join("history").join("fresh.txt"))
                    .expect_err("snapshotted history/fresh.txt should stay absent")
                    .raw_os_error(),
                Some(libc::ENOENT)
            );
            assert_eq!(
                fs::read(
                    mounted
                        .path()
                        .join("commits")
                        .join(before_head.as_str())
                        .join("README.md"),
                )
                .expect("commit-addressed snapshot should remain readable"),
                b"release v1\n"
            );
            assert_eq!(
                fs::read(
                    mounted
                        .path()
                        .join("commits")
                        .join(after_head.as_str())
                        .join("fresh.txt"),
                )
                .expect("commit-addressed fresh file should remain reachable by SHA"),
                b"fresh from v2\n"
            );
        },
    );
    assert!(
        snapshot_mount.stderr.is_empty(),
        "snapshot release mount emitted unexpected stderr: {}",
        snapshot_mount.stderr
    );
}

fn release_fixture(prefix: &str) -> RepositoryFixture {
    let fixture = RepositoryFixture::new(prefix);
    fs::write(fixture.path().join("README.md"), b"release v1\n")
        .expect("release fixture write should succeed");
    fixture.git(["add", "README.md"]);
    fixture.git(["commit", "-m", "Create release fixture"]);
    fixture
}

fn advance_release_fixture(fixture: &RepositoryFixture) {
    fs::write(fixture.path().join("README.md"), b"release v2\n")
        .expect("release fixture update should succeed");
    fs::write(fixture.path().join("fresh.txt"), b"fresh from v2\n")
        .expect("release fixture fresh file write should succeed");
    fixture.git(["add", "README.md", "fresh.txt"]);
    fixture.git(["commit", "-m", "Advance release fixture"]);
}

fn sorted_dir_entries(path: &std::path::Path) -> Vec<Vec<u8>> {
    let mut entries = fs::read_dir(path)
        .expect("directory should be readable")
        .filter_map(Result::ok)
        .map(|entry| entry.file_name().into_vec())
        .collect::<Vec<_>>();
    entries.sort();
    entries
}