Learning Platform
Глоссарий Troubleshooting
Урок 05.05 · 22 мин
Начальный
GitObjectsBlobTreeCommitTagSHA-1

Объектная модель Git: blob, tree, commit, tag

В уроке 1 модуля 2 мы прошли content-addressable storage концептуально: Git хранит каждый объект по его SHA-1 хешу. В уроке 4 этого модуля мы посмотрели commit-объект изнутри. Теперь финальный кусок пазла: какие вообще типы объектов есть в Git, и как они связаны.

Это самый «низкоуровневый» урок курса. Junior может пропустить и продолжать работать с Git — миллион людей делают это. Но если вы прочитали и поняли — у вас будет ментальная модель, которая делает Git кристально прозрачным. Любая команда, любая ошибка, любое странное состояние — вы будете понимать «что произошло на уровне объектов».


4 типа объектов

В Git ровно 4 типа объектов:

Объектная модель Git
blobСодержимое файла. Просто байты. Не знает, как называется файл, в какой директории лежит, какие у него права. Только содержимое
treeSnapshot директории. Список записей: mode, тип (blob/tree), SHA, имя
commitSnapshot tree + metadata (author, committer, parent, message). Образует историю через parent-ссылки
tagAnnotated tag — именованный указатель с метаданными (автор тега, сообщение). Lightweight tags — это просто ref, не объект
ВСЁЛюбая операция Git сводится к чтению/созданию этих 4 типов. Других объектов нет. Это и есть «простая модель данных», давшая Git преимущество над Mercurial

Каждый объект:

  1. Имеет один из этих типов
  2. Хранится в .git/objects/<первые-2-hex>/<остальные-38-hex>
  3. Адресуется по SHA-1 хешу от своего содержимого (с заголовком: <type> <size>\0<content>)
  4. 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
NOTE

Этот 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

Записи:

  • 100644 mode — обычный файл, не executable
  • 100755 mode — обычный файл, executable (есть бит исполнения, скрипт)
  • 040000 mode — директория (ссылка на другой tree)
  • 120000 mode — symbolic link
  • 160000 mode — submodule (ссылка на другой репозиторий)

Tree описывает директорию рекурсивно: подкаталоги — это ссылки на другие tree-объекты. Корень репозитория — один tree, который ссылается на blobs (файлы корня) и trees (поддиректории), те ссылаются на свои blobs/trees, и так до листьев.

Tree описывает директорию рекурсивно
root treeКорневой tree-объект всего репозитория. На него ссылается commit
README.mdФайл в корне. Mode 100644, ссылка на blob
src/Поддиректория. Mode 040000, ссылка на другой tree
LICENSEФайл в корне
src/main.pyФайл в src/
src/utils/Ещё поддиректория
src/utils/io.pyФайл в src/utils/

Команда 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 указывает на:

  1. tree — состояние всех файлов в этой версии
  2. 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. Используется для официальных релизов.

TIP

Правило: для официальных релизов — annotated tags. Для временных пометок (например, deployment marker) — lightweight. На практике 95% production-релизов помечают annotated, и многие проекты считают best practice использовать только annotated.

Подробно про теги — модуль 11.


Полный граф объектов

Вот как четыре типа связаны на типичном репозитории:

Граф объектов в репозитории
HEADСсылка (не объект) на текущую ветку
refs/heads/mainФайл с SHA коммита (не объект)
указывает на
commit CПоследний commit на ветке main
commit Ctree -> snapshot, parent -> commit B
tree
tree XSnapshot состояния файлов в commit C
blobs + treesДеревья и blob-ы, образующие полную структуру файлов
commit Cparent ->
parent
commit BПредыдущий коммит
parent
commit ARoot commit (без parent)
refs/tags/v1.0Tag-ref указывает на annotated tag-объект
tag TAnnotated tag с метаданными
object ->
commit BTag указывает на конкретный коммит

