Chapters ▾ 2nd Edition

10.2 Git bakom kulisserna - Git-objekt

Git-objekt

Git är ett innehållsadresserbart filsystem. Bra. Vad betyder det? Det betyder att kärnan i Git är ett enkelt nyckel‑värde‑lager. Det innebär att du kan lägga in vilken typ av innehåll som helst i ett Git‑kodförråd, och Git ger dig tillbaka en unik nyckel som du senare kan använda för att hämta innehållet.

Som demonstration tittar vi på lågnivåkommandot git hash-object, som tar lite data, lagrar det i din .git/objects‑katalog (objektdatabasen) och ger dig tillbaka den unika nyckel som nu pekar på det här dataobjektet.

Först initierar du ett nytt Git‑kodförråd och verifierar att det (förutsägbart) inte finns något i katalogen 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 har initierat katalogen objects och skapat underkatalogerna pack och info i den, men det finns inga vanliga filer. Nu använder vi git hash-object för att skapa ett nytt dataobjekt och lagra det manuellt i din nya Git‑databas:

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

I sin enklaste form skulle git hash-object ta innehållet du gav det och bara returnera den unika nyckel som skulle användas för att lagra det i din Git‑databas. Flaggan -w säger sedan åt kommandot att inte bara returnera nyckeln utan också skriva objektet till databasen. Slutligen säger alternativet --stdin åt git hash-object att hämta innehållet som ska behandlas från stdin; annars skulle kommandot förvänta sig ett filnamn i slutet av kommandot som innehåller innehållet som ska användas.

Utdata från kommandot ovan är en kontrollsumma på 40 tecken. Det är SHA‑1‑summan – en kontrollsumma av innehållet du lagrar plus ett huvud, som du får lära dig mer om strax. Nu kan du se hur Git har lagrat din data:

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

Om du tittar på katalogen objects igen ser du att den nu innehåller en fil för det nya innehållet. Så här lagrar Git innehåll i början – som en enda fil per innehållsdel, namngiven med SHA‑1‑kontrollsumman av innehållet och dess huvud. Underkatalogen namnges med de två första tecknen i SHA‑1:an, och filnamnet är de återstående 38 tecknen.

När du har innehåll i objektdatabasen kan du undersöka det med kommandot git cat-file. Detta kommando är som en schweizisk armékniv för att inspektera Git‑objekt. Om du skickar -p till cat-file instruerar du kommandot att först ta reda på typen av innehåll och sedan visa det på lämpligt sätt:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Nu kan du lägga till innehåll i Git och hämta tillbaka det igen. Du kan också göra detta med innehåll i filer. Du kan till exempel göra enkel versionshantering av en fil. Först skapar du en ny fil och sparar dess innehåll i databasen:

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

Sedan skriver du nytt innehåll till filen och sparar den igen:

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

Din objektdatabas innehåller nu båda versionerna av den nya filen (samt det första innehållet du lagrade där):

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

Vid det här laget kan du ta bort din lokala kopia av filen test.txt och sedan använda Git för att hämta antingen den första versionen du sparade från objektdatabasen:

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

eller den andra versionen:

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

Men att komma ihåg SHA‑1‑nyckeln för varje version av filen är inte praktiskt; dessutom lagrar du inte filnamnet i systemet – bara innehållet. Denna objekttyp kallas en blob. Du kan låta Git tala om objekttypen för vilket objekt som helst i Git, givet dess SHA‑1‑nyckel, med git cat-file -t:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

Trädobjekt

Nästa typ av Git‑objekt vi tittar på är trädet, som löser problemet med att lagra filnamnet och också låter dig lagra en grupp filer tillsammans. Git lagrar innehåll på ett sätt som liknar ett UNIX‑filsystem, men lite förenklat. Allt innehåll lagras som träd‑ och blob‑objekt, där träd motsvarar UNIX‑katalogposter och blobbar motsvarar ungefär inoder eller filinnehåll. Ett enskilt trädobjekt innehåller en eller flera poster, där varje post är SHA‑1‑summan för en blob eller ett underträd tillsammans med tillhörande läge, typ och filnamn. Låt oss till exempel säga att du har ett projekt där det senaste trädet ser ut ungefär så här:

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

Syntaxen master^{tree} anger trädobjektet som pekas på av den senaste incheckningen på din master‑gren. Observera att underkatalogen lib inte är en blob utan en pekare till ett annat träd:

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

