Git
Chapters ▾ 2nd Edition

10.2 Git зсередини - Об’єкти Git

Об’єкти Git

Git є файловою системою адресованого вмісту. Чудово. Що це означає? Це означає, що в підвалинах Git — це просте сховище даних для ключів-значень. А це означає, що ви можете вставити будь-який вміст до сховища Git, а Git поверне вам унікальний ключ, який ви можете потім використати для отримання цього вмісту.

Кухонна команда git hash-object приймає дані, зберігає їх у вашій директорії .git/objects (база даних обʼєктів), та повертає вам унікальний ключ, що тепер вказує на цей обʼєкт.

Спочатку, ви створюєте нове сховище Git та перевіряєте, що директорія objects (очікувано) порожня:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

Git створив директорію objects та піддиректорії pack та info в ній, проте звичайних файлів там немає. Тепер використаймо git hash-object, щоб створити новий обʼєкт даних та вручну зберегти їх у вашій новій базі даних Git:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

У найпростішому варіанті git hash-object приймає вміст та просто повертає унікальний ключ, що використовувався би для збереження цих даних у базі даних Git. Опція -w інструктує команду не лише просто повернути ключ, а й зберегти його до бази даних. Нарешті, опція --stdin каже git hash-object отримати вміст з stdin; інакше команда очікує отримати шлях до файлу в аргументах.

Вивід від команди — 40 символьна хеш сума. Це SHA-1 хеш — перевірочна сума вмісту, який ви зберігаєте плюс заголовок (header), про який ви дізнаєтесь трошки пізніше. Тепер ви можете побачити, як Git зберігає ваші дані:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Якщо ви знову перевірите, що містить ваша директорія objects, то побачите новий файл. Ось так Git зберігає вміст спочатку — як один файл для кожного шматочку вмісту, названий SHA-1 сумою вмісту та заголовком. Піддиректорія названа першими двома символами SHA-1, а файл рештою 38 символами.

Щойно вміст зберігається у вашій базі даних, ви можете переглянути його за допомогою команди git cat-file. Ця команда є чимось на кшталт швейцарського ножа для перегляду об’єктів Git. Якщо передати -p, то команда cat-file спочатку розбереться, якого формату вміст, та відобразить його відповідно:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Тепер, ви можете додавати вміст до Git та отримувати його назад. Ви також можете робити це з вмістом файлів. Наприклад, ви можете зробити просте керування версіями файлу. Спочатку, створіть новий файл та збережіть його вміст у базі даних:

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

Потім, запишіть новий вміст до файлу та збережіть його знову:

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

Ваша база даних обʼєктів тепер містить обидві версії цього нового файлу (а також перший вміст, який ви там зберегли):

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Зараз ви можете видалити локальну копію файлу test.txt і використати Git, щоб отримати з бази даних обʼєктів або першу збережену версію:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

або другу:

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

Проте запам’ятовувати SHA-1 ключ для кожної версії вашого файлу непрактично; на додаток, ви не зберігаєте ім’я файлу у вашій системі — лише вміст. Цей тип об’єкта називається блоб. Git може вам видати тип будь-якого об’єкта, якщо надати його SHA-1 ключ команді git cat-file -t:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Об’єкти дерева

Далі ми розглянемо тип об’єкта Git дерево, який вирішує проблему збереження імені файлу, а також дозволяє зберігати групу файлів разом. Git зберігає вміст у схожий на файлову систему UNIX спосіб, проте дещо спрощений. Весь вміст зберігається як об’єкти дерева та блоби, дерева відповідають UNIX директоріям, а блоб схожий на inode чи вміст файлу. Один об’єкт дерево містить один чи більше елементів дерева, кожен з яких містить SHA-1 вказівник на блоб або піддерево з асоційованими правами доступу, типом та іменем файлу. Наприклад, найновіше дерево в проекті може виглядати так:

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

