Git
Chapters ▾ 2nd Edition

10.2 Гит изнутра - Гит објекти

Гит објекти

Гит је фајл систем који се адресира садржајем. Одлично. Али шта то значи? Значи да је у срцу програма Гит једноставно складиште парова кључ-вредност. Можете да убаците било какав садржај у Гит репозиторијум и он ће вам вратити јединствени кључ који касније можете употребити да дођете до тог садржаја.

Да бисте ово показали, хајде да видимо водоводну команду hash-object која узима неке податке, чува их у директоријуму .git/objectsбази података објеката) и враћа вам јединствени кључ који од сада показује на тај објекат података.

Најпре иницијализујете нови Гит репозиторијум и да се уверите да у директоријуму 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

Програм Гит је иницијализовао директоријум objects и поддиректоријуме pack и info, али нема никаквих обичних фајлова. Сада, хајде да употребимо git hash-object, креирамо нови објекат података и ручно га сместимо у нову Гит базу података:

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

У својом најједноставнијем облику, команда git hash-object би преузела садржај који јој проследите и само вратила јединствени кључ који би се користио да се он сачува у вашу Гит базу података. Опција -w говори команди hash-object да поред тога што врати кључ, сачува и објекат у базу података. Коначно, опција --stdin говори команди git hash-object да садржај за обраду преузме са стандардног улаза; у супротном команда очекује име фајла у којем се налази садржај као последњи аргумент у командној линији.

Излаз из команде је хеш контролна сума дужине четрдесет карактера. Ово је SHA-1 хеш — контролна сума садржаја који чувате, плус заглавља, о коме ћете научити више мало касније. Сада можете да видите како је програм Гит сачувао податке:

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

Ако поново погледате свој objects директоријум, видећете да се у њему сада налази фајл за тај нови садржај. Ово је начин на који програм Гит иницијално смешта садржај — као један фајл за један део садржаја, чије име је SHA-1 контролна сума садржаја и његовог заглавља. Поддиректоријум добија име по прва два карактера SHA-1 контролне суме, а име фајла је преосталих 38 карактера.

Када се садржај нађе у вашој бази података објеката, можете га испитати командом cat-file. Ова команда је нешто као швајцарски војнички нож за инспекцију Гит објеката. Када јој проследите опцију -p, наређујете јој да најпре открије врсту садржаја о ком се ради, па да га прикаже на одговарајући начин:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Сада знате како да у програм Гит додате садржај у Гит и да га поново извучете назад. Ово такође можете да радите и са садржајем у фајловима. На пример, можете да обавите једноставну контролу верзије над фајлом. Најпре креирајте нови фајл и сачувајте његов садржај у базу података:

$ 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 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 кључа за сваку верзију фајла није практично; сем тога, у фајл систему не чувате име фајла, већ само његов садржај. Овај тип објекта се зове блоб. Програм Гит може да вам каже тип објекта било ког објекта у репозиторијуму, ако му уз команду cat-file -t задате SHA-1 кључ.

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Објекти стабла

Следећи тип Гит објеката који ћемо испитати је стабло, које решава проблем чувања имена фајлова, а поред тога вам дозвољава и да заједно ускладиштите групу фајлова. Програм Гит садржај чува на сличан начин као Јуникс фајл систем, али нешто једноставније. Сав садржај се састоји од објеката стабала и блобова, при чему стабла одговарају Јуникс директоријумима, а блобови су мање-више пандан и-чворовима или садржају фајлова. Један објекат стабла садржи једну или више ставки, од којих свака садржи 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
Белешка

Зависно од љуске коју користите, можете наићи на проблеме при употреби master^{tree} синтаксе.

У CMD на Виндоуз систему, карактер ^ се користи за означавање, тако да га морате удвојити ако желите да избегнете проблем: git cat-file -p master^^{tree}. Када се користи PowerShell, параметри који садрже {} карактере морају да се цитирају, јер се тако спречава погрешно парсирање параметра: git cat-file -p 'master^{tree}'.

Ако користите ZSH, карактер ^ се употребљава за globbing (генерисање имена фајлова и директоријума регуларним изразима), тако да комплетан израз морате да поставите унутар знакова навода: git cat-file -p "master^{tree}".

Концептуално, подаци које чува програм Гит изгледају отприлике на следећи начин:

Једноставна верзија модела података програма Гит
Слика 147. Једноставна верзија модела података програма Гит

