Veröffentlicht am: 16. Juni 2025
12 Minuten Lesezeit
Beiträge des Git-Teams von GitLab und der Git-Community, inklusive der Befehl git-diff-pairs(1) und die Option git-rev-list(1) für gebündelte Referenz-Updates.
Das Git-Projekt hat kürzlich Git Version 2.50.0 veröffentlicht. Werfen wir einen Blick auf die Highlights dieser Veröffentlichung, die Beiträge des Git-Teams von GitLab und der gesamten Git-Community enthält.
Diffs sind das Herzstück jeder Code Review und zeigen alle Änderungen, die zwischen zwei Revisionen vorgenommen wurden. GitLab zeigt Diffs an verschiedenen Stellen an, am häufigsten aber auf der Registerkarte „Änderungen“ (in englischer Sprache verfügbar) eines Merge Requests.
Im Hintergrund wird die Diff-Generierung von git-diff(1)
verwendet. Ein Beispiel:
$ git diff HEAD~1 HEAD
Dieser Befehl gibt das vollständige Diff für alle geänderten Dateien zurück. Dies kann eine Herausforderung für die Skalierbarkeit darstellen, vor allem, wenn die Anzahl der Dateien, die innerhalb einer Reihe von Revisionen geändert wurden, sehr groß ist. Dies kann dazu führen, dass der Befehl selbst auferlegte Zeitlimits für das GitLab-Backend erreicht. Bei großen Änderungen wäre es besser, wenn
es eine Möglichkeit gäbe, die Diff-Berechnung in kleinere, leichter verarbeitbare Blöcke zu unterteilen.
Eine Möglichkeit dafür ist die Verwendung von
git-diff-tree(1)
(in englischer Sprache verfügbar), um Informationen
über alle geänderten Dateien abzurufen:
$ git diff-tree -r -M --abbrev HEAD~ HEAD
:100644 100644 c9adfed339 99acf81487 M Documentation/RelNotes/2.50.0.adoc
:100755 100755 1047b8d11d 208e91a17f M GIT-VERSION-GEN
Git bezeichnet diese Ausgabe als „unbearbeitetes“ Format (in englischer Sprache verfügbar).
Kurz gesagt, listet jede Zeile der Ausgabe Dateipaare und die dazugehörigen Metadaten
darüber auf, was sich zwischen dem Anfangscode und der letzten Revision geändert hat. Im Vergleich zur
Erzeugung der „Patch“-Ausgabe für große Änderungen verläuft dieser Prozess relativ
schnell und liefert eine Zusammenfassung aller Änderungen. Dieser Befehl kann optional eine Umbenennungserkennung durchführen, indem das Flag -M
angehängt wird. So kannst du überprüfen, ob identifizierte Änderungen auf eine Dateiumbenennung zurückzuführen sind.
Mit diesen Informationen könnten wir git-diff(1)
verwenden, um jedes der
Dateipaar-Diffs einzeln zu erstellen. Zum Beispiel können wir die Blob-IDs
direkt angeben:
$ git diff 1047b8d11de767d290170979a9a20de1f5692e26 208e91a17f04558ca66bc19d73457ca64d5385f
Wir können diesen Vorgang für jedes der Dateipaare wiederholen, aber es ist nicht sehr effizient, für jede einzelne Datei einen
separaten Git-Prozess zu starten.
Außerdem verliert das Diff bei der Verwendung von Blob-IDs einige Kontextinformationen,
wie den Änderungsstatus und die Dateimodi, die im übergeordneten
Baumobjekt gespeichert sind. Was wir wirklich möchten, ist ein Mechanismus, um „unbearbeitete“ Dateipaarinformationen einzuspeisen und
die entsprechende Patch-Ausgabe zu generieren.
Mit der Version 2.50 bietet Git einen neuen integrierten Befehl mit der Bezeichnung
git-diff-pairs(1)
(in englischer Sprache verfügbar. Dieser Befehl
akzeptiert „unbearbeitete“ formatierte Dateipaarinformationen als Eingabe auf stdin, um exakt zu bestimmen, welche Patches ausgegeben werden sollen. Das folgende Beispiel zeigt, wie dieser Befehl
verwendet werden kann:
$ git diff-tree -r -z -M HEAD~ HEAD | git diff-pairs -z
Bei dieser Nutzung ist die resultierende Ausgabe identisch mit der Verwendung von git-diff(1)
.
Durch einen separaten Befehl zur Generierung der Patch-Ausgabe kann die „unbearbeitete“ Ausgabe von
git-diff-tree(1)
in kleinere Chargen von Dateipaaren aufgeteilt und separaten
git-diff-pairs(1)
-Prozessen zugeführt werden. Dies löst das zuvor erwähnte
Skalierbarkeitsproblem, da die Diffs nicht länger alle auf einmal berechnet werden müssen. Zukünftige
GitLab-Versionen könnten auf diesem Mechanismus aufbauen, um die Leistung der
Diff-Generierung zu verbessern, insbesondere wenn es sich um große Änderungssätze
handelt. Weitere Informationen zu dieser Änderung findest du im entsprechenden
Dieses Projekt wurde von Justin Tobler geleitet.
Mit dem Git-Befehl git-update-ref(1)
(in englischer Sprache verfügbar)
kannst du Referenzaktualisierungen durchführen. Bei Verwendung mit dem Flag --stdin
können
mehrere Referenzaktualisierungen in einer einzigen Transaktion gebündelt werden, indem Anweisungen für jede Referenzaktualisierung
angegeben werden, die auf stdin durchgeführt werden soll.
Die Massenaktualisierung von Referenzen auf diese Weise zeigt auch ein atomares Verhalten, bei dem ein
einzelner Fehler bei der Referenzaktualisierung eine Transaktion abbricht und
Referenzen nicht aktualisiert werden. Hier ist ein Beispiel für dieses Verhalten:
# Erstelle ein Repository mit drei leeren Commits und einem Branch mit dem Namen „foo“
$ git init
$ git commit --allow-empty -m 1
$ git commit --allow-empty -m 2
$ git commit --allow-empty -m 3
$ git branch foo
# Gib die Commit-IDs aus
$ git rev-list HEAD
cf469bdf5436ea1ded57670b5f5a0797f72f1afc
5a74cd330f04b96ce0666af89682d4d7580c354c
5a6b339a8ebffde8c0590553045403dbda831518
# Versuche, eine neue Referenz zu erstellen und die vorhandene Referenz in der Transaktion zu aktualisieren.
# Es wird erwartet, dass die Aktualisierung fehlschlägt, da die angegebene alte Objekt-ID nicht richtig ist.
$ git update-ref --stdin <<EOF
> create refs/heads/bar cf469bdf5436ea1ded57670b5f5a0797f72f1afc
> update refs/heads/foo 5a6b339a8ebffde8c0590553045403dbda831518 5a74cd330f04b96ce0666af89682d4d7580c354c
> EOF
fatal: cannot lock ref 'refs/heads/foo': is at cf469bdf5436ea1ded57670b5f5a0797f72f1afc but expected 5a74cd330f04b96ce0666af89682d4d7580c354c
# Die Referenz „bar“ wurde nicht erstellt.
$ git switch bar
fatal: invalid reference: bar
Im Vergleich zur einzelnen Aktualisierung vieler Referenzen ist die Massenaktualisierung
auch viel effizienter. Das ist zwar grundsätzlich eine gute Lösung, aber es kann bestimmte
Umstände geben, unter denen es akzeptabel ist, wenn ein Teil der angeforderten Referenzaktualisierungen
fehlschlägt, wir aber dennoch die Effizienzvorteile von
Massenaktualisierungen nutzen möchten.
Ab dieser Version verfügt git-update-ref(1)
über die neue Option --batch-updates
, mit
der die Aktualisierungen auch dann fortgesetzt werden können, wenn eine oder mehrere Referenzaktualisierungen
fehlschlagen. In diesem Modus werden einzelne Fehler im folgenden Format gemeldet:
rejected SP (<old-oid> | <old-target>) SP (<new-oid> | <new-target>) SP <rejection-reason> LF
Dadurch können erfolgreiche Referenzaktualisierungen fortgesetzt werden, während gleichzeitig angegeben wird, unter welchen Umständen Aktualisierungen abgelehnt wurden und aus welchem Grund. Wir verwenden noch einmal das gleiche beispielhafte Repository wie im vorherigen Beispiel:
# Versuche, eine neue Referenz zu erstellen und die vorhandene Referenz in der Transaktion zu aktualisieren.
$ git update-ref --stdin --batch-updates <<EOF
> create refs/heads/bar cf469bdf5436ea1ded57670b5f5a0797f72f1afc
> update refs/heads/foo 5a6b339a8ebffde8c0590553045403dbda831518 5a74cd330f04b96ce0666af89682d4d7580c354c
> EOF
rejected refs/heads/foo 5a6b339a8ebffde8c0590553045403dbda831518 5a74cd330f04b96ce0666af89682d4d7580c354c incorrect old value provided
# Die Referenz „bar“ wurde erstellt, obwohl die Aktualisierung auf „foo“ abgelehnt wurde.
$ git switch bar
Switched to branch 'bar'
Mit der Option --batch-updates
war die Referenzerstellung diesmal erfolgreich,
obwohl die Aktualisierung nicht funktioniert hat. Diese Patch-Serie legt den Grundstein für
zukünftige Leistungsverbesserungen in git-fetch(1)
und git-receive-pack(1)
,
wenn Referenzen in großer Zahl aktualisiert werden. Weitere Informationen findest du im
Dieses Projekt wurde von Karthik Nayak geleitet.
Mit git-cat-file(1)
(in englischer Sprache verfügbar) ist es möglich,
Informationen für alle im Repository enthaltenen Objekte über die Option
--batch–all-objects
auszugeben. Zum Beispiel:
# Richte ein einfaches Repository ein.
$ git init
$ echo foo >foo
$ git add foo
$ git commit -m init
# Erstelle ein nicht erreichbares Objekt.
$ git commit --amend --no-edit
# Verwende git-cat-file(1), um Informationen über alle Objekte einschließlich nicht erreichbarer Objekte auszugeben.
$ git cat-file --batch-all-objects --batch-check='%(objecttype) %(objectname)'
commit 0b07e71d14897f218f23d9a6e39605b466454ece
tree 205f6b799e7d5c2524468ca006a0131aa57ecce7
blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99
commit c999f781fd7214b3caab82f560ffd079ddad0115
In einigen Situationen möchte ein(e) Benutzer(in) möglicherweise alle Objekte im
Repository durchsuchen, aber nur eine Teilmenge basierend auf einem bestimmten Attribut ausgeben. Wenn
wir beispielsweise nur die Objekte anzeigen möchten, die Commits sind, könnten wir
grep(1)
verwenden:
$ git cat-file --batch-all-objects --batch-check='%(objecttype) %(objectname)' | grep ^commit
commit 0b07e71d14897f218f23d9a6e39605b466454ece
commit c999f781fd7214b3caab82f560ffd079ddad0115
Das funktioniert zwar, aber ein Nachteil beim Filtern der Ausgabe ist, dass
git-cat-file(1)
nach wie vor alle Objekte im Repository durchlaufen muss, auch
diejenigen, an denen wir nicht interessiert sind. Dies kann ziemlich ineffizient sein.
Mit dieser Version verfügt git-cat-file(1)
jetzt über die Option --filter
, die nur jene Objekte
anzeigt, die den angegebenen Kriterien entsprechen. Dies ähnelt der gleichnamigen Option
für git-rev-list(1)
, unterstützt jedoch nur eine Teilmenge der
Filter. Die folgenden Filter werden unterstützt: blob:none
, blob:limit=
und
object:type=
. Ähnlich wie im vorherigen Beispiel können Objekte mit Git direkt nach
ihrem Typ gefiltert werden:
$ git cat-file --batch-all-objects --batch-check='%(objecttype) %(objectname)' --filter='object:type=commit'
commit 0b07e71d14897f218f23d9a6e39605b466454ece
commit c999f781fd7214b3caab82f560ffd079ddad0115
Es ist nicht nur praktisch, dass Git die Verarbeitung übernimmt, sondern bei großen
Repositories mit vielen Objekten ist dies möglicherweise auch effizienter. Wenn ein
Repository über Bitmap-Indizes verfügt, kann Git Objekte eines bestimmten Typs effizient
nachschlagen und so das Durchsuchen der
Paketierungsdatei vermeiden, wodurch die Geschwindigkeit deutlich erhöht wird. Benchmarks, die im
Chromium-Repository durchgeführt wurden, zeigen signifikante Verbesserungen:
Benchmark 1: git cat-file --batch-check --batch-all-objects --unordered --buffer --no-filter Time (mean ± σ): 82.806 s ± 6.363 s [User: 30.956 s, System: 8.264 s] Range (min … max): 73.936 s … 89.690 s 10 runs
Benchmark 2: git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=tag Time (mean ± σ): 20.8 ms ± 1.3 ms [User: 6.1 ms, System: 14.5 ms] Range (min … max): 18.2 ms … 23.6 ms 127 runs
Benchmark 3: git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=commit Time (mean ± σ): 1.551 s ± 0.008 s [User: 1.401 s, System: 0.147 s] Range (min … max): 1.541 s … 1.566 s 10 runs
Benchmark 4: git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=tree Time (mean ± σ): 11.169 s ± 0.046 s [User: 10.076 s, System: 1.063 s] Range (min … max): 11.114 s … 11.245 s 10 runs
Benchmark 5: git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=blob Time (mean ± σ): 67.342 s ± 3.368 s [User: 20.318 s, System: 7.787 s] Range (min … max): 62.836 s … 73.618 s 10 runs
Benchmark 6: git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=blob:none Time (mean ± σ): 13.032 s ± 0.072 s [User: 11.638 s, System: 1.368 s] Range (min … max): 12.960 s … 13.199 s 10 runs
Summary git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=tag 74.75 ± 4.61 times faster than git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=commit 538.17 ± 33.17 times faster than git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=tree 627.98 ± 38.77 times faster than git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=blob:none 3244.93 ± 257.23 times faster than git cat-file --batch-check --batch-all-objects --unordered --buffer --filter=object:type=blob 3990.07 ± 392.72 times faster than git cat-file --batch-check --batch-all-objects --unordered --buffer --no-filter
Interessanterweise zeigen diese Ergebnisse, dass die Berechnungszeit jetzt mit
der Anzahl der Objekte für einen bestimmten Typ skaliert, anstatt mit der Anzahl der gesamten Objekte
in der Paketierungsdatei. Den ursprünglichen (englischsprachigen) Mailinglisten-Thread findest du
hier.
Dieses Projekt wurde von Patrick Steinhardt geleitet.
Git bietet die Möglichkeit, über den Befehl
git-bundle(1)
(in englischer Sprache verfügbar) ein Archiv eines Repositories zu generieren, das einen
bestimmten Satz von Referenzen und zugehörigen erreichbaren Objekten enthält. Dieser Vorgang
wird von GitLab verwendet, um Repository-Backups zu erstellen, und ist auch ein Teil des
Bundle-URI (in englischer Sprache verfügbar)-Mechanismus.
Bei großen Repositories mit Millionen von Referenzen kann dieser Vorgang Stunden oder sogar Tage
dauern. Zum Beispiel lagen die Backup-Zeiten für das Haupt-GitLab-Repository
(gitlab-org/gitlab), bei
etwa 48 Stunden. Die Untersuchung zeigte einen Leistungsengpass, der
auf die Art zurückzuführen war, wie Git eine Überprüfung durchführte, um zu vermeiden, dass doppelte Referenzen
in das Bundle aufgenommen wurden. Die Implementierung verwendete eine verschachtelte for
-Schleife, um alle aufgelisteten Referenzen zu durchlaufen und zu
vergleichen, was zu einer Zeitkomplexität von O(N^2) führte. Die Skalierbarkeit
ist sehr schlecht, wenn die Anzahl der Referenzen in einem Repository zunimmt.
In dieser Version wurde dieses Problem behoben, indem die verschachtelten Schleifen durch eine
Datenzuordnungsstruktur ersetzt wurden, was die Geschwindigkeit erheblich erhöht. Der folgende Benchmark zeigt
die Leistungssteigerung beim Erstellen eines Bundles mit einem Repository, das
100 000 Referenzen enthält:
Benchmark 1: bundle (refcount = 100000, revision = master) Time (mean ± σ): 14.653 s ± 0.203 s [User: 13.940 s, System: 0.762 s] Range (min … max): 14.237 s … 14.920 s 10 runs
Benchmark 2: bundle (refcount = 100000, revision = HEAD) Time (mean ± σ): 2.394 s ± 0.023 s [User: 1.684 s, System: 0.798 s] Range (min … max): 2.364 s … 2.425 s 10 runs
Summary bundle (refcount = 100000, revision = HEAD) ran 6.12 ± 0.10 times faster than bundle (refcount = 100000, revision = master)
Weitere Informationen findest du in unserem Blogbeitrag
Den ursprünglichen englischsprachigen Mailinglisten-Thread findest du
hier.
Dieses Projekt wurde von Karthik Nayak geleitet.
Durch den Bundle-URI (in englischer Sprache verfügbar)-Mechanismus in Git können den Clients
Orte zum Abrufen von Bundles zur Verfügung gestellt werden, um
Klone und Abrufe zu beschleunigen. Wenn ein Client ein Bundle herunterlädt, werden Referenzen
unter refs/heads/*
zusammen mit
den zugehörigen Objekten aus dem Bundle in das Repository kopiert. Ein Bundle kann zusätzliche Referenzen
außerhalb von refs/heads/*
enthalten, wie z. B. refs/tags/*
, die einfach ignoriert werden, wenn
die Bundle-URI beim Klonen verwendet wird.
In Git 2.50 wird diese Einschränkung aufgehoben und alle Referenzen, die mit
refs/*
übereinstimmen und im heruntergeladenen Bundle enthalten sind, werden kopiert.
Scott Chacon, der diese Funktionalität beigesteuert hat,
demonstriert den Unterschied beim Klonen von
$ git-v2.49 clone --bundle-uri=gitlab-base.bundle https://gitlab.com/gitlab-org/gitlab-foss.git gl-2.49
Cloning into 'gl2.49'...
remote: Enumerating objects: 1092703, done.
remote: Counting objects: 100% (973405/973405), done.
remote: Compressing objects: 100% (385827/385827), done.
remote: Total 959773 (delta 710976), reused 766809 (delta 554276), pack-reused 0 (from 0)
Receiving objects: 100% (959773/959773), 366.94 MiB | 20.87 MiB/s, done.
Resolving deltas: 100% (710976/710976), completed with 9081 local objects.
Checking objects: 100% (4194304/4194304), done.
Checking connectivity: 959668, done.
Updating files: 100% (59972/59972), done.
$ git-v2.50 clone --bundle-uri=gitlab-base.bundle https://gitlab.com/gitlab-org/gitlab-foss.git gl-2.50
Cloning into 'gl-2.50'...
remote: Enumerating objects: 65538, done.
remote: Counting objects: 100% (56054/56054), done.
remote: Compressing objects: 100% (28950/28950), done.
remote: Total 43877 (delta 27401), reused 25170 (delta 13546), pack-reused 0 (from 0)
Receiving objects: 100% (43877/43877), 40.42 MiB | 22.27 MiB/s, done.
Resolving deltas: 100% (27401/27401), completed with 8564 local objects.
Updating files: 100% (59972/59972), done.
Wenn wir diese Ergebnisse vergleichen, sehen wir, dass Git 2.50 43 887 Objekte
(40,42 MiB) abruft, nachdem das Bundle extrahiert wurde, während Git 2.49
insgesamt 959 773 Objekte (366,94 MiB) abruft. Git 2.50 ruft etwa 95 % weniger
Objekte und 90 % weniger Daten ab, was vorteilhaft sowohl für den Client als auch für den Server ist. Der
Server muss viel weniger Daten für den Client verarbeiten und der Client muss weniger Daten
herunterladen und extrahieren. In dem von Scott angegebenen Beispiel führte dies zu einer
Beschleunigung um 25 %.
Weitere Informationen findest du im entsprechenden englischsprachigen
TDiese Patch-Serie wurde von Scott Chacon beigesteuert.
In diesem Artikel werden nur einige der Beiträge von GitLab und
der größeren Git-Community für diese neueste Veröffentlichung vorgestellt. Mehr darüber erfährst du in
der offiziellen Veröffentlichungsankündigung des Git-Projekts. Sieh dir auch
unsere letzten Blogbeiträge zu Git-Releases (in englischer Sprache verfügbar)
an, um weitere wichtige Beiträge von GitLab-Teammitgliedern zu entdecken.