Внутреннее устройство I: объектная база Git

Рейтинг: 67.6% · 8 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Внутреннее устройство I: объектная база Git

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git (вы здесь)
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Ты уже умеешь коммитить, ветвиться, ребейзить. Но рано или поздно случается момент, когда Git перестаёт быть набором заклинаний и хочется понять: а что вообще лежит внутри? Куда уходит файл, когда ты делаешь commit? Почему два одинаковых файла в разных папках не раздувают репозиторий вдвое? Почему checkout старой версии мгновенный, хотя там были тысячи файлов? Ответ на все эти вопросы один и тот же, и он про устройство git на самом нижнем уровне - про объектную базу. Разберёшься с ней - и половина "магии" Git превратится в понятную механику, которую можно потрогать руками. Именно это мы и сделаем: в конце урока ты соберёшь коммит вручную, по кирпичику, без единого git commit, и увидишь его в git log.

Git - это content-addressable файловая система

Запомни главную мысль, всё остальное вырастает из неё: в основе Git лежит не "система контроля версий", а простое key-value хранилище. Ты даёшь ему кусок данных - он возвращает ключ. Даёшь тот же ключ - он возвращает те же данные. Вот и всё хранилище.

Хитрость в том, что ключ - это не случайный номер и не автоинкремент. Ключ вычисляется ИЗ самого содержимого: берётся SHA-хеш данных. Такой подход называется content-addressable storage, адресация по содержимому. У него есть мощное следствие: если содержимое изменилось хоть на один байт - изменился и адрес. А если два файла побайтово одинаковы - у них один и тот же адрес, и физически они хранятся ОДИН раз. Это и есть дедупликация, бесплатная и автоматическая.

Все эти кусочки данных Git называет объектами и складывает в каталог .git/objects. Когда говорят "git internals" или "как работает git внутри", на 90 процентов имеют в виду именно объектную базу. Объектов всего четыре типа, и их стоит знать наизусть:
  • blob - содержимое одного файла. Голые байты, без имени, без прав, без даты. Просто "вот эти данные".
  • tree - каталог. Список записей вида "режим + имя + ссылка на blob или на вложенный tree".
  • commit - снимок: ссылка на один корневой tree, ссылки на родителей, автор, коммиттер, сообщение.
  • tag - аннотированный тег: ссылка на объект (обычно commit), имя тега, автор тега, сообщение, опционально подпись.
Когда говорят про "объекты git" и связку "blob tree commit", речь ровно об этом. Дальше мы каждый из них пощупаем плоумбингом - низкоуровневыми командами, на которых построены привычные high-level команды.

Изображение

SHA как адрес: как он вычисляется на самом деле

Тут многие спотыкаются, поэтому медленно. Хеш объекта - это НЕ хеш просто содержимого файла. Git сначала добавляет к содержимому заголовок, а потом хеширует всё вместе. Формула такая:

Код: Выделить всё

sha( "<тип> <длина-в-байтах>\0<содержимое>" )
То есть строка "blob", пробел, длина содержимого числом, нулевой байт-разделитель, и сразу за ним сами данные. Проверим это голыми руками, без всякой веры на слово. Создадим файл и спросим у Git его адрес:

Код: Выделить всё

$ printf 'hello\n' > a.txt
$ git hash-object a.txt
ce013625030ba8dba906f756967f9e9ca394464a
А теперь вычислим то же самое сами. Файл содержит 6 байт (hello плюс перевод строки), значит заголовок - "blob 6\0":

Код: Выделить всё

$ printf 'blob 6\0hello\n' | shasum
ce013625030ba8dba906f756967f9e9ca394464a  -
Совпало байт в байт. Никакой магии - чистая арифметика над байтами. Команда git hash-object - это и есть калькулятор адреса. Сама по себе она ничего не записывает, только считает. Чтобы заодно положить объект в базу, добавляют флаг -w:

Код: Выделить всё

$ git hash-object -w a.txt
ce013625030ba8dba906f756967f9e9ca394464a
Пара слов про сам хеш на 2026 год. Исторически Git использует SHA-1, и большинство репозиториев в мире до сих пор на нём. С Git 2.51 для НОВЫХ репозиториев дефолтным форматом стал SHA-256 (адреса там 64 символа вместо 40), а полное закрепление SHA-256 и интероп со старыми репо доезжают к Git 3.0 в конце 2026. От подделок через коллизию (помнишь атаку SHAttered?) Git защищён: детектор коллизий SHA-1 включён по умолчанию. Для нас сейчас принципиально одно: адрес детерминированно вытекает из байтов, какой бы алгоритм ни стоял.