Прилично лако можете направити своје сопствено стабло. Програм Гит обично креира стабло тако што узима стање стејџа или индекса и на основу њега исписује низ објеката стабла. Дакле, да бисте креирали стабло, прво треба да поставите индекс тако што стејџујете неке фајлове. Да бисте креирали индекс са само једном ставком — првом верзијом фајла test.txt — можете да употребите водоводну команду update-index. Ова команду можете искористити да на стејџ вештачки додате старију верзију фајла test.txt. Морате да јој проследите опцију -add јер фајл још увек не постоји на стејџу (у овом тренутку немате ни стејџ) и --cacheinfo јер се фајл који додајете не налази у вашем директоријуму, већ у бази података. Затим наведете режим, SHA-1 и име фајла:

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

У овом случају наводите режим 100644, што значи да се ради о обичном фајлу. Остале опције су 100755, што значи да се ради о извршном фајлу; и 120000, што наводи симболички линк. Режим је преузет од уобичајених Јуникс режима али је много мање флексибилан — за фајлове (блобове) у програму Гит важе само ова три режима (мада се други режими користе за поддиректоријуме и подмодуле).

Сада можете да искористите команду 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 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” од раније (1f7a7a). Забаве ради, додаћете прво стабло као поддиректоријум у овом. Стабла можете учитати на свој стејџ командом git read-tree. У овом случају, учитавате постојеће стабло на стејџ као подстабло користећи опцију --prefix уз команду read-tree:

$ 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. Податке које Гит чува за ове структуре можете да замислите овако:

Структура садржаја ваших текућих података у програму Гит
Слика 148. Структура садржаја ваших текућих података у програму Гит

Комит објекти

Ако сте урадили све претходно наведено, сада имате три стабла који представљају различите снимке пројекта који желите да пратите, али остаје проблем од раније: да бисте вратили ранији снимак, морате да упамтите све три SHA-1 вредности. Поред тога немате ни информацију о томе ко је сачувао снимак, када је то учињено, нити зашто. Ово су основни подаци које се бележе у комит објекту.

Да бисте креирали комит објекат, позовите commit-tree, наведите SHA-1 једног стабла и ако их уопште има, који комит објекти непосредно претходе овом који креирате. Почните са првим стаблом које сте записали:

$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Белешка

Добићете другачију хеш вредност јер се подаци о времену креирања и аутору разликују. Уз то, мада се било који комит објекат може прецизно репродуковати помоћу ових података, историјски детаљи конструкције ове књиге значе да одштампани комит хешеви можда не одговарају датим комитовима. У наставку овог поглавља, замените наведене комит хешеве и хешеве ознака са вредностима које ви добијете.

Сада командом 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 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 add и git 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

Ako ispratite sve unutrašnje pokazivače, dobićete graf objekata koji izgleda nekako ovako:

Сви доступни објекти из Гит директоријума
Слика 149. Сви доступни објекти из Гит директоријума

Складиште објеката

Раније смо поменули да постоји заглавље које се чува заједно са сваким објектом који комитујете у Гит базу података објеката. Погледајмо накратко како Гит чува своје објекте. Видећете како да сачувате блоб објекат — у овом случају, стринг „what is up, doc?”. — интерактивно у језику Руби.

Интерактивни Руби режим покрећете командом irb.

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

Програм Гит конструише заглавље које почиње навођењем врсте објекта —  у овом случају је то блоб. Затим му додаје размак иза којег следи величина у бајтовима, па на крају null бајт:

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

Програм Гит надовезује на заглавље оригинални садржај, па онда рачуна SHA-1 контролну суму тог новог садржаја. SHA-1 вредност стринга у Рубију можете да израчунате укључивањем SHA-1 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

Програм Гит компресује нови садржај користећи zlib, што можете да урадите у Рубију користећи библиотеку 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 је име фајла унутар тог директоријума). За креирање поддиректоријума, ако већ не постоји, у Рубију можете да искористите функцију FileUtils.mkdir_p(). Затим са File.open() отворите фајл и позивом методе write() над враћеном ручком фајла испишете претходно zlib-компресован садржај у фајл:

>> 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?
---

I to je sve — kreirali ste важећи Git blob objekat.

Сви Гит објекти се чувају на исти начин, само са другачијим типовима — уместо стринга blob, заглавље ће почети са commit или tree. Сем тога, мада садржај блоба може да буде скоро све, садржај комитова и стабала су форматирани на прецизно дефинисан начин.