Hoe groot is een Git-commit?

Referentie: git-scm.com/docs/gitformat-loose

Een andere factor is dat Git objectbestanden uiteindelijk omzet in nog efficiëntere packfiles, waardoor extra ruimte wordt bespaard door redundantie tussen objecten te verwijderen.

Referentie: git-scm.com/book/en/v2/Git-Internals-Packfiles

Experimentele resultaten

Ter illustratie zijn er experimenten uitgevoerd met uitsluitend "losse objecten" (loose objects), waarbij geen packfiles zijn aangemaakt. Dit zijn de resultaten:

  • 64.828 bytes: git init
  • 47.640 bytes: commiten van 250 kleine bestanden
  • 17.145 bytes: commiten van een wijziging van 3 bytes in een bestand binnen een directory van 50 bestanden
  • 8.709 bytes: commiten van een wijziging van 3 bytes in een repository met slechts één bestand
  • 297.873 bytes: commiten van een binair bestand van 583.840 bytes
  • 1.622.188 bytes: commiten van een enorm bronbestand van 16.415.223 bytes

Hieruit blijkt dat er kilobytes aan overhead zijn bij zeer kleine commits. De exacte hoeveelheid hangt af van de grootte van de bestands- of directorystructuur die bij de wijzigingen betrokken is.

Bij grotere commits met een klein aantal bestanden is de opgeslagen data in .git ongeveer 10% tot 50% van de ongecomprimeerde gecommitteerde bestanden. Met andere woorden: het is extreem efficiënt.

---

Uitgebreid logboek van de experimenten

Initialisatie en 250 bestanden

Hoe groot is een gloednieuwe, lege Git-directory (.git)?

$ git init
Initialized empty Git repository in /home/dave/tmp/foo/.git/
$ du -sb .git
64828	.git

Dit is ongeveer 64 Kb. (De du-opties zijn: s voor samenvatting en b voor het weergeven van bytes.)

Vervolgens worden er met een set geneste shell-loops veel kleine bestanden aangemaakt:

$ for d in {1..5};do
mkdir $d;
for f in {1..50};do
echo foo > $d/$f;
done;
done

Er zijn nu 250 bestanden in 5 directories die allemaal de string "foo" bevatten:

$ ls
1  2  3  4  5
$ cat 2/45
foo

Na het commiten van deze bestanden:

$ git add .
$ git commit -m 'initial commit'
$ du -sb .git
112468	.git

De commit heeft ongeveer 47 Kb toegevoegd aan de initiële lege repo. (Berekening via dc, de reverse-polish notation calculator: 112468 64828 - p = 47.640).

Wanneer er één kleine wijziging in één bestand wordt aangebracht en gecommit:

$ echo bar > 1/1
$ git commit -am 'changed 1/1'
$ du -sb .git
129613
$ dc -e '129613 112468 - p'
17145

Er is dus 17 Kb nodig om ongeveer 3 bytes aan wijzigingen in de repo op te slaan.

Waar komt deze omvang vandaan?

Om te analyseren waar de ruimte naartoe gaat, kijken we naar de grootte van de bestanden in .git vóór en na een wijziging.

Situatie ervoor:

4096	.git/objects/info
4096	.git/objects/pack
4115	.git/objects/25
4155	.git/objects/7b
4225	.git/objects/af
4115	.git/objects/57
4271	.git/objects/07
4177	.git/objects/61
4255	.git/objects/bb
4299	.git/objects/a8
45900	.git/objects
4336	.git/info
4096	.git/refs/tags
4137	.git/refs/heads
12329	.git/refs
4096	.git/branches
4411	.git/logs/refs/heads
8507	.git/logs/refs
12918	.git/logs
27538	.git/hooks
129613	.git

Na het wijzigen van bestand 1/1 naar de string 'baz' en het commiten daarvan:

Situatie erna:

4096	.git/objects/info
4096	.git/objects/pack
4115	.git/objects/25
4253	.git/objects/81 <-- nieuw 4 Kb
4155	.git/objects/7b
4225	.git/objects/af
4115	.git/objects/57
4271	.git/objects/07
4177	.git/objects/61
4302	.git/objects/fc <-- nieuw 4 Kb
4255	.git/objects/bb
4115	.git/objects/76 <-- nieuw 4 Kb
4299	.git/objects/a8
4177	.git/objects/92 <-- nieuw 4 Kb
62747	.git/objects
4336	.git/info
4096	.git/refs/tags
4137	.git/refs/heads
12329	.git/refs
4096	.git/branches
4561	.git/logs/refs/heads <-- toegevoegd 0.1 Kb
8657	.git/logs/refs
13218	.git/logs
27538	.git/hooks
146759	.git

Er zijn 4 nieuwe objecten aangemaakt voor een commit van één enkel bestand. Met behulp van de documentatie over het Git-datamodel (jvns.ca) en het commando git show kunnen deze objecten worden geïdentificeerd:

  • 81…: de eigenlijke commit.
  • fc…: het tree-bestand voor de gewijzigde directory 1., met een lijst van alle bestanden in die directory (1 t/m 50).
  • 76…: de eigenlijke inhoud van bestand 1/1 (nu de string 'baz').
  • 92…: een andere tree voor de root-directory van de repo.

Er is dus sprake van één commit, twee trees en één bestand (blob).

Kleine repositories

Hoeveel ruimte kost een commit in een repository met slechts één klein bestand?

$ git init
$ echo foo > foo
$ git add foo
$ git commit -m 'initial'
$ du -sb .git
94208
$ echo bar > foo
$ git -am 'change1'
$ du -sb .git
102917
$ dc -e '102917 94208 - p'
8709

Dit komt neer op 8 Kb per commit, ongeveer de helft van de grootte van een repo met een grotere bestandsstructuur. Veel van deze omvang is overhead; bij grotere commits zullen de relatieve besparingen aanzienlijk zijn.

"Grote" bestanden

Wat gebeurt er bij het toevoegen van een "groot" binair bestand (580 Kb)?

$ cp /usr/bin/librewolf .
$ du -b librewolf
583840
$ git add .
$ git commit -m 'added librewolf'
$ du -sb .git
400790
$ dc -e '400790 102917 -  p'
297873

Het binaire bestand wordt behoorlijk gecomprimeerd, waardoor de commit kleiner is dan het toegevoegde bestand (300 Kb voor een bestand van 580 Kb).

Bij een enorm tekst-bronbestand (16,2 Mb) is het resultaat nog indrukwekkender:

$ cp /usr/src/linux...mask.h .
$ du -b nbio_7_2_0_sh_mask.h
16415223
$ git add .
$ git commit -m 'huge source'
$ du -sb .git
2022978
$ dc -e '2022978 400790 - p'
1622188

Er is slechts 1,6 Mb nodig om een bronbestand van 16,4 Mb te commiten. Hoewel te verwachten was dat tekst goed zou comprimeren, is dit resultaat zeer indrukwekkend.