GitHub - matthiasgoergens/git-snap-fs

4 min read Original article ↗

GitSnapFS

GitSnapFS exposes snapshots of a Git repository as a read-only filesystem designed for safe inspection, automated audits, and tooling integration.

Highlights

  • commits/<revspec> presents the tree for an individual commit; annotated tags and other revspecs are peeled to their commit (commits/v1.0 works). Likewise trees/<revspec> peels to a tree, so trees/HEAD names HEAD's root tree.
  • branches/ and tags/ mirror the ref hierarchy: refs/heads/feature/foo appears as branches/feature/foo, with intermediate directories, and each ref is a symlink into commits/ (or trees/ for tag objects pointing at trees). HEAD is a symlink into commits/.
  • Git tree-entry modes are honoured: 100755 blobs are executable (0o555), 100644 blobs are 0o444, 120000 blobs are symlinks. Submodule gitlinks appear as empty directories.
  • The filesystem is strictly read-only and answers requests lazily; updates in the underlying repo are surfaced without a pre-scan.
  • Directory listings leave . and .. to the kernel, letting path caches stay in userspace.
  • We leverage the kernel’s zero-message open/opendir paths (NO_OPEN_SUPPORT, NO_OPENDIR_SUPPORT) for near-native performance once data is cached.

Caching: the kernel owns it

gix can only inflate a blob in its entirety, but the kernel asks for file data in readahead-sized chunks. Instead of keeping a userspace cache, the first read of a file inflates the blob once, replies with the requested chunk, and pushes the whole blob into the kernel page cache via FUSE_NOTIFY_STORE. Two details are load-bearing: the push runs on a dedicated notifier thread (pushing from the request-handling thread deadlocks against the folio locks of in-flight reads; see tests/mount_read.rs), and it is issued in segments from the back of the file forwards, so it meets the kernel's front-to-back readahead wave head-on instead of trailing it (measured on an 8 MiB file: 8 daemon reads instead of 46). Combined with FOPEN_KEEP_CACHE (the kernel default under zero-message open), subsequent reads — same pass, later opens, mmap — are served entirely from the page cache, and eviction under memory pressure is the kernel's call. GitSnapFS retains no blob data of its own; a dropped push merely means the kernel keeps asking chunk by chunk, each answered by a full re-inflation.

Inode scheme and collisions

Inodes are stateless: the top nibble tags the inode class (directory, regular blob, executable blob, symlink blob, gitlink, ref-tree node), and the low 60 bits are the object id's leading 15 hex digits (or a name hash for ref nodes). Folding the tree-entry mode into the inode means the same blob reachable as both 100644 and 100755 gets two distinct inodes, so attributes are always coherent. See src/inode.rs for the full layout.

Truncating object ids to 60 bits admits birthday collisions (probability ≈ n²/2⁶¹ for n objects, i.e. ~4×10⁻⁵ at 10 million objects). Collisions are detected, not silent: inodes resolve through the object database's unambiguous-prefix lookup, so a collision surfaces as EIO plus an error log rather than another object's content. In addition, each object entry carries object-id bits 64..128 in the FUSE generation field: the kernel treats (nodeid, generation) as the inode identity, so on a collision it evicts the stale inode instead of serving one object's cached pages or attributes as the other's, and NFS file handles for the evicted object return ESTALE. Enumerating commits/ or trees/ is deliberately unsupported and returns ENOTSUP.

Requirements

  • Linux with FUSE kernel support that advertises EXPORT_SUPPORT, ZERO_MESSAGE_OPEN, and ZERO_MESSAGE_OPENDIR.
  • fusermount/fusermount3 (typically provided by fuse packages).
  • Rust toolchain nightly or stable recent enough to build the dependency graph (cargo, rustc).

Quick Start

cargo run -- --repo path/to/.git --mountpoint /tmp/gitfs

The mount exposes the root layout (commits, branches, tags, HEAD). Unmount with:

fusermount -u /tmp/gitfs   # or fusermount3 -u

Or by just terminating the process.

Development

  • Design notes live in codex_spec.md.

  • Formatting and linting are enforced with the following commands:

    cargo fmt
    cargo clippy --all-targets --all-features -- -D clippy::pedantic -D clippy::style -D clippy::cargo
  • The clippy.toml documents unavoidable duplicate crate versions coming from upstream dependencies.

  • Please keep the filesystem read-only and avoid libfuse/libgit2 shims; all Git access goes through gix and FUSE plumbing through fuse-backend-rs.