Каталог .git и адресация по содержимому
Вся магия системы контроля версий сосредоточена в скрытой папке с именем .git в корне проекта. Если стереть эту папку, проект превратится в обычный набор файлов на диске, полностью лишённый истории изменений. Внутри этой директории находятся конфигурация репозитория, указатели на ветки и журнал операций, но центральную роль играет папка objects.
Git работает по принципу хранилища ключ-значение. Значением выступают произвольные бинарные данные, а ключом становится криптографический хеш от этих данных. Исторически Git вычисляет контрольную сумму по алгоритму SHA-1, генерируя сороказначную шестнадцатеричную строку. Первые два символа хеша используются как имя подпапки внутри каталога objects, а оставшиеся тридцать восемь символов служат именем файла. Такое разбиение предотвращает деградацию файловой системы при наличии сотен тысяч записей в одной директории.
Каждый сохраняемый объект снабжается стандартизированным заголовком. Заголовок состоит из названия типа объекта, пробела, размера содержимого в байтах и нулевого байта в качестве терминатора. После этого заголовок конкатенируется с полезной нагрузкой, сжимается алгоритмом zlib и записывается на диск по вычисленному пути.
Четыре базовых объекта объектной модели
Для фиксации абсолютно любого состояния проекта Git задействует всего четыре типа сущностей. Они неизменяемы: изменение хотя бы одного символа в исходном коде меняет итоговый хеш, порождая полностью новый объект.
Блобы
Блоб хранит только бинарное содержимое файла. Внутри блоба отсутствуют имя файла, права доступа, временные метки создания или редактирования. Если создать десять файлов с разным именем в разных папках, но с абсолютно одинаковым текстом внутри, Git сохранит на диске ровно один блоб. Это обеспечивает автоматическую дедупликацию данных без дополнительных вычислительных затрат.
Деревья
Объект дерева решает задачу организации иерархии каталогов. Дерево представляет собой плоский бинарный список записей. Каждая запись содержит режим доступа в восьмеричной системе (например, 100644 для обычного файла или 100755 для исполняемого скрипта), тип целевого объекта, его SHA-хеш и имя файла или директории. Дерево может ссылаться на блобы, соответствующие конкретным файлам, или на другие дочерние деревья, образуя вложенную структуру папок проекта.
Коммиты
Коммит фиксирует конкретное состояние всего проекта в определённый момент времени. По своей структуре коммит является текстовым файлом с чётким форматом. В нём содержится ссылка на корневой объект-дерево верхнего уровня, одна или несколько ссылок на хеши родительских коммитов, информация об авторе и коммитере с временной меткой, а также сообщение с описанием внесённых изменений. Наличие ссылки на родителя превращает последовательность коммитов в направленный ациклический граф, разворачивающийся от самых свежих правок к первому коммиту.
Аннотированные теги
Четвёртый объект похож на коммит, но указывает на другой объект (чаще всего на коммит, хотя технически может ссылаться даже на отдельный блоб). Аннотированный тег содержит собственное имя, сообщение создателя, дату, имя автора метки и опциональную цифровую подпись GPG. Легковесные теги отдельным объектом не являются, оставаясь простыми текстовыми файлами ссылок.
Индекс как промежуточный слой между диском и базой
Многие воспринимают область подготовки (staging area) как абстрактное состояние, но физически это один двоичный файл с именем index внутри папки .git. Индекс содержит упорядоченный список путей всех отслеживаемых файлов, их текущие права, временные метки изменения на диске и SHA-1 хеши блобов.
При выполнении команды git add Git немедленно создаёт блобы в базе объектов и записывает соответствие имён и хешей в файл index. Когда запускается команда фиксации изменений, Git не сканирует файлы на диске. Он моментально собирает объекты деревьев прямо на основе бинарной структуры index, создаёт коммит и перемещает указатель текущей ветки.
Как устроены ветки и указатель HEAD
Ветки в Git предельно легковесны. Ветка представляет собой обычный текстовый файл весом в сорок один байт, расположенный в каталоге .git/refs/heads/. Внутри этого файла записан только хеш последнего коммита ветки и символ перевода строки. Создание новой ветки сводится к созданию крошечного текстового файла на диске.
Файл HEAD указывает на то место в графе, где пользователь находится прямо сейчас. В штатном режиме HEAD содержит символическую ссылку на файл конкретной ветки, например ref: refs/heads/main. При создании коммита Git считывает имя ветки из HEAD, записывает новый коммит с родителем из текущей ветки, а затем перезаписывает хеш в файле этой ветки на новый. Состояние detached HEAD наступает тогда, когда в файл HEAD записывается прямой хеш коммита вместо символической ссылки на ветку.
Механика слияния веток и возникновение конфликтов
Операция слияния веток опирается на топологию графа коммитов. В простейшем случае, когда целевая ветка содержит текущую ветку в своей истории без параллельных коммитов, происходит перемотка (fast-forward). Git просто переписывает сорокабайтный хеш в файле ветки на более свежий.
Если ветки разошлись, запускается алгоритм трёхстороннего слияния (three-way merge). Для этого Git выполняет следующие шаги:
- Находит ближайшего общего предка двух сливаемых веток с помощью топологической сортировки графа коммитов.
- Сравнивает слепок корневого дерева общего предка со слепком дерева первой ветки, вычисляя первую дельту.
- Сравнивает слепок корневого дерева общего предка со слепком дерева второй ветки, вычисляя вторую дельту.
- Объединяет изменения обеих дельт в рабочий каталог и индекс, если они затрагивают разные файлы или разные строки одного файла.
- Создаёт специальный коммит слияния, содержащий сразу двух родителей, запечатывая объединённый снимок проекта.
Конфликт слияния возникает в строго определённой ситуации. Если в обеих ветках относительно общего предка были изменены одни и те же строки одного файла (либо строки, расположенные вплотную друг к другу без неизменного контекста), алгоритм не может автоматически решить, какую версию предпочесть. Процесс слияния останавливается. Git оставляет в индексе три разные версии блоба (базовую, версию ветки А и версию ветки Б) и добавляет в рабочий файл специальные маркеры конфликта для ручного разрешения разработчиком.
Частые вопросы
Что происходит на диске при выполнении команды git add?
Git считывает файл из рабочей директории, формирует блоб с бинарным заголовком, сжимает его через zlib и сохраняет в каталоге .git/objects под именем его SHA-хеша. Затем путь к файлу и полученный хеш заносятся в бинарный файл .git/index.
Чем на уровне объектов отличаются режимы soft, mixed и hard команды git reset?
Режим soft просто перемещает указатель ветки на указанный коммит в графе, оставляя индекс и рабочую директорию без изменений. Режим mixed перемещает указатель ветки и обновляет файл index содержимым дерева целевого коммита. Режим hard дополнительно перезаписывает файлы в рабочем каталоге, удаляя незафиксированные локальные правки.
Зачем Git постепенно переходит с алгоритма SHA-1 на SHA-256?
В 2017 году исследователи практически продемонстрировали коллизию SHA-1, сгенерировав два разных файла PDF с одинаковым хешем. Для исключения теоретической подмены содержимого в цепочке коммитов в архитектуру Git заложили поддержку криптографически более стойкого алгоритма SHA-256.
Как Git определяет висячие объекты и удаляет их?
Если коммит был отменён через reset, он выпадает из графа доступности, достижимого из веток и тегов. Команда git gc проверяет журнал ссылок reflog. После истечения срока хранения записей в reflog сборщик мусора физически стирает недостижимые блобы, деревья и коммиты с диска.