Автор задумался: а кто-нибудь когда-нибудь делал FUSE-файловую систему для git-репозитория, где каждый коммит — это папка? Оказывается, да: есть giblefs, GitMounter и git9 для Plan 9. Но на Mac FUSE неудобен — нужно ставить расширение ядра, а macOS всё сильнее его блокирует. И у автора были свои идеи, как организовать такую ФС. Так появился проект git-commit-folders. Он работает с FUSE и NFS, а ещё есть сломанная реализация на WebDav.
Главная идея: каждый коммит git — это обычная папка с файлами. Ветки и теги — симлинки на эти папки. Можно ходить по старой истории как по обычному дереву каталогов. Например, ls commits/8d/8dc0/... покажет содержимое конкретного коммита. Утилитарно это позволяет делать grep по всем версиям файла, быстро смотреть файл из другой ветки через vim branches/other-branch/go.mod — в обход сложного синтаксиса git log -S.
С реализацией было много проблем. Из трёх интерфейсов (fs.FS для FUSE, billy.Filesystem для NFS, webdav.Filesystem) автор решил не дублировать код — друг Дэйв посоветовал написать одну логику на fs.FS, а потом два адаптера: Fuse2NFS и Fuse2Dav. WebDav отпал потому, что библиотека golang.org/x/net/webdav не поддерживает симлинки (там упёрлись в интерфейс io/fs).
Проблема с миллионами коммитов: нельзя показывать их все в commits/. Автор сделал пустой корень в FUSE — коммит доступен, если знаешь его хеш, но не листится. В NFS такой фокус не прошёл: пустая папка — значит пустая. Решение — организовать хранение с двумя уровнями префиксов (как в .git/objects): commits/18/18df/. Все упакованные хеши кэшируются в памяти (20 байт на коммит, для миллиона — 20 МБ), обновляются только «рыхлые» объекты. На ядре Linux (~1 млн коммитов) начальная загрузка занимает минуту, дальше — быстро.
Автор хотел загружать коммиты лениво, но используемая git-библиотека не умела делать двоичный поиск по паковым файлам. Попытка переписать это заняла пару дней, производительность не устроила — бросил.
Странные ошибки NFS: Not a directory (os error 20) на самом деле означала сбой при листинге папки. cd: system call interrupted — просто баг в коде. Помог Wireshark — автор стал смотреть NFS-пакеты. Ещё он случайно ставил всем inode = 0, из-за чего find думал, что файлы зациклены. Починил хеш-функцией от ID дерева/блобов.
Главная нерешённая проблема — Stale NFS file handle. Библиотека go-nfs кэширует file handle в фиксированном кэше, который переполняется на больших репозиториях. Автор пока не знает, как это исправить. Ему кажется, что можно просто кодировать путь в 64-байтовый handle и не кэшировать вовсе.
branch_histories/ пока показывает только последние 100 коммитов на ветку. Субмодули игнорируются (автор в них не разбирается). Используется NFSv3, потому что под рукой была только эта библиотека; уже после работы автор нашёл сборку buildbarn с NFSv4 — но не уверен, стоит ли переходить.
Проект экспериментальный, но автору нравится самому в нём копаться. Отдельное спасибо другу vasi — тот объяснил кучу тонкостей про файловые системы.