Git 🌙
Chapters ▾ 2nd Edition

10.2 Notranjost Gita - Objekti Git

Objekti Git

Git je sistem za shranjevanje vsebine s poudarkom na datotečnem sistemu. Odlično. Kaj to pomeni? To pomeni, da je v jedru Git preprosta shramba podatkov ključ-vrednost. To pomeni, da lahko v repozitorij Git vstavite katero koli vrsto vsebine, za katero vam bo Git vrnil edinstven ključ, ki ga lahko pozneje uporabite za pridobitev te vsebine.

Kot demonstracijo si poglejmo ukaz napeljave git hash-object, ki sprejme nekaj podatkov, jih shrani v vaš direktorij .git/objects (objektna baza podatkov) in vam vrne edinstven ključ, ki se nanaša na ta podatkovni objekt.

Najprej inicializirajte nov repozitorij Git in preverite, da v mapi objects (pričakovano) ni ničesar:

$ 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 je inicializiral mapo objects in v njej ustvaril podmapi pack in info, vendar v njej ni navadnih datotek. Sedaj pa uporabimo git hash-object, da ustvarimo nov podatkovni objekt in ga ročno shranimo v vašo novo podatkovno zbirko Git:

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

V najpreprostejši obliki bi git hash-object sprejel vsebino, ki ste jo posredovali, in preprosto vrnil edinstven ključ, ki bi bil uporabljen za shranjevanje v vaši podatkovni zbirki Git. Možnost -w ukazu pove, naj ne vrne le ključa, ampak objekt tudi zapiše v zbirko. Možnost --stdin pove git hash-object, naj vsebino za obdelavo prebere iz stdin; sicer bi ukaz na koncu pričakoval argument imena datoteke, ki vsebuje vsebino, ki jo je treba uporabiti.

Izhod iz zgornjega ukaza je 40 znakov dolga zgoščena vrednost kontrolne vsote. To je zgoščena vrednost SHA-1 oz. kontrolna vsota vsebine, ki jo shranjujete, in glava, o kateri boste izvedeli več v kratkem. Sedaj lahko vidite, kako je Git shranil vaše podatke:

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

Če ponovno pregledate mapo objects, lahko vidite, da vsebuje datoteko za to novo vsebino. To je način, kako Git sprva shranjuje vsebino — kot posamezno datoteko za vsak kos vsebine, poimenovan s kontrolno vsoto SHA-1 vsebine in njegove glave. Podmapa je poimenovana s prvima dvema znakoma SHA-1, ime datoteke pa predstavlja preostalih 38 znakov.

Ko imate vsebino v svoji objektni podatkovni bazi, lahko to vsebino pregledate z ukazom git cat-file. Ta ukaz je nekakšen švicarski vojaški nož za pregledovanje objektov Git. Možnost -p, podana cat-file, ukazu naroči, naj najprej ugotovi vrsto vsebine in jo ustrezno prikaže:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Sedaj lahko dodajate vsebino v Git in jo nato spet povlečete iz njega. To lahko storite tudi z vsebino v datotekah. Na primer, lahko opravite nekaj preprostega nadzora različic datoteke. Najprej ustvarite novo datoteko in shranite njeno vsebino v svojo podatkovno zbirko:

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

Nato napišite nekaj vsebine v to datoteko in jo ponovno shranite:

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

Vaša objektna podatkovna baza sedaj vsebuje obe različici te nove datoteke (kot tudi prvo vsebino, ki ste jo tam shranili):

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

V tem trenutku lahko izbrišete svojo lokalno kopijo datoteke test.txt in nato uporabite Git, da iz objektne podatkovne baze pridobite prvo različico, ki ste jo shranili:

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

ali pa drugo različico:

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

Vendar zapomniti si ključa SHA-1 za vsako različico datoteke ni praktično; poleg tega ne shranjujete imena datoteke v svojem sistemu — samo vsebino. Ta vrsta objekta se imenuje blob. Git vam lahko pove, kakšne vrste objekta je katerikoli objekt v Git, glede na njegov ključ SHA-1, z git cat-file -t:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Drevesni objekti

Naslednjo vrsto objekta Git, ki jo bomo pregledali, je drevo (angl. tree), ki rešuje problem shranjevanja imena datoteke in vam omogoča, da skupino datotek shranite skupaj. Git vsebino shranjuje na način, podoben UNIX-ovi datotečni strukturi, vendar nekoliko poenostavljeno. Vse vsebine so shranjene kot objekti dreves in blobov, pri čemer drevesa ustrezajo vnosom v UNIX-ovem imeniku, blobi pa približno ustrezajo inodom ali vsebini datotek. Posamezni objekt drevesa vsebuje enega ali več vnosov, pri čemer je vsak vnos zgoščene vrednosti SHA-1 koda bloba ali poddrevesa s pripadajočim načinom, vrsto in imenom datoteke. Na primer, recimo, da imate projekt, kjer je zadnje drevo videti nekako takole:

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

