Git repositories as browseable filesystems

Git commits are conceptually folders: each one contains a complete directory listing of the project at that point in time. The object database just stores them more compactly. A handful of FUSE-based projects already exploit this idea to let you navigate history as directories, but FUSE on macOS requires kernel extensions that Apple has made increasingly difficult to install. As an experiment in avoiding FUSE, git-commit-folders mounts a git repository so every commit appears as a folder, with support for both FUSE and NFS (and a WebDAV implementation that doesn't fully work).

The project is experimental, but the author reports it being genuinely useful on small repositories for tasks like:

  • Searching for a deleted function with grep someFunction branch_histories/main/*/commit.go
  • Quickly viewing a single file from another branch via vim branches/other-branch/go.mod
  • Running grep someFunction branches/*/commit.go across every branch

These are all symlink shortcuts to commits. The syntax of git log -S or git grep may be more efficient, but filesystem navigation is often easier to remember, and a full git worktree feels like overkill for inspecting one file.

One core implementation, three frontends

Supporting FUSE, NFS, and WebDAV means implementing three distinct filesystem interfaces. To avoid triplicating the logic, the core lives behind the standard fs.FS interface, and two adapter functions translate it for the other protocols:

  • func Fuse2Dav(fs fs.FS) webdav.FileSystem
  • func Fuse2NFS(fs fs.FS) billy.Filesystem

The translation wasn't conceptually hard, just full of small bugs. WebDAV was dropped early because Go's golang.org/x/net/webdav library relies on io/fs, which doesn't support symlinks. The NFS client uses the go-nfs NFSv3 library. (macOS's FileProvider was mentioned as another possibility but not explored.)

Handling repositories with many commits

Repositories can have millions of commits, and listing all of them in a directory doesn't scale. The initial idea was to make the commits/ directory appear empty—every commit still resolvable by direct reference, but not enumerable. That works fine in FUSE but not NFS, which treats an empty directory listing as authoritative.

The workaround mirrors how git itself organizes loose objects in .git/objects: commits are stored under two levels of two-character prefixes, so a hash like 18d46e76d7c2eedd8577fae67e3f1d4db25018b0 lives at commits/18/18df/18d46e76d7c2eedd8577fae67e3f1d4db25018b0. Packed commit hashes are cached in memory (at 20 bytes each, a million hashes is only 20 MB) and only loose objects are refreshed afterward, since repacking is rare. On the Linux kernel repository (~1 million commits), the initial load takes about a minute, after which updates are fast.

A more elegant approach would lazily load commit listings via binary search in the packfile, since packfiles are sorted by commit ID. But the go-git library doesn't support that well—listing every commit in a repository is an unusual operation—and a couple days of effort didn't yield acceptable performance.

Debugging NFS: confusing errors

Several issues surfaced only as cryptic NFS error messages. A persistent not a directory error, for instance, turned out to be the library's generic response to any directory-listing failure. Likewise, cd: system call interrupted traced back to unrelated bugs in the program. Packet inspection with Wireshark proved invaluable for diagnosing these problems.

Inode numbers were another trap: setting every directory's inode to zero causes find to report filesystem loops. The fix was an inode(string) function that hashes the tree ID or blob ID to produce unique inode numbers.

Stale file handles and other loose ends

NFS file handles are opaque 64-byte identifiers, and the go-nfs library maps them to files via a fixed-size cache. Large repositories overflow that cache, producing Stale NFS file handle errors. Since 64 bytes is quite large, encoding the full path directly into the handle might avoid caching altogether—a possible future fix.

Several other limitations remain:

  • The branch_histories/ directory only lists the latest 100 commits per branch.
  • Submodules are ignored entirely.
  • The implementation uses NFSv3 because that was the only Go library available at the time; buildbarn's NFSv4 server exists but may not be intended for external use.

Despite these rough edges, the project demonstrates a viable path for mounting git history without kernel extensions, and it makes the "commit as folder" mental model tangible for anyone exploring how git works internally.