Git изнутри: объекты, HEAD, индекс и cat-file
Git изнутри: объекты, HEAD, индекс и как это всё работает #
Статья обновлена: март 2026
Git — не просто система контроля версий. В своей основе это контентно-адресуемая файловая система с пользовательским интерфейсом поверх неё. Понимание внутреннего устройства Git не обязательно для повседневной работы, но оно объясняет почему Git настолько быстрый, надёжный и гибкий. После прочтения этой статьи команды git add, git commit и git branch перестанут казаться «магией».
Сантехника и фарфор #
Команды Git делятся на два уровня. Пользовательский уровень (porcelain — «фарфор») — это то, чем пользуешься каждый день: git add, git commit, git checkout, git log. Служебный уровень (plumbing — «сантехника») — низкоуровневые команды, из которых состоят высокоуровневые: git hash-object, git cat-file, git write-tree, git commit-tree.
В этой статье мы спустимся на уровень «сантехники», чтобы понять как Git хранит данные.
Структура папки .git #
Всё, чем управляет Git, живёт в папке .git:
$ git init test-repo && cd test-repo
$ ls .git/
HEAD config description hooks/ info/ objects/ refs/
Четыре ключевых элемента:
objects/— база данных всех объектов Git: версии файлов, деревья директорий, коммитыrefs/— ссылки: указатели на коммиты (ветки, теги, HEAD удалённых репозиториев)HEAD— указатель на текущую ветку (или конкретный коммит в режиме detached HEAD)index— индекс (staging area): то, что будет в следующем коммите
Остальное — config (настройки репозитория), hooks/ (скрипты автоматизации), description (для GitWeb, неважен).
Объекты Git: три типа #
Git хранит данные в виде объектов трёх типов. Всё остальное строится на них.
Blob — содержимое файла #
Blob (binary large object) — это просто содержимое файла. Без имени, без метаданных — только содержимое. Имя файла и расположение хранятся отдельно, в объекте-дереве.
# Создать blob-объект из строки (команда сантехнического уровня)
$ echo 'hello git' | git hash-object -w --stdin
8d0e41234f24b6da002d962a26c2495ea16a425f
# Убедиться что объект создан
$ find .git/objects -type f
.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
# Прочитать содержимое объекта
$ git cat-file -p 8d0e41234f24b6da002d962a26c2495ea16a425f
hello git
# Проверить тип объекта
$ git cat-file -t 8d0e41234f24b6da002d962a26c2495ea16a425f
blob
Ключевая идея: два файла с одинаковым содержимым в Git хранятся как один blob. Это одна из причин почему Git не раздувается со временем.
Tree — дерево директорий #
Tree (дерево) — это аналог директории в файловой системе. Оно содержит список записей: blob-объекты (файлы) и другие tree-объекты (поддиректории).
# Посмотреть дерево для текущего коммита
$ git cat-file -p HEAD^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README.md
100644 blob 8f94139338f9404f26296befa88755fc2598c289 main.py
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 src
Каждая запись содержит: режим доступа (100644 — обычный файл, 040000 — директория), тип объекта, SHA-1 хеш и имя файла.
Структура данных в Git выглядит примерно так:
Коммит
└── Tree (корень)
├── blob "README.md" → содержимое README
├── blob "main.py" → содержимое main.py
└── Tree "src/"
├── blob "utils.py" → содержимое utils.py
└── blob "app.py" → содержимое app.py
Commit — снимок с метаданными #
Commit (коммит) — это объект, который связывает всё воедино: указывает на дерево (состояние проекта), содержит метаданные (автор, дата, сообщение) и ссылается на родительский коммит.
# Посмотреть содержимое коммита
$ git cat-file -p HEAD
tree 1a4e1b629cc3f88e55cb13ec1ea6f92de7eb3278
parent d3ad21b7f21f9ef5793f47e79d22c04cdd7e4eda
author Иван <[email protected]> 1710286800 +0300
committer Иван <[email protected]> 1710286800 +0300
Добавлена функция авторизации
Цепочка коммитов — это цепочка указателей родитель→потомок:
[Commit A] ← [Commit B] ← [Commit C] ← HEAD
tree: T1 tree: T2 tree: T3
parent: - parent: A parent: B
Каждый коммит знает только своего родителя. История — это именно эта цепочка.
SHA-1: содержимое как ключ #
Git идентифицирует каждый объект по SHA-1 хешу его содержимого. Это делает Git контентно-адресуемой системой: зная содержимое, ты знаешь адрес.
# SHA-1 зависит только от содержимого
$ echo 'test' | git hash-object --stdin
9daeafb9864cf43055ae93beb0afd6c7d144bfa4
# Одинаковое содержимое → одинаковый хеш (на любом компьютере, в любом репозитории)
$ echo 'test' | git hash-object --stdin
9daeafb9864cf43055ae93beb0afd6c7d144bfa4
Это свойство гарантирует целостность данных: невозможно изменить исторический коммит так, чтобы не изменился его хеш — а значит и хеши всех последующих коммитов.
Объекты хранятся в .git/objects/ по первым двум символам хеша как имени директории и остальным 38 — как имени файла:
.git/objects/
├── 9d/
│ └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
├── a9/
│ └── 06cb2a4a904a152e80877d4088654daad0c859
└── pack/ ← упакованные объекты для экономии места
Индекс (staging area) #
Индекс — это промежуточная зона между рабочей директорией и следующим коммитом. Технически это бинарный файл .git/index, который хранит список отслеживаемых файлов и их текущие SHA-1.
# Посмотреть содержимое индекса
$ git ls-files --stage
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README.md
100644 8f94139338f9404f26296befa88755fc2598c289 0 main.py
Когда ты делаешь git add файл.py:
- Git вычисляет SHA-1 содержимого файла
- Сохраняет blob-объект в
.git/objects/ - Обновляет
.git/index— добавляет или обновляет запись
Когда ты делаешь git commit:
- Git создаёт tree-объект из текущего состояния индекса
- Создаёт commit-объект, указывающий на это дерево
- Обновляет ссылку текущей ветки
Файл HEAD #
HEAD — это указатель на текущую ветку (или конкретный коммит). Это просто текстовый файл:
$ cat .git/HEAD
ref: refs/heads/main
Он говорит: «ты сейчас на ветке main». А ветка main — это ссылка в .git/refs/heads/main:
$ cat .git/refs/heads/main
3a8f2c1b4e9d7a5f6c2e1b0a3d4e5f6a7b8c9d0e
Вот и всё. Ветка в Git — это просто файл, содержащий SHA-1 хеш последнего коммита на этой ветке. Когда ты делаешь новый коммит, Git обновляет этот файл.
# Посмотреть куда указывает HEAD
$ git symbolic-ref HEAD
refs/heads/main
# Посмотреть хеш коммита на который указывает HEAD
$ git rev-parse HEAD
3a8f2c1b4e9d7a5f6c2e1b0a3d4e5f6a7b8c9d0e
Detached HEAD #
Если ты переключаешься на конкретный коммит (не ветку), HEAD содержит хеш напрямую:
$ git checkout 3a8f2c1
Note: switching to '3a8f2c1'.
You are in 'detached HEAD' state.
$ cat .git/HEAD
3a8f2c1b4e9d7a5f6c2e1b0a3d4e5f6a7b8c9d0e
Это называется detached HEAD. Коммиты в этом состоянии «висят в воздухе» — ни одна ветка на них не указывает. Чтобы сохранить их, нужно создать ветку: git branch new-branch HEAD.
Ссылки: refs, ветки и теги #
Ссылки (refs) — это именованные указатели на SHA-1. Они живут в .git/refs/:
.git/refs/
├── heads/ ← локальные ветки
│ ├── main → SHA-1 последнего коммита main
│ └── feature/auth → SHA-1 последнего коммита ветки
├── remotes/ ← удалённые ветки
│ └── origin/
│ ├── main → SHA-1 последнего известного состояния origin/main
│ └── develop
└── tags/ ← теги
└── v1.0.0 → SHA-1 коммита или tag-объекта
Именно поэтому ветки в Git бесплатны: создать новую ветку — значит создать файл в .git/refs/heads/ с SHA-1 текущего коммита.
Полезные служебные команды #
# Показать тип объекта по хешу
git cat-file -t <hash>
# blob / tree / commit / tag
# Показать содержимое объекта
git cat-file -p <hash>
# Показать размер объекта в байтах
git cat-file -s <hash>
# Вычислить SHA-1 файла (без сохранения)
git hash-object path/to/file
# Вычислить SHA-1 и сохранить объект
git hash-object -w path/to/file
# Посмотреть индекс
git ls-files --stage
# Посмотреть все объекты в репозитории
git cat-file --batch-all-objects --batch-check
# Проверить целостность репозитория
git fsck
# Упаковать объекты и убрать «мусор»
git gc
Как работает git commit под капотом #
Теперь, зная о блобах, деревьях и коммитах, посмотрим что происходит при git commit:
# Ты пишешь код и делаешь:
git add src/auth.py
# Что происходит:
# 1. Git читает содержимое src/auth.py
# 2. Вычисляет SHA-1: допустим ab12cd...
# 3. Сохраняет blob: .git/objects/ab/12cd...
# 4. Обновляет индекс: src/auth.py → ab12cd...
git commit -m "Add auth module"
# Что происходит:
# 1. Git берёт текущий индекс и создаёт tree-объект
# 2. Создаёт commit-объект: tree + parent + author + message
# 3. Обновляет .git/refs/heads/main → новый хеш коммита
Именно поэтому git status мгновенный: Git сравнивает SHA-1 файлов на диске с SHA-1 в индексе, а не само содержимое.
Обслуживание репозитория #
Со временем в .git/objects/ накапливаются отдельные объекты. Git периодически (или по команде) упаковывает их в pack-файлы — это сокращает объём занимаемого места:
# Проверить целостность объектов
git fsck
# Dangling objects — это объекты без ссылок (нормально после rebase/амбдов)
# Убрать недоступные объекты и упаковать остальные
git gc
# Принудительная агрессивная упаковка (для уменьшения размера)
git gc --aggressive
⚠️ Осторожно:
git gcудаляет объекты, на которые нет ссылок. Если ты делалgit rebaseилиgit reset --hard, потерянные коммиты могут исчезнуть. Убедись что не нужны — или создай ветку на них перед запуском gc.
Часто задаваемые вопросы #
Что такое SHA-1 в Git и зачем он нужен? #
SHA-1 — это криптографическая хеш-функция. Git вычисляет SHA-1 для каждого объекта на основе его содержимого. Это даёт два свойства: уникальный идентификатор (40 символов) и гарантию целостности — изменить объект незаметно невозможно.
В чём разница между blob и commit? #
Blob хранит содержимое одного файла — без имени и метаданных. Commit хранит ссылку на дерево (состояние всего проекта), информацию об авторе, дате и сообщение. Tree связывает blobs с именами файлов.
Что такое detached HEAD? #
HEAD обычно указывает на ветку (например refs/heads/main). В «detached HEAD» (оторванный HEAD) HEAD указывает напрямую на хеш коммита. Это происходит при git checkout <хеш>. Если сделать коммиты в этом состоянии — они будут без ветки и могут быть удалены git gc. Создай ветку чтобы сохранить их: git branch my-work.
Почему создание ветки в Git мгновенное? #
Потому что ветка — это просто файл в .git/refs/heads/ с 40-символьным SHA-1 хешем. Создать ветку = создать файл. Это не зависит от размера репозитория или количества коммитов.
Что делает git fsck? #
git fsck (file system check) проверяет целостность базы данных объектов: ищет повреждённые объекты, «висячие» ссылки и объекты без ссылок (dangling commits/blobs). Полезно после сбоев диска или подозрительного поведения Git.