Sintaksa master^{drevo} določa objekt drevesa, na katerega kaže zadnja potrditev v vaši veji master. Bodite pozorni, da poddirektorij lib ni blob ampak kazalec na drugo drevo:

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

Odvisno od uporabljene lupine se lahko pri uporabi sintakse master^{drevo} srečate z napakami.

V CMD na sistemu Windows se znak ^ uporablja kot ubežni znak, zato ga morate podvojiti, da se izognete temu: git cat-file -p master^^{drevo}. Pri uporabi PowerShell je treba parametre, ki uporabljajo znake {} , navesti v narekovajih, da se izognete napačnemu razčlenjevanju parametra: git cat-file -p 'master^{drevo}'.

Če uporabljate ZSH, se znak ^ uporablja za razširjanje vzorcev, zato morate celoten izraz obdati z navednicami: git cat-file -p "master^{drevo}".

Konceptualno so podatki, ki jih Git shranjuje, videti nekako takole:

Enostavna različica modela podatkov Git
Slika 173. Enostavna različica modela podatkov Git

Dokaj enostavno lahko ustvarite lastno drevo. Git običajno ustvari drevo tako, da vzame stanje vašega področja za pripravo ali indeksa in iz njega napiše zaporedje drevesnih objektov. Zato morate za ustvarjanje drevesnega objekta najprej nastaviti indeks tako, da nekatere datoteke pripravite. Za ustvarjanje indeksa s posameznim vnosom — prvo različico datoteke test.txt — lahko uporabite ukaz git update-index. Ta ukaz uporabite, da umetno dodate prejšnjo različico datoteke test.txt v novo področje za pripravo. Podati mu morate možnost --add, ker datoteka še ni v vašem področju za pripravo (niti še niste nastavili področja za pripravo) in --cacheinfo, ker datoteke, ki jo dodajate, ni v vaši mapi, ampak v vaši bazi podatkov. Nato navedete način, SHA-1 in ime datoteke:

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

V tem primeru navajate način 100644, kar pomeni, da je to običajna datoteka. Druge možnosti so 100755, kar pomeni, da gre za izvršljivo datoteko; in 120000, kar določa simbolično povezavo. Način je vzet iz običajnih načinov UNIX, vendar je veliko manj prilagodljiv — ti trije načini so edini veljavni za datoteke (spletne objekte) v Gitu (čeprav se za mape in podmodule uporabljajo drugi načini).

Zdaj lahko uporabite git write-tree, da izpišete področje za pripravo v drevesni objekt. Možnost -w ni potrebna — klic tega ukaza samodejno ustvari drevesni objekt iz stanja indeksa, če tega drevesa še ni:

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

Prav tako lahko preverite, da je to drevesni objekt z uporabo enakega ukaza git cat-file, kot ste ga videli prej:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Zdaj boste ustvarili novo drevo z drugo različico test.txt in tudi z novo datoteko:

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

Vaše področje za pripravo zdaj vsebuje novo različico test.txt in novo datoteko new.txt. Izpišite to drevo (zabeležite stanje področja za pripravo ali indeksa v drevesni objekt) in preverite, kakšno je videti:

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

Opazite, da ima to drevo vnos tako za datoteke kot tudi to, da je SHA-1 test.txt »različica 2« SHA-1 iz prejšnjega (1f7a7a). Le za zabavo boste dodali prvo drevo kot podmapo v to. Drevesa lahko preberete v vaše področje za pripravo s klicem git read-tree. V tem primeru lahko obstoječe drevo preberete v svoje področje za pripravo kot poddrevo s tem ukazom, ki uporablja možnost --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

Če bi iz novega drevesa ustvarili delovni imenik, bi dobili dve datoteki na vrhnji ravni delovnega imenika in podmapo z imenom bak, ki bi vsebovala prvo različico datoteke test.txt. Podatke, ki jih Git vsebuje za te strukture, si lahko predstavljate takole:

Struktura vsebine vaših trenutnih podatkov Git
Slika 174. Struktura vsebine vaših trenutnih podatkov Git

Objekti potrditev

Če ste naredili vse zgoraj našteto, imate sedaj tri drevesa, ki predstavljajo različne posnetke vašega projekta, ki jih želite spremljati, vendar ostaja prejšnji problem: morate si zapomniti vse tri vrednosti SHA-1, da se lahko spomnite posnetkov. Prav tako nimate nobenih informacij o tem, kdo je shranil posnetke, kdaj so bili shranjeni ali zakaj so bili shranjeni. To je osnovna informacija, ki jo za vas shrani objekt potrditve.

Za ustvarjanje objekta potrditve kličete commit-tree in navedete eno SHA-1 drevesa ter kakršne koli objekte potrditev, ki so ji neposredno sledili. Začnite s prvim drevesom, ki ste ga napisali:

$ echo 'First commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Opomba