Важно понимать:

  • 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-курса. Главное знать:

  1. Это оптимизация storage, не изменение модели
  2. Логически объекты остаются те же (blob, tree, commit, tag), SHA те же
  3. git cat-file работает прозрачно — может читать loose objects и packfiles одинаково

После git gc директории objects/<2hex>/ могут оказаться пустыми (объекты ушли в pack). Это нормально.


Garbage collection и unreachable objects

Когда commit становится недоступным (нет ветки, тега, reflog, который на него указывает) — Git помечает его как «unreachable». При git gc такие объекты УДАЛЯЮТСЯ.

Это и есть способ Git «забывать» что-то. Если вы:

  1. Сделали коммит
  2. git reset --hard HEAD~1 (откатили его)
  3. Никакая ветка/тег на этот коммит не указывает
  4. Прошло > 30 дней
  5. Запустился git gc

-> Объект удаляется навсегда.

Но до этого объект жив в reflog. Можно восстановить (модуль 10). Это и есть «безопасный» Git: ничто не удаляется сразу, всё доступно через reflog некоторое время.


Попробуй сам

  1. Создайте репозиторий и сделайте несколько коммитов:
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"
  1. Посмотрите все объекты:
find .git/objects -type f
  1. Вскройте каждый и определите тип:
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
  1. Найдите blob с содержимым «hello\n» и убедитесь, что он один на оба коммита (где a.txt не менялся, хотя в этом примере менялся):
echo "hello" | git hash-object --stdin
# и проверьте есть ли такой SHA в objects/
  1. Посмотрите tree коммита:
git ls-tree HEAD
git ls-tree -r HEAD
  1. Вскройте tree последнего коммита через rev-parse:
TREE_SHA=$(git rev-parse HEAD^{tree})
git cat-file -p $TREE_SHA

Что это всё даёт

Понимание объектной модели даёт практическую пользу:

  1. Дебаг странного состояния — если что-то непонятное, всегда можно git cat-file -p на любой SHA и посмотреть, что Git реально знает.
  2. Восстановление потерянных коммитов — даже если ветка удалена, объект коммита всё ещё в .git/objects/. Через git fsck --lost-found или git reflog можно найти и восстановить.
  3. Понимание команд — каждая команда становится «вот что я делаю с какими объектами». git reset двигает refs. git commit создаёт commit-объект. git checkout обновляет working tree из tree-объекта.
  4. Доверие к Git — Git не магия. Это 4 типа объектов в .git/objects/ + указатели в .git/refs/. Всё видимо. Всё инспектируется.

В этот момент Junior, который понял этот урок, перестаёт бояться Git. Можно дальше — модули 04-20 будут даваться на порядок легче.


Форматы хранения данных: Parquet, Avro, JSON
Проверка знанийKnowledge check
Вы сделали `git reset --hard HEAD~1` и потеряли последний коммит. На следующий день вспомнили его SHA: a1b2c3d. Сможете ли вы восстановить коммит и его содержимое?
ОтветAnswer
Скорее всего да, если прошло меньше 30 дней (стандартный grace period для unreachable объектов до git gc). Содержимое коммита всё ещё хранится в `.git/objects/` как objects типа commit, tree, blob — content-addressable storage не удаляет объекты при reset, только перемещает refs. Восстановление: 1) `git cat-file -t a1b2c3d` — убедиться, что объект существует. 2) `git cat-file -p a1b2c3d` — посмотреть содержимое коммита (tree, parent, message). 3) `git checkout a1b2c3d` — переключиться на этот коммит (detached HEAD). 4) `git switch -c restored-branch` — создать ветку на этом коммите для безопасности. Или быстрее: `git reflog` (модуль 10) покажет журнал последних движений HEAD, можно сделать `git reset --hard a1b2c3d` чтобы вернуться. Объект жив, пока на него есть хоть одна ссылка (включая reflog) или пока не прошёл grace period.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Сколько типов объектов в Git и какие они?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5