Beroende på vilket skal du använder kan du stöta på fel när du använder syntaxen master^{tree}.

I CMD på Windows används tecknet ^ för escape, så du måste dubblera det för att undvika detta: git cat-file -p master^^{tree}. När du använder PowerShell måste parametrar som använder {}‑tecken citeras för att undvika att parametern tolkas fel: git cat-file -p 'master^{tree}'.

Om du använder ZSH används tecknet ^ för globbning, så du måste omsluta hela uttrycket med citationstecken: git cat-file -p "master^{tree}".

Konceptuellt ser de data Git lagrar ut ungefär så här:

En enkel version av Gits datamodell
Figur 171. En enkel version av Gits datamodell

Du kan ganska enkelt skapa ditt eget träd. Git skapar normalt ett träd genom att ta tillståndet i din köyta eller index och skriva en serie trädobjekt utifrån det. För att skapa ett trädobjekt måste du alltså först sätta upp ett index genom att köa några filer. För att skapa ett index med en enda post – den första versionen av din fil test.txt – kan du använda lågnivåkommandot git update-index. Du använder detta kommando för att manuellt lägga till den tidigare versionen av filen test.txt i en ny köyta. Du måste skicka alternativet --add eftersom filen ännu inte finns i din köyta (du har inte ens en köyta uppsatt än) och --cacheinfo eftersom filen du lägger till inte finns i din katalog utan i din databas. Sedan anger du läge, SHA‑1 och filnamn:

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

I det här fallet anger du läget 100644, vilket betyder att det är en vanlig fil. Andra alternativ är 100755, vilket betyder att det är en körbar fil, och 120000, som anger en symbolisk länk. Läget kommer från vanliga UNIX‑lägen men är mycket mindre flexibelt – dessa tre lägen är de enda som är giltiga för filer (blobbar) i Git (även om andra lägen används för kataloger och submoduler).

Nu kan du använda git write-tree för att skriva ut köytan till ett trädobjekt. Ingen -w‑flagga behövs – att anropa kommandot skapar automatiskt ett trädobjekt från indexets tillstånd om det trädet ännu inte finns:

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

Du kan också verifiera att detta är ett trädobjekt med samma git cat-file‑kommando som du såg tidigare:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Nu skapar du ett nytt träd med den andra versionen av test.txt och en ny fil också:

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

Din köyta har nu den nya versionen av test.txt samt den nya filen new.txt. Skriv ut det trädet (genom att registrera tillståndet i köytan eller indexet som ett trädobjekt) och se hur det ser ut:

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

Observera att detta träd har både filposter och att SHA‑1‑summan för test.txt är “version 2”‑SHA‑1‑summan från tidigare (1f7a7a). Bara för att prova lägger du till det första trädet som en underkatalog i detta. Du kan läsa in träd i din köyta genom att anropa git read-tree. I det här fallet kan du läsa in ett befintligt träd i köytan som ett underträd genom att använda alternativet --prefix med detta kommando:

$ 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

Om du skapade en arbetskatalog från det nya träd du just skrev skulle du få de två filerna på toppnivån i arbetskatalogen och en underkatalog som heter bak som innehåller den första versionen av filen test.txt. Du kan tänka på den data Git innehåller för dessa strukturer ungefär så här:

Innehållsstrukturen för dina nuvarande Git‑data
Figur 172. Innehållsstrukturen för dina nuvarande Git‑data

Incheckningsobjekt

Om du har gjort allt ovan har du nu tre träd som representerar de olika ögonblicksbilder av ditt projekt som du vill spåra, men det tidigare problemet kvarstår: du måste komma ihåg alla tre SHA‑1‑värden för att kunna återkalla ögonblicksbilderna. Du har heller ingen information om vem som sparade ögonblicksbilderna, när de sparades eller varför de sparades. Det är den grundläggande informationen som incheckningsobjektet lagrar åt dig.

För att skapa ett incheckningsobjekt anropar du git commit-tree och anger ett enda träd‑SHA‑1 och vilka incheckningsobjekt som i förekommande fall direkt föregick det. Börja med det första trädet du skrev:

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

