A read-only FUSE filesystem that exposes the full commit history of every file in a git repository as a browsable directory tree.
Files in the mount are directly readable (showing their latest version), and each directory contains a .gitversions/ folder with the full commit history of every file — named with the commit hash and message — accessible to any tool that can read files: editors, diff utilities, file managers, grep, scripts. Convenience symlinks are created in your actual repo directories so you can cd .gitversions from anywhere.
Git stores rich version history, but accessing it requires specialized commands. To read an old version of a file, you need to know the commit hash and run git show. To compare two versions, you need git diff with exact revisions. To browse how a file evolved, you need git log and then manually extract each version.
This is fine for developers, but it creates friction in workflows where version history should be ambient — always accessible, never requiring you to context-switch into git's CLI.
In academic writing — drafting papers, maintaining research notes, revising after feedback — you frequently need to:
- Reference earlier drafts: "What did I write in the version I submitted?" Open it directly.
- Compare revisions:
diff submitted-draft.md current.mdwith standard tools. - Search across history:
grepa concept across all versions to find when you added or removed it. - Tag milestones: mark submissions, feedback rounds, revisions — and find them instantly.
gitversions makes all of this possible with regular filesystem operations, no git commands required.
repo/
├── .gitversions → .git/versions/.gitversions ← symlink (auto-created)
├── drafts/
│ ├── .gitversions → ../.git/versions/drafts/.gitversions
│ └── paper.md
└── sources/
├── .gitversions → ../.git/versions/sources/.gitversions
└── @Kant-1781-critique.md
.git/versions/ ← mount point
├── _tags/
│ ├── submission/ ← full repo tree at tag "submission"
│ │ └── paper.md ← the file as it was when you tagged it
│ └── after-feedback/
│ └── paper.md
├── sources/
│ ├── @Kant-1781-critique.md ← readable file (latest version)
│ ├── deleted-file.md ← deleted files still accessible
│ └── .gitversions/ ← version history for this directory
│ ├── @Kant-1781-critique.md/ ← version directory per file
│ │ ├── abc1234--initial-notes.md ← version per commit
│ │ └── submission--ghi9012--final.md ← tagged versions show tag name
│ └── authors/ ← subdirs are symlinks for navigation
│ → ../authors/.gitversions
└── drafts/
├── paper.md ← readable file
└── .gitversions/
└── paper.md/
├── submission--abc1234--submit-v1.md
├── def5678--revisions-after-review.md
└── ghi9012--first-draft.md
Nothing is stored on disk. All file content is served on-demand via git show. Directory listings are resolved lazily from git ls-tree and git log. Results are cached in memory with a 5-second TTL so new commits appear quickly.
Each directory in your repo gets a .gitversions symlink pointing into the FUSE mount. From any directory, cd .gitversions gives you:
- File version directories: each file becomes a folder of its versions
- Subdirectory symlinks: navigate into child directories' histories seamlessly
working-copy.<ext>: symlink pointing to the actual file in your repoHEAD--prefix: the version matching the current HEAD is clearly marked
- Modified time (
st_mtime): set to the commit timestamp, sols -lshows when each version was committed - Extended attributes: every version file carries
user.git.hash,user.git.subject,user.git.author,user.git.date, anduser.git.tags— queryable viagetfattr
Git tags are exposed in two ways:
_tags/directory: browse the complete repo tree at any tag. Access_tags/submission/path/to/file.mdto read files as they were at that milestone.- Version filename prefix: in per-file version listings, tagged commits show the tag name first:
submission--abc1234--commit-message.md, making them immediately visible in any listing.
Tag names containing / are displayed with __ substitution (e.g., v1/release becomes v1__release).
Requires Linux with FUSE support (or macOS with macFUSE).
The script is a self-contained uv script — uv handles the Python environment and fusepy dependency automatically.
# Install uv if you don't have it
curl -LsSf https://astral.sh/uv/install.sh | sh
# Copy the script to your PATH
cp gitversions ~/.local/bin/
chmod +x ~/.local/bin/gitversionspip install fusepy
cp gitversions ~/.local/bin/
chmod +x ~/.local/bin/gitversions# Mount (runs in background by default)
mkdir -p .git/versions
gitversions /path/to/repo /path/to/repo/.git/versions
# Mount in foreground (for debugging)
gitversions /path/to/repo /path/to/repo/.git/versions --foreground
# Mount without creating .gitversions symlinks
gitversions /path/to/repo /path/to/repo/.git/versions --no-symlink
# Unmount
fusermount -u /path/to/repo/.git/versionsOn mount, .gitversions symlinks are automatically created in the repo root and every subdirectory. They are cleaned up on unmount/exit.
# From any directory in your repo, access version history
cd .gitversions
ls # files and subdirectory links
ls paper.md/ # all versions of paper.md
# Navigate into subdirectories (symlinks chain through)
ls .gitversions/sources/ # follows symlink to sources' history
# Or access via the mount directly
ls .git/versions/path/to/file.md # read latest version
ls .git/versions/.gitversions/file.md/ # list all versions
# Open a specific version in your editor
hx .gitversions/notes/idea.md/abc1234--first-draft.md
# Read an old version
cat .gitversions/paper.md/def5678--submitted-version.md# Diff two versions with any diff tool
diff .gitversions/paper.md/abc1234--draft-1.md \
.gitversions/paper.md/def5678--draft-2.md
# Side-by-side in vimdiff
vimdiff .gitversions/paper.md/abc1234--draft-1.md \
.gitversions/paper.md/def5678--final.md# Find when a concept was added
grep -l "phenomenology" .gitversions/notes/idea.md/*
# Search all versions of all files in a directory
grep -r "Heidegger" .git/versions/sources/# List all tags
ls .git/versions/_tags/
# Browse the repo at a tagged milestone
ls .git/versions/_tags/submission/
# Read a file as it was at a tag
cat .git/versions/_tags/after-feedback/paper.md
# Tagged versions are visible in per-file listings
ls .gitversions/paper.md/ | grep submission# Show all extended attributes on a version
getfattr -d .gitversions/paper.md/abc1234--some-change.md
# Get just the commit message
getfattr -n user.git.subject .gitversions/paper.md/abc1234--some-change.md
# Get the commit date
getfattr -n user.git.date .gitversions/paper.md/abc1234--some-change.md
# Check file timestamps (shows commit dates)
ls -lt .gitversions/paper.md/To keep the mount running automatically:
# ~/.config/systemd/user/gitversions.service
[Unit]
Description=Git Versions FUSE mount
After=local-fs.target
[Service]
Type=simple
ExecStartPre=-/usr/bin/fusermount -u %h/repo/.git/versions
ExecStartPre=/usr/bin/mkdir -p %h/repo/.git/versions
ExecStart=%h/.local/bin/gitversions %h/repo %h/repo/.git/versions --foreground
ExecStop=/usr/bin/fusermount -u %h/repo/.git/versions
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.targetThe fusermount -u in ExecStartPre (with - prefix to ignore failure) cleans up stale mounts from previous crashes.
systemctl --user daemon-reload
systemctl --user enable --now gitversions- Read-only: writes return
EROFS. This is by design — git history is immutable. - Main tree includes deleted files: the top-level directory structure shows all files that ever existed (including deleted ones). The
.gitversions/directories only show current HEAD files. - No Windows support: requires FUSE (Linux kernel module or macFUSE on macOS). Windows would need WinFsp with a different Python binding.
- Single-threaded: uses
nothreads=Truefor simplicity. Fine for interactive use on small-to-medium repos. - Symlinks require repo write access:
.gitversionssymlinks are created in the actual repo. Use--no-symlinkif the repo is read-only.
Existing git FUSE tools take a commit-centric approach — mounting one commit at a time (git-fs) or syncing a remote repo (gitfs). None of them provide a file-centric view where you can browse all versions of individual files.
gitversions is the inverse: organized around files, not commits. You navigate to a file and see its full history, rather than navigating to a commit and seeing its full tree.
MIT