↓Перейти к содержанию
  1. Архив/

Git изнутри: объекты, HEAD, индекс и cat-file

··8 минут·

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:

  1. Git вычисляет SHA-1 содержимого файла
  2. Сохраняет blob-объект в .git/objects/
  3. Обновляет .git/index — добавляет или обновляет запись

Когда ты делаешь git commit:

  1. Git создаёт tree-объект из текущего состояния индекса
  2. Создаёт commit-объект, указывающий на это дерево
  3. Обновляет ссылку текущей ветки

Файл 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.


Читай также #