Git — контентно-адресуемая файловая система
Механизмы работы внутренней базы данных, типы объектов и их жизненный цикл.
Хотите проверить свои представления по теме? Небольшой тест из 7 вопросов.
Основные принципы работы.
В большинстве сфер нашей жизни, и IT не стало здесь исключением, поведение чего-то становится для нас достаточно понятным и логичным, если есть представление о внутреннем устройстве данных механизмов. Одним из базовых определений, которое используют все, является репозиторий.
Репозиторий Git - это хранилище данных, представленное специальным набором файлов и "папок". В локальной копии обычно существует директория .git, которая и является непосредственным корнем репозитория, а не его частью. Файлы и директории, которые находятся на одном уровне с .git - это рабочая область/дерево (working directory/tree/space). Рабочая область является разделом проекта Git, но не частью репозитория. Третьим основным разделом проекта выделяют индекс (index/staging area), данные относящиеся к индексу хранятся внутри репозитория.
На серверах же создаются голые (bare) репозитории, т.е. какой-нибудь [email protected]:akorolev-dev/git-plumbing-and-porcelain-content.git представляет собой директорию аналогичной структуры, что и локальная .git. Рабочая область и индекс в них отсутствуют.
Обычно говорят, что есть два варианта создания нового репозитория:
Информационные сообщения команды init подтверждают, что корнем репозитория является либо .git, либо корень проекта при варианте bare.
Но по факту клонирование просто начинается с инициализации пустого локального репозитория.
Это можно воспроизвести разными способами.
Например, запросить какой-то репозиторий на существующем сервере, где потребуется пройти аутентификацию вызова.
В момент остановки команды клонирования из-за запроса аутентификационных данных уже произойдет инициализация нового пустого репозитория.
А в файл конфигурации проекта Git будет добавлен первый псевдоним удаленного репозитория **origin**.
Если аутентификация происходит успешно, то начинается загрузка данных (git fetch).
Но по факту клонирование просто начинается с инициализации пустого локального репозитория. Это можно воспроизвести разными способами.
Например, запросить какой-то репозиторий на существующем сервере, где потребуется пройти аутентификацию вызова. В момент остановки команды клонирования из-за запроса аутентификационных данных уже произойдет инициализация нового пустого репозитория. А в файл конфигурации проекта Git будет добавлен первый псевдоним удаленного репозитория origin. Если аутентификация происходит успешно, то начинается загрузка данных (git fetch).
Также, если предварительно изменить шаблон локального репозитория, то мы увидим данные изменения по итогам завершения клонирования.
Устройство файловой системы
Пустой репозиторий
Репозиторий Git реализован в виде набора файлов и директорий определенной структуры, с адресацией на основе содержимого. Подробно внутреннее устройство описано в 10 разделе книги, мы затронем только отдельные моменты.
Создадим новый проект content с веткой content-article-01, и для отслеживания изменений внутренней структуры этого репозитория
сделаем его самого рабочей областью второго Git проекта, который для удобства будем называть наблюдателем (watcher), с веткой watcher-article-01.
Зафиксируем это начальное состояние файловой системы.
Обратите внимание, что в репозитории уже существуют директории objects и refs, но в коммит они добавлены не были, так как еще не содержат никаких файлов.
Создание первого файла
Давайте в качестве первого файла создадим MIT лицензию, скачав её с github.
После создания нового файла, в репозитории никаких изменений не произошло.
Но вот после включения файла в индекс, создался не только сам файл индекса, но и новый файл в поддиректории внутри objects.
В Git существует забавное разделение всех команд на два типа:
- "Фарфор" (Porcelain) - это как раз привычные большинству пользователей команды.
- "Сантехника" (Plumbing) - это команды низкого уровня, которые в первую очередь предназначены для создания инструментов работы с репозиторием Git. Но именно "сантехника" позволяет понять принципы работы данной системы контроля версий.
Посмотрим содержимое нового объекта. Только стоит учесть, что все объекты базы данных Git сжаты алгоритмом zlib и открыть напрямую их не получится.
После первых 9 символов наблюдаем полный текст лицензии, которая была включена в индекс. Воспользовавшись некоторыми "сантехническими" командами, можно объяснить происхождение всех данных, авторами которых мы являемся опосредованно.
Репозиторий Git представляет собой базу данных типа ключ-значение. Значение называется объектом Git и может быть одного из четырёх типов. Ранее была упомянута "адресация на основе содержимого", этот принцип заключается в способе формирования ключа, который представляет собой контрольную сумму по алгоритму SHA-1. Каждый сорокасимвольный хеш вычисляется на основе содержимого соответствующего объекта.
Любой объект репозитория создается в поддиректории каталога objects.
Наименование файла объекта и наименование его родительской директории получается путем разделения вычисленной контрольной суммы на две части:
- Первые 2 символа формируют название родительской директории.
- Оставшиеся 38 символов становятся наименованием файла с данными по объекту.
Blob - это первый тип создаваемых объектов, который представляет собой снимок отдельного файла. А 1069 - количество байт исходной информации в файле LICENSE, до её сжатия. Метаинформация о типе и размере отделяются от исходного содержимого нулевым символом (NUL/null byte). Стоит обратить внимание, что метаинформация не содержит сведений о наименовании и временных метках, как и назначенных на файл прав.
Модификация первого файла
Проверим что будет, если удалить файл из индекса, изменить в нём строку и опять добавить в индекс.
Удаление файла лицензии из индекса не приводит к удалению соответствующего объекта из базы данных. А после добавления в индекс уже модифицированного файла, происходит создание второго объекта.
Таким образом можно выделить еще несколько ключевых принципов работы репозитория Git:
- База данных практически всегда работает только на добавление новых объектов, не торопясь удалять ранее созданные, для которых уже нет исходных файлов.
- Даже при небольшом изменении файла (модификация 1 строки из 22, и добавлении 18 символов к 1069) производится полный снимок содержимого исходного файла.
Создание первого коммита
Что же, самое время отследить изменения, которые произойдут после создания первого коммита в репозитории "content".
Можно увидеть, что создание коммита привело к появлению уже не просто одного нового объекта, а шести разных файлов, среди которых два новых объекта. Многие из этих файлов будут рассмотрены в рамках других статей этой серии, сейчас проанализируем только объекты. И не забудем предварительно зафиксировать состояние отслеживаемого репозитория внутри watcher.
Первый объект обладает типом commit. В содержимом можно увидеть данные по пользователям, которые сделали изменение и сформировали сам коммит, в данном случае они совпадают. Содержится хеш объекта tree, он как раз совпадает с адресом второго объекта, который появился после создания коммита. После почтового ящика пользователя содержится временная метка создания коммита, с точностью до секунд.
Объект tree — это представление какой-то директории, в данном случае корня рабочей области. Он содержит ключи объектов дочерних tree и blob.
Именно в нем отражаются информация по правам доступа и наименованию файлов или директорий.
Таким образом любой коммит кроме метаинформации о себе содержит ключ полного снимка состояния рабочей директории, начиная с её корня. Да, Git не является системой контроля версий, работающей с отдельными дельтами изменений. Но и снимки состояний делаются только при необходимости.
Создание второго коммита
Если добавить второй файл и создать новый коммит, то будет создано три новых объекта:
А вот по части файла LICENSE, содержимое которого не поменялось, будет переиспользован ранее созданный объект, ведь его хеш-адрес остался тем же. Так что "полный снимок" не влечет многократное дублирование tree и blob объектов, если их содержимое не было изменено.
Единственное на что хотелось бы здесь заострить внимание, так это поле parent, которое содержит адрес предыдущего коммита. Именно оно формирует историю изменений. И одновременно является причиной обновления всех хеш-ключей в ветке коммитов, если внести изменения в любое не последнее звено этой цепочки.
Дополнение
Внимательный читатель мог заметить, что мы рассмотрели объекты трех типов, а в тексте было упоминание, что всего их четыре. Это не опечатка. Четвертым типом является tag, но такие объекты порождаются для аннотированных тегов. А вот "лёгкие" теги работают только через механизм ссылок и не приводят к созданию нового объекта.