git cat-file: смотрим внутрь объектов

Раз есть адреса, нужна команда, чтобы по адресу доставать содержимое и метаданные. Это git cat-file, твой основной инструмент для прогулок по базе. Три ключевых флага:
  • -t - тип объекта (blob / tree / commit / tag)
  • -s - размер содержимого в байтах
  • -p - "pretty print", человекочитаемый вывод с учётом типа

Код: Выделить всё

$ git cat-file -t ce01362
blob
$ git cat-file -s ce01362
6
$ git cat-file -p ce01362
hello
Адрес можно сокращать до уникального префикса (тут 7 символов) - Git сам достроит полный. Теперь соберём дерево. Положим blob в индекс и попросим Git записать из индекса объект-tree:

Код: Выделить всё

$ git add a.txt
$ git write-tree
2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1
$ git cat-file -p 2e81171
100644 blob ce013625030ba8dba906f756967f9e9ca394464a	a.txt
Вот он, tree, изнутри: режим 100644 (обычный файл, не исполняемый), тип записи blob, адрес содержимого и имя файла. Имя файла живёт ИМЕННО здесь, в дереве, а не в blob. Поэтому переименование без изменения содержимого создаёт новый tree, но переиспользует старый blob. Если бы файл был исполняемым, режим был бы 100755; для подкаталога стоял бы режим 40000 и тип tree. Это вся "файловая система" Git в одной строке.

Потоковый обход базы: --batch и --batch-check

Когда объектов десятки тысяч, дёргать cat-file по одному медленно - каждый запуск это новый процесс. Для массовой работы есть пакетные режимы, которые читают список объектов со stdin и отвечают потоком, не перезапускаясь. Это то, чем под капотом пользуются GitHub, GitLab и любой серьёзный инструментарий над Git.

Код: Выделить всё

$ echo ce01362 | git cat-file --batch-check
ce013625030ba8dba906f756967f9e9ca394464a blob 6
Флаг --batch-check отдаёт только метаданные (адрес, тип, размер), --batch добавляет ещё и само содержимое. А связка с --batch-all-objects проходит вообще по ВСЕМ объектам в базе без всякого stdin - удобно для аудита:

Код: Выделить всё

$ git cat-file --batch-check --batch-all-objects
2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1 tree 33
ce013625030ba8dba906f756967f9e9ca394464a blob 6
Видишь? У нас в базе ровно два объекта: один blob и один tree. Этим приёмом ищут, например, самые тяжёлые блобы в истории - сортируешь вывод по размеру и сразу видишь, какой бинарник раздул репозиторий.

Собираем commit руками: магия становится механикой

Теперь главное упражнение урока. У нас есть tree - снимок состояния. Превратим его в коммит низкоуровневой командой git commit-tree: она берёт адрес дерева, сообщение со stdin (или через -m) и при желании родителей через -p, а возвращает адрес нового commit-объекта.

Код: Выделить всё

$ git commit-tree 2e81171 -m "first commit by hand"
2c6121f56cc9b8bab3dbbcff9b52b8c845a735d7
$ git cat-file -p 2c6121f
tree 2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1
author tester <a@b.c> 1781546774 +0300
committer tester <a@b.c> 1781546774 +0300

first commit by hand
Вот он, commit, голышом: ссылка на один tree, строки author и committer (имя, email, unix-время, смещение часового пояса), пустая строка и сообщение. Строки parent тут нет - это самый первый коммит, корень истории. У второго коммита появилась бы строка parent с адресом предыдущего; у merge-коммита - две строки parent.

Но git log его пока не покажет. Почему? Потому что мы создали объект, но ни одна ветка на него не смотрит. Ветка в Git - это просто файл-указатель с адресом коммита. Направим main на наш рукотворный коммит и заглянем в лог:

Код: Выделить всё

$ git update-ref refs/heads/main 2c6121f
$ git log --oneline
2c6121f first commit by hand
Готово. Мы собрали полноценный коммит из объектов вручную, и Git принял его как родной - потому что для Git он и ЕСТЬ родной, ничем не отличается от сделанного через git commit. Обычный git commit под капотом делает ровно эти три шага: write-tree, commit-tree, обновление ветки. Теперь ты это видел своими глазами.

