Skip to content

gitmount

Mount a git repository as a read-only filesystem.

Browse every branch, tag and commit with plain ls, cat, grep and diff — no checkout, no worktree, no archive.

CI License: GPL v3 C++17 Platform libgit2 FUSE 3

English · 简体中文


$ sudo mount -t gitmount /srv/repos/linux.git /mnt/linux
$ ls -A /mnt/linux
.gitmount.json  HEAD  branch  commit  commits  remote  tag
$ ls /mnt/linux/tag
v5.4  v6.1  v6.6  v6.12  v6.13
$ grep -n '^VERSION\|^PATCHLEVEL' /mnt/linux/tag/v6.13/Makefile
1:VERSION = 6
2:PATCHLEVEL = 13
$ diff -r --brief /mnt/linux/tag/v6.12 /mnt/linux/tag/v6.13 | head -3
Files /mnt/linux/tag/v6.12/Makefile and /mnt/linux/tag/v6.13/Makefile differ
Only in /mnt/linux/tag/v6.13: .cargo
...
$ grep ^a1b2c3d /mnt/linux/commits          # resolve an oid prefix
a1b2c3d4e5f6...  (full 40-char oid)
$ sudo umount /mnt/linux

Why gitmount?

Comparing files across revisions normally means juggling git worktree add / git archive, or cloning twice. gitmount makes every ref and commit of a local repository appear as a plain directory tree, once, and lets the tools you already have do the rest:

  • Diff anything against anything. diff -r /mnt/tag/v6.12 /mnt/tag/v6.13 — branches, tags, remotes and raw commits are all just directories.
  • Safe by construction. The mount is forcibly ro,nosuid,nodev,default_permissions; every write path returns EROFS. Nothing you do through the mount can modify the repository.
  • Ordinary tooling. Editors, grep, find, tar, rsync, IDEs — no git knowledge required on the consumer side.
  • Self-contained. A single mount(8) helper built on libgit2 and libfuse3; no git subprocesses, no network access.

A note on naming: during early development this project was briefly called "gitfs". That name belongs to presslabs/gitfs — a different, older project (Python, read-write, cloud-storage backends) — hence the rename to gitmount. See the RFC for the comparison.

Features

  • Full tree semantics: directories, regular/executable files, symlinks (raw-byte names included), submodules as empty dirs + marker files
  • All ref namespaces: branch/, tag/ (annotated tags peeled), remote/, HEAD/, and any commit by full oid under commit/
  • Live refs: branches and tags created after mounting show up without a remount; already-resolved objects stay accessible
  • Deterministic listings: every directory enumerates in raw byte (memcmp) dictionary order — stable, reproducible, locale-free
  • Caching that keeps its promises: blob LRU + libgit2 tree cache; oversized blobs decompress exactly once per open (asserted by tests, observable via -v)
  • Hardened baseline: refs/replace never followed, non-UTF-8 names passed through verbatim, pathological hand-crafted objects skipped with warnings
  • First-class mount(8) citizen: mount -t gitmount, /etc/fstab entries and direct invocation are all equivalent; man page included

Requirements

Dependency Version Debian/Ubuntu Fedora Arch
C++ compiler C++17 build-essential gcc-c++ gcc
CMake ≥ 3.16 cmake cmake cmake
pkg-config — pkg-config pkgconf pkgconf
libgit2 ≥ 1.4 libgit2-dev libgit2-devel libgit2
libfuse3 ≥ 3.10 libfuse3-dev fuse3-devel fuse3

Unit tests fetch Catch2 v3 via CMake FetchContent (network at configure time), or point FETCHCONTENT_SOURCE_DIR_CATCH2 at a local copy. Integration tests need /dev/fuse + fusermount3 and skip cleanly otherwise.

Installation

Arch Linux (AUR)

paru -S gitmount          # or: yay -S gitmount

No AUR helper needed:

git clone https://aur.archlinux.org/gitmount.git
cd gitmount
makepkg -si

The package builds from the release source tarball and runs the full test suite in check() — the integration tests perform real FUSE mounts and self-skip where /dev/fuse is unavailable. It installs /usr/bin/mount.gitmount together with its man page (man 8 mount.gitmount).

Binary tarballs

Each GitHub release attaches prebuilt tarballs for two glibc bases (2.35 / 2.39), with SPDX SBOMs, SHA-256 checksums and build attestations.

From source

git clone https://github.com/OrbitZore/gitmount
cd gitmount
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build             # optional: unit + integration
sudo cmake --install build         # /usr/sbin/mount.gitmount + man8 page

Quick start

The three equivalent invocation forms:

sudo mount -t gitmount /path/to/repo.git /mnt/gitmount     # via mount(8)
sudo mount /mnt/gitmount                                # via /etc/fstab
sudo mount.gitmount /path/to/repo.git /mnt/gitmount        # direct

/etc/fstab example (auto-mount at boot, survive missing repo):

/srv/repos/linux.git  /mnt/linux  gitmount  ro,noatime,nofail  0  0

Unmount with sudo umount /mnt/gitmount (or fusermount3 -u). The daemon also exits gracefully on SIGINT/SIGTERM. Validate a configuration without mounting — including repository readability — with -f:

mount.gitmount /srv/repos/linux.git /mnt/linux -f && echo config OK