Синтаксис master^{tree} визначає об’єкт дерево, на яке вказує останній коміт вашої гілки master. Зверніть увагу, що піддиректорія lib — це не блоб, а вказівник на інше дерево:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb

Концептуально, дані, які зберігає Git, скидаються на щось таке:

Проста версія моделі даних Git.
Figure 148. Проста версія моделі даних Git.

Ви можете доволі легко створити власне дерево. Git, зазвичай, створює дерево, коли бере стан вашого індексу чи області додавання (staging area) та записує низку об’єктів дерев з нього. Отже, щоб створити об’єкт дерево, спочатку треба налаштувати індекс, додавши до нього деякі файли. Щоб створити індекс з єдиним елементом — першою версією вашого файлу test.txt — ви можете скористатись кухонною командою git update-index. Ви використовуєте цю команду, щоб штучно додати більш ранню версію файлу test.txt до нового індексу. Ви маєте передати опцію --add, оскільки файл ще не існує в нашому індексі (у вас навіть немає індексу покищо) та --cacheinfo, оскільки файлу, який ви додаєте, не існує у вашій директорії, проте існує у вашій базі даних. Потім, ви визначаєте права доступу, SHA-1 та ім’я файлу:

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

У даному випадку, ви встановили права доступу 100644, що означає звичайний файл. Інші можливі значення — 100755, що означає виконанний файл; та 120000, що означає символічне посилання. Права доступу походять від звичайних прав доступу UNIX, проте вони набагато менш гнучкі — ці три варіанти єдині, які можна задати для файлів (блобів) у Git (хоча інші використовуються для директорія та підмодулів).

