Was wirklich unter .git/objects liegt
Branches, Commits, Diffs und Merges wirken wie unabhängige Konzepte, sind aber technisch nur Zeiger in denselben Objektgraphen. Dieser Artikel steigt bewusst unter add, commit und branch und zeigt mit git cat-file und git hash-object direkt am Objektspeicher, wie Blobs, Trees und Commits aufgebaut sind, wie sie sich gegenseitig referenzieren, und warum Git seine Objekte per SHA-1 oder SHA-256 statt über Dateinamen adressiert.
Inhaltsverzeichnis
- 1. Warum das Objektmodell das Fundament von allem in Git ist
- 2. Blobs: wie Git den Inhalt einer Datei speichert
- 3. Trees: Verzeichnisstruktur als eigenständige Objekte
- 4. Commits: Snapshot-Zeiger auf Tree und Elternobjekte
- 5. Content-adressierbarer Speicher: SHA-1 versus SHA-256
- 6. Objekte direkt inspizieren mit git cat-file
- 7. Objekte manuell erzeugen mit git hash-object
- 8. Loose Objects: ein komprimiertes File pro Objekt
- 9. Objekttypen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum das Objektmodell das Fundament von allem in Git ist
Git wird meist als Werkzeug zur Verwaltung von Branches und Commits verstanden, doch unter dieser Oberfläche ist praktisch alles ein Graph aus vier simplen Objekttypen: Blobs, Trees, Commits und Tags. Ein Branch ist nichts weiter als ein beweglicher Zeiger auf einen Commit-Hash. Ein Commit ist ein Zeiger auf genau einen Tree. Ein Merge-Commit unterscheidet sich von einem normalen Commit nur dadurch, dass er zwei statt einen Elternhash referenziert. Selbst git diff und git blame sind letztlich nichts anderes als Traversierungen und Vergleiche über denselben Objektspeicher.
Wer dieses Modell einmal verinnerlicht hat, versteht sofort, warum bestimmte Git-Operationen so günstig sind, wie sie sind: Ein Branch-Wechsel kostet nur das Umbiegen eines Zeigers, ein Commit erzeugt keine neuen Blobs für unveränderte Dateien, und ein Klon überträgt am Ende schlicht eine Menge von Objekten, adressiert über ihre Hashes. Dieser Artikel steigt deshalb bewusst unter die Kommandozeilen-Abstraktion von add, commit und branch und zeigt mit git cat-file und git hash-object direkt, wie die vier Objekttypen intern aufgebaut sind.
2. Blobs: wie Git den Inhalt einer Datei speichert
Ein Blob ist die einfachste der vier Objektarten und speichert ausschließlich den rohen Byte-Inhalt einer Datei, komprimiert mit zlib. Kein Dateiname, kein Pfad, keine Zugriffsrechte und kein Zeitstempel sind Teil eines Blobs. Zwei Dateien mit identischem Inhalt an völlig verschiedenen Stellen im Repository erzeugen deshalb exakt denselben Blob-Hash und werden intern als ein einziges Objekt gespeichert, unabhängig davon, wie oft sie im Projekt vorkommen. Diese Content-Adressierung ist die Grundidee, aus der sich fast alle Effizienzvorteile von Git ableiten.
Der Hash eines Blobs wird nicht allein aus dem Dateiinhalt berechnet, sondern aus einem kurzen Header der Form blob <größe-in-bytes>\0 gefolgt vom eigentlichen Inhalt. Diese Konstruktion stellt sicher, dass unterschiedliche Objekttypen mit zufällig identischem Byte-Inhalt trotzdem unterschiedliche Hashes erhalten. Wird eine Datei umbenannt oder in ein anderes Verzeichnis verschoben, bleibt ihr Blob-Hash unverändert, denn Dateiname und Pfad werden ausschließlich im übergeordneten Tree-Objekt gespeichert, niemals im Blob selbst.
# Create a blob object manually and inspect it directly
$ printf "Tailwind CSS v4 + Alpine.js, no jQuery.\n" > note.txt
$ git hash-object -w note.txt
fb5dea5c2a8ff1580c941793d6bc47cfe4158e70
# Ask Git only for the object type
$ git cat-file -t fb5dea5c2a8ff1580c941793d6bc47cfe4158e70
blob
# Pretty-print the decompressed content
$ git cat-file -p fb5dea5c2a8ff1580c941793d6bc47cfe4158e70
Tailwind CSS v4 + Alpine.js, no jQuery.
# Size of the uncompressed content in bytes
$ git cat-file -s fb5dea5c2a8ff1580c941793d6bc47cfe4158e70
40
3. Trees: Verzeichnisstruktur als eigenständige Objekte
Ein Tree-Objekt bildet genau eine Verzeichnisebene ab und besteht aus einer sortierten Liste von Einträgen. Jeder Eintrag besteht aus vier Teilen: dem Dateimodus (zum Beispiel 100644 für eine reguläre Datei oder 040000 für ein Unterverzeichnis), dem Objekttyp (blob oder tree), dem Hash des referenzierten Objekts, und dem Namen innerhalb dieses Verzeichnisses. Anders als ein Blob enthält ein Tree also durchaus Namen, allerdings nur relativ zu seiner eigenen Ebene, niemals als vollständigen Pfad ab Projektwurzel.
Verschachtelte Verzeichnisse werden durch verschachtelte Tree-Objekte abgebildet: Ein Unterverzeichnis wird im übergeordneten Tree als eigener Eintrag vom Typ tree referenziert, dessen Hash wiederum auf ein eigenes Tree-Objekt mit den Einträgen genau dieses Unterverzeichnisses zeigt. Der Root-Tree eines Commits verweist also über eine Kette solcher Tree-Objekte auf den kompletten Verzeichnisbaum des Projekts. Auch hier gilt Content-Adressierung: Zwei identische Unterverzeichnisse an verschiedenen Stellen im Projekt erzeugen denselben Tree-Hash und teilen sich dasselbe Objekt im Speicher.
# Build a tree object manually from a staged blob entry
$ HASH=$(git hash-object -w note.txt)
$ git update-index --add --cacheinfo 100644 $HASH note.txt
$ git write-tree
9d7f97f9c4862742cfcbba6b406b251f7818a83b
# Inspect the resulting tree, -l also shows the blob size
$ git ls-tree -l 9d7f97f9c4862742cfcbba6b406b251f7818a83b
100644 blob fb5dea5c2a8ff1580c941793d6bc47cfe4158e70 40 note.txt
4. Commits: Snapshot-Zeiger auf Tree und Elternobjekte
Ein Commit-Objekt referenziert genau einen Root-Tree, der den vollständigen Zustand des Projekts zu diesem Zeitpunkt beschreibt, sowie null, einen oder mehrere Elterncommits. Ein initialer Commit hat keinen Elternteil, ein regulärer Commit genau einen, ein Merge-Commit zwei oder mehr. Dazu kommen Metadaten: Autor mit Name, E-Mail und Zeitstempel, Committer mit denselben Feldern, die etwa bei einem Rebase oder Cherry-Pick vom Autor abweichen können, sowie die eigentliche Commit-Message als freier Text am Ende des Objekts.
Diese Struktur erklärt, warum ein Commit ein vollständiger Snapshot ist und kein Diff: Der referenzierte Tree beschreibt jede Datei im Projekt zu diesem Zeitpunkt vollständig, nicht nur die Änderung gegenüber dem Vorgänger-Commit. Ein Diff zwischen zwei Commits ist deshalb keine gespeicherte Information, sondern wird bei Bedarf on-the-fly durch den Vergleich der beiden referenzierten Trees berechnet. Das erklärt auch, warum git log -p für uralte Commits genauso schnell funktioniert wie für ganz neue: Es gibt keine Kette von Deltas, die sequenziell angewendet werden müsste, jeder Snapshot ist direkt adressierbar.
# Build a commit object manually, referencing a tree and no parent
$ git commit-tree 9d7f97f9c4862742cfcbba6b406b251f7818a83b -m "Initial commit: add note.txt"
40198ede659b31953652f31e67947aa29f3940e2
# A second commit with the same tree, now with an explicit parent
$ git commit-tree 9d7f97f9c4862742cfcbba6b406b251f7818a83b \
-p 40198ede659b31953652f31e67947aa29f3940e2 \
-m "Second commit: same tree content, new parent link"
1bd19922bf345863a07e1d9d80561db2b85f6e66
# Inspect the commit object: tree, parent, author, committer, message
$ git cat-file -p 1bd19922bf345863a07e1d9d80561db2b85f6e66
tree 9d7f97f9c4862742cfcbba6b406b251f7818a83b
parent 40198ede659b31953652f31e67947aa29f3940e2
author Mira Novak <mira@mironsoft.de> 1752300000 +0200
committer Mira Novak <mira@mironsoft.de> 1752300000 +0200
Second commit: same tree content, new parent link
5. Content-adressierbarer Speicher: SHA-1 versus SHA-256
Jedes Git-Objekt wird über den Hash seines eigenen Inhalts identifiziert, genau das ist die Definition von "content-addressable storage". Historisch verwendet Git dafür SHA-1 mit einer 40 Zeichen langen hexadezimalen Darstellung (160 Bit). Seit Git 2.29 unterstützt Git zusätzlich SHA-256 mit 64 hexadezimalen Zeichen (256 Bit) als alternatives Objektformat, das bei der Repository-Erstellung explizit über git init --object-format=sha256 gewählt werden muss. Ein Repository liegt dabei immer vollständig in einem der beiden Formate vor, ein Mischbetrieb einzelner Objekte innerhalb desselben Repositorys ist nicht vorgesehen.
Der Umstieg auf SHA-256 ist vor allem eine Antwort auf theoretische und mittlerweile teils praktisch demonstrierte Kollisionsangriffe auf SHA-1, etwa den 2017 veröffentlichten SHAttered-Angriff, auch wenn Git durch eine zusätzliche Kollisionserkennung namens SHA-1DC bereits gegen die bekannten Angriffsvarianten gehärtet ist. Welches Format ein bestehendes Repository verwendet, steht in .git/config unter extensions.objectFormat; fehlt dieser Eintrag komplett, ist SHA-1 der implizite Standard. Wichtig für die Praxis: SHA-256- und SHA-1-Repositories sind derzeit nicht direkt austauschbar, ein Wechsel im laufenden Projekt erfordert explizite Konvertierungswerkzeuge.
# SHA-1 repository (Git default)
$ git init sha1-repo && cd sha1-repo
$ printf "Tailwind CSS v4 + Alpine.js, no jQuery.\n" > note.txt
$ git hash-object note.txt
fb5dea5c2a8ff1580c941793d6bc47cfe4158e70
$ cd ..
# SHA-256 repository (explicit object format)
$ git init --object-format=sha256 sha256-repo && cd sha256-repo
$ printf "Tailwind CSS v4 + Alpine.js, no jQuery.\n" > note.txt
$ git hash-object note.txt
8e6f45c08caf69d04db237ee305bbc655ed3a5d78c57835341179cc9128f3234
# same content, but the digest is 64 hex chars instead of 40
# Bulk-list every object currently in an object store
$ git cat-file --batch-all-objects --batch-check='%(objectname) %(objecttype) %(objectsize)'
8e6f45c08caf69d04db237ee305bbc655ed3a5d78c57835341179cc9128f3234 blob 40
6. Objekte direkt inspizieren mit git cat-file
git cat-file ist das zentrale Werkzeug, um roh in den Objektspeicher zu schauen, ganz ohne den Umweg über Working Directory oder Index. Die drei wichtigsten Flags beantworten jeweils eine andere Frage: -t liefert ausschließlich den Objekttyp zurück (blob, tree, commit oder tag), -p gibt den dekomprimierten Inhalt pretty-printed und passend zum jeweiligen Typ formatiert aus, und -s liefert die Größe des unkomprimierten Inhalts in Bytes. Alle drei Flags akzeptieren sowohl volle 40- beziehungsweise 64-stellige Hashes als auch eindeutige Kurzformen ab meist sieben Zeichen.
Für Bulk-Operationen über den gesamten Objektspeicher bietet sich git cat-file --batch-all-objects in Kombination mit --batch-check an: Der Befehl listet jedes Objekt im Repository mit Hash, Typ und Größe in einem Durchgang auf, ganz ohne dass man vorher wissen muss, welche Hashes überhaupt existieren. Das ist enorm nützlich beim Debuggen kaputter Repositories, beim Aufspüren ungewöhnlich großer Blobs, die versehentlich committet wurden, oder schlicht um ein Gefühl dafür zu bekommen, wie viele Objekte ein Repository tatsächlich enthält, unabhängig von der Anzahl der Branches oder Tags, die darauf zeigen.
7. Objekte manuell erzeugen mit git hash-object
git hash-object ist gewissermaßen das Gegenstück zu cat-file: Es berechnet den Objekt-Hash eines gegebenen Inhalts nach exakt demselben Verfahren, das Git intern verwendet, standardmäßig jedoch ohne das Objekt tatsächlich zu schreiben. Erst das Flag -w schreibt das entstehende Blob-Objekt komprimiert nach .git/objects. Ohne -w liefert der Befehl nur den Hash zurück, den der Inhalt hätte, ideal um vorab zu prüfen, ob eine bestimmte Version einer Datei schon irgendwo im Repository als Blob existiert, ohne dabei ein neues Objekt anzulegen.
Mit --stdin liest git hash-object den Inhalt nicht aus einer Datei, sondern direkt von der Standardeingabe, was sich gut mit anderen Unix-Werkzeugen kombinieren lässt, etwa um den Hash eines generierten oder gefilterten Inhalts zu berechnen, ohne eine Zwischendatei anlegen zu müssen. Genau dieser Mechanismus steckt intern auch hinter git add: Beim Stagen einer Datei ruft Git denselben Hashing- und Schreibvorgang auf, den hash-object -w von Hand ausführt, und aktualisiert anschließend nur den passenden Index-Eintrag auf den neuen Hash. git add ist im Kern also nichts anderes als hash-object -w plus ein Update der Staging Area.
8. Loose Objects: ein komprimiertes File pro Objekt
Jedes neu erzeugte Objekt landet zunächst als sogenanntes Loose Object unter .git/objects/, benannt nach seinem eigenen Hash. Die ersten zwei Zeichen des Hashes bilden dabei den Verzeichnisnamen, die restlichen 38 (bei SHA-1) beziehungsweise 62 (bei SHA-256) Zeichen den Dateinamen darin, zum Beispiel .git/objects/8e/6f45c0...f234. Diese zweistellige Sharding-Strategie verhindert, dass ein einzelnes Verzeichnis bei größeren Repositories zehntausende Dateien enthält, was auf vielen Dateisystemen spürbar langsamer wird als viele kleinere Verzeichnisse.
Jede dieser Dateien ist einzeln mit zlib deflate komprimiert und enthält exakt ein Objekt samt seinem Typ-Header. Das ist einfach und robust, aber langfristig ineffizient: Bei tausenden kleinen Änderungen entstehen tausende einzelne kleine Dateien, ohne dass Git dabei Redundanz zwischen ähnlichen, aber nicht identischen Blobs ausnutzt. Deshalb packt git gc lose Objekte irgendwann zu Packfiles zusammen, in denen ähnliche Objekte per Delta-Kompression gegeneinander referenziert werden. Dieser Packfile-Mechanismus ist ein eigenes, deutlich komplexeres Thema und bewusst nicht Teil dieses Artikels; wichtig ist an dieser Stelle nur, dass Packfiles nichts an der grundsätzlichen Objektstruktur aus Blobs, Trees und Commits ändern, sondern nur, wie diese Objekte physisch auf der Festplatte abgelegt werden.
# .git/config of a repository created with --object-format=sha256
[core]
repositoryformatversion = 1
filemode = true
bare = false
logallrefupdates = true
[extensions]
objectFormat = sha256
# repositoryformatversion = 1 signals that "extensions" entries
# must be understood by the reading Git version, or it must refuse
# to operate on this repository at all
9. Objekttypen im direkten Vergleich
Alle bisher besprochenen Konzepte, Blobs, Trees, Commits und implizit auch Tags, folgen demselben Muster: Inhalt hinein, SHA-Hash heraus, unveränderlich abgelegt unter .git/objects. Trotzdem hält sich hartnäckig eine Reihe von Fehlannahmen darüber, was jeder Objekttyp tatsächlich speichert. Die folgende Tabelle stellt die häufigsten Irrtümer dem tatsächlichen Verhalten von Git gegenüber, zusammen mit dem Befehl, mit dem sich jede Behauptung selbst nachprüfen lässt.
| Objekttyp | Häufige Fehlannahme | Tatsächliches Verhalten | Praxis-Befehl |
|---|---|---|---|
| Blob | Speichert auch Dateiname und Pfad | Nur roher Inhalt, Name lebt ausschließlich im Tree | git cat-file -p <hash> |
| Tree | Ist rekursiv der komplette Verzeichnisbaum | Bildet nur eine Ebene ab, referenziert Sub-Trees | git ls-tree -l <hash> |
| Commit | Speichert ein Diff zum Vorgänger-Commit | Referenziert einen vollständigen Tree-Snapshot | git cat-file -p <hash> |
| Hash-Format | SHA-1- und SHA-256-Repos sind einfach kompatibel | Ein Repository liegt immer vollständig in einem Format vor | grep objectFormat .git/config |
| Loose Object | Bleibt für immer eine einzelne lose Datei | git gc packt lose Objekte irgendwann in Packfiles | git count-objects -v |
Wer diese fünf Zeilen verinnerlicht hat, betrachtet Git nicht mehr als Sammlung isolierter Befehle, sondern als einen einzigen, konsistenten Objektgraphen. Jeder weitere Befehl, von git rebase bis git bisect, lässt sich auf dieselbe Frage zurückführen: Welche Blobs, Trees und Commits werden hier gelesen, und welche neuen Objekte werden dabei erzeugt?
Mironsoft
Git-Workflows, Repository-Wartung und CI/CD-Pipelines für PHP- und Magento-Teams
Repository-Interna verstehen und sauber nutzen?
Wir helfen Entwicklerteams, Git-Repositories gesund zu halten, von Objektspeicher-Wartung über Packfile-Optimierung bis zu Branching-Strategien, die auf einem soliden technischen Verständnis von Git aufbauen.
Git-Schulung
Praxisnahe Workshops zu Git-Interna, Objektmodell und fortgeschrittenen Workflows
Repository-Audit
Bestehende Repositories auf Größe, Historie und Objektspeicher-Hygiene prüfen
CI/CD-Integration
Automatisierte Pipelines und Hooks rund um Git für Magento-Projekte aufsetzen
10. Zusammenfassung
Das Git-Objektmodell aus Blobs, Trees und Commits ist kein Detailwissen für Interna-Fans, sondern die Grundlage, auf der jeder höhere Git-Befehl aufbaut. Ein Blob speichert ausschließlich den rohen, komprimierten Inhalt einer Datei, ohne Namen oder Pfad. Ein Tree bildet genau eine Verzeichnisebene ab und referenziert Blobs sowie weitere Trees über deren Hashes. Ein Commit ist ein vollständiger Snapshot, kein Diff: Er zeigt auf genau einen Root-Tree sowie auf null, einen oder mehrere Elterncommits und trägt Autor-, Committer- und Message-Metadaten.
Jedes dieser Objekte wird über den SHA-Hash seines eigenen Inhalts identifiziert, historisch SHA-1 mit 40 Hex-Zeichen, seit Git 2.29 optional SHA-256 mit 64 Hex-Zeichen. git cat-file -t/-p/-s macht diese Objekte direkt sichtbar, git hash-object -w erzeugt sie manuell nach demselben Verfahren, das auch git add intern verwendet. Physisch liegt jedes Objekt zunächst als einzelne, zlib-komprimierte Datei unter .git/objects, bevor git gc es irgendwann in ein platzsparenderes Packfile überführt.
Das Git-Objektmodell auf einen Blick
Blob
Roher, komprimierter Dateiinhalt. Kein Name, kein Pfad. Erzeugt durch git add oder git hash-object -w.
Tree
Eine Verzeichnisebene: Modus, Typ, Hash, Name pro Eintrag. Verschachtelt über weitere Tree-Objekte.
Commit
Vollständiger Snapshot: ein Tree-Hash, Elternhash(es), Autor/Committer, Message. Kein gespeicherter Diff.
Hashing & Speicherung
SHA-1 (40 Zeichen) oder SHA-256 (64 Zeichen). Lose Dateien unter .git/objects, später gepackt.