Когда объект только появился (от git add, git commit), он
лежит отдельным файлом - loose object: .git/objects/8d/0e41....
Удобно для записи, неудобно для хранения миллионов файлов.
Поэтому Git периодически сжимает их в packfiles.
Что внутри packfile
В .git/objects/pack/ появляются пары файлов:
pack-abc123def456.pack # сжатые объекты
pack-abc123def456.idx # индекс по SHA для быстрого поиска
Внутри .pack:
- объекты упакованы один за другим, каждый в zlib-сжатом виде;
- между похожими объектами вычисляется дельта - хранится только разница относительно базы;
- всё это дополнительно сжато на уровне потока.
Дельта-компрессия
Главная экономия в Git происходит здесь. Допустим, есть две версии большого файла, отличающиеся на пять строк. В loose-формате это два blob'а, каждый занимает почти полный размер файла. В packfile одна версия лежит целиком, вторая - как «возьми вот ту, примени следующие правки».
Алгоритм похож на xdelta/bsdiff. Git подбирает базу не по имени файла, а по эвристике: пытается найти похожий объект схожего типа и размера. Поэтому могут быть дельты между двумя совершенно разными файлами, если они случайно похожи.
Когда packfiles создаются
- При
git gc(manual или auto). - При
git push/fetch- стороны обмениваются объектами в packfile- формате, а не по одному.- При
git clone- сервер сразу отдаёт всё содержимое в packfile.
- При
Авто-gc срабатывает при превышении порогов:
- больше 6700 loose-объектов;
- больше 50 packfile'ов.
Эти пороги настраиваются: gc.auto, gc.autoPackLimit.
Чтение объектов из packfile
Команды Git работают одинаково независимо от того, где лежит
объект - в loose-формате или в packfile. git cat-file -p <sha>
найдёт объект в любой форме.
Просмотр содержимого pack'а руками:
git verify-pack -v .git/objects/pack/pack-abc.idx
# SHA type size packfile-offset base-SHA?
Видно, какие объекты - base, какие - дельта, и от кого они зависят.
Подводные камни
- Дельта не значит, что Git «диффы хранит». Логически модель данных остаётся snapshot-based (commit держит tree, а не diff). Дельта - это уровень физического хранения.
- При очень большом packfile отдельный read может быть медленным:
Git распаковывает цепочку дельт. Поэтому есть лимит
pack.deltaCacheSize. git gc --aggressiveпересчитывает дельты с нуля, выбирая лучшие базы. Долго, но даёт меньший размер. Стоит делать раз в несколько месяцев на больших проектах.