Тепер, ви можете використати git write-tree`, щоб записати індекс до об’єкта дерева. Опція -w не потрібна — виклик цієї команди автоматично створює об’єкт дерева з індексу, якщо цього дерева ще не існує:

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

Ви також можете переконатись, що цей об’єкт є деревом за допомогою вже знайомої вам команди git cat-file:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Тепер ви створите нове дерево з другою версією test.txt та також новий файл:

$ echo 'new file' > new.txt
$ git update-index --add --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt

Ваш індекс тепер має нову версію test.txt, а також новий файл new.txt. Збережіть це дерево (запишіть стан індексу до об’єкта дерева) та подивіться, як воно виглядає:

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Зауважте, що це дерево містить обидва файли, а також, що SHA-1 файлу test.txt таке саме, як у вищенаведеній “верії 2” цього файлу. Заради розваги, додайте перше дерево як піддиректорію другого. Ви можете читати дерева до індексу, якщо викликаєте git read-tree. У даному випадку, ви можете зчитати існуюче дерево до вашого індексу як піддерево за допомогою опції --prefix цієї команди:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Якщо ви створили робочу директорію з нового дерева, яке ви щойно записали, то отримаєте два файли на верхньому рівні робочої директорії та піддиректорію під назвою bak, яка містить першу версію файлу test.txt. Ви можете вважати, що Git зберігає дані для цих структур наступним чином:

Структура вмісту ваших поточних даних Git.
Figure 149. Структура вмісту ваших поточних даних Git.

Об’єкти комітів

Якщо ви виконали всі вищенаведені команди, то у вас тепер є три дерева, які відповідають різним відбитки вашого проекту, за якими ви бажаєте слідкувати, проте залишається попередня проблема: ви маєте пам’ятати всі три значення SHA-1, щоб згадати відбитки. Ви також не маєте будь-якої інформації про те, хто створив відбитки, коли вони були створені, чи чому їх створили. Це базова інформація, яку для вас зберігає об’єкт коміту.

Щоб створити об’єкт коміту, треба викликати commit-tree та задати єдиний SHA-1 дерева, та які об’єкти комітів, якщо такі існують, йому безпосередньо передували. Почніть з першого збереженого дерева:

$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d

Ви отримаєте інше значення хешу через інший час створення й авторські дані. Замінюйте хеши комітів і міток на власні суми надалі в цьому розділі. Тепер ви можете подивитись на свій новий об’єкт коміт за допомогою git cat-file:

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

first commit

Формат об’єкта коміту простий: він задає дерево верхнього рівня для відбитку проекту на той час; інформацію про автора та того, хто створив коміт (використовує ваші налаштування user.name та user.email, а також час створення); порожній рядок, а потім повідомлення коміту.

Далі, ви запишете інші два коміти об’єктів, кожен з яких посилається на той, що був перед ним:

$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

Кожен з трьох об’єктів комітів вказує на одне з трьох дерев, які ви створили. Доволі дивно, проте у вас тепер є справжня історія Git, яку ви можете переглядати командою git log, якщо виконаєте її з SHA-1 останнього коміту:

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

	third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:14:29 2009 -0700

	second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:09:34 2009 -0700

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

Дивовижно. Ви щойно використовували низькорівневі операції, щоб створити історію Git без використання бодай однієї клієнтської. Це, по суті, Git і робить, коли ви виконуєте команди git add та git commit — зберігає блоби для файлів, які змінилися, оновлює індекс, записує дерева, та записує об’єкти комітів, які посилаються на дерева верхнього рівня та коміти, які йшли безпосередньо перед ними. Ці три головні об’єкти Git — блоб, дерево та коміт — спочатку зберігаються як окремі файли у вашій директорії .git/objects. Ось всі об’єкти в директорії нашого прикладу, з коментарем про те, що вони зберігають:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Якщо ви прослідкуєте за всіма внутрішніми вказівниками, то отримаєте граф об’єктів, схожий на цей:

Усі досяжні об’єкти у вашій директорії Git.
Figure 150. Усі досяжні об’єкти у вашій директорії Git.

Зберігання обʼєктів

Ми вже згадували, що разом з вмістом кожного обʼєкта бази даних Git зберігається заголовок. Приділімо хвилинку тому, щоб подивитись, як Git зберігає свої обʼєкти. Ви побачите, як зберегти обʼєкт блоб — у цьому випадку, рядок “what is up, doc?” — в інтерактивному режимі скриптової мови Ruby.

Ви можете запустити інтерактивний режим Ruby командою irb:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"

Спочатку Git формує заголовок, що починається з типу обʼєкта —  у цьому випадку це блоб. Після цієї першої частини заголвку йде пробіл, а потім — розмір вмісту в байтах, та нарешті нульовий байт:

>> header = "blob #{content.length}\0"
=> "blob 16\u0000"

Git зчіплює заголовок з власне вмістом, а потім обчислює суму SHA-1 цього нового вмісту. Ви можете обчислити SHA-1 суму рядка в Ruby, якщо підключите бібліотеку SHA1 командою requier, а потім викличете Digest::SHA1.hexdigest() для рядка:

>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

Git стискає новий вміст за допомогою zlib, що також може зробити Ruby, використовуючи бібліотеку zlib. Спершу, треба підключити бібліотеку, а потім виконати Zlib::Deflate.deflate() з вмістом:

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"

Нарешті, ви запишете вміст, що був зменшений за допомогою zlib, до обʼєкту на диску. Ви визначите шлях до обʼєкту, який ви бажаєте записати (перші два символи значення SHA-1 будуть імʼям піддиректорії, решта 38 символів будуть імʼям файлу в цій піддиректорії). У Ruby, ви можете використати функцію FileUtils.mkdir_p() для створення піддиректорії, якщо вона не існує. Потім, відкрийте файл: File.open(); та запишіть вже стиснутий zlib вміст до файлу, викликавши write() з отриманим дескриптором файлу:

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32

Ось і все — ви створили чинний обʼєкт Git типу блоб. Усі обʼєкти Git зберігаються однаково, лише з різними типами — замість рядка blob, заголовок починатиметься з commit або tree. Також, хоча вміст блобу може бути майже будь-чим, вміст коміту та дерева мають строгий формат.