Du kommer att få ett annat hashvärde på grund av olika skapandetid och författardata. Dessutom, även om varje incheckningsobjekt i teorin kan reproduceras exakt utifrån dessa data, gör historiska detaljer i hur den här boken byggts att de tryckta incheckningshasharna kanske inte motsvarar de givna incheckningarna. Ersätt inchecknings- och tagghashar med dina egna kontrollsummor längre fram i detta kapitel.

Nu kan du titta på ditt nya incheckningsobjekt med 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

Formatet för ett incheckningsobjekt är enkelt: det anger toppnivåträdet för projektets ögonblicksbild vid den tidpunkten; föräldraincheckningar om det finns några (incheckningsobjektet ovan har inga föräldrar); författar-/incheckarinformation (som använder dina user.name‑ och user.email‑inställningar samt en tidsstämpel); en tom rad; och sedan incheckningsmeddelandet.

Därefter skriver du de andra två incheckningsobjekten, där varje objekt refererar till incheckningen som kom direkt före den:

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

Var och en av de tre incheckningsobjekten pekar på ett av de tre ögonblicksbildsträd du skapade. Intressant nog har du nu en riktig Git‑historik som du kan se med kommandot git log, om du kör det på den senaste incheckningens 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(+)

Toppen. Du har just gjort lågnivåoperationerna för att bygga upp en Git‑historik utan att använda några av gränssnittskommandona. I stort sett är det här vad Git gör när du kör git add och git commit: lagrar blobbar för ändrade filer, uppdaterar indexet, skriver träd och skapar incheckningsobjekt som pekar på toppnivåträd och föregående incheckningar. Dessa tre huvudsakliga Git‑objekt – blobben, trädet och incheckningen – lagras initialt som separata filer i din .git/objects‑katalog. Här är alla objekt i exempelkatalogen nu, kommenterade med vad de lagrar:

$ 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

Om du följer alla interna pekare får du en objektgraf som ser ut ungefär så här:

Alla nåbara objekt i din Git‑katalog
Figur 173. Alla nåbara objekt i din Git‑katalog

Objektlagring

Vi nämnde tidigare att det finns ett huvud som lagras tillsammans med varje objekt du checkar in i Git‑objektdatabasen. Låt oss kort se hur Git lagrar sina objekt. Du får se hur man lagrar ett blob‑objekt – i det här fallet strängen “what is up, doc?” – interaktivt i Ruby‑skriptspråket.

Du kan starta interaktivt Ruby‑läge med kommandot irb:

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

Git konstruerar först ett huvud som börjar med att identifiera objektets typ – i det här fallet en blob. Till den första delen av huvudet lägger Git till ett blanksteg följt av innehållets storlek i byte och lägger sedan till en avslutande nollbyte:

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

Git slår ihop huvudet och det ursprungliga innehållet och beräknar sedan SHA‑1‑kontrollsumman av det nya innehållet. Du kan beräkna SHA‑1‑värdet för en sträng i Ruby genom att inkludera SHA1‑digest‑biblioteket med kommandot require och sedan anropa Digest::SHA1.hexdigest() med strängen:

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

Låt oss jämföra det med utdata från git hash-object. Här använder vi echo -n för att undvika att lägga till en radbrytning i indata.

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

Git komprimerar det nya innehållet med zlib, vilket du kan göra i Ruby med zlib‑biblioteket. Först behöver du kräva biblioteket och sedan köra Zlib::Deflate.deflate() på innehållet:

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

Till sist skriver du ditt zlib‑komprimerade innehåll till ett objekt på disk. Du bestämmer sökvägen till objektet du vill skriva ut (de två första tecknen i SHA‑1‑värdet är underkatalognamnet och de sista 38 tecknen är filnamnet i den katalogen). I Ruby kan du använda funktionen FileUtils.mkdir_p() för att skapa underkatalogen om den inte finns. Öppna sedan filen med File.open() och skriv ut det tidigare zlib‑komprimerade innehållet till filen med ett write()‑anrop på den resulterande filhandtaget:

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

Låt oss kontrollera innehållet i objektet med git cat-file:

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

Det var allt – du har skapat ett giltigt Git‑blob‑objekt.

Alla Git‑objekt lagras på samma sätt, bara med olika typer – i stället för strängen blob kommer huvudet att börja med commit eller tree. Dessutom kan blob‑innehållet vara nästan vad som helst, men innehållet i incheckning‑ och träd‑objekt är mycket strikt formaterat.