Kapitel ▾ 2. Auflage

10.4 Git Internals - Packfiles

Packfiles

Wenn Sie alle Anweisungen aus dem vorherigen Abschnitt befolgt haben, sollten Sie nun ein Test-Git-Repository mit 11 Objekten haben – vier Blobs, drei Trees, drei Commits und ein Tag.

$ 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/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.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

Git komprimiert den Inhalt dieser Dateien mit zlib, und da Sie nicht viel speichern, nehmen all diese Dateien zusammen nur 925 Bytes ein. Nun fügen wir dem Repository weitere umfangreiche Inhalte hinzu, um eine interessante Funktion von Git zu demonstrieren. Zur Demonstration fügen wir die Datei repo.rb aus der Grit-Bibliothek hinzu – das ist eine Quelldatei mit etwa 22 KB.

$ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
$ git checkout master
$ git add repo.rb
$ git commit -m 'Create repo.rb'
[master 484a592] Create repo.rb
 3 files changed, 709 insertions(+), 2 deletions(-)
 delete mode 100644 bak/test.txt
 create mode 100644 repo.rb
 rewrite test.txt (100%)

Wenn Sie sich den resultierenden Tree ansehen, können Sie den SHA-1-Wert sehen, der für Ihr neues repo.rb Blob-Objekt berechnet wurde.

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

Sie können dann git cat-file verwenden, um zu sehen, wie groß dieses Objekt ist.

$ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044

Ändern Sie die Datei an diesem Punkt ein wenig und sehen Sie, was passiert.

$ echo '# testing' >> repo.rb
$ git commit -am 'Modify repo.rb a bit'
[master 2431da6] Modify repo.rb a bit
 1 file changed, 1 insertion(+)

Überprüfen Sie den Tree, der von diesem letzten Commit erstellt wurde, und Sie werden etwas Interessantes sehen.

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

Das Blob ist jetzt ein anderes Blob, was bedeutet, dass obwohl Sie nur eine einzige Zeile am Ende einer 400-Zeilen-Datei hinzugefügt haben, Git diesen neuen Inhalt als ein komplett neues Objekt gespeichert hat.

$ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054

Sie haben zwei nahezu identische 22K-Objekte auf Ihrer Festplatte (jeweils auf etwa 7K komprimiert). Wäre es nicht schön, wenn Git eines davon vollständig speichern könnte und das zweite Objekt nur als die Differenz zwischen ihm und dem ersten?

Es stellt sich heraus, dass es das kann. Das anfängliche Format, in dem Git Objekte auf der Festplatte speichert, wird als „lose“ Objektformat bezeichnet. Gelegentlich packt Git jedoch mehrere dieser Objekte in eine einzige Binärdatei namens „Packfile“, um Platz zu sparen und effizienter zu sein. Git tut dies, wenn Sie zu viele lose Objekte herumliegen haben, wenn Sie den Befehl git gc manuell ausführen oder wenn Sie auf einen Remote-Server pushen. Um zu sehen, was passiert, können Sie Git manuell auffordern, die Objekte zu packen, indem Sie den Befehl git gc aufrufen.

$ git gc
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)

Wenn Sie in Ihr objects-Verzeichnis schauen, werden Sie feststellen, dass die meisten Ihrer Objekte verschwunden sind und ein neues Paar von Dateien aufgetaucht ist.

$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack

Die verbleibenden Objekte sind die Blobs, auf die kein Commit verweist – in diesem Fall die Blobs aus dem Beispiel „what is up, doc?“ und das Beispiel „test content“, die Sie zuvor erstellt haben. Da Sie sie nie zu Commits hinzugefügt haben, gelten sie als verwaist und werden nicht in Ihr neues Packfile aufgenommen.

Die anderen Dateien sind Ihr neues Packfile und ein Index. Das Packfile ist eine einzelne Datei, die den Inhalt aller Objekte enthält, die von Ihrem Dateisystem entfernt wurden. Der Index ist eine Datei, die Offsets in diesem Packfile enthält, damit Sie schnell zu einem bestimmten Objekt springen können. Das Coole daran ist, dass die Objekte auf der Festplatte vor der Ausführung des gc-Befehls zusammen etwa 15 KB groß waren, während das neue Packfile nur 7 KB groß ist. Sie haben Ihren Festplattenverbrauch durch das Packen Ihrer Objekte halbiert.

Wie macht Git das? Wenn Git Objekte packt, sucht es nach Dateien, die ähnlich benannt und in der Größe sind, und speichert nur die Deltas von einer Version der Datei zur nächsten. Sie können in das Packfile schauen und sehen, was Git getan hat, um Platz zu sparen. Der Plumbing-Befehl git verify-pack ermöglicht es Ihnen zu sehen, was gepackt wurde.

$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag    130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree   136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree   36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree   136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree   6 17 1314 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree   8 19 1331 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree   71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob   10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob   9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob   22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   9 20 7262 1 \
  b042a60ef7dff760008df33cee372b945b6e884e
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob   10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok

Hier verweist das 033b4-Blob, das, wenn Sie sich erinnern, die erste Version Ihrer repo.rb-Datei war, auf das b042a-Blob, das die zweite Version der Datei war. Die dritte Spalte der Ausgabe ist die Größe des Objekts im Pack, sodass Sie sehen können, dass b042a 22 KB der Datei belegt, aber 033b4 nur 9 Bytes belegt. Interessant ist auch, dass die zweite Version der Datei intakt gespeichert wird, während die ursprüngliche Version als Delta gespeichert wird – dies liegt daran, dass Sie höchstwahrscheinlich schnelleren Zugriff auf die aktuellste Version der Datei benötigen.

Das wirklich Schöne daran ist, dass es jederzeit neu verpackt werden kann. Git verpackt Ihre Datenbank gelegentlich automatisch neu und versucht immer, mehr Platz zu sparen, aber Sie können jederzeit manuell neu verpacken, indem Sie git gc von Hand ausführen.