Git 🌙
Chapters ▾ 2nd Edition

8.2 Git на ниско ниво - Git обекти

Git обекти

Git е content-addressable файлова система. Супер. Какво означава това? Това означава, че по същество Git е просто склад за данни от типа key-value (ключ-стойност). Което от своя страна значи, че можете да вмъкнете произволен тип съдържание в Git хранилище и ще получите за него уникален идентификатор, който можете по-късно да използвате, за да извлечете съдържанието обратно.

За демонстрация, нека погледнем plumbing командата 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 за да създадем и запишем в базата данни нов data обект ръчно:

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

В най-простата си форма, git hash-object ще вземе данните, които сте ѝ подали и само ще върне уникалния ключ. който ще бъде използван за съхранението им в базата данни. Флагът -w казва на командата не само да върне ключа, но и да запише обекта в базата. Последно, --stdin инструктира git hash-object да вземе съдържанието, което ще обработва от stdin, в противен случай командата ще очаква като аргумент име на файла със съответното съдържание.

Изходът на екрана е 40-символен стринг. Това е SHA-1 хешът — чексума на съдържанието, което съхранявате плюс един хедър, за който ще научим по-късно. Сега може да видим как Git е съхранил данните ни:

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

В директорията ни objects вече съществува файл за новото ни съдържание. Първоначално Git запазва данните ни по този начин — като единичен файл за частичка съдържание, с наименование SHA-1 чексумата на съдържанието и хедъра. Поддиректорията (d6) се именува с първите два символа от SHA-1 чексумата, а името на файла се формира от останалите 38 символа.

Веднъж записали съдържание в базата данни, можете да го изследвате с командата git cat-file. Тази команда е като швейцарско ножче за инспектиране на Git обекти. Параметърът -p инструктира командата първо да определи вида на съдържанието и след това да го покаже по съответния начин:

$ 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 ключа за всяка версия на файла едва ли е практично и освен това вие не пазите името на файла, а само съдържанието му. Този тип обект се нарича blob. Можете да кажете на Git да ви извлече типа на всеки обект по дадена SHA-1 стойност с git cat-file -t:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Tree обекти

Следващият тип обект в Git е tree, който решава проблема със записа на името на файла и също така позволява да запазвате група файлове заедно. Git пази съдържанието по маниер подобен на UNIX файлова система, но една идея по-опростено. Цялото съдържание се съхранява под формата на tree и blob обекти, като дърветата играят ролята на съответните UNIX директории и blob-обектите съответстват малко или много на inodes или файлово съдържание. Единичен tree обект съдържа един или повече tree елемента, всеки от които съдържа SHA-1 указател към blob или поддърво със съответните име на файл, режим и тип. Например, най-новото дърво в проект може да изглежда по такъв начин:

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

Синтаксисът master^{tree} указва tree обекта, към който сочи последния къмит в master клона. Забелязваме, че lib поддиректорията не е blob, а указател към друго дърво:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb
Забележка

В зависимост от шела, който използвате, може да срещнете грешки при използване на синтаксиса master^{tree}.

В CMD под Windows, символът ^ се използва за escaping, така че трябва да го въведете два пъти: git cat-file -p master^^{tree}. Когато използвате PowerShell, параметрите съдържащи символите {} трябва да се оградят в кавички, за да не се интерпретират погрешно: git cat-file -p 'master^{tree}'.

Ако имате ZSH, тогава символът ^ се използва за globbing, така че трябва да оградите целия израз с кавички: git cat-file -p "master^{tree}"

Концептуално, данните които Git запазва изглеждат така:

Проста версия на data модела на Git
Фигура 147. Проста версия на data модела на Git

Можете сравнително лесно да създадете собствено дърво. Git нормално създава дърво вземайки статуса на индексната област и записвайки серия от tree обекти от нея. Така, за да създадете tree обект, първо трябва да инициализирате индекса с някакви файлове. За да създадете индекс с един елемент — първата версия на файла test.txt, може да използвате plumbing командата git update-index. Използвайки я, добавяте изкуствено най-ранната версия на файла към нова индексна област. Трябва да подадете аргумента --add, защото файлът все още не съществува в индекса (и дори самият индекс още не съществува) и също --cacheinfo, защото файлът, който добавяте не е в работната директория, а е в базата данни. След това подавате режима, SHA-1 стойността и името на файла:

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

В този случай указваме режим 100644, което ще рече, че това е обикновен файл. Другите опции са 100755 за изпълним файл и 120000, който указва символна връзка (symlink). Режимът се взема съобразно стандартните UNIX правила, но е много по-малко гъвкав — това са единствените три режима, които са валидни за файлове (blobs) в Git (въпреки че други режими се използват за директории и подмодули).

