Объектная модель и история
Что это такое
Git хранит объекты четырёх видов. Blob содержит содержимое файла без имени. Tree хранит имена и указывает на blob’ы и другие tree, то есть представляет каталог, а commit указывает на один tree, на свои родительские коммиты, а также на автора, сообщение и метку времени. Tag даёт имя коммиту. Каждый объект адресуется хешем своего содержимого — поэтому историю можно проверить и поэтому одинаковое содержимое хранится один раз.
Почему это важно
Почти каждое непонятное поведение git становится очевидным, как только модель перед глазами. Становится ясно, почему коммит — снимок, а не диф, почему изменение старого коммита меняет все хеши после него, почему ветка дёшева и не занимает места и почему «потерянный» коммит можно восстановить, если знать его хеш. Это знание превращает набор заученных команд в мысленную модель, из которой можно рассуждать, и важнее всего оно именно там, где заученная команда неверна и разрушительна.
Ключевые идеи
Всё есть объект, адресуемый по содержимому
Имя каждого объекта — хеш его содержимого, поэтому два файла с одинаковыми байтами делят один объект, а изменение любого байта даёт другое имя. Так хранилище становится надёжным и дедуплицированным: коммит нельзя незаметно изменить, не сменив его имя, и каждый клон может проверить всю историю.
blob "hello\n" -> один хеш для этого содержимого
tree [ "readme.md" -> blob ]
commit [ tree, parent, author, message ]
Коммит — снимок, а не диф
Коммит указывает на полный tree вашего проекта в тот момент. Git хранит его эффективно, переиспользуя объекты, которые не изменились, однако читать коммит следует как «проект выглядел вот так». Поэтому переключение на старый коммит безопасно и полно, и поэтому диф — это то, что git вычисляет для показа, сравнивая два снимка.
commit B: tree contains readme.md v2, src/app.ts v3
commit A: tree contains readme.md v1, src/app.ts v3
diff A..B -> only readme.md changed
История — это граф; ветки — просто указатели
Коммит называет своих родителей, поэтому история — направленный граф, а ветка — изменяемое имя, указывающее на один коммит. Создание ветки пишет файл на сорок байт; никакого копирования нет. При коммите имя ветки переезжает на новый коммит, а HEAD следует за веткой, на которой вы находитесь. Отделённый HEAD — это просто HEAD, который указывает на коммит, а не на ветку.
A <- B <- C (main)
\
D (feature)
Индекс — настоящий объект, а не формальность
Между рабочим деревом и коммитом лежит индекс, он же область подготовки: предлагаемый следующий tree. git add записывает содержимое файла в хранилище объектов и обновляет индекс, а рабочее дерево остаётся как было. Поэтому можно подготовить часть изменения и закоммитить ровно её. Именно трёхстороннее отношение между HEAD, индексом и рабочим деревом и показывает git status.
HEAD tree index working tree
app.ts v1 app.ts v2 app.ts v2 + extra edit
^ staged ^ unstaged
Ссылки — это имена, а reflog запоминает, где они были
Ветки, теги и HEAD — всё это ссылки: файлы в небольшом каталоге, хранящие хеш. Когда ветка двигается, git записывает прежнее значение в журнал, поэтому коммит, который после reset выглядит потерянным, обычно всё ещё достижим по имени или по хешу. Это механизм, лежащий в основе любой процедуры «восстановления потерянной работы».
Практика
Прочитайте репозиторий как объекты
В небольшом репозитории перечислите объекты и выведите по одному каждого вида, не пользуясь высокоуровневой командой.
Готово, когда: вы можете опознать blob, tree и commit и объяснить, почему в blob нет имени файла.
Пройдите граф вручную
Начиная с текущего коммита, идите по родительским ссылкам и выпишите граф для последних пяти коммитов, включая любую ветку или тег, которые на них указывают.
Готово, когда: ваш рисунок совпадает с выводом команды log с опцией графа, включая форму любого слияния.
Частые ошибки
- Уверенность, что коммит хранит диф — тогда мысленная модель делает rebase и слияние похожими на магию и риск; исправить: читайте коммиты как полные снимки, которые делят неизменённые объекты.
- Вера, что ветка — копия файлов — люди избегают ветвления из страха цены; исправить: помните, что ветка — указатель, и создание её мгновенно.
- Предположение, что история линейна — слияние даёт двух родителей, и диф к одному из них — не вся картина; исправить: нарисуйте граф и проверьте число родителей.
- Удаление ветки, чтобы выбросить работу, с ожиданием, что коммиты исчезнут — они ещё некоторое время достижимы через reflog; исправить: узнайте правила сборки мусора, прежде чем полагаться на удаление.
- Правка старого коммита в общей ветке — каждый коммит после него переписывается, и клон каждого коллеги расходится; исправить: считайте опубликованную историю неизменяемой и добавьте новый коммит.
Что читать дальше
- Ежедневный рабочий процесс (01-cs-fundamentals/12-git/02-the-daily-workflow) — как применить эту модель к коммитам, веткам, слиянию и отмене без страха.
- GitHub (01-cs-fundamentals/23-github) — как граф объектов становится местом, где команда делится работой.
- Внутреннее устройство баз данных (01-cs-fundamentals/04-database-internals) — хранилище с адресацией по содержимому, та же идея git, но в другом контексте.