Объектная модель Git: blob, tree, commit, tag
В уроке 1 модуля 2 мы прошли content-addressable storage концептуально: Git хранит каждый объект по его SHA-1 хешу. В уроке 4 этого модуля мы посмотрели commit-объект изнутри. Теперь финальный кусок пазла: какие вообще типы объектов есть в Git, и как они связаны.
Это самый «низкоуровневый» урок курса. Junior может пропустить и продолжать работать с Git — миллион людей делают это. Но если вы прочитали и поняли — у вас будет ментальная модель, которая делает Git кристально прозрачным. Любая команда, любая ошибка, любое странное состояние — вы будете понимать «что произошло на уровне объектов».
4 типа объектов
В Git ровно 4 типа объектов:
Каждый объект:
- Имеет один из этих типов
- Хранится в
.git/objects/<первые-2-hex>/<остальные-38-hex> - Адресуется по SHA-1 хешу от своего содержимого (с заголовком:
<type> <size>\0<content>) - Immutable — никогда не меняется. Если меняется содержимое, создаётся новый объект с новым SHA
Разберём каждый тип в деталях.
blob: содержимое файла
blob — это просто последовательность байт. Содержимое файла, без имени, без прав, без даты.
echo "hello world" | git hash-object --stdin
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
Это SHA blob-объекта, чьё содержимое — «hello world\n» (echo добавляет newline). Этот SHA одинаков на любой машине в мире — детерминированная функция содержимого.
Если вы хотите создать blob в реальном репо (без файла):
git init my-repo && cd my-repo
# Создать blob из stdin (с записью в objects/)
echo "hello world" | git hash-object --stdin -w
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# Проверить, что объект создан
ls .git/objects/3b/
# 18e512dba79e4c8300dd08aeb37f8e728b8dad
# Посмотреть содержимое
git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# hello world
# Узнать тип
git cat-file -t 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# blob
# Размер
git cat-file -s 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# 12
Этот SHA 3b18e512... — это «hello world\n» в Git навсегда. Создайте файл с таким содержимым в любом репозитории — у его blob будет ТОТ ЖЕ SHA. Это и есть content-addressable storage.
Почему blob не знает имя
Тот же blob может быть в разных файлах:
echo "hello world" > foo.txt
echo "hello world" > bar.txt
git add foo.txt bar.txt
git commit -m "Two files"
# В objects/ создан ОДИН blob для обоих файлов
git ls-tree HEAD
# 100644 blob 3b18e512... foo.txt
# 100644 blob 3b18e512... bar.txt
Оба файла имеют один blob. Хранится один раз, на него ссылаются два пути в tree. Это и есть автоматическая дедупликация.
Имя файла, права, mtime — всё это хранится в tree-объекте, не в blob.
tree: snapshot директории
tree — это снимок директории. Список записей вида:
<mode> <type> <SHA> <name>
Пример:
git cat-file -p HEAD^{tree}
# 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad foo.txt
# 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad bar.txt
# 100755 blob 7a4e5f6c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a run.sh
# 040000 tree d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7e8f9c0d1 src
Записи:
100644mode — обычный файл, не executable100755mode — обычный файл, executable (есть бит исполнения, скрипт)040000mode — директория (ссылка на другой tree)120000mode — symbolic link160000mode — submodule (ссылка на другой репозиторий)
Tree описывает директорию рекурсивно: подкаталоги — это ссылки на другие tree-объекты. Корень репозитория — один tree, который ссылается на blobs (файлы корня) и trees (поддиректории), те ссылаются на свои blobs/trees, и так до листьев.
Команда git ls-tree
Для удобного просмотра tree:
git ls-tree HEAD # tree коммита HEAD
git ls-tree HEAD src/ # tree подкаталога
git ls-tree -r HEAD # рекурсивно весь tree
git ls-tree -r --name-only HEAD # только имена
Рекурсивный вывод покажет всю «плоскую» структуру файлов:
git ls-tree -r HEAD
# 100644 blob 3b... foo.txt
# 100644 blob 7a... src/main.py
# 100644 blob 8b... src/utils/io.py
commit: snapshot + history
commit мы разбирали в предыдущем уроке. Кратко: commit содержит SHA tree (snapshot), SHA parent (или несколько), author, committer, message.
git cat-file -p HEAD
# tree 3a79be23c0d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8
# parent a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# author Ivan Petrov <[email protected]> 1715600000 +0300
# committer Ivan Petrov <[email protected]> 1715600000 +0300
#
# Add OAuth flow
Каждый commit указывает на:
- tree — состояние всех файлов в этой версии
- parent commit — предыдущая версия в истории
Через цепочку parent-ссылок образуется история репозитория. Это DAG (directed acyclic graph) — направленный граф без циклов.
tag: именованный указатель
tag бывает двух типов:
Lightweight tag
Это просто ref — файл в .git/refs/tags/ с SHA коммита. Никакого объекта нет, никаких метаданных.
git tag v1.0 # lightweight tag
cat .git/refs/tags/v1.0
# a1b2c3d4... ← SHA коммита
Annotated tag
Это объект типа tag в .git/objects/. Содержит метаданные:
git tag -a v1.1 -m "Version 1.1 release"
# Файл refs/tags/v1.1 указывает на TAG-объект, не на commit
cat .git/refs/tags/v1.1
# t1u2v3... ← SHA tag-объекта
git cat-file -t t1u2v3...
# tag
git cat-file -p t1u2v3...
# object a1b2c3d4... ← на какой commit указывает
# type commit
# tag v1.1
# tagger Ivan Petrov <[email protected]> 1715600000 +0300
#
# Version 1.1 release
Annotated tag — это полноценный объект, как commit. Имеет SHA, может быть подписан GPG/SSH. Используется для официальных релизов.
Правило: для официальных релизов — annotated tags. Для временных пометок (например, deployment marker) — lightweight. На практике 95% production-релизов помечают annotated, и многие проекты считают best practice использовать только annotated.
Подробно про теги — модуль 11.
Полный граф объектов
Вот как четыре типа связаны на типичном репозитории:
Важно понимать:
- refs (HEAD, branches, tags) — это указатели, не объекты. Файлы в
.git/refs/и.git/HEAD. - objects — это бессмертное содержимое в
.git/objects/. По SHA. - Refs могут двигаться (например, ветка main двигается при каждом коммите). Objects никогда не меняются.
Команды для работы с объектами
Plumbing-команды (низкоуровневые), полезные для дебага:
# Содержимое объекта
git cat-file -p <SHA>
# Тип объекта
git cat-file -t <SHA>
# Размер
git cat-file -s <SHA>
# Проверка существования
git cat-file -e <SHA> && echo "exists"
# Создать blob из файла
git hash-object <file> # только хеш, не пишет
git hash-object -w <file> # пишет в objects/
# Содержимое tree
git ls-tree <SHA>
git ls-tree -r <SHA> # рекурсивно
# Конвертация ref -> SHA
git rev-parse HEAD # SHA коммита HEAD
git rev-parse main # SHA коммита main
git rev-parse HEAD^{tree} # SHA tree коммита HEAD
git rev-parse HEAD~3 # SHA коммита 3 поколения назад
# Все объекты в репо
find .git/objects -type f # увидите все объекты как файлы
git cat-file --batch-all-objects --batch-check
# выведет SHA + тип + размер для каждого объекта
Packfiles: оптимизация хранения
После git gc или git push, Git сжимает «loose objects» (один файл на объект) в packfile (один большой файл, содержащий много объектов с дельта-компрессией).
ls .git/objects/pack/
# pack-abc123.idx ← индекс
# pack-abc123.pack ← собственно packfile
Подробности packfile-формата выходят за рамки junior-курса. Главное знать:
- Это оптимизация storage, не изменение модели
- Логически объекты остаются те же (blob, tree, commit, tag), SHA те же
git cat-fileработает прозрачно — может читать loose objects и packfiles одинаково
После git gc директории objects/<2hex>/ могут оказаться пустыми (объекты ушли в pack). Это нормально.
Garbage collection и unreachable objects
Когда commit становится недоступным (нет ветки, тега, reflog, который на него указывает) — Git помечает его как «unreachable». При git gc такие объекты УДАЛЯЮТСЯ.
Это и есть способ Git «забывать» что-то. Если вы:
- Сделали коммит
git reset --hard HEAD~1(откатили его)- Никакая ветка/тег на этот коммит не указывает
- Прошло > 30 дней
- Запустился
git gc
-> Объект удаляется навсегда.
Но до этого объект жив в reflog. Можно восстановить (модуль 10). Это и есть «безопасный» Git: ничто не удаляется сразу, всё доступно через reflog некоторое время.
Попробуй сам
- Создайте репозиторий и сделайте несколько коммитов:
mkdir -p ~/git-sandbox/lesson-03-objects
cd ~/git-sandbox/lesson-03-objects
git init
echo "hello" > a.txt
mkdir src && echo "print('hi')" > src/main.py
git add .
git commit -m "Initial"
# Меняем
echo "hello again" > a.txt
git add . && git commit -m "Update"
- Посмотрите все объекты:
find .git/objects -type f
- Вскройте каждый и определите тип:
for f in $(find .git/objects -type f); do
SHA=$(echo $f | sed 's|.git/objects/||' | tr -d '/')
echo "=== $SHA ($(git cat-file -t $SHA)) ==="
git cat-file -p $SHA
echo
done
- Найдите blob с содержимым «hello\n» и убедитесь, что он один на оба коммита (где a.txt не менялся, хотя в этом примере менялся):
echo "hello" | git hash-object --stdin
# и проверьте есть ли такой SHA в objects/
- Посмотрите tree коммита:
git ls-tree HEAD
git ls-tree -r HEAD
- Вскройте tree последнего коммита через rev-parse:
TREE_SHA=$(git rev-parse HEAD^{tree})
git cat-file -p $TREE_SHA
Что это всё даёт
Понимание объектной модели даёт практическую пользу:
- Дебаг странного состояния — если что-то непонятное, всегда можно
git cat-file -pна любой SHA и посмотреть, что Git реально знает. - Восстановление потерянных коммитов — даже если ветка удалена, объект коммита всё ещё в
.git/objects/. Черезgit fsck --lost-foundилиgit reflogможно найти и восстановить. - Понимание команд — каждая команда становится «вот что я делаю с какими объектами».
git resetдвигает refs.git commitсоздаёт commit-объект.git checkoutобновляет working tree из tree-объекта. - Доверие к Git — Git не магия. Это 4 типа объектов в
.git/objects/+ указатели в.git/refs/. Всё видимо. Всё инспектируется.
В этот момент Junior, который понял этот урок, перестаёт бояться Git. Можно дальше — модули 04-20 будут даваться на порядок легче.
Форматы хранения данных: Parquet, Avro, JSON