Von losen Objekten zu delta-komprimierten Packs
Ein gewachsenes .git-Verzeichnis, das mehrere Gigabyte umfasst, ist selten ein Zufall, sondern die Summe jedes Commits, jedes Rebase und jeder versehentlich eingecheckten Binärdatei, die Git niemals von selbst löscht. Dieser Artikel erklärt, wie lose Objekte zu delta-komprimierten Packfiles werden, was git gc technisch wirklich tut, welche Rolle der Reflog beim Schutz vor Datenverlust spielt, und wie man ein aufgeblähtes Repository gezielt diagnostiziert und wieder verschlankt.
Inhaltsverzeichnis
- 1. Warum Git-Repositories mit der Zeit aufblähen
- 2. Lose Objekte: eine Datei pro Objekt
- 3. Packfiles: viele Objekte in einer einzigen .pack-Datei
- 4. Delta-Kompression: Objekte als Differenzen speichern
- 5. git gc im Detail: was beim Garbage Collection wirklich passiert
- 6. Unreachable Objects und die Rolle des Reflogs
- 7. Pruning: wann Objekte tatsächlich gelöscht werden
- 8. Repository-Bloat diagnostizieren und .git verkleinern
- 9. Speicher- und Wartungsstrategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Git-Repositories mit der Zeit aufblähen
Jeder Commit, jedes Amend, jedes Rebase erzeugt neue Objekte in .git/objects, aber Git löscht diese Objekte bei normaler Arbeit nie automatisch. Auch ein git commit --amend oder ein interaktives Rebase überschreibt keinen alten Commit, sondern erstellt einen neuen und lässt den alten Commit unangetastet im Objektspeicher zurück, erreichbar über den Reflog. Über Monate summiert sich das: Hunderte überschriebene Commits, verworfene Branches und experimentelle Merges bleiben technisch vorhanden, auch wenn kein Branch mehr auf sie zeigt.
Besonders schwer wiegen versehentlich committete Binärdateien: Ein zehn Megabyte großes PDF oder ein Datenbank-Dump, der Sekunden später mit git rm wieder entfernt wird, bleibt trotzdem für immer Teil der Historie, weil jede Dateiversion als eigenes, unveränderliches Blob-Objekt gespeichert wird. Das erklärt, warum ein .git-Verzeichnis oft um ein Vielfaches größer ist als der aktuelle Checkout, und warum git clone in gewachsenen Projekten überraschend lange dauert.
2. Lose Objekte: eine Datei pro Objekt
Jedes neue Objekt landet zunächst als loses Objekt unter .git/objects/xx/yyyy..., wobei xx die ersten zwei Zeichen des SHA-Hashes sind und der Rest den Dateinamen bildet. Der Inhalt wird per zlib deflate komprimiert, aber jedes Objekt bleibt eine eigene Datei auf der Festplatte. Für ein kleines Repository mit wenigen hundert Commits ist das völlig unproblematisch und sogar praktisch, weil jedes Objekt sofort einzeln lesbar ist.
Bei tausenden Commits wird die Zahl der losen Objekte schnell zum Problem, nicht wegen der Kompression, sondern wegen des Dateisystem-Overheads: Jede Datei belegt mindestens einen Inode, viele Dateisysteme werden bei hunderttausenden kleinen Dateien in wenigen Verzeichnissen spürbar langsamer, und jeder Lesevorgang bedeutet einen eigenen Systemaufruf. Das folgende Kommando macht sichtbar, wie viele lose Objekte tatsächlich existieren, bevor überhaupt ein git gc gelaufen ist.
# Count loose object files directly on disk
$ find .git/objects -type f -not -path "*/pack/*" | wc -l
14832
# Total size of the .git directory
$ du -sh .git
612M .git
# Git's own summary: loose vs. packed objects and their sizes
$ git count-objects -v
count 14832
size 187344
in-pack 92150
packs 3
size-pack 421088
prune-packable 0
garbage 0
size-garbage 0
3. Packfiles: viele Objekte in einer einzigen .pack-Datei
Ein Packfile bündelt viele einzelne Objekte in genau zwei Dateien: eine .pack-Datei mit den eigentlichen, oft delta-komprimierten Objektdaten, und eine begleitende .idx-Datei, die einen sortierten Index aus Hash und Byte-Offset enthält, damit Git ein einzelnes Objekt finden kann, ohne das gesamte Pack sequenziell zu durchsuchen. Beide Dateien liegen zusammen unter .git/objects/pack/.
Aus tausenden losen Dateien werden so oft nur zwei, was Checkout- und Clone-Geschwindigkeit spürbar verbessert, weil der Netzwerktransfer nur noch ein einziges großes, bereits komprimiertes Objekt statt tausender kleiner SSH- oder HTTP-Anfragen umfasst. git gc erstellt Packfiles automatisch, aber auch git repack -a -d lässt sich jederzeit manuell aufrufen, um alle losen Objekte und bestehenden Packs zu einem einzigen, optimierten Pack zusammenzufassen.
4. Delta-Kompression: Objekte als Differenzen speichern
Innerhalb eines Packs speichert Git die meisten Objekte nicht als vollständige Kopie, sondern als Delta gegenüber einem ähnlichen Basisobjekt, meist einer früheren Version derselben Datei. Zwei Commits, die sich nur in wenigen Zeilen einer großen Datei unterscheiden, benötigen dann lediglich die Differenz plus einen Verweis auf das Basisobjekt, statt den vollständigen Inhalt zweimal zu speichern.
Die Auswahl der Delta-Kandidaten erfolgt heuristisch: Git gruppiert Objekte ähnlicher Größe und ähnlichen Dateinamens und testet dann, welche Kombination die beste Kompression ergibt. pack.window (Standard 10) begrenzt, wie viele Objekte als Basiskandidat verglichen werden, und pack.depth (Standard 50) begrenzt, wie viele Deltas in einer Kette hintereinander erlaubt sind, bevor wieder ein vollständiges Objekt gespeichert wird, um die Rekonstruktionszeit beim Lesen zu begrenzen.
5. git gc im Detail: was beim Garbage Collection wirklich passiert
git gc ist kein einzelner Vorgang, sondern eine Kette mehrerer Wartungsschritte: Lose Objekte werden zu einem neuen Pack zusammengefasst und delta-komprimiert, bestehende, ineffiziente Packs werden gegebenenfalls neu gepackt, abgelaufene Reflog-Einträge werden entfernt, und Objekte, die danach von keinem Branch, Tag oder Reflog-Eintrag mehr erreichbar sind, werden für die Löschung vorgemerkt beziehungsweise entfernt.
Git ruft git gc --auto ohnehin regelmäßig automatisch nach Operationen wie git commit oder git fetch auf, aber nur, wenn Schwellenwerte überschritten sind: gc.auto (Standard 6700 lose Objekte) und gc.autoPackLimit (Standard 50 Packs) bestimmen, ab wann automatisch aufgeräumt wird. Ein manueller Lauf mit --aggressive erzwingt eine gründlichere, aber deutlich langsamere Delta-Suche über das gesamte Repository.
# Standard maintenance: repack loose objects, expire old reflog entries
$ git gc
Enumerating objects: 14832, done.
Counting objects: 100% (14832/14832), done.
Delta compression using up to 8 threads
Compressing objects: 100% (12904/12904), done.
Writing objects: 100% (14832/14832), done.
Total 14832 (delta 8210), reused 0 (delta 0), pack-reused 0
$ du -sh .git
198M .git
# Aggressive repack with a wider delta search, plus immediate pruning
$ git gc --aggressive --prune=now
Enumerating objects: 106982, done.
Compressing objects: 100% (98410/98410), done.
Total 106982 (delta 61230), reused 92150 (delta 55870)
$ du -sh .git
164M .git
6. Unreachable Objects und die Rolle des Reflogs
Ein Commit gilt als unreachable, sobald kein Branch, kein Tag und kein anderer erreichbarer Commit mehr auf ihn verweist, etwa nach git commit --amend, git rebase oder git branch -D. Git löscht solche Objekte trotzdem nicht sofort, weil der Reflog, eine lokale Historie jeder Bewegung von HEAD und Branch-Zeigern, sie weiterhin referenziert und damit künstlich am Leben hält.
Standardmäßig verfallen Reflog-Einträge für erreichbare Commits nach 90 Tagen (gc.reflogExpire) und für bereits unreachable Commits nach 14 Tagen (gc.reflogExpireUnreachable). Genau dieses Sicherheitsnetz macht git reflog zum wichtigsten Werkzeug, um versehentlich gelöschte Commits, verlorene Branches oder einen fehlgeschlagenen Rebase innerhalb dieses Zeitfensters problemlos wiederherzustellen, ohne auf ein externes Backup angewiesen zu sein.
7. Pruning: wann Objekte tatsächlich gelöscht werden
Ein Objekt wird erst dann tatsächlich von der Festplatte entfernt, wenn es sowohl unreachable ist, als auch nicht mehr durch einen Reflog-Eintrag geschützt wird, und zusätzlich älter ist als die in gc.pruneExpire konfigurierte Gnadenfrist, standardmäßig zwei Wochen. git gc allein löscht daher in der Praxis selten sofort etwas, sondern räumt hauptsächlich auf und packt neu.
git prune ohne Optionen respektiert dieselbe zweiwöchige Frist, git gc --prune=now hebt sie vollständig auf und entfernt sofort alles, was nicht mehr erreichbar ist. Genau das macht --prune=now riskant: Ist ein Commit gerade erst durch einen Fehlgriff unreachable geworden und noch nicht im Reflog vermerkt, verschwindet er unwiderruflich, ohne die übliche zweiwöchige Rettungsfrist.
# Full sequence to actually purge unreachable history immediately
# WARNING: destructive, only use when you are certain nothing is needed
# 1. Expire every reflog entry right now, removing the safety net
$ git reflog expire --expire=now --all
# 2. List what would be purged: unreachable objects not protected by reflog
$ git fsck --unreachable --no-reflog
unreachable commit a1b2c3d4e5f67890abcdef1234567890abcdef12
unreachable blob 9f8e7d6c5b4a3928170695847362514039281706
# 3. Actually remove the unreachable objects from disk
$ git prune -v
Removing a1b2c3d4e5f67890abcdef1234567890abcdef12
Removing 9f8e7d6c5b4a3928170695847362514039281706
$ git count-objects -v
count 0
size 0
in-pack 92150
packs 1
size-pack 164024
8. Repository-Bloat diagnostizieren und .git verkleinern
git count-objects -v liefert in Sekunden eine Übersicht über Anzahl und Größe loser Objekte sowie bereits vorhandener Packs, ganz ohne das Repository zu verändern. du -sh .git zeigt die tatsächliche Gesamtgröße auf der Festplatte, oft überraschend größer als der aktuelle Arbeitsstand, wenn die Historie viele große, längst gelöschte Dateien enthält.
Um die größten Übeltäter innerhalb eines Packs zu finden, hilft git verify-pack -v kombiniert mit einer Sortierung nach Objektgröße. Sind die größten Objekte identifiziert und stellen sich als versehentlich committete Binärdateien heraus, reicht git gc allein nicht aus, denn diese Objekte bleiben Teil der Historie. Nur ein Historien-Rewrite mit git filter-repo entfernt sie endgültig aus allen Commits, gefolgt von einem harten Prune aller alten Referenzen. Für regelmäßige, unbeaufsichtigte Wartung auf zentralen Bare-Repositories eignet sich ein geplanter Workflow-Lauf besser als manuelle Eingriffe.
# Find the largest objects inside a packfile by uncompressed size
$ git verify-pack -v .git/objects/pack/pack-*.idx \
| sort -k3 -n -r \
| head -n 5
7c2b1a9f... blob 9484032 2103841 481920
3e8d4f21... blob 4210688 982104 512004
b1a0f7c3... blob 2094112 601022 918233
90fa2e11... blob 1887744 399102 105881
e4c5d6a7... blob 1024006 287441 22019
# Cross-reference the largest hash with the file path it belongs to
$ git rev-list --objects --all | grep 7c2b1a9f
7c2b1a9f... assets/legacy-export-2019.sql.gz
# .github/workflows/git-maintenance.yml
# Scheduled maintenance for a self-hosted bare mirror repository
name: Git repository maintenance
on:
schedule:
# Every night at 02:15 UTC
- cron: "15 2 * * *"
workflow_dispatch: {}
jobs:
maintain:
runs-on: self-hosted
steps:
- name: Run incremental git maintenance
run: |
cd /srv/git/mirrors/mironsoft.git
git maintenance run --task=loose-objects
git maintenance run --task=incremental-repack
- name: Full weekly repack with aggressive delta search
if: github.event.schedule == '15 2 * * 0'
run: |
cd /srv/git/mirrors/mironsoft.git
git gc --aggressive --prune=now
- name: Report repository size after maintenance
run: |
du -sh /srv/git/mirrors/mironsoft.git
9. Speicher- und Wartungsstrategien im Vergleich
Die folgende Übersicht fasst zusammen, welches Symptom auf welche Ursache hindeutet, mit welchem Befehl sich das diagnostizieren lässt, und welche Maßnahme tatsächlich hilft, statt pauschal git gc aufzurufen und zu hoffen.
| Situation | Symptom | Diagnose-Befehl | Lösung |
|---|---|---|---|
| Viele kleine Commits ohne Wartung | Tausende lose Dateien unter .git/objects | git count-objects -v | git gc (repackt automatisch) |
| Versehentlich committetes Binary | .git um ein Vielfaches größer als Checkout | git verify-pack -v | sort -k3 -nr | git filter-repo + git gc --prune=now |
| Amend- und Rebase-lastige Historie | git gc löscht sichtbar nichts | git reflog | 90 Tage warten oder reflog gezielt expiren |
| Viele kleine Packs statt einem großen | Clone und Checkout werden langsamer | ls .git/objects/pack | wc -l | git repack -a -d oder git maintenance run |
| Sofortiges Löschen gewünscht | git prune tut scheinbar nichts | git count-objects -v | git gc --prune=now (riskant, kein Sicherheitsnetz) |
Für dauerhaft schlanke Repositories lohnt sich außerdem ein regelmäßiger, automatisierter Wartungslauf statt punktueller manueller Eingriffe, insbesondere auf zentralen Mirror- oder Bare-Repositories, auf die viele CI-Jobs täglich zugreifen.
Mironsoft
Repository-Wartung, CI/CD-Performance und Git-Infrastruktur für PHP- und Magento-Teams
Ein aufgeblähtes Repository wieder schlank machen?
Wir analysieren eure .git-Historie, finden die größten Übeltäter, entfernen sie sicher aus der Vergangenheit und richten automatisierte Wartungsläufe ein, damit euer Repository dauerhaft schlank bleibt.
Repository-Audit
Größte Objekte, unreachable Commits und Bloat-Ursachen identifizieren
History-Rewrite
Versehentliche Binärdateien und Secrets sicher aus der Historie entfernen
Wartungsautomatisierung
Geplante git maintenance und gc-Läufe für Mirrors und CI-Pipelines
10. Zusammenfassung
Git speichert jedes Objekt zunächst als lose Datei, bündelt sie aber über git gc oder git repack zu Packfiles, die mittels Delta-Kompression gegen ähnliche Basisobjekte deutlich kleiner sind als die Summe ihrer Einzeldateien. Unreachable Commits verschwinden nicht sofort, sondern bleiben über den Reflog standardmäßig 90 Tage lang als Sicherheitsnetz erreichbar, bevor sie überhaupt für ein Pruning infrage kommen.
Erst wenn ein Objekt weder erreichbar noch durch den Reflog geschützt noch jünger als die zweiwöchige gc.pruneExpire-Frist ist, entfernt Git es tatsächlich von der Festplatte, sei es über den regulären git gc-Zyklus oder explizit über git prune. Für strukturell große Objekte wie versehentlich committete Binärdateien reicht das allein nicht: Nur ein Historien-Rewrite mit git filter-repo, gefolgt von einem sofortigen Prune, entfernt sie endgültig und macht Diagnosewerkzeuge wie git count-objects und git verify-pack zum festen Bestandteil regelmäßiger Repository-Wartung.
Packfiles und Garbage Collection auf einen Blick
Lose Objekte vs. Packfiles
Eine Datei pro Objekt unter .git/objects/xx/ wird zu einer einzigen .pack- und .idx-Datei gebündelt.
Delta-Kompression
Ähnliche Objekte werden als Differenz gegenüber einem Basisobjekt gespeichert, statt vollständig dupliziert.
Reflog & Pruning
Unreachable Objekte bleiben 90 Tage über den Reflog geschützt, gc.pruneExpire verzögert die Löschung um 2 Wochen.
Diagnose & Wartung
git count-objects -v, git verify-pack -v und geplante git maintenance-Läufe halten Repositories schlank.