Как работает Git

Введение

Git, несомненно, является одним из главных краеугольных камней современной разработки программного обеспечения. Это обязательный инструментарий для координации работы между разработчиками и стал фундаментальным двигателем для движения с открытым исходным кодом на протяжении многих лет. Чтобы иметь простую идею, по состоянию на ноябрь 2021 года GitHub, главный менеджер репозитория Git, сообщил, что имеет более 73 миллионов разработчиков и более 200 миллионов репозиториев.

Несколько программистов имеют дело с Git каждый день и обычно применяют ключевые концепции. В этой лекции мы сделаем следующий шаг, глубоко погрузившись во внутренние органы и исследуя основные основы Git. Что такое филиал? Что такое голова? Что значит объединить ветвь? Сегодня мы ответим на эти и другие вопросы.

Прежде чем мы начнем, я хотел бы выразить особую благодарность Раджу Ганди, который помог создать эту статью благодаря своей замечательной лекции «Git next steps», которую можно найти на O’Reilly. Ясность и полнота его объяснений были для меня источником вдохновения.

Фундамент

Большие двоичные объекты, деревья и фиксации являются основными компонентами структуры данных Git. Точно так же, как дом построен из кирпича, или граф образован ребрами и узлами, эти элементы образуют фундамент Git.

Чтобы понять все это, начнем с примера. Предположим, мы создаем пустой репозиторий. Когда мы запускаем команду git init, git автоматически создает скрытую папку с именем .git, которая используется для хранения внутренних компонентов.

Большие двоичные объекты

Теперь предположим, что мы создаем файл с именем myfile.txt и добавим его в наш репозиторий с помощью команды git add myfile.txt.

Когда мы выполняем эту операцию, Git создает BLOB-объект, файл, расположенный в подпапке .git/objects, в котором хранится содержимое myfile.txt, без включения каких-либо связанных метаданных (таких как метка времени создания, автор и т. Д.). Следовательно, создание большого двоичного объекта похоже на хранение изображения содержимого файла.

Имя большого двоичного объекта связано с хэшем его содержимого. После хэширования содержимого первые два символа используются для создания вложенной папки в .git/objects, а остальные символы хэша составляют имя большого двоичного объекта.

Таким образом, при добавлении файла в Git выполняются следующие действия:

  1. Git принимает содержимое файла и хэширует его
  2. Git создает большой двоичный объект в папке .git/objects. Первые два символа хэша используются для создания вложенной папки в этом пути. В нем Git создает большой двоичный объект с именем, образованным оставшимися символами хэша.
  3. Git хранит содержимое исходного файла (его сжатой версии) в большом двоичном объекте.

Обратите внимание, что если у нас есть файл с именем myfile.txt и другой файл с именем ourfile.txt, и оба они имеют одно и то же содержимое, у них один и тот же хэш, и поэтому они хранятся в одном и том же большом двоичном объекте.

Также обратите внимание, что если мы немного изменим myfile.txt и повторно добавим его в репозиторий, Git выполнит тот же процесс, и поскольку содержимое изменено, создается новый BLOB-объект.

Дерево

Предположим, теперь мы создадим вложенную папку в нашем репозитории с именем subfolder. Также давайте создадим файл с именем yourfile.txt в этой подпапке, и добавим его в репозиторий. При этом Git создает новый большой двоичный объект для вашего файла.txt в соответствии с процессом, который мы определили в предыдущем абзаце.

На этом этапе мы фиксируем как myfile.txt так и yourfile.txt с помощью команды git commit. При этом Git выполняет два шага:

  • Создается корневое дерево репозитория
  • Он создает фиксацию

Давайте сосредоточимся на первом шаге. Итак, что же такое корневое дерево? Корневое дерево хранит структуру файлов и папок всего репозитория. Это файл, содержащий ссылку на каждый большой двоичный объект или вложенную папку, включенную в репозиторий, построенный рекурсивным образом.

Каждая строка корневого дерева ссылается на большой двоичный объект или другие поддеревья, которые, в свою очередь, таким же образом ссылаются на другие большие двоичные объекты или другие поддеревья. Следовательно, дерево является эквивалентом каталога: так же, как мы можем получить доступ к файлам и подпапкам из каталога, так и мы можем получить доступ к большим двоичным объектам и поддеревам из дерева.

После того, как Git создал корневое дерево и все связанные с ним поддеревья, он выполняет те же операции хэширования и хранения, которые мы описали выше. Точнее, он хэширует каждое дерево и использует первые два символа для создания подпапки в .git/objects, в то время как остальные символы хэширования образуют имя сохраненного файла. Следовательно, из этого процесса мы получаем столько новых файлов, сколько деревьев в структуре данных.

Выполнение

При выполнении команды git commit вторым шагом является создание фиксации. Содержимое фиксации хранится в файле, содержащем информацию, связанную с корневым деревом, родительской фиксацией (если таковая имеется) и некоторыми метаданными, такими как имя и адрес электронной почты коммиттера и сообщение фиксации.

После создания файла фиксации Git хэширует свое содержимое и использует хэш-имя для хранения содержимого в новом файле, точно так же, как и выше (первые два символа образуют имя подпапки в .git/objects, в то время как остальная часть хэша составляет фактическое имя).

И это так! Поздравляем, вы только что поняли, как структурирован Git. Теперь, с этими понятиями, чрезвычайно просто определить понятия ветви, тега, головы и слияния!

Блоки

Ветви

Ветви именуются ссылками на фиксацию. Например, при создании новой ветви с именем mybranch (например, с помощью команды git checkout -b mybranch) Git генерирует новый файл в пути .git/refs/heads с именем mybranch. Содержимое этого файла является хэшем фиксации, из которой создается ветвь.

Затем, когда мы фиксируем на mybranch, Git выполняет операции, определенные ранее (он создает корневое дерево и файл фиксации), а затем обновляет файл ветви новым хэшем фиксации.

Следовательно, ветви — это файлы, отслеживающие фиксации, и содержимое этих файлов обновляется при каждой фиксации, которую мы выполняем.

Теги

Теги — это постоянные ссылки на конкретные фиксации. Например, когда мы создаем новый тег с именем mytag (с помощью команды git tag mytag), Git генерирует новый файл по пути .git/refs/tags с именем mytag. Как и в случае с ветвями, этот файл содержит хэш фиксации, из которой создается тег.

Однако, когда мы продолжаем нашу работу и фиксируем на той же (или другой) ветви, файл тега не обновляется и продолжает указывать на конкретную фиксацию, из которой он был создан. В отличие от файлов ветвей, теги не перемещаются при выполнении новых фиксаций.

HEAD

HEAD выполняет несколько задач в Git:

  • Именно так Git узнает, какая фиксация извлечена, поэтому, когда мы делаем ветвь git, Git смотрит на HEAD, чтобы узнать, на какой ветви мы находимся.
  • Он ссылается на родительский элемент следующего коммита, поэтому фиксация, на которую указывает HEAD, будет родительской для следующей фиксации. Напомним, что когда мы выполняем фиксацию, родительская фиксация сохраняется в файле фиксации.

Если мы находимся на хозяине ветви, HEAD ссылается на эту ветвь. Если мы откроем файл HEAD, мы увидим «ref: refs/heads/master». Вместо этого, если мы переключимся на ветвь mybranch и откроем файл HEAD в папке .git, мы увидим: «ref: refs/heads/mybranch». Следовательно, HEAD не указывает на фиксацию напрямую, а скорее на ветвь, которая, в свою очередь, указывает на последний коммит в этой ветви. Таким образом, Git отслеживает, какая фиксация в настоящее время проверяется.

Когда мы находимся в ветви и выполняем фиксацию, Git считывает содержимое файла HEAD и записывает фиксацию, которая упоминается как родительская фиксация. В этом смысле HEAD предоставляет (косвенно) родителя следующего коммита.

Теперь, в Git, мы можем проверить предыдущую фиксацию и начать вносить изменения оттуда. Этот режим называется «отсоединенный режим». В этой ситуации HEAD указывает непосредственно на фиксацию, а не на ветвь. Обратите внимание, что это может быть опасно, потому что мы рискуем потерять новые коммиты. Фактически, после выполнения фиксации, если мы проверим ветвь, мы больше не сможем вернуться к этой новой фиксации, потому что на нее не ссылается ни одна ветвь! Вот почему всегда рекомендуется создавать новую ветвь перед внесением каких-либо изменений, когда мы находимся в автономном режиме!

Слияние

Слияние позволяет объединить два или более коммита. Существует два типа слияния:

  • Первый вид возникает, когда две ветви расходятся. Git создает нового ребенка, у которого двое родителей. Первый родительский элемент — это ветвь, в которой мы находимся, а второй родительский — это ветвь, которая будет объединена. Файл фиксации будет иметь двух родителей, и HEAD будет перемещен в новый дочерний узел.
  • Второй вид возникает, когда две ветви не расходятся, но на самом деле одна ветвь является преследованием другой. В этом случае слияние называется слиянием fast-forward, и оно не является реальным слиянием, потому что нет конфликтов. В этом случае Git просто перемещает HEAD и текущую ветвь в одну и ту же фиксацию, указанную из присоединяемой ветви.

Источник