Сега може да използвате git write-tree за да запишете индексната област в tree обект. Не се изисква флаг -w — изпълнението на тази команда автоматично създава tree обект съобразно статуса на индекса, ако такова дърво вече не съществува:

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

Може да проверите, че това е tree обект със същата git cat-file команда от по-рано:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Сега ще създадем ново дърво с втората версия на test.txt файла и също така още един нов файл:

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

Сега индексът ни има новата версия на test.txt, както и новия файл new.txt. Записваме това дърво в tree обект и поглеждаме как изглежда:

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

Забелязваме, че сега това дърво съдържа и двата файла, и че SHA-1 стойността на test.txt вече е “version 2” от по-рано (1f7a7a). Само за идеята, ще добавим първото дърво като поддиректория в това. Може да прочитаме дървета от индекса с командата 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 данни
Фигура 148. Структура на съдържанието на текущите Git данни

Commit обекти

Ако сте изпълнили всички стъпки досега, разполагате с три дървета, които представят различните snapshot-и на проекта, който искате да следите. Но проблемът все още стои: трябва да помните всичките три SHA-1 стойности, за да извлечете съдържанието им. Също така, нямате никаква информация за това кой е съхранил тези snapshot-и, кога са записани или защо са записани. Това е информацията, която съхраняват къмит обектите.

За да създадем такъв обект, използваме командата commit-tree, която очаква като аргументи SHA-1 хеша на единично дърво и също така, кой къмит обект (ако има такъв) директно го предшества. Започваме с първото съхранено дърво:

$ echo 'First commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Забележка

Получаваме различна хеш стойност поради различното време на създаване на обекта и данните за автора. Освен това, въпреки че по принцип всеки къмит обект може да се пресъздаде прецизно с тези данни, хронологичните особености на конструкцията на тази книга означават, че отпечатаните къмит хешове може да не съответстват на дадените къмити. По-натам в главата, замествайте commit и tag хешовете с вашите собствени чексуми.

Сега можем да разгледаме новия ни къмит обект с командата 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

Форматът е прост: той указва top-level дървото за snapshot-а на проекта в този момент; родителските къмити, ако съществуват такива (описаният отгоре обект няма никакви родители); author/committer информацията (ще се използват текущите конфигурационни настройки за user.name и user.email и timestamp); следва празен ред и накрая е къмит съобщението.

Следва да запишем другите два къмит обекта, всеки от които сочи към къмита дошъл директно преди него:

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

Всеки от трите къмит обекта сочи към едно от трите snapshot дървета, които създадохме. Сега вече имаме и реална 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 история без да използваме нито една porcelain команда. По същество това прави Git, когато изпълните git add и git commit — записва blob обекти за променените файлове, обновява индекса, записва дървета и записва къмит обекти, които сочат към top-level дърветата и къмитите дошли непосредствено преди тях. Тези три основни Git обекта — blob, tree, и commit, първоначално се съхраняват като отделни файлове в директорията .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 директорията
Фигура 149. Всички достъпни обекти в Git директорията

Съхранение на обектите

По-рано казахме, че с всеки обект, който къмитваме в базата данни се пази и по един хедър. Нека видим как Git съхранява обектите си. Ще видим как да съхраним blob обект, в този случай стринга “what is up, doc?” — интерактивно в Ruby scripting езика.

Може да стартирате интерактивния Ruby режим с irb командата:

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

Git първо конструира хедър, който започва с идентифициране на типа на обекта — в този случай blob. Към тази първа част от хедъра, Git добавя интервал последван от размера на съдържанието в байтове и финален null байт:

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

Git конкатенира хедъра и оригиналното съдържание, след което калкулира нова SHA-1 стойност на резултата. В Ruby, можете да калкулирате SHA-1 стойност на стринг като инклуднете SHA1 digest библиотеката с require команда и след това викайки метода Digest::SHA1.hexdigest() със стринга:

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

Нека сравним това с изхода от git hash-object. Тук използваме echo -n за да избегнем добавянето на нов ред към входа.

$ echo -n "what is up, doc?" | git hash-object --stdin
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-deflated съдържанието в обект на диска. Установявате пътя за обекта (първите два символа от 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 cat-file:

---
$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37
what is up, doc?
---

Това е, вече създадохте валиден Git blob обект.

Всички Git обекти се записват по същия начин, само с различни типове — вместо със стринга blob, хедърът ще започва с commit или tree. Също така, въпреки че blob съдържанието може да е практически всякакво, то commit и tree съдържанията се форматират строго специфично.

scroll-to-top