Usage

Layout

/mnt/gitmount/
├── branch/      # local branches (nested names render as directories)
├── tag/         # tags, peeled to commits
├── commit/      # any commit by full oid — never listable
├── remote/      # remote-tracking refs: <remote>/<branch>
├── HEAD/        # snapshot of the current HEAD
├── commits      # every reachable commit oid, one per line
└── .gitmount.json  # mount metadata (immutable snapshot)

Everyday tasks

diff -r /mnt/tag/v6.12 /mnt/tag/v6.13              # compare two tags
grep -rn "TODO" /mnt/branch/topic-branch/src       # search a branch
grep ^a1b2c3d /mnt/commits                         # resolve an oid prefix
rsync -a /mnt/commit/<oid>/ /tmp/snapshot/         # materialize a commit
tar -C /mnt/tag/v6.13 -czf v6.13.tgz .             # archive a tag

/commit/<oid> accepts only the full lowercase hex oid (40 or 64 chars); commit/ itself is never listable by design — grep the commits file instead.

Options

-o blob-cache-size=<MiB>    blob LRU cache limit (default 64)
-o tree-cache-size=<MiB>    libgit2 tree/commit cache budget (default 256)
--blob-cache-size <MiB>     same as -o blob-cache-size=<MiB> (joined = form works too)
--tree-cache-size <MiB>     same as -o tree-cache-size=<MiB>
--foreground                stay in the foreground (default: daemonize)
-f                          fake: validate arguments and repository, don't mount
-v, --verbose               resolution logs + one line per blob decompression
--version, --help

rw in -o is accepted as a no-op with a warning (mount(8) pre-seeds it unconditionally). Irrelevant VFS keys (noatime, nofail, user, …) are accepted and ignored. suid, dev, remount, uid=, gid=, umask=, context=-family options and subtype= are rejected — the read-only baseline is not negotiable. Anything else passes through to libfuse (e.g. kernel_cache, allow_other).

Exit codes: 0 success · 1 parameter error · 2 repository unreadable · 3 mount failure.

Documentation

Performance

Measured on a 5,000-file synthetic tree (details in the maintenance checklist):

Workload ext4 gitmount
find -type f (metadata walk) 6 ms 49 ms
open+read+close, 4-deep path 5 µs ~350 µs
direct blob reads (libgit2, no FUSE) — 4.1 µs/blob

Content-heavy loads scale with the data plane; metadata storms over tiny files are latency-bound because metadata timeouts are pinned to zero (a moved or deleted ref must be visible immediately — correctness first). Oversized blobs decompress exactly once per open.

Semantics worth knowing

The sharp edges pinned by the design (full contract: docs/filesystem-semantics.md):

  • /commits pauses on first open — a full revision walk runs once, on the first open(). First cat on a Linux-kernel-scale repository blocks for seconds; stat before that reports size 0 and never triggers it.
  • Oversized blobs pin at open — a blob at/above the blob-cache limit decompresses once at open() and stays in memory until close. Opening a multi-GB blob takes seconds; concurrent requests are not blocked (decompression runs outside the global lock).
  • Disable gc while mounted (recommended) — external git gc/git prune can cause transient ENOENT/EIO for in-flight requests; they recover once gc finishes. Or set git config gc.auto 0 on long-mounted repositories.
  • refs/replace is never followed — output matches git --no-replace-objects.
  • st_ino is path-derived and never recycled — budget tens of MB per million distinct touched paths.
  • Cache limits are byte-exact — --blob-cache-size and --tree-cache-size bound their caches precisely (metadata is cached as raw bytes + a tiny index, not parsed objects). RSS additionally contains the st_ino registry (~230 B per touched path) and mmap'd packfile pages (file-backed, reclaimable). Tune --tree-cache-size first on memory-constrained machines.

FAQ

Can I write through the mount? No — every write path returns EROFS. gitmount is a read-only data plane by design.

Is it safe to mount untrusted repositories? Symlink targets are repository-controlled and passed through verbatim (like git checkout), and paths are confined to the mountpoint by the VFS. Do not browse untrusted repositories as root.

sha256 repositories? Yes — oids are 64 hex chars throughout.

Why does df show 100% usage? A read-only volume has no writable space; statfs reports the local object database as the volume size and zero free space. Alternates-backed storage is not counted.

Why do some filenames look garbled? Tree entry names are raw bytes; a non-UTF-8 name appears exactly as it would after git checkout — no escaping, no renaming.

How do I match mounts? /proc/mounts shows the type as fuse.gitmount with source gitmount: findmnt -t fuse.gitmount.

Wasn't this called gitfs? During early development, yes. That name belongs to presslabs/gitfs (Python, read-write, cloud backends — a different project), so this project became gitmount.

Contributing

Contributions follow CONTRIBUTING.md (Conventional Commits, tests required, -Werror CI). The community standards are in CODE_OF_CONDUCT.md; security issues follow SECURITY.md; changes are tracked in CHANGELOG.md.

Acknowledgments

Built on libgit2, libfuse and Catch2; validated against the behavior of util-linux mount(8) and git itself. The design was shaped by 39 rounds of review recorded in the RFC.

License

GPL-3.0-or-later © The gitmount authors.

About

Browse every git branch, tag and commit as a read-only filesystem.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages