open atlas
↑ К треку
Git: от нуля до сеньора GIT · 08 · 01

Хранилище объектов — blob, tree, commit, tag

Git хранит всё как четыре типа объектов с адресацией по содержимому: blob (байты файла), tree (каталог), commit (одно дерево плюс родители и метаданные) и tag. Имя объекта — это SHA его содержимого, поэтому одинаковое содержимое хранится один раз. Смотри через git cat-file.

GIT Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Ты делал git commit тысячу раз. Но что такое коммит на диске? Не дифф, не строка в базе данных — это крошечный файл, чьё имя есть хеш его собственного содержимого, указывающий на другие файлы, чьи имена тоже хеши их содержимого. Как только ты это увидишь, ветки, слияния и reset перестанут быть магией и станут арифметикой указателей над хранилищем объектов, в которое только дописывают.

К концу этого урока ты сможешь назвать четыре типа объектов git, объяснить, почему их имена — это хеши содержимого, и спуститься от коммита к байтам файла с помощью git cat-file.

Цель

После этого урока ты сможешь описать четыре типа объектов git — blob, tree, commit, tag — объяснить адресацию по содержимому и дедупликацию, которую она даёт, и использовать git cat-file и git hash-object, чтобы рассмотреть реальные объекты за коммитом.

1

Всё, что хранит git, — это один из четырёх типов объектов. Под .git/objects git держит key-value-хранилище неизменяемых объектов, каждый из ровно четырёх видов:

  • blob — сырые байты содержимого одного файла. У blob нет имени файла и нет прав; это просто содержимое.
  • tree — один каталог. Tree — это список записей, каждая mode + type + hash + name, указывающих на blob (файлы) и другие tree (подкаталоги). Имена живут здесь, а не в blob.
  • commit — снимок. Указывает ровно на одно корневое дерево, перечисляет ноль или больше родительских коммитов и несёт автора, коммиттера, метки времени и твоё сообщение.
  • tag — объект аннотированного тега: имя, указывающее на другой объект (обычно коммит), плюс тэггер и сообщение. (Легковесные теги — это просто ссылки, объект они не создают.)
2

Имя объекта — это хеш его содержимого; это и есть адресация по содержимому. Git берёт тип объекта, длину и байты, хеширует их и использует этот хеш как адрес объекта. Исторически хеш — это SHA-1 (40 hex-символов); git мигрирует на SHA-256. Следствие огромно: имя выводится из содержимого, поэтому ты не можешь изменить содержимое, не изменив имя.

# Захешировать содержимое так, как это делает git, не сохраняя его
echo 'hello' | git hash-object --stdin
# -> ce013625030ba8dba906f756967f9e9ca394464a
3

Одинаковое содержимое хранится ровно один раз. Поскольку адрес — это хеш содержимого, два файла с одинаковыми байтами — или один и тот же неизменный файл в сотне коммитов — дают одинаковый blob-хеш и занимают один объект. Коммит, который трогает один файл в дереве из 10 000 файлов, не копирует остальные 9 999 blob; его новые tree просто заново указывают на неизменные хеши. Снимки дёшевы именно потому, что общее содержимое разделяется по адресу.

git rev-parse HEAD            # хеш объекта-коммита
git cat-file -t HEAD          # -> commit   (спросить тип объекта)
git cat-file -p HEAD          # -> красиво вывести: tree, parent, author, message
Почему это работает

Адресация по содержимому — это ещё и проверка целостности git. Если байт объекта повреждён на диске, его содержимое больше не совпадает с именем, и git fsck (или любое чтение, которое его перехеширует) обнаружит расхождение. Вся история — это Merkle DAG: хеш коммита фиксирует хеш своего дерева, который фиксирует каждый blob-хеш под ним, а это значит, что один SHA на вершине криптографически фиксирует всю достижимую историю. Измени что-нибудь глубоко в прошлом — и каждый хеш-потомок изменится.

4

Ты можешь спуститься от коммита к байтам файла вручную. Каждый cat-file -p снимает один слой:

git cat-file -p HEAD          # commit -> показывает "tree <hash>"
git cat-file -p HEAD^{tree}   # tree   -> строки: mode type hash name
git cat-file -p <blob-hash>   # blob   -> реальное содержимое файла

Строка дерева вроде 100644 blob a1b2c3… README.md говорит: обычный файл (100644), хранится как blob a1b2c3…, имя README.md. Исполняемые файлы используют режим 100755; подкаталоги выглядят как 040000 tree …. Это и есть вся модель файловой системы.

5

Loose-объекты упаковываются ради эффективности. Только что записанный объект — это loose-объект: один сжатый zlib файл по пути .git/objects/ab/cdef… (первые два hex-символа — каталог). Когда их накапливается много, git gc переписывает многие из них в packfile (.git/objects/pack/*.pack), который хранит объекты вместе и дельта-сжимает похожие друг против друга. Те же идентичности с адресацией по содержимому, но более плотное хранение — поэтому репозиторий с глубокой историей куда меньше суммы своих снимков.

Разбор примера

Доказываем дедупликацию двумя коммитами.

Инициализируем репозиторий и коммитим файл:

git init demo && cd demo
printf 'hello\n' > a.txt
git add a.txt && git commit -m "add a"
git rev-parse HEAD:a.txt      # -> ce013625…  (blob-хеш для a.txt)

Теперь копируем те же байты во второй файл и коммитим:

cp a.txt b.txt
git add b.txt && git commit -m "add b (те же байты)"
git rev-parse HEAD:b.txt      # -> ce013625…  (ИДЕНТИЧНЫЙ хеш)

Оба файла называют один и тот же blob — git сохранил содержимое один раз и направил на него две записи дерева. Два коммита отличаются только своими деревьями (которые теперь перечисляют два имени) и ссылками на родителя. Измени один байт b.txt — и его blob-хеш полностью изменится; старый blob останется, по-прежнему достижимый из первого коммита. Ничего никогда не правится на месте — хранилище только растёт.

Частая ошибка

Частое заблуждение: «blob хранит имена файлов и права». Нет. Blob — это только содержимое; именно поэтому одинаковый файл под двумя именами дедуплицируется в один blob. Имена и режимы живут в записи tree, которая указывает на blob. Из-за этого же разделения переименование файла с неизменным содержимым вообще не добавляет нового blob; меняется только дерево.

Проверь себя
Викторина

Два разных файла в одном коммите содержат побайтово идентичное содержимое. Сколько объектов blob git сохранит для них?

Итог

Git — это хранилище объектов с адресацией по содержимому и четырьмя типами: blob держат байты файлов, tree — это каталоги, отображающие имена и режимы на хеши blob/tree, commit указывают на одно дерево плюс родительские коммиты и метаданные, а tag аннотируют другой объект. Имя каждого объекта — это SHA его содержимого, что даёт бесплатную дедупликацию (одинаковое содержимое хранится один раз) и целостность (повреждение меняет имя). Любой коммит можно спустить до байтов через git cat-file -p, а git gc упаковывает loose-объекты в дельта-сжатые packfile. Дальше ты увидишь, как удобные для человека ссылки — ветки, теги и HEAD — это просто имена, указывающие в это хранилище.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.