Пост

Как git хранит изменения

Принцип хранения объектов в 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 - это контент файлов, на основании него он строит хеш.

Авторский пост защищен лицензией CC BY 4.0 .