Zdaj boste dobili drugačno zgoščeno vrednost zaradi različnega časa ustvarjanja in avtorskih podatkov. Poleg tega, čeprav se lahko v teoriji vsak objekt potrditve natančno reproducira s temi podatki, zgodovinski podatki o gradnji te knjige pomenijo, da se natisnjene zgoščene vrednosti potrditve morda ne ujemajo z določenimi potrditvami. V tem poglavju nadomestite zgoščene vrednosti potrditve in oznak s svojimi kontrolnimi vsotami.

Sedaj lahko pogledate vaš novi objekt potrditve z 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

Oblika objekta potrditve je preprosta: določi se drevo najvišje ravni za posnetek projekta v tem trenutku, predhodnih potrditev, če obstajajo (zgoraj opisana potrditev nima nobenih predhodnikov); informacije o avtorju/potrjevalcu (ki uporabljajo vaše nastavitve konfiguracije user.name in user.email ter časovni žig); prazna vrstica in nato besedilo potrditve.

Nato boste napisali še drugi dve potrditvi, ki se sklicujeta na potrditev, ki je neposredno sledila potrditvi:

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

Vsak izmed treh objektov potrditev kaže na eno od treh posnetkov dreves, ki ste jih ustvarili. Presenetljivo imate sedaj dejansko zgodovino Git, ki jo lahko pregledujete z ukazom git log, če ga poženete na SHA-1 zadnje potrditve:

$ 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(+)

Neverjetno. Pravkar ste izvedli nizko nivojske operacije za gradnjo zgodovine Git brez uporabe nobenega od ukazov ospredja. To je v bistvu tisto, kar Git naredi, ko zaženete ukaze git add in git commit — shrani blobe za datoteke, ki so se spremenile, posodablja indeks, zapisuje drevesa in zapisuje objekte potrditev, ki se sklicujejo na drevesa najvišje ravni in potrditve, ki so jim takoj sledile. Ti trije glavni objekti Git — blob, drevo in potrditev — so na začetku shranjeni kot ločene datoteke v vaši mapi .git/objects. Tukaj so vsi objekti v mapi iz primera, opremljeni s komentarji o tem, kaj shranjujejo:

$ 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

Če ste sledili vsem internim kazalnikom, dobite objektni graf, ki je videti nekako takole:

Vsi dosegljivi objekti v vašem imeniku Git
Slika 175. Vsi dosegljivi objekti v vašem imeniku Git

Shramba objekta

Zgoraj smo omenili, da ima vsak objekt, ki ga potrdimo v objektno bazo podatkov Git, shranjeno glavo. Za minuto si oglejmo, kako Git shranjuje svoje objekte. Prikazali bomo, kako shraniti objekt blob — v tem primeru niz »what is up, doc?« — interaktivno v programskega jezika Ruby.

Interaktivni način Ruby lahko zaženete z ukazom irb:

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

Git najprej sestavi glavo, ki se začne z identifikacijo vrste objekta — v tem primeru gre za blob. Git k prvemu delu glave doda presledek, nato pa še velikost v bajtih vsebine in na koncu še ničelni bajt:

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

Git združuje glavo in izvirno vsebino ter izračuna kontrolne vsote SHA-1 te nove vsebine. Vrednost SHA-1 niza lahko izračunate v Rubyju z vključitvijo knjižnice SHA1 digest s pomočjo ukaza require, nato pa kličete Digest::SHA1.hexdigest() s tem nizom:

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

Primerjajmo to z izhodom git hash-object. Tukaj uporabljamo echo -n, da preprečimo dodajanje nove vrstice vhodu.

$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37

Git stisne novo vsebino z zlib, kar lahko storimo v Rubyju z uporabo knjižnice zlib. Najprej morate knjižnico zahtevati in nato na vsebini zagnati 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"

Na koncu boste napisali vašo zlib-stisnjeno vsebino na objekt na disku. Določili boste pot objekta, ki ga želite zapisati (prvi dve številki vrednosti SHA-1 predstavljata ime poddirektorija, preostalih 38 znakov pa predstavlja ime datoteke znotraj tega direktorija). V jeziku Ruby lahko funkcijo FileUtils.mkdir_p() uporabite za ustvarjanje poddirektorija, če ta še ne obstaja. Nato odprite datoteko s File.open() in zapišite predhodno zlib-stisnjeno vsebino v datoteko s klicem write() na nastalem datotečnem oprimku:

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

Poglejmo vsebino objekta z uporabo git cat-file:

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

To je to — ustvarili ste veljaven Gitov objekt blob.

Vsi objekti Git se shranjujejo na enak način, samo z različnimi vrstami — namesto niza blob, se bo glava začela s potrditvijo ali drevesom. Poleg tega, čeprav je lahko vsebina bloba skorajda karkoli, je vsebina potrditve in drevesa zelo natančno oblikovana.

scroll-to-top