Git
Chapters ▾ 2nd Edition

10.2 Git iznutra - Git objekti

Git objekti

Git je datotčeni sistem adresabilan po sadžaju. Baš super. Ali šta to znači?

Znači da je u srcu Git jednostavno skladište parova ključ-vrednost. Možete da ubacite bilo kakav sadržaj u njega, i daće vam ključ koji možete da koristite da ponovo pribavite taj podatak u bilo kom trenutku. Da biste videli ovo, možete da uskorisite vodovodnu komandu hash-object, koja uzima neke podatke, čuva ih u direktorijumu .git, i vraća vam ključ pod kojim je podataka smešten. Prvo treba da inicijalizujete novi Git repozitorijum i da se uverite da u direktorijumu objects nema ničega.

$ 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 j inicijalizovao direktorijum objects i poddirektorijume pack i info, ali nema običnih datoteka. Sada, da ubacimo neki tekst u Gitovoj bazi podataka:

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

Opcija -w govori hash-object-u da sačuva objekat; inače, komanda vam jednostavno vraća šta bi bio ključ. --stdin govori komandi da pročita sadržaj sa standardnog ulaza; ako ne navedete ovo, hash-object očekuje putanju do datoteke na kraju. Izlaz iz komande je kontrolnom sumom od četrdeset karaktera. Ovo je SHA-1 heš — kontrolna suma sadržaja koji čuvate, plus zaglavlje, o kome ćete naučiti više malo kasnije. Sada možete da vidite kako je Git sačuvao podatke:

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

Vidite datoteku u direktorijumu objects. Ovako Git inicijalno čuva sadržaj — jeddna datoteka po delu sadržaja, imenovana po SHA-1 čeksumi sadržaja i zaglavlja. Poddirektorijum dobija ime po prva dva karaktera SHA-1 čeksume, a ime datoteke je preostalih 38 karaktera.

Možete da izvučete sadržaj iz Gita komandom cat-file. Ova komanda je kao švajcarski vojnički nož za inspekciju Git objekata. Kada joj prosledite opciju -p, naređujete joj da otkrije vrstu sadržaja o kom se radi i da ga prikaže:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Sada možete da dodate sadržaj u Git i da ga ponovo izvučete nazad. Možete da ovo radite i sa sadržajem u datotekama. Na primer, možete da obavite jednostavnu kontrolu verzije nad datotekom. Prvo, kreirajte novu datoteku i sačuvajte njen sadržaj u bazu podataka:

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

Zatim dopišite neki nov sadržaj u datoteci, i sačuvajte je opet:

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

Baza podataka sadrži dve nove verzije datoteke kao i prvi sadržaj koji ste sačuvali tamo:

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

Sada možete da vratite datoteku na prvu verziju,

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

ili na drugu verziju.

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

Ali pamćenje SHA-1 ključa za svaku verziju nije praktično; sem toga, ne čuvate ime datoteke u sistem, već samo njen sadržaj. Ovaj tip objekta se zove blob. Git može da vam kaže tip objekta bilo kog objekta u Gitu, ako mu date SHA-1 ključ, komandom cat-file -t.

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Stabla

Sledeći tip koji ćemo pogledati je stablo, koje rešava problem čuvanja imena datoteke, a pored toga vam dozvoljava i da grupu datoteka uskladištite zajedno. it čuva sadržaj na sličan način kao Juniksovi datotečni sistemi, ali nešto jednostavnije. Sav sadržaj se sastoji od stabala i blobova, pri čemu stabla odgovaraju direktorijumima u Juniksu a blobovi su manje-više pandam i-čvorovima ili sadrživanam datoteka. Jedno stablo sadrži jednu ili više stavki, od kojih svaka sadrži SHA-1 pokazivač na blob ili podstablo, uz informaciju o režimu, vrsti i imenu datoteke. Na primer, najskorašnjije stablo u projektu bi moglo da izgleda nekako ovako:

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

Sintaksa master^{tree} specificira stablo na koje pokazuje poslednji komit na grani master. Obratite pažnju na to da poddirektorijum lib nije blob već pokazivač na drugo stablo:

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

Konceptualno, Git čuva podatke otprilike na sledeći način:

Jednostavna verzija Gitovog modela podataka.
Figure 149. Jednostavna verzija Gitovog modela podataka.

Možete lako napraviti svoje sopstveno stablo. Git obično kreira stablo tako što uzima stanje stejdža ili indeksa i ispisuje niz stabala iz njega. Dakle, da biste kreirali stablo, prvo treba da podesite indeks tako što ćete stejdžovati neke datoteke. Da biste kreirali indeks sa samo jednom stavkom — prva verzija datoteke test.txt — možete da iskoristite vodovodnu komandu update-index. Ova komanda se koristi da biste vštački dodali stariju verziju datoteke test.txt na stejdž. Morate da joj prosledite opciju -add jer datoteka još uvek ne postoji na stejdžu (čak nemate ni stejdž) i --cacheinfo jer datoteka koju dodajete nije u direktorijumu, već u bazi podataka. Onda odredite režim, SHA-1 i ime datoteke:

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

U ovom slučaju, specificirate režim 100644, što znači da se radi o normalnoj datoteci. Druge opcije su 100755, što znači da se radi o izvršnoj datoteci; i 120000, što specificira simbolički link. Režim je preuzet iz Juniksovih režima ali je mnogo manje fleksibilan — samo su ova tri moda validna za datoteke (blobove) u Gitu (mada se drugi modovi koriste za poddirektorijume i podmodule).

Sada, možete da iskoristite komandu write-tree da biste stejdž ispisali u obliku stabla. Nije potrebno proslediti opciju -w — komanda write-tree automatski kreira novo stablo iz stanja indeksa ako takvo stablo već ne postoji.

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

