Устройство .git: packfiles, индекс, gc

Заглядываем внутрь каталога .git: где лежат объекты, что такое packfile и как git экономит место.

Packfile — это сжатый архив, в который git упаковывает множество объектов, чтобы хранилище занимало мало места и быстро передавалось по сети.

Мы разобрали объекты, ссылки и reflog. Осталось собрать всё в одном месте — каталоге .git/. Это и есть весь ваш репозиторий: история, ветки, настройки. Поняв его устройство, вы перестаёте воспринимать .git как чёрный ящик и осознанно пользуетесь git gc, понимаете формат индекса и знаете, что именно копируется при clone.

Зачем это знать на практике

Это знание выручает, когда репозиторий «вдруг» занимает гигабайты, когда нужно понять, почему clone тянется долго, или когда коллега спрашивает, можно ли удалить .git (нельзя — это и есть весь проект с историей). Плюс становится ясно, что git add — это не «пометка», а реальная запись в дерево индекса.

Что внутри .git

Заглянем в каталог сразу после git init:

ls -F .git
# HEAD          config        index
# objects/      refs/         hooks/
# logs/         info/         description
ПутьНазначение
objects/База всех объектов: blob, tree, commit, tag.
refs/Ветки (heads/), теги (tags/), remote-ветки.
HEADГде вы сейчас (ссылка на текущую ветку).
indexИндекс (staging area) — следующий коммит в виде дерева.
configНастройки этого репозитория (remote, user и пр.).
logs/reflog — журнал движений HEAD и веток.

Объекты: loose против packed

Новый объект git сначала записывает как loose — отдельный файл в objects/. Имя — хеш объекта: первые 2 символа становятся именем подкаталога, остальные 38 — именем файла. Так быстрее искать и не возникает каталога с миллионом файлов:

find .git/objects -type f | head
# .git/objects/1f/2e3d4c5b6a7980...   (один loose-объект)
# .git/objects/4b/6f0d8a9c1e2f3a...

Каждый loose-объект сжат zlib, но лежит сам по себе. Когда объектов становятся тысячи, это расточительно: много мелких файлов, повторяющиеся куски не делят между собой место. Тогда git переходит к упаковке.

Packed-объекты — это когда git собирает множество loose-объектов в один packfile (.git/objects/pack/pack-*.pack) и строит к нему индекс (pack-*.idx) для быстрого поиска по хешу:

ls .git/objects/pack/
# pack-7d3a....idx
# pack-7d3a....pack

Дельта-сжатие в packfile

Вот где появляются дельты, которых нет в модели. Внутри packfile git хранит часть объектов не целиком, а как разницу от похожего объекта. Если у вас 10 версий большого файла, git может сохранить одну версию целиком, а остальные — как «отличия от неё». Это резко уменьшает размер: текстовый файл, выросший за сотни коммитов, в паке занимает на порядки меньше, чем сумма его loose-версий.

Подчеркнём важное, чтобы не путать уровни: модель git по-прежнему оперирует снимками (каждый коммит ссылается на полный tree), а дельты — это деталь хранения внутри packfile. Снаружи вы всегда видите полные объекты; git cat-file -p отдаст содержимое одинаково, лежит объект loose или в паке.

git gc: уборка и упаковка

Команда git gc (garbage collection) наводит порядок: упаковывает loose-объекты в packfile, применяет дельта-сжатие, удаляет недостижимые объекты (с учётом сроков из урока про reflog) и подчищает старые записи. Git запускает её и сам, фоном, после некоторых операций, но можно вызвать вручную:

git count-objects -vH      # сколько объектов и сколько весят
# count: 1280            (loose-объектов)
# size: 5.10 MiB
# in-pack: 0

git gc
git count-objects -vH
# count: 0
# in-pack: 1280          (теперь упакованы)
# size-pack: 740.00 KiB  (стало в разы меньше)

Видно главный эффект: 1280 loose-объектов на 5 МиБ превратились в один пак на 740 КиБ. Та же история, меньше места, быстрее доступ. Агрессивную (более медленную, но плотную) упаковку даёт git gc --aggressive — её имеет смысл изредка запускать на больших старых репозиториях.

Индекс и staging как дерево

Файл .git/index — это и есть staging area. Внутри он бинарный и хранит отсортированный список записей: путь файла, его права, хеш blob'а и кеш метаданных (время изменения, размер) для быстрого git status. По сути индекс — это плоское представление будущего корневого tree.

Поэтому git add file делает не «пометку», а реальную работу: создаёт blob из текущего содержимого файла и записывает его хеш в индекс. Отсюда классическая ловушка: если после git add ещё раз отредактировать файл, в коммит попадёт версия на момент add — потому что её blob уже зафиксирован в индексе. Заглянуть в индекс можно пламбингом:

git ls-files --stage
# 100644 1f2e3d4c... 0    README.md
# 100644 8a9b0c1d... 0    src/app.py

А команда git write-tree превращает текущий индекс в настоящий tree-объект — ровно это git и делает внутри git commit: фиксирует индекс как корневой tree нового коммита.

Как это работает под капотом

Соберём полный цикл. git add пишет blob'ы и обновляет .git/index. git commit вызывает (логически) write-tree, превращая индекс в tree, создаёт commit-объект со ссылкой на этот tree и на родителя, затем сдвигает указатель ветки. Объекты копятся как loose в .git/objects/. Периодически git gc упаковывает их в packfile с дельта-сжатием. При git clone сервер отдаёт историю в основном сразу паком — поэтому передаётся компактно, а у вас на диске сразу появляется pack-*.

Частые ошибки

  • Удалить .git, чтобы «почистить». Это удаляет весь репозиторий: историю, ветки, настройки. Останется только рабочая копия файлов без git.
  • Считать, что модель git хранит дельты. Дельты — только внутри packfile (деталь хранения). Модель — снимки; снаружи объекты всегда целые.
  • Думать, что git add просто помечает файл. Он создаёт blob и пишет его в индекс; правки после add в коммит не попадут без повторного add.
  • Бояться, что gc сломает историю. gc не трогает достижимые объекты — только упаковывает и убирает действительно ненужное; история сохраняется.

Итоги

  • .git/ — это весь репозиторий: objects/ (объекты), refs/ (ветки/теги), index (staging), config, logs/ (reflog).
  • Объекты бывают loose (отдельные сжатые файлы) и packed (упакованы в packfile с дельта-сжатием).
  • Дельты живут только внутри packfile — это деталь хранения; модель git остаётся снимками.
  • git gc упаковывает loose-объекты, применяет дельты и убирает недостижимое, резко уменьшая размер.
  • Индекс (.git/index) — плоское дерево будущего коммита; git add уже создаёт blob, а git commit фиксирует индекс как tree.
Проверьте себя
1. В чём разница между loose- и packed-объектами в git?
Aloose-объекты хранятся на сервере, а packed — локально
Bloose — отдельные сжатые файлы в objects/, packed — собраны в один packfile с дельта-сжатием
Cloose-объекты нельзя прочитать через git cat-file, а packed можно
Dloose хранят снимки, а packed — только дельты модели
2. Где в git реально появляются дельты (разницы между версиями)?
AВ каждом коммите — он хранит дельту от предыдущего
BТолько внутри packfile как деталь хранения; сама модель остаётся снимками
CВ индексе .git/index при каждом git add
DНигде, git вообще не использует дельты
3. Что делает команда git add с файлом на уровне объектной модели?
AПросто помечает файл как готовый к коммиту, не трогая содержимое
BСоздаёт blob из текущего содержимого и записывает его хеш в индекс
CСразу создаёт commit-объект
DКопирует файл в .git/objects/pack/