Das Git-Projekt hat kürzlich Git-Version 2.45.0 veröffentlicht. Werfen wir einen Blick auf die Highlights dieser Version, die Beiträge des Git-Teams von GitLab und der gesamten Git-Community enthält.
Reftables: Ein neues Backend zum Speichern von Referenzen
Jedes Git-Repository muss zwei grundlegende Datenstrukturen erfassen:
- Den Objektgraphen, der die Daten deiner Dateien, die Verzeichnisstruktur, Commit-Nachrichten und Tags speichert.
- Referenzen, die auf diesen Objektgraphen verweisen, um bestimmte Objekte mit einem leichter verständlichen Namen zu verknüpfen. Ein Branch ist zum Beispiel eine Referenz, deren Name mit dem Präfix
refs/heads/
beginnt.
Das Format, in dem Referenzen in einem Repository auf der Festplatte gespeichert werden, ist seit der Einführung von Git weitgehend unverändert geblieben und wird als „files“-Format bezeichnet. Jedes Mal, wenn du eine Referenz erstellst, legt Git eine sogenannte „lose Referenz“ an. Das ist eine einfache Datei in deinem Git-Repository, deren Pfad mit dem Namen der Referenz übereinstimmt. Zum Beispiel:
$ git init .
Initialized empty Git repository in /tmp/repo/.git/
# Das Updaten einer Referenz veranlasst Git dazu, eine “lose Referenz”
# zu erstellen. Diese lose Referenz ist eine einfache Datei, welche die
# Objekt-ID des Commits enthält.
$ git commit --allow-empty --message "Initial commit"
[main (root-commit) c70f266] Initial commit
$ cat .git/refs/heads/main
c70f26689975782739ef9666af079535b12b5946
# Wenn man eine zweite Referenz erstellt, wird diese als zweite lose
# Referenz gespeichert.
$ git branch feature
$ cat .git/refs/heads/feature
c70f26689975782739ef9666af079535b12b5946
$ tree .git/refs
.git/refs/
├── heads
│ ├── feature
│ └── main
└── tags
3 directories, 2 files
Von Zeit zu Zeit packt Git diese Referenzen in ein „gepacktes“ Dateiformat, damit es effizienter wird, Referenzen nachzuschlagen. Zum Beispiel:
# Wenn man Referenzen packt, erstellt Git eine “packed-refs” Datei.
# Diese Datei enthält die sortierte Liste von vorher losen Referenzen.
# Die losen Referenzen existieren nicht mehr.
$ git pack-refs --all
$ cat .git/refs/heads/main
cat: .git/refs/heads/main: No such file or directory
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
c70f26689975782739ef9666af079535b12b5946 refs/heads/feature
c70f26689975782739ef9666af079535b12b5946 refs/heads/main
Dieses Format ist zwar ziemlich simpel, hat aber Einschränkungen:
- In großen Mono-Repos mit vielen Referenzen stießen wir auf Probleme mit der Skalierbarkeit. Das Löschen von Referenzen ist besonders ineffizient, da die gesamte „packed-refs“-Datei neu geschrieben werden muss, um die gelöschte Referenz zu entfernen. In unseren größten Repositorys kann dies dazu führen, dass bei jedem Löschen einer Referenz mehrere Gigabyte an Daten neu geschrieben werden müssen.
- Da mehrere Dateien gelesen werden müssen, um alle Referenzen des Repos zu ermitteln, ist dies atomar nicht möglich. Sobald andere Prozesse existieren, die in das Repo schreiben wollen, kann es dadurch zu Inkonsistenzen kommen.
- Es ist unmöglich, atomar mehrere Referenzen gleichzeitig zu schreiben, weil dafür mehrere Dateien erstellt oder aktualisiert werden müssen.
- Das Packen von Referenzen lässt sich nicht gut skalieren, weil die gesamte „packed-refs“-Datei neu geschrieben werden muss.
- Da lose Referenzen den Dateisystempfad als Namen verwenden, unterliegen sie dem dateisystemspezifischen Verhalten. So können z. B. Dateisysteme, die Groß- und Kleinschreibung nicht unterscheiden, keine Referenzen speichern, bei denen nur die Groß- und Kleinschreibung unterschiedlich ist.
Um diese Probleme zu lösen, wurde mit Git v2.45.0 ein neues „reftable“-Backend eingeführt, das ein neues Binärformat zum Speichern von Referenzen verwendet. Dieses neue Backend wird schon sehr lange entwickelt: es wurde ursprünglich von Shawn Pearce im Juli 2017 vorgeschlagen und zunächst in JGit implementiert. Das Gerrit-Projekt nutzt es bereits ausgiebig. Im Jahr 2021 hat Han-Wen Nienhuys die Bibliothek in Git hochgeladen, die es ermöglicht, das reftable-Format zu lesen und zu schreiben.
Das neue „reftable“-Backend, das wir in Git v2.45.0 hochgeladen haben, bringt nun endlich die reftable-Bibliothek und Git zusammen, so dass du das neue Format als Speicher-Backend in deinen Git-Repositorys verwenden kannst.
Wenn du mindestens Git v2.45.0 verwendest, kannst du neue Repositorys mit dem „reftable“-Format erstellen, indem du den Schalter --ref-format=reftable
entweder an git-init(1)
oder an git-clone(1)
übergibst. Zum Beispiel:
$ git init --ref-format=reftable .
Initialized empty Git repository in /tmp/repo/.git/
$ git rev-parse --show-ref-format
reftable
$ find -type f .git/reftable/
.git/reftable/0x000000000001-0x000000000001-01b5e47d.ref
.git/reftable/tables.list
$ git commit --allow-empty --message "Initial commit"
$ find -type f .git/reftable/
.git/reftable/0x000000000001-0x000000000001-01b5e47d.ref
.git/reftable/0x000000000002-0x000000000002-87006b81.ref
.git/reftable/tables.list
Wie du siehst, werden die Referenzen jetzt im Verzeichnis .git/reftable
statt in .git/refs
gespeichert. Die Referenzen und die Reflogs werden in „tables“ gespeichert. Das sind die Dateien, die auf .ref
enden. Die Datei tables.list
enthält die Liste aller derzeit aktiven Tabellen. Die technischen Details rund um die Funktionsweise werden wir in einem separaten Blogbeitrag erklären. Bleib dran!
Das „reftable“-Backend ist als vollwertiger Ersatz für das „files“-Backend gedacht. Aus der Sicht der Benutzer(innen) sollte also alles gleich funktionieren.
Dieses Projekt wurde von Patrick Steinhardt geleitet. Dank gebührt auch Shawn Pearce, dem Erfinder des Formats, und Han-Wen Nienhuys, dem Autor der reftable-Bibliothek.
Bessere Tools für Referenzen
Das Format „reftable“ löst zwar viele der Probleme, die wir haben, es bringt allerdings auch einige neue Probleme mit sich. Eines der wichtigsten Probleme ist die Zugänglichkeit der darin enthaltenen Daten.
Mit dem „files“-Backend kannst du im schlimmsten Fall deine normalen Unix-Tools verwenden, um den Zustand der Referenzen zu überprüfen. Sowohl die „gepackten“ als auch die „losen“ Referenzen enthalten menschenlesbare Daten, die man leicht verstehen kann. Das ist beim „reftable“-Format anders, da es sich um ein Binärformat handelt. Daher muss Git alle notwendigen Tools bereitstellen, um Daten aus dem neuen „reftable“-Format zu extrahieren.
Auflisten aller Referenzen
Das erste Problem bestand darin, dass es im Grunde unmöglich ist, alle Referenzen zu ermitteln, die ein Repository kennt. Das ist zunächst etwas rätselhaft: Du kannst über Git Referenzen erstellen und ändern, aber es kann nicht alle Referenzen auflisten, die es kennt?
Während es problemlos alle „normalen“ Referenzen auflisten kann, die mit dem Präfix refs/
beginnen, verwendet Git auch sogenannte Pseudo-Referenzen. Diese Dateien befinden sich direkt im Stammverzeichnis des Git-Verzeichnisses und wären zum Beispiel Dateien wie .git/MERGE_HEAD
. Das Problem dabei ist, dass diese Pseudo-Referenzen neben anderen Dateien liegen, die Git speichert, wie z. B. .git/config
.
Während einige Pseudo-Referenzen bekannt und daher leicht zu identifizieren sind, gibt es theoretisch keine Grenzen dafür, welche Referenzen Git schreiben kann. Nichts hält dich davon ab, eine Referenz mit dem Namen „foobar“ zu erstellen.
Zum Beispiel:
$ git update-ref foobar HEAD
$ cat .git/foobar
f32633d4d7da32ccc3827e90ecdc10570927c77d
Das Problem des „files“-Backends ist, dass es Referenzen nur aufzählen kann, indem es Verzeichnisse durchsucht. Um herauszufinden, dass es sich bei .git/foobar
tatsächlich um eine Referenz handelt, müsste Git die Datei öffnen und prüfen, ob sie wie eine Referenz formatiert ist oder nicht.
Das „reftable“-Backend hingegen kennt sämtliche Referenzen, die es enthält: Sie sind in seinen Datenstrukturen kodiert, so dass es lediglich diese Referenzen dekodieren und zurückgeben muss. Aufgrund der Einschränkungen des „files“-Backends gibt es jedoch kein Tool, mit dem du alle vorhandenen Referenzen ermitteln kannst.
Um dieses Problem zu lösen, haben wir git-for-each-ref(1)
mit dem neuen Flag --include-root-refs
ausgestattet, das auch alle Referenzen auflistet, die im Stammverzeichnis der Referenznamen-Hierarchie existieren. Zum Beispiel:
$ git for-each-ref --include-root-refs
f32633d4d7da32ccc3827e90ecdc10570927c77d commit HEAD
f32633d4d7da32ccc3827e90ecdc10570927c77d commit MERGE_HEAD
f32633d4d7da32ccc3827e90ecdc10570927c77d commit refs/heads/main
Für das „files“-Backend wird dieses neue Flag nach dem Best-Effort-Prinzip behandelt, d.h. es werden alle Referenzen aufgenommen, die mit einem bekannten Pseudo-Referenznamen übereinstimmen. Für das „reftable“-Backend können wir einfach alle bekannten Referenzen auflisten.
Dieses Projekt wurde von Karthik Nayak geleitet.
Auflisten aller reflogs
Jedes Mal, wenn du einen Branch aktualisierst, zeichnet Git diese Branch-Aktualisierung standardmäßig in einem sogenannten reflog auf. Dieses reflog ermöglicht es dir, Änderungen an diesem Branch rückgängig zu machen, falls du eine unbeabsichtigte Änderung vorgenommen hast, und kann daher sehr hilfreich sein.
Mit dem „files“-Backend werden diese Protokolle in deinem .git/logs
-Verzeichnis gespeichert:
$ find -type f .git/logs/
.git/logs/HEAD
.git/logs/refs/heads/main
Tatsächlich ist die Auflistung der Dateien in diesem Verzeichnis die einzige Möglichkeit, um herauszufinden, welche Referenzen überhaupt ein reflog haben. Dies ist ein Problem für das „reftable“-Backend, das diese Logs zusammen mit den Referenzen speichert. Folglich gibt es keine Möglichkeit mehr, herauszufinden, welche reflogs im Repository überhaupt existieren, wenn du das „reftable“ Format verwendest.
Dies ist jedoch nicht wirklich die Schuld des „reftable“-Formats, sondern eine Lücke in den von Git bereitgestellten Tools. Um diese Lücke zu schließen, haben wir einen neuen Unterbefehl list
für git-reflog(1)
eingeführt, mit dem du alle vorhandenen reflogs auflisten kannst:
$ git reflog list
HEAD
refs/heads/main
Dieses Projekt wurde von Patrick Steinhardt geleitet.
Effizienteres Packen von Referenzen
Um effizient zu bleiben, müssen Git-Repositorys regelmäßig gewartet werden. Normalerweise wird diese Wartung durch verschiedene Git-Befehle ausgelöst, die Daten in die Git-Repositorys schreiben, indem sie den Befehl git maintenance run --auto
ausführen. Dieser Befehl optimiert nur die Datenstrukturen, die tatsächlich optimiert werden müssen, damit Git keine Computeressourcen verschwendet.
Eine Datenstruktur, die von der Git-Wartung optimiert wird, ist die Referenzdatenbank, die mit dem Befehl git pack-refs --all
optimiert wird. Für das „files“-Backend bedeutet dies, dass alle Referenzen in die „packed-refs“-Datei gepackt und die losen Referenzen gelöscht werden, während für das „reftable“-Backend alle Tabellen in einer einzigen Tabelle zusammengefasst werden.
Im Hinblick auf das „files“-Backend können wir nicht viel besser vorgehen. Da die gesamte „packed-refs“-Datei ohnehin neu geschrieben werden muss, ist es sinnvoll, alle losen Referenzen zu packen.
Für das „reftable“-Backend ist dies jedoch suboptimal, da sich das „reftable“-Backend selbst optimiert. Jedes Mal, wenn Git eine neue Tabelle an das „reftable“-Backend anhängt, führt es eine automatische Komprimierung durch und führt die Tabellen nach Bedarf zusammen. Deshalb sollte sich die Referenzdatenbank immer in einem gut optimierten Zustand befinden, sodass das Zusammenführen aller Tabellen vergebliche Mühe wäre.
In Git v2.45.0 haben wir daher einen neuen Modus git pack-refs --auto
eingeführt, der das Referenz-Backend auffordert, nach Bedarf zu optimieren. Während das „files“-Backend auch bei gesetztem --auto
-Flag weiterhin gleich funktioniert, verwendet das „reftable“-Backend die gleichen Heuristiken, die es bereits für seine automatische Komprimierung verwendet. In der Praxis sollte dies in den meisten Fällen kein Problem darstellen.
Außerdem wurde git maintenance run --auto
so angepasst, dass das Flag --auto
an git-pack-refs(1)
übergeben wird, um diesen neuen Modus standardmäßig zu verwenden.
Dieses Projekt wurde von Patrick Steinhardt geleitet.
Weiterlesen
In diesem Blogbeitrag ging es vor allem um das neue „reftable“-Backend, das es uns ermöglicht, in großen Repositorys mit vielen Referenzen besser zu skalieren, sowie um die zugehörigen Tools, die wir parallel dazu eingeführt haben, damit es gut funktioniert. Natürlich hat die Git-Community mit dieser Version auch verschiedene Leistungsverbesserungen, Fehlerbehebungen und kleinere Funktionen eingeführt. Diese kannst du in der offiziellen Versionsankündigung des Git-Projekts nachlesen.