Das Git-Objektmodell verstehen: Blobs, Trees, Commits
git
HEAD
Git · Objektmodell · Interna · Entwickler-Basics
Das Git-Objektmodell verstehen: Blobs, Trees, Commits
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.

14 Min. Lesezeit Blobs · Trees · Commits Git 2.x · CLI · SHA-1/SHA-256

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.

11. FAQ: Git-Objektmodell, Blobs, Trees, Commits

1Was ist der Unterschied zwischen einem Blob und einer Datei im Working Directory?
Eine Datei hat Namen, Pfad und liegt auf der Festplatte. Ein Blob speichert nur den komprimierten Inhalt, adressiert per Hash. Name und Pfad stehen ausschließlich im Tree-Objekt.
2Speichert ein Tree-Objekt vollständige Pfade oder nur Namen?
Nur Namen relativ zur eigenen Ebene. Verschachtelte Verzeichnisse entstehen durch verschachtelte Tree-Objekte, die sich gegenseitig per Hash referenzieren.
3Warum ist ein Commit ein Snapshot und kein Diff?
Ein Commit referenziert einen vollständigen Root-Tree. Diffs werden erst bei Bedarf durch Vergleich zweier Trees berechnet, sie sind keine gespeicherte Information.
4Was ist der Unterschied zwischen SHA-1 und SHA-256 in Git?
SHA-1: 40 Hex-Zeichen, historischer Standard. SHA-256: 64 Hex-Zeichen, seit Git 2.29 wählbar über --object-format=sha256. Ein Repository nutzt immer nur ein Format.
5Wie finde ich heraus, welches Hash-Format mein Repository verwendet?
extensions.objectFormat in .git/config zeigt sha256 an, falls gesetzt. Fehlt der Eintrag, verwendet das Repository SHA-1 als Standard.
6Was macht git cat-file -p genau?
Dekomprimiert ein Objekt und zeigt es passend zum Typ: Blob als rohen Inhalt, Tree als Eintragsliste, Commit mit Tree, Parent, Autor und Message.
7Wozu dient git hash-object ohne das Flag -w?
Berechnet nur den Hash, den ein Inhalt als Blob hätte, ohne ihn zu schreiben. Nützlich um zu prüfen, ob eine Version bereits als Objekt existiert.
8Was hat git add mit git hash-object zu tun?
git add nutzt intern denselben Hashing- und Schreibvorgang wie hash-object -w und aktualisiert danach nur den passenden Index-Eintrag auf den neuen Hash.
9Wo genau liegen Git-Objekte auf der Festplatte?
Als lose Dateien unter .git/objects/xx/yyyy..., wobei die ersten zwei Hex-Zeichen den Verzeichnisnamen bilden. Jede Datei ist einzeln zlib-komprimiert.
10Was ist der Unterschied zwischen einem Loose Object und einem Packfile?
Loose Object: eine Datei pro Objekt. Packfile: viele Objekte gebündelt mit Delta-Kompression zwischen ähnlichen Objekten, deutlich platzsparender. git gc erledigt die Umwandlung automatisch.