Объектная модель Git: blob, tree, commit
Открываем капот: как git на самом деле хранит ваши файлы, коммиты и историю.
Объектная модель git — это маленькая база данных типа «ключ-значение», где ключ — хеш содержимого, а значение — один из четырёх объектов: blob, tree, commit или tag.
Вы уже умеете коммитить, ветвиться и сливать ветки. Но что такое коммит физически? Где лежат файлы? Почему хеш коммита выглядит как a1b2c3d? Ответы на эти вопросы превращают git из набора заклинаний в понятный инструмент: вы перестаёте бояться reset, начинаете чинить «безнадёжно сломанную» историю и понимаете, почему git такой быстрый.
Зачем это знать на практике
Понимание объектной модели — не академическая роскошь. Оно напрямую экономит время в трёх ситуациях:
- Восстановление данных. Зная, что коммиты — это объекты с хешами, вы понимаете, что «удалённый» коммит обычно физически на месте, и его можно вернуть.
- Отладка странного поведения. Когда merge или rebase ведёт себя неожиданно, привычка думать «какой объект на что указывает» быстро выводит на причину.
- Уверенность. Команды вроде
reset --hardперестают пугать, когда вы знаете, что они двигают указатели, а не стирают историю.
Content-addressable storage
Git — это content-addressable хранилище: адрес объекта вычисляется из его содержимого. Берём содержимое, прогоняем через хеш-функцию, получаем 40-символьную строку в шестнадцатеричном виде (SHA-1) — это и есть имя объекта. Одинаковое содержимое всегда даёт один и тот же хеш, поэтому два одинаковых файла хранятся ровно один раз.
Проверим руками. Команда git hash-object считает хеш так же, как это делает git:
echo "hello" | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a
Этот хеш не случайный. Git берёт не голый текст, а добавляет заголовок: тип объекта, пробел, длину в байтах и нулевой байт. Для нашего примера склеивается строка blob 6\0hello\n, и уже от неё считается SHA-1. Поэтому пустой файл и файл со словом «hello» дают разные предсказуемые хеши на любой машине в мире.
Четыре типа объектов
В хранилище живут всего четыре вида объектов. Большую часть истории составляют первые три.
| Объект | Что хранит |
blob | Содержимое одного файла — только байты, без имени и прав. |
tree | Каталог: список имён с правами и ссылками на blob'ы и вложенные tree. |
commit | Снимок: ссылка на корневой tree, родитель(и), автор, дата, сообщение. |
tag | Аннотированный тег: ссылка на объект, имя тегировщика, дата, сообщение. |
blob — содержимое файла
Blob хранит только байты файла. В нём нет имени файла, нет даты, нет прав доступа — всё это живёт уровнем выше, в tree. Поэтому если у вас в проекте два файла с одинаковым содержимым (скажем, две пустые __init__.py), git хранит один blob и ссылается на него дважды.
tree — каталог
Tree — это и есть «папка». Каждая его строка — это режим доступа, тип, хеш и имя. Заглянуть внутрь дерева последнего коммита можно так:
git cat-file -p HEAD^{tree}
# 100644 blob 1f7a7a4... README.md
# 040000 tree 9d2e3b1... src
Режим 100644 — обычный файл, 100755 — исполняемый, 040000 — вложенный каталог (ещё один tree). Так из плоских объектов собирается дерево всего проекта.
commit — снимок во времени
Коммит на удивление маленький. Он не содержит файлов — только ссылку на корневой tree и метаданные:
git cat-file -p HEAD
# tree 8e5a9c...
# parent 4b6f0d...
# author Anna <[email protected]> 1718000000 +0300
# committer Anna <[email protected]> 1718000000 +0300
#
# Добавить форму входа
Строка parent связывает коммиты в цепочку — так получается история. У обычного коммита один родитель, у merge-коммита их два и больше, у самого первого коммита родителя нет вовсе.
Снимки, а не дельты
Это ключевая идея, отличающая git от старых систем (Subversion, CVS). Они хранили историю как дельты — наборы изменений «что добавилось и удалилось» от версии к версии. Git хранит полные снимки: каждый коммит ссылается на дерево, описывающее состояние всех файлов целиком.
Звучит расточительно, но спасает дедупликация по содержимому. Если между коммитами файл не менялся, его blob имеет тот же хеш — и новый tree просто ссылается на старый blob. Не дублируется ни байта. Изменился один файл из тысячи — появится один новый blob и цепочка новых tree до корня, всё остальное переиспользуется. Git хранит снимки, но платит только за реальные изменения.
SHA-1 и переход на SHA-256
Исторически git использует SHA-1 — 160-битный хеш, те самые 40 hex-символов. В 2017 году исследователи показали практическую коллизию SHA-1 (атака SHAttered). Для git риск ограничен: репозиторий — не средство защиты от злоумышленника с доступом к истории, плюс начиная с версии 2.13 включена защита от известного класса коллизий. Тем не менее проект движется к SHA-256: его можно выбрать при создании репозитория.
git init --object-format=sha256 myrepo
В таком репозитории хеши становятся 64-символьными. Совместимость SHA-1 и SHA-256 репозиториев между собой пока ограничена, поэтому в реальных проектах по умолчанию всё ещё SHA-1 — но саму архитектуру это не меняет: модель «хеш содержимого → объект» одинакова.
Как это работает под капотом
Соберём цепочку целиком. Когда вы делаете git commit, git: (1) для каждого файла в индексе создаёт или переиспользует blob; (2) строит tree-объекты, описывающие каталоги; (3) создаёт commit-объект, ссылающийся на корневой tree и на предыдущий коммит. Все объекты записываются в каталог .git/objects/ под именем своего хеша. Указатель текущей ветки сдвигается на новый коммит.
Важно: объекты иммутабельны. Изменить существующий объект нельзя — любое изменение содержимого даёт другой хеш, то есть другой объект. Поэтому git commit --amend и rebase на самом деле не «правят» коммиты, а создают новые объекты и переставляют на них указатели. Старые объекты остаются в базе, пока их не уберёт сборка мусора.
Частые ошибки
- «Git хранит дельты». Нет, на уровне модели это снимки; дельты появляются только при упаковке в packfile (об этом — в уроке про
.git), и это деталь хранения, а не модели. - «Хеш коммита зависит только от файлов». Нет: в него входят автор, дата, сообщение и родитель. Поэтому два коммита с идентичными файлами, но разным временем — это разные объекты с разными хешами.
- «hash-object что-то меняет в репозитории». Без флага
-wкоманда только считает хеш и ничего не записывает;-wдобавляет объект в базу. - Путать blob и файл. Blob не знает своего имени — имя хранится в tree. Один blob может быть «несколькими файлами» одновременно.
Итоги
- Git — content-addressable хранилище: имя объекта = хеш его содержимого (SHA-1, опционально SHA-256).
- Четыре объекта:
blob(файл),tree(каталог),commit(снимок + метаданные),tag(аннотированный тег). - Коммиты хранят полные снимки, а не дельты; одинаковое содержимое дедуплицируется по хешу.
- Объекты иммутабельны:
amendиrebaseсоздают новые объекты, а не правят старые. - Инструменты для исследования:
git hash-objectиgit cat-file -p <hash>.