Как Git хранит данные

Когда я начал использовать Git, я делал то же, что и большинство людей. Я запоминал команды для выполнения работы, не понимая, что происходит под капотом. В большинстве случаев я получал нужные результаты. Но меня все равно расстраивало то, что я иногда «ломал» репозиторий — приводил его в состояние, которого не ожидал, и не знал, как это исправить.

Есть ли у вас похожий опыт?

Короткий подход к использованию репозитория — это попытка использовать инструмент без выполнения необходимой домашней работы, чтобы узнать, как он работает. В моем случае, все «щелкнуло», как только я прочитал о внутренней модели данных, используемой в Git. Видите ли, Git — это своего рода база данных, а человек никогда не сможет работать, например, с SQL, не зная, что такое таблица, запись и т. д. Давайте восполним пробел в знаниях и посмотрим немного на внутреннее устройство репозитория Git.

.git

Git — это распределенное программное обеспечение для контроля версий, что означает, что вам не нужен внешний сервер для его использования. Все данные, необходимые Git’у, хранятся в папке .git. Как пользователь Git, вы не имеете права изменять эти файлы, но для целей этой статьи мы заглянем внутрь, чтобы увидеть, как Git хранит данные.

Сразу после создания репозитория с помощью git init, вы обнаружите внутри:

$ ls -R .git
HEAD            config          description     hooks           info            objects         refs

.git/hooks:
applypatch-msg.sample           pre-applypatch.sample           pre-rebase.sample               update.sample
commit-msg.sample               pre-commit.sample               pre-receive.sample
fsmonitor-watchman.sample       pre-merge-commit.sample         prepare-commit-msg.sample
post-update.sample              pre-push.sample                 push-to-checkout.sample

.git/info:
exclude

.git/objects:
info    pack

.git/objects/info:

.git/objects/pack:

.git/refs:
heads   tags

.git/refs/heads:

.git/refs/tags:
Войти в полноэкранный режим Выйти из полноэкранного режима

Сейчас он почти пуст: у нас есть несколько папок, в основном это файлы примеров для хуков. Мы проигнорируем их; наше внимание в этой статье будет сосредоточено в основном на содержимом .git/objects — основном хранилище данных в Git.

Блобы

Git хранит все версии каждого отслеживаемого файла в виде блобов. Git идентифицирует блобы по хэшу их содержимого и хранит их в .git/objects. Любое изменение содержимого файла создаст совершенно новый объект blob.

Самый простой способ создать объект — это добавить объект в stage. То, что находится в stage, будет частью следующего коммита. Staging — это состояние «до коммита» в git. Это место, где мы храним файлы, которые еще не зафиксированы, но уже отслеживаются Git’ом.

Пример

Давайте создадим простой файл и создадим блоб для его представления:

$ echo "Test" > test.txt
Войти в полноэкранный режим Выйти из полноэкранного режима

С помощью этой команды мы запишем «Test» в файл test.txt. Чтобы сделать его блобом, нам нужно просто добавить его на сцену, выполнив команду:

$ git add .
Войти в полноэкранный режим Выйти из полноэкранного режима

После добавления нашего нового файла на сцену, внутри .git/objects, мы имеем:

$ ls -R .git/objects
34      info    pack

.git/objects/34:
5e6aef713208c8d50cdea23b85e6ad831f0449

.git/objects/info:

.git/objects/pack:
Войти в полноэкранный режим Выйти из полноэкранного режима

У нас есть новая папка 34, а внутри нее файл 5e6aef713208c8d50cdea23b85e6ad831f0449. Это происходит потому, что хэш содержимого 345e....: два символа спереди используются в качестве каталога. Содержимое этого файла следующее:

$ cat .git/objects/34/5e6aef713208c8d50cdea23b85e6ad831f0449
xKOR0I-.
Вход в полноэкранный режим Выход из полноэкранного режима

Он сжат для эффективности хранения. Мы можем увидеть, что находится внутри, выполнив следующую команду Git:

$ git cat-file blob 345e6aef713208c8d50cdea23b85e6ad831f0449
Test
Enter fullscreen mode Выйти из полноэкранного режима

У нас есть только содержимое внутри — никаких метаданных для файла.

Пример модификации

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

$ echo "Test 2" >> test.txt
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта команда добавляет новую строку, «Test 2», в существующий файл test.txt.

Добавим текущую версию на сцену:

$ git add .
Войти в полноэкранный режим Выйти из полноэкранного режима

И посмотрим, что у нас есть внутри папки .git/objects:

$ ls -R .git/objects
34      d2      info    pack

.git/objects/34:
5e6aef713208c8d50cdea23b85e6ad831f0449

.git/objects/d2:
77ba2806ce99d418b0b5d6c28643deca0e36dc

...
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь у нас есть два объекта, второй находится в подпапке d2. Его содержимое следующее:

$ git cat-file blob d277ba2806ce99d418b0b5d6c28643deca0e36dc
Test
Test 2
Войти в полноэкранный режим Выйти из полноэкранного режима

Это то же самое, что и наш обновленный text.txt:

$ cat test.txt
Test
Test 2
Войти в полноэкранный режим Выйти из полноэкранного режима

Как мы видим, Git хранит полный файл для каждой версии.

Дерево

Объекты дерева — это то, как Git хранит папки. Они ссылаются на другие объекты в качестве своего содержимого:

  • файлы добавляются по их блобу
  • вложенные папки добавляются по их дереву

Для каждой ссылки дерево хранит:

  • имя файла или папки
  • хэш блоба или дерева
  • тип объекта
  • разрешения

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

Аналогично, поскольку различные версии одного и того же файла будут иметь несколько блобов, Git создаст еще один объект дерева для каждой версии папки.

Создание дерева

Обычно вы создаете дерево как часть коммита. Мы рассмотрим коммиты позже в этой статье, а пока давайте воспользуемся git write-tree — сантехнической командой, которая создает дерево на основе того, что находится внутри нашего стейджа.

Сантехнические и фарфоровые команды происходят от аналогии, используемой в Git:

  • фарфор — удобная в использовании команда, предназначенная для конечных пользователей. То же самое, что душевая лейка или кран в вашей ванной комнате.
  • сантехника — внутренние команды, необходимые для работы фарфора. То же самое, что и водопровод в вашем доме.

Если вы не занимаетесь продвинутыми вещами, вам не нужно знать команды водопровода.

Пример

С нашей постановкой, как и раньше, мы запускаем:

$ git write-tree
fd4f9947de2805e460bfeeca3346e3d36d617d37
Войти в полноэкранный режим Выйти из полноэкранного режима

Возвращаемое значение — это ID нашего нового объекта дерева. Чтобы заглянуть внутрь, можно выполнить:

$ git cat-file -p fd4f9947de2805e460bfeeca3346e3d36d617d37
100644 blob d277ba2806ce99d418b0b5d6c28643deca0e36dc    test.txt
Войти в полноэкранный режим Выйти из полноэкранного режима

Несмотря на то, что это другой тип данных, чем blobs, их значение хранится в том же месте:

$ ls -R .git/objects
34      d2      fd      info    pack

.git/objects/34:
5e6aef713208c8d50cdea23b85e6ad831f0449

.git/objects/d2:
77ba2806ce99d418b0b5d6c28643deca0e36dc

.git/objects/fd:
4f9947de2805e460bfeeca3346e3d36d617d37

…
Войти в полноэкранный режим Выйти из полноэкранного режима

Все данные находятся в одной и той же структуре папок.

Вложенный пример

Теперь мы добавим еще одну папку внутрь, чтобы посмотреть, как хранятся вложенные деревья:

  • создайте новую папку:
$ mkdir nested
Войти в полноэкранный режим выйти из полноэкранного режима
  • добавить папку & ее содержимое
$ echo 'lorem' > nested/ipsum
Войти в полноэкранный режим Выйти из полноэкранного режима
  • добавление на сцену
$ git add .
Войти в полноэкранный режим Выход из полноэкранного режима

Создав дерево, мы получим новый идентификатор:

$ git write-tree
25517090ae5d0eb08f694de6d38d613615fe99e4
Войти в полноэкранный режим Выйти из полноэкранного режима

Его содержимое:

$ git ls-tree 25517090ae5d0eb08f694de6d38d613615fe99e4
040000 tree bc9a36d27aa303a3b1cab543b64c6944fea5ce8b    nested
100644 blob d277ba2806ce99d418b0b5d6c28643deca0e36dc    test.txt
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы видим, что nested был добавлен как ссылка на дерево. Давайте посмотрим, что находится внутри:

$ git ls-tree bc9a36d27aa303a3b1cab543b64c6944fea5ce8b
100644 blob 3e9ffe066cd7b2ce4c6fb5c8f858496194e1c251    ipsum
Войти в полноэкранный режим Выход из полноэкранного режима

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

Коммиты

Коммит — это полное описание состояния хранилища. Он содержит следующую информацию:

  • ссылка на объект дерева, который описывает самую верхнюю папку
  • автор коммита, коммиттер и время
  • родительский(ие) коммит(ы) — коммиты, на которых основан этот коммит.

Большинство коммитов имеют только одного родителя, за следующими исключениями:

  • первый коммит в истории не имеет родителей
  • коммиты слияния имеют более одного родителя

Как и раньше, Git идентифицирует каждый коммит по хэшу его содержимого. Поэтому любое изменение файлов, папки или метаданных коммита создаёт новый коммит.

Первый коммит

Мы можем создать наш первый коммит с помощью стандартной команды commit:

$ git commit -m 'first commit'
[main (root-commit) 26349a2] first commit
 2 files changed, 3 insertions(+)
 create mode 100644 nested/ipsum
 create mode 100644 test.txt
Войти в полноэкранный режим Выйти из полноэкранного режима

На выходе мы получим усечённый идентификатор фиксации. Давайте найдем полное значение:

$ git show
commit 26349a25253f9b316db1a5d3c3f23c1ca5ca4e0e (HEAD -> main)
Author: Marcin Wosinek <marcin.wosinek@gmail.com>
Date:   Thu Apr 28 18:18:07 2022 +0200

    first commit
…
Enter fullscreen mode Выйти из полноэкранного режима

Чтобы увидеть содержимое объекта коммита, мы можем использовать:

$ git cat-file -p 26349a25253f9b316db1a5d3c3f23c1ca5ca4e0e
tree 25517090ae5d0eb08f694de6d38d613615fe99e4
author Marcin Wosinek <marcin.wosinek@gmail.com> 1651162687 +0200
committer Marcin Wosinek <marcin.wosinek@gmail.com> 1651162687 +0200

first commit
Войти в полноэкранный режим Выйти из полноэкранного режима

Ссылка на дерево такая же, как и в предыдущем примере. Мы видим, что коммиты остаются в той же папке, что и другие объекты:

ls -R .git/objects
25      26      34      3e      bc      d2      fd      info    pack

…

.git/objects/26:
349a25253f9b316db1a5d3c3f23c1ca5ca4e0e

…
Войти в полноэкранный режим Выйти из полноэкранного режима

Следующий коммит

Давайте восстановим первую версию нашего файла test.txt:

$ echo "Test" > test.txt 
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта команда перезаписывает существующий файл с именем «Test».

$ git add .
Войти в полноэкранный режим Выйти из полноэкранного режима

Добавляет обновленную версию в стейджинг.

$ git commit -m 'second commit'
[main 7f54a43] second commit
 1 file changed, 1 deletion(-)
Войти в полноэкранный режим Выйти из полноэкранного режима

Фиксирует изменения.

Находим полный идентификатор:

$ git show
commit 7f54a437d87cd1f241cfb893c4823bc7e60c19ec (HEAD -> main)
Author: Marcin Wosinek <marcin.wosinek@gmail.com>
Date:   Thu Apr 28 18:37:55 2022 +0200

    second commit
…
Войти в полноэкранный режим Выйти из полноэкранного режима

Содержание коммита таково:

$ git cat-file -p 7f54a437d87cd1f241cfb893c4823bc7e60c19ec
tree 04b0192c1c88ac1c1a96f386e84e5388ef8a509a
parent 26349a25253f9b316db1a5d3c3f23c1ca5ca4e0e
author Marcin Wosinek <marcin.wosinek@gmail.com> 1651163875 +0200
committer Marcin Wosinek <marcin.wosinek@gmail.com> 1651163875 +0200

second commit
Войти в полноэкранный режим Выйти из полноэкранного режима

Git добавил родительскую строку, потому что мы фиксируем поверх другого коммита.

Ветви и метки

Другие важные данные, хранимые Git’ом, — это просто ссылки на последний коммит. Так, моя главная ветвь хранится в .git/refs/heads/main, и её содержимое таково

$ cat .git/refs/heads/main
7f54a437d87cd1f241cfb893c4823bc7e60c19ec
Вход в полноэкранный режим Выйти из полноэкранного режима

или ID самого верхнего коммита. Мы можем найти всю необходимую информацию в постоянно расширяющемся дереве коммитов:

  • история ветки, рассказанная в сообщениях коммитов
  • кто и когда внес изменение
  • взаимосвязь между различными ветками и метками

Когда я создаю простой тег:

$ git tag v1
Войти в полноэкранный режим Выход из полноэкранного режима

Создается файл в .git/refs/tags:

$ cat .git/refs/tags/v1
7f54a437d87cd1f241cfb893c4823bc7e60c19ec
Войти в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, и теги, и ветви являются явными ссылками на коммит. Единственное различие между ними заключается в том, как Git обращается с ними, когда мы создаём новый коммит:

  • текущая ветвь перемещается в новый коммит
  • теги остаются без изменений

Резюме

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

Вы нашли эту статью полезной? Подпишитесь на рассылку, чтобы получать уведомления о публикации новых статей по программированию и JavaScript.

Оцените статью
Procodings.ru
Добавить комментарий