Как git хранит изменения
Принцип хранения объектов в git
Определения
- Рабочий каталог - непосредственно файлы проекта в файловой системе.
- Локальное хранилище - это папка
.git
. - Индекс (staging) - это промежуточный каталог изменений, которые произошли в рабочем каталоге. То есть когда мы делаем
git add
изменения попадают сюда.
.git
По сути папка .git
это и есть база данных и метаданные проекта, там есть все что нужно для управления репозиторием.
1
2
3
4
5
6
7
8
project
.git
... Скрытые объекты
file1
file2
folder1
folder2
... Другие файлы проекта
То есть, есть проект, а есть его база данных. Физически это все хранится в одной папке.
Хранилище объектов
Хранилище объектов git
содержит файлы данных, сведенья о датах, авторах, то что нужно для восстановления любой версии на конкретный момент времени.
В хранилище могут быть:
BLOB - двоичные объекты
Каждая версия файла представлена в виде BLOB
объекта.
Деревья
Это структура одного уровня каталога, в которой регистрируются идентификаторы BLOB
объектов, пути, метаданные.
Также содержит ссылки на другие поддеревья.
Коммиты
Коммит содержит метаданные каждого внесенного в репозиторий изменения. Каждый коммит указывает на объект дерева, отражающее состояние репозитория на момент фиксации изменений.
У большинства коммитов есть один родитель, но это не всегда так.
Теги
Тег присваивается коммиту и имеет имя
Про хранение объектов
Эти 4 объекта выше являются неизменяемыми.
Хранилище объектов основано на вычислении хеша содержимого объектов, а не на именах файлов или каталогов.
То есть когда, объект помещается в хранилище на основе хеша его данных (содержимого).
git отслеживает содержимое, а не файлы.
Если например у нас есть два одинаковых файла, пусть даже с разными именами, то git создаст один BLOB
объект для их содержимого.
В качестве имени файла используется хеш его содержимого.
А история изменения файла вычисляется как набор изменений между разными BLOB
объектами, а не именами файлов и отличиями непосредственно.
Так же внутренняя база данных хранит каждую версию каждого файла. Git рассматривает имя файла как элемент данных, не моделируя физическую раскладку файлов по каталогам.
То есть у нас есть большой BLOB
данных, который нужно поместить по пути path/test/test2/file
. Git распознает эту строку как путь до файла.
В самом низу структуры находится BLOB
объект, он не ссылается на другие объекты, а на него ссылаются объекты дерева. Будем считать это листом дерева. Объекты дерева указывают на другие деревья и на BLOB
объекты. Коммит указывает на конкретное дерево, то есть добавляет его.
1
2
3
4
5
Коммит ert456ss - Теги и ветки привязываются к коммиту
Дерево 23asdfgw
Дерево 234uy6
BLOB 13dcaawe
BLOB 7567567x
Получается, что каждый коммит указывает на своего родителя ориентируясь на его состояние.
О коммитах и ветках напишу отдельные заметки.
Внутри .git
Обычно не требуется управлять или просматривать файлы в каталоге .git
. Это делается с помощью команд.
С целью изучения создадим пустой репозиторий и просмотрим содержимое каталога .git
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mkdir test
cd test/
git init -b main
tree .git/
.git/
├── branches
├── config
├── description
├── HEAD
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── fsmonitor-watchman.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── pre-merge-commit.sample
│ ├── prepare-commit-msg.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ ├── pre-receive.sample
│ ├── push-to-checkout.sample
│ ├── sendemail-validate.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
Теперь добавим файл
1
2
3
4
5
6
7
8
echo "hello world" > hello.txt
git add hello.txt
tree .git/objects/
.git/objects/
├── 3b
│ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── info
└── pack
Как видим создался BLOB
объект, когда мы выполняли команду git add hello.txt
.
Здесь абсолютно не важно как называется файл, важно только его содержимое.
Конкретно текст "hello world"
. Git вычисляет его хеш 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
, и добавляет объект в хранилище.
Можем посмотреть его содержимое.
1
2
git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
hello world
Вот так хранится их содержимое. Но как понять к какому файлу принадлежит это содержимое.
Пути git отслеживает с помощью деревьев. Когда мы выполняем git add
, git создает объект для содержимого файла, но не объект для дерева. В данном случае система работает с индексом .git/index
и при каждом взаимодействии с файлами add rm mv
, git будет обновлять информацию о пути и BLOB
объекте.
Теперь воспользуемся низкоуровневой командой write-tree
, чтобы сделать снимок состояния индекса, тем самым создать дерево.
1
2
3
4
5
6
7
8
9
10
git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
tree .git/objects/
.git/objects/
├── 3b
│ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── 68
│ └── aba62e560c0ebc3396e8ae9335232cd93a3f60
├── info
└── pack
Теперь посмотрим, что входит в дерево, как видим это тот же BLOB
объект.
1
2
git cat-file -p 68aba6
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello.txt
Если создавать дерево несколько раз одной и той же командой, не добавляя и не удаляя ничего из индекса.
Новое дерево при этом не будет создано.
1
2
3
4
5
6
7
8
9
10
11
12
git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
Теперь можем создать объект коммита
1
2
echo -n "first commit" | git commit-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60
9153afe6085a630dbb7565b4ac66ccc045003889
И посмотреть его
1
2
3
4
5
6
git cat-file -p 9153afe
tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60
author Alexey Shmelev <alexsey_89@bk.ru> 1733053715 +0300
committer Alexey Shmelev <alexsey_89@bk.ru> 1733053715 +0300
first commit
Естественно хеши коммитов будут отличиться, так как временная метка и имя автора у всех разные.
Коммит считается основной единицей информации.
Теперь создадим тег.
Тег может быть двух видов:
- Легковесный - просто ссылка на объект коммита
- Аннотируемый - создает полноценный объект тега
1
git tag -a V1.0 9153afe6085a630dbb7565b4ac66ccc045003889
Тег привязывается к коммиту.
Индекс
Каталог индексирования содержит временное описание структуры всего репозитория в момент времени, отражающая представление и стояние всех объектов.
Информация здесь временная и показывает состояния проекта между рабочим каталогом и хранилищем объектов.
То есть благодаря индексу можно поэтапно вносить изменения, которые будут включены в следующий коммит. Так как индекс временная информация, из индекса можно исключить.
Для каждого файла генерируются SHA-1 хеш, это значит, что один и тот же файл в разных каталогах и на разных машинах будет давать одинаковый хеш
Что мы поняли
git
хранит изменения файлов в объектном хранилище, рассмотрели 4 типа неизменяемых объектов. Воспользовались для этого низкоуровневыми командами.
Поняли, что главное для git - это контент файлов, на основании него он строит хеш.