Možete se uvedite da se radi o stablu na sledeći način.

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Sada ćete kreirati novo stablo sa drugom verzijom datoetke test.txt, kao i sa novom datotekom:

$ echo 'new file' > new.txt
$ git update-index test.txt
$ git update-index --add new.txt

Stejdž sada ima novu verziju datoteke test.txt, kao i novu datoteku new.txt. Ispišite to stablo (pritom beležeći stanje stejdža ili indeksa u stablo) i pogledajte kako izgleda.

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

Obratite pažnju na to da ovo stablo ima po stavku za obe datoteke i da je SHA-1 datoteke test.txt onaj iz "verzije 1" od ranije (1f7a7a). Zabave radi, dodaćete prvo stablo kao poddirektorijum u ovom. Možete čitati stabla u stejdž naredbom read-tree. U ovom slučaju, možete da pročitate postojeće stablo u stejdžu kao podstablo koristeći opciju prefiks uz naredbu 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

Ako ste kreirali radni direktorijum iz novog stabla koje ste upravo ispisali, dobićete dve datoteke na najvišem nivou radnog direktorijuma i poddirektorijum s imenom bak koji je sadržao prvu verziju datoteke test.txt. Podatke koje Git čuva za ove strukture možete da zamislite ovako:

Strukturni sadržaj trenutnih podataka u Gitu.
Figure 150. Strukturni sadržaj trenutnih podataka u Gitu.

Komit-objekti

Imate tri stabla koji određuju različite snimke projekta koje želite da pratite, ali ostaje problem od ranije: morate da upamtite sve tri SHA-1 vrednosti kako biste se vratili na snimak. Pored toga nemate informaciju o tome ko je sačuvao snimak, kada je to učinjeno, niti zašto. Ovo su osnovni podaci koje komit-objekat beleži.

Da biste kreirali komit-objekat, pozovite commit-tree i specificirajte jedan SHA-1 stabla i koji komit-objekti, ako ih uopšte ima, neposredno prethodne njemu. Počnite sa prvim stablom koje ste napisali:

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

Sada možete da pogledate u novi komit-objekat sa 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

Format komit-objekta je jednostavan: određuje vršno stablo za snimak projekta u tom trenutku; informacije o autoru/komiteru (što koristi user.name i user.email) iz konfiguracionih podešavanja i vremensku oznaku); praznu liniju, a zatim komit-poruku.

Sada ćete zapisati još dva komit-objekta, pri čemu svaki referencira na komit koji je došao odmah pre njega:

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

Svaki od tri komit-objekata pokazuje na jedan od tri snimaka koje ste kreirali. Začudo, sada imate pravu Git istoriju koju možete da pogledate komandom git log, ako je pokrenete kad SHA-1 kontrolnom sumom poslednjeg komita.

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

Neverovatno. Upravo ste operacijama niskog nivoa izgradili Git istoriju, ne koristeći nijednu porcelansku komandu. Suštinski, ovo je ono što Git radi kada pokrećete komande git add i git commit — čuva blobove za datoteke koje su se promenile, ažurira indeks, ispisuje stabla i komit-objekte koji referenciraju vršna stabla i komitove koji su došli neposredno pre njih. Ova tri glavna Git objekta — blob, stablo i komit — se inicijalno čuvaju kao posebne datoteke u direktorijumu .git/objects. Evo svih objekata iz trenutnog stanja našeg direktorijuma koje koristimo za ovaj primer, uz komentar o tome šta čuvaju.t/objects` directory.

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

Svi objekti iz Git repozitorijuma.
Figure 151. Svi objekti iz Git repozitorijuma.

Skladište objekata

Ranije smo pomenuli da se zaglavlje čuva zajedno sa sadržajem. Pogledajmo nakratko kako Git čuva svoje objekte. Videćete kako da interaktivno sačuvate blob pomoću skripte napisane u Rubiju — u ovom slučaju, string "what is up, doc?".

Možete da pokrenete interaktivni Rubi mod komandom irb.

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

Git konstruiše zaglavlje koje počinje vrstom objekta; u ovom slučaju je to blob. Zatim dodaje razmak iza kojeg sledi veličina sadržaja, i na kraju null.

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

Git konkatenira zaglavlje i prvobitni sadržaj, pa onda računa SHA-1 kontrolnu sumu tog novog sadržaja. Možete da izračunate SHA-1 vrednost nad stringom koristeći Rubi tako što ćete priključiti SHA-1 zbirnu biblioteku koristeći naredbu require, a onda pozvati Digest::SHA1.hexdigest() nad stringom.

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

Git kompresuje novi sadržaj koristeći zlob, što možete da uradite u Rubiju koristeći biblioteku zlib. Prvo treba da se zatražite i onda pokrenete Zlib::Deflate.deflate() nad sadržajem.

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

I na kraju, treba da zapišete kompresovan sadržaj kao objekat na disku. ODredićete putanju gde treba da smesite objekat (prva dva karatera SHA-1 vrednosti su ime poddirektorijuma, a poslednjih 38 je ime datoteke unutar njega). U Rubiju, možete da iskoristite funkciju FileUtils.mkdir_p() kako biste kreirali poddirektorijum ako on već ne postoji. Zatim otvorite datoteku sa File.open() i ispišite već kompresovan sadržaj u datoteku pozivom metode write() nad objektom koji je vezan za datoteku.

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

I to je sve — kreirali ste validan Git blob objekat. Svi Git objekti se čuvaju na isti način, samo sa drugačijim tipovima — umesto stringa blob, zaglavlje će početi sa commit ili tree. Sem toga, sadržaj bloba može da bude skoro sve, ali sadržaj komitova i stabala su formatirani na precizno definisan način.