Для полноты картины ещё два плоумбинг-инструмента. git mktree собирает tree напрямую из текстового описания записей (минуя индекс) - полезно, когда лепишь деревья скриптом. А git mktag создаёт аннотированный tag-объект из описания на stdin:

Код: Выделить всё

$ printf 'object %s\ntype commit\ntag handmade\ntagger tester <a@b.c> 1700000000 +0000\n\nmade by mktag\n' 2c6121f | git mktag
30e3ffa078d06d8388c676a99c189dc9de4735e2
$ git cat-file -t 30e3ffa
tag
mktag не просто хеширует текст - он валидирует структуру (есть ли object, корректен ли type, на месте ли tagger). Если строка кривая, он откажет, в отличие от тупого hash-object.

Как HEAD -> commit -> tree -> blob образуют снимок

Сложим всё в единую цепочку, ради которой урок и затевался. Когда ты делаешь checkout, Git идёт по ссылкам сверху вниз:
  • HEAD указывает на ветку (refs/heads/main) или прямо на коммит.
  • commit указывает на ровно один корневой tree - это и есть снимок ВСЕГО проекта на момент коммита.
  • tree указывает на blob-ы (файлы) и на вложенные tree (подкаталоги), те - дальше вглубь.
  • blob - это конечные листья, содержимое файлов.
Ключевой момент, который ломает интуицию новичков: коммит хранит не дельту, не "что изменилось", а ПОЛНЫЙ снимок дерева. Каждый коммит ссылается на полное состояние проекта. "Дельты" между коммитами Git вычисляет на лету при показе диффа - в хранилище их нет. Звучит расточительно, но тут и спасает content-addressable модель: если между коммитами поменялся один файл из тысячи, новым будет только один blob, один корневой tree и tree изменённого каталога. Все остальные 999 файлов и нетронутые папки переиспользуют СТАРЫЕ объекты по их адресам. Снимок логически полный, а физически почти бесплатный.

Эту структуру можно обойти целиком командой git rev-list --objects - она перечисляет все объекты, достижимые от веток, вместе с их путями:

Код: Выделить всё

$ git rev-list --objects --all
2c6121f56cc9b8bab3dbbcff9b52b8c845a735d7
2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1
ce013625030ba8dba906f756967f9e9ca394464a a.txt
Сверху commit, под ним tree, внизу blob с приписанным путём a.txt. Имена путей rev-list берёт из деревьев. Именно этот обход лежит в основе git fetch и git push (что слать, а что у той стороны уже есть) и сборки мусора (что достижимо, а что осиротело).

Loose objects, zlib и физика на диске

Где всё это физически лежит? Каждый свежий объект пишется отдельным файлом - это loose object, "рыхлый объект". Адрес из 40 символов разбивается: первые 2 символа становятся именем подкаталога, остальные 38 - именем файла. Так Git избегает каталога с миллионом файлов в одной папке.

Код: Выделить всё

$ ls .git/objects/2e/
81171448eb9f2ee3821e3d447aa6b2fe3ddba1
Это наш tree 2e81171... . Содержимое файла - это тот самый "blob 6\0hello\n" (заголовок плюс данные), сжатый zlib (алгоритм deflate). Поэтому cat на этот файл покажет бинарный мусор: читать его надо через git cat-file, который разожмёт zlib и снимет заголовок. Хешируется при этом РАСжатое содержимое с заголовком - сжатие влияет на место на диске, но не на адрес.

Когда loose-объектов накапливается много, они неэффективны, и Git упаковывает их в packfile-ы при обслуживании. На 2026 году дефолтная стратегия ручного обслуживания (git maintenance run, начиная с Git 2.54) - геометрическая упаковка, а старый прямолинейный git gc стал легаси-путём. Объекты, ставшие недостижимыми, не стираются мгновенно, а складываются в cruft-пакеты с отметками времени .mtimes, чтобы их можно было подержать перед удалением. Но это уже тема следующего урока про упаковку - сейчас держим в голове только loose-уровень.

Типичные грабли
  • Путают хеш файла и хеш blob. git hash-object файла НЕ равен sha256sum/md5sum того же файла - Git добавляет заголовок "blob <длина>\0". Всегда помни про заголовок.
  • Думают, что blob хранит имя файла. Нет. Имя и режим живут в tree. Один и тот же blob может фигурировать под разными именами в разных деревьях.
  • Ждут, что commit-tree сразу появится в git log. Не появится, пока на него не смотрит ни одна ветка/HEAD. Объект есть, но он недостижим. Нужен update-ref.
  • cat на файл в .git/objects показывает мусор и человек решает, что репо побит. Это просто zlib-сжатие. Читай через git cat-file -p.
  • Считают, что одинаковая правка в двух ветках = два хранения. Нет: одинаковое содержимое = один blob по одному адресу, дедупликация работает поверх всего репозитория.
  • Боятся, что снимок на каждый коммит = копия всего проекта на диск. Нет: неизменённые объекты переиспользуются по адресу, дублируется только реально новое.
Мини-лаба: собери коммит с нуля

Повтори руками, не подсматривая в текст выше. Засекай - на это уходит минут пять, а понимания прибавляет на годы.

Код: Выделить всё

$ mkdir lab && cd lab && git init
$ printf 'hello\n' > a.txt

# 1. посчитай адрес сам и сверь с Git
$ printf 'blob 6\0hello\n' | shasum
$ git hash-object a.txt          # должно совпасть

# 2. запиши blob и tree в базу
$ git add a.txt
$ git write-tree                 # запомни адрес дерева T

# 3. собери коммит из дерева
$ git commit-tree <T> -m "by hand"   # запомни адрес коммита C

# 4. посмотри коммит изнутри
$ git cat-file -p <C>

# 5. направь ветку и проверь лог
$ git update-ref refs/heads/main <C>
$ git log --oneline

# 6. обойди всю базу двумя способами
$ git cat-file --batch-check --batch-all-objects
$ git rev-list --objects --all
Если в шаге 5 git log показал твой коммит - поздравляю, ты только что сделал commit без commit и понял, из чего он состоит.

Контрольные вопросы
  • Почему git hash-object файла не совпадает с обычным sha256sum того же файла? Что именно Git добавляет перед хешированием?
  • Ты собрал объект через git commit-tree, но git log его не видит. Что произошло и какой одной командой это чинится?
  • В двух разных каталогах лежат побайтово одинаковые файлы. Сколько blob-объектов создаст Git и почему?
  • Чем отличается то, на что ссылается commit, от того, что ссылается tree? Что именно хранит каждый из них?
Итог

Git внутри - это content-addressable хранилище с четырьмя типами объектов. blob держит содержимое файла, tree - структуру каталога с именами и режимами, commit - снимок одного корневого дерева плюс родители и метаданные, tag - аннотированную метку. Адрес каждого объекта - это SHA от строки "<тип> <длина>\0<содержимое>", поэтому одинаковые данные хранятся один раз, а любое изменение даёт новый адрес. Плоумбинг (hash-object, cat-file, write-tree, commit-tree, mktree, mktag, rev-list --objects) даёт прямой доступ к этим кирпичам, и ты лично собрал из них живой коммит. Цепочка HEAD -> commit -> tree -> blob и есть полный снимок проекта. Дальше будет про то, как Git упаковывает всё это в packfile-ы и делает быстрым на больших репозиториях.
👍5 ❤️3 🔥3 😄 🤔
Аватара пользователя
rjholland
Сообщения: 2
Зарегистрирован: 12 май 2026, 07:57

Re: Внутреннее устройство I: объектная база Git

Сообщение rjholland »

Ого, реально собрал коммит руками и он в git log появился. До этого commit-tree казался какой-то черной магией, а тут все по шагам. Спасибо!
👍2 ❤️2 🔥 😄 🤔
Аватара пользователя
asyncghost
Сообщения: 1
Зарегистрирован: 12 май 2026, 08:16

Re: Внутреннее устройство I: объектная база Git

Сообщение asyncghost »

Вопрос: а если у меня два одинаковых файла но один исполняемый - blob один или два? По уроку выходит что blob один, а режим 100644 vs 100755 живет в tree, я правильно понял?
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Подпись коммитов и тегов: SSH, GPG и Sigstore
Следующая глава →
Внутреннее устройство II: ссылки, HEAD, notes и reftable

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Внутреннее устройство Git

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость