Объектная модель и история

Что это такое

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, но в другом контексте.