Когда я начал использовать 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
У нас есть только содержимое внутри — никаких метаданных для файла.
Пример модификации
Давайте посмотрим, что произойдет, если мы внесем некоторые изменения в файл и добавим обновленную версию:
$ 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
…
Чтобы увидеть содержимое объекта коммита, мы можем использовать:
$ 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.