Wie .git/index wirklich funktioniert
"Staging Area" ist nur ein Spitzname. Dahinter steckt eine reale, binäre Datei mit dokumentiertem Format, eigener Prüfsumme und Stage-Nummern pro Eintrag. Dieser Artikel geht direkt in die Internals: Dateiformat, was git add technisch schreibt, wie git status und git diff den Index als Angelpunkt nutzen, wie man ihn mit git ls-files und git update-index direkt inspiziert und manipuliert, und warum git add -p überhaupt nur funktioniert, weil der Index vom Working Directory entkoppelt ist.
Inhaltsverzeichnis
- 1. Was der Index wirklich ist: eine binäre Datei mit eigenem Format
- 2. Der Index als Vorschau des nächsten Commits
- 3. git add im Detail: wie Inhalte wirklich in den Index gelangen
- 4. git status und git diff: der Index als Angelpunkt
- 5. Den Index direkt inspizieren mit git ls-files
- 6. Low-Level-Zugriff mit git update-index
- 7. Partielles Stagen mit git add -p
- 8. Der Index während Merge-Konflikten: Stage 1, 2 und 3
- 9. Index-Operationen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was der Index wirklich ist: eine binäre Datei mit eigenem Format
Wer den Git-Index nur als Metapher für eine "Zwischenablage" versteht, hat lediglich die Hälfte der Wahrheit erfasst. Tatsächlich ist der Index eine reale, binäre Datei unter .git/index mit einem dokumentierten, versionierten Format. Der Dateikopf beginnt mit der Signatur DIRC (dircache), gefolgt von einer Versionsnummer, üblicherweise 2, 3 oder 4, und der Anzahl der Einträge. Jeder folgende Eintrag speichert Pfad, Dateimodus, Blob-Hash, Stage-Nummer sowie Stat-Metadaten wie ctime, mtime, Geräte- und Inode-Nummer. Am Dateiende steht eine SHA-1- oder SHA-256-Prüfsumme über den gesamten Inhalt, mit der Git Korruption zuverlässig erkennt.
Ab Version 3 unterstützt das Format zusätzliche Extensions, etwa den Cache-Tree, der bereits berechnete Tree-Hashes für unveränderte Verzeichnisse zwischenspeichert und so git commit erheblich beschleunigt, sowie die Resolve-Undo-Extension, die zuletzt aufgelöste Merge-Konflikte für ein mögliches erneutes git checkout -m vorhält. Wichtig für den Alltag: Der Index ist immer nach Pfadname sortiert, was binäre Suche erlaubt und erklärt, warum Befehle wie git ls-files ihre Ausgabe stets alphabetisch liefern, unabhängig davon, in welcher Reihenfolge Dateien gestaged wurden.
2. Der Index als Vorschau des nächsten Commits
Jeder Eintrag im Index ist wörtlich das, was git commit in ein Tree-Objekt verwandelt. Es gibt keine Übersetzung, keinen Zwischenschritt: Pfad, Modus und Blob-Hash aus dem Index werden direkt in die entsprechenden Tree-Einträge kopiert, hierarchisch nach Verzeichnissen aufgeteilt. Wer git commit ausführt, erzeugt also keinen Diff zum letzten Stand, sondern einen vollständigen, adressierbaren Snapshot exakt des aktuellen Index-Inhalts.
Der entscheidende Effizienzgewinn: Für Dateien, die seit dem letzten Commit nicht verändert wurden, erzeugt Git keinen neuen Blob. Der Index trägt für diese Pfade weiterhin den bereits bekannten Hash, und genau dieser Hash wird beim Bau des neuen Tree-Objekts einfach wiederverwendet. Nur Pfade mit tatsächlich geändertem Inhalt bekommen einen frischen Blob spendiert. Das macht den Index nicht nur zur Vorschau des nächsten Commits, sondern zum eigentlichen Bauplan, den git write-tree beim Committen abarbeitet, komplett unabhängig davon, wie viele Dateien im Repository insgesamt existieren.
3. git add im Detail: wie Inhalte wirklich in den Index gelangen
git add tut technisch zwei unabhängige Dinge. Erstens: Es liest den aktuellen Inhalt der Datei aus dem Working Directory, komprimiert ihn und schreibt ihn als neues Blob-Objekt nach .git/objects, adressiert über seinen SHA-Hash. Das passiert unabhängig davon, ob jemals ein Commit folgt. Zweitens: Es überschreibt den passenden Eintrag im Index mit diesem neuen Hash sowie aktuellen Stat-Daten wie Dateigröße, mtime und Inode-Nummer.
Diese Stat-Daten sind kein Beiwerk, sondern eine gezielte Optimierung: Ein späteres git status muss den Dateiinhalt nicht erneut hashen, wenn Größe und Zeitstempel im Dateisystem exakt mit den im Index gespeicherten Werten übereinstimmen. Erst bei einer Abweichung greift Git zum tatsächlichen Content-Hashing. Auf großen Magento-Repositories mit tausenden Dateien ist dieser Stat-Cache der Grund, warum git status in Sekundenbruchteilen antwortet, statt jede Datei neu einzulesen.
# git add: new blob object plus updated index entry
$ echo "<?php echo 'v2';" > app/code/Mironsoft/SeoSuite/Helper.php
$ git add app/code/Mironsoft/SeoSuite/Helper.php
$ git status --short
A app/code/Mironsoft/SeoSuite/Helper.php
# git ls-files -s: mode, blob hash, stage, path, straight from the index
$ git ls-files -s app/code/Mironsoft/SeoSuite/Helper.php
100644 8f94139338f9404f26296befa88755fc2598c289 0 app/code/Mironsoft/SeoSuite/Helper.php
# The blob already exists in the object store, even without a commit
$ git cat-file -p 8f94139338f9404f26296befa88755fc2598c289
<?php echo 'v2';
4. git status und git diff: der Index als Angelpunkt
git status ist im Kern ein doppelter Vergleich, der beide Male den Index als Angelpunkt nutzt: einmal Working Directory gegen Index, einmal Index gegen HEAD. "Changes to be committed" listet den zweiten Vergleich, "Changes not staged for commit" den ersten. Für den ersten Vergleich reicht meist der Stat-Cache aus dem vorherigen Abschnitt, für den zweiten wird der Index-Tree tatsächlich gegen den im letzten Commit gespeicherten Tree aufgelöst.
Dieselbe Zwei-Wege-Logik gilt für git diff, nur dass der Befehl je nach Flag ein anderes Bereichspaar vergleicht. git diff ohne Parameter vergleicht Working Directory gegen Index, exakt die Änderungen, die ein nachfolgender git add erfassen würde. git diff --staged (Synonym --cached) vergleicht stattdessen Index gegen HEAD, exakt das, was der nächste Commit tatsächlich aufzeichnen würde. Wer beide verwechselt, sieht scheinbar keine Änderungen, obwohl der Index längst vom letzten Commit abweicht.
# git diff: Working Directory vs Index
$ echo "<?php echo 'v3';" > app/code/Mironsoft/SeoSuite/Helper.php
$ git diff -- app/code/Mironsoft/SeoSuite/Helper.php
diff --git a/app/code/Mironsoft/SeoSuite/Helper.php b/app/code/Mironsoft/SeoSuite/Helper.php
index 8f94139..a1c2e3f 100644
--- a/app/code/Mironsoft/SeoSuite/Helper.php
+++ b/app/code/Mironsoft/SeoSuite/Helper.php
-<?php echo 'v2';
+<?php echo 'v3';
# git diff --staged: Index vs HEAD (only what was already git add'ed)
$ git diff --staged -- app/code/Mironsoft/SeoSuite/Helper.php
diff --git a/app/code/Mironsoft/SeoSuite/Helper.php b/app/code/Mironsoft/SeoSuite/Helper.php
index e69de29..8f94139 100644
--- a/app/code/Mironsoft/SeoSuite/Helper.php
+++ b/app/code/Mironsoft/SeoSuite/Helper.php
-
+<?php echo 'v2';
5. Den Index direkt inspizieren mit git ls-files
git ls-files macht den Index ohne Umwege sichtbar. Der Flag -s (bzw. --stage) listet jeden Eintrag exakt so, wie er im Index steht: Modus, Blob-Hash, Stage-Nummer und Pfad, getrennt durch Tabs. -u zeigt nur unaufgelöste Merge-Konflikte mit ihren mehreren Stage-Einträgen, -m listet Pfade, deren Working-Directory-Inhalt laut Stat-Vergleich vom Index abweicht, und -o zusammen mit --exclude-standard zeigt untracked Dateien, die weder im Index noch von .gitignore erfasst werden.
Wie der Index solche Vergleiche technisch interpretiert, hängt zusätzlich von core-Einstellungen in der Konfiguration ab. core.filemode bestimmt, ob reine Berechtigungsänderungen (chmod +x) überhaupt als Modifikation zählen, wichtig auf Dateisystemen ohne echte Unix-Rechte wie manchen Netzwerk-Mounts. core.ignorecase steuert, ob Pfadvergleiche Groß- und Kleinschreibung ignorieren, relevant für Entwickler, die zwischen macOS und Linux wechseln. sparseCheckoutCone beeinflusst schließlich, welche Pfade im Index überhaupt einen Working-Tree-Eintrag erwarten.
# .git/config: [core] settings that change how the index behaves
[core]
# If false, chmod +x on files is never recorded as a modification
# (important on filesystems without real Unix permission bits)
filemode = true
# If true, "APP.PHP" and "app.php" are treated as the same index path
# (relevant when switching a checkout between macOS and Linux)
ignorecase = false
# Enables sparse-checkout in cone mode: paths outside the cone stay
# in the index but are marked skip-worktree, no file on disk expected
sparseCheckoutCone = true
[extensions]
# Newer index format extension that speeds up status on large repos
worktreeConfig = true
6. Low-Level-Zugriff mit git update-index
git update-index ist die Low-Level-Schnittstelle, mit der Skripte den Index direkt manipulieren, ohne den Umweg über das Working Directory. Mit --add --cacheinfo <mode> <hash> <pfad> lässt sich ein bereits existierendes Blob-Objekt in den Index eintragen, selbst wenn im Arbeitsverzeichnis gar keine passende Datei liegt. Das nutzen etwa Merge-Treiber oder Build-Pipelines, die generierte Artefakte gezielt in einen Commit einschleusen wollen, ohne sie temporär auf die Festplatte zu schreiben.
Für die Performance auf großen Repositories bietet update-index zwei weitere Schalter: --assume-unchanged markiert einen Pfad so, dass Git dessen Stat-Daten beim Status-Check gar nicht erst prüft, praktisch für lokal überschriebene Konfigurationsdateien, die sich nie im Repository selbst ändern sollen. --skip-worktree geht einen Schritt weiter und wird zusätzlich von Sparse-Checkout-Mechanismen genutzt, um Pfade im Index zu behalten, ohne sie überhaupt im Working Directory zu erwarten. Beide Flags sind rein lokale Einstellungen und werden nicht mitcommittet.
# git update-index: insert a blob into the index without a worktree file
$ git hash-object -w --stdin <<< "return ['generated' => true];"
c7a1b2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9
$ git update-index --add --cacheinfo 100644 \
c7a1b2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9 \
generated/config-cache.php
$ git ls-files -s generated/config-cache.php
100644 c7a1b2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9 0 generated/config-cache.php
# git update-index: skip stat checks for a locally overridden file
$ git update-index --assume-unchanged src/app/etc/env.php
$ git status --short
# env.php no longer shows up as modified, even after local edits
# Undo it again when the file needs tracking normally
$ git update-index --no-assume-unchanged src/app/etc/env.php
7. Partielles Stagen mit git add -p
Partielles Stagen mit git add -p ist die praktische Konsequenz aus einer einzigen Tatsache: Der Index ist vollständig entkoppelt vom Working Directory und arbeitet nicht auf Dateiebene, sondern auf Blob-Ebene. Der Befehl zerlegt jede geänderte Datei in Hunks, einzelne zusammenhängende Änderungsblöcke, und fragt für jeden Hunk einzeln nach: y stagt den Hunk, n überspringt ihn, s versucht, einen zu großen Hunk in kleinere Teile zu splitten, und q bricht die gesamte Sitzung ab.
Technisch erzeugt add -p dabei intern einen Patch, der nur die ausgewählten Hunks enthält, wendet ihn über git apply --cached ausschließlich auf den Index an und lässt das Working Directory dabei komplett unberührt. Das Ergebnis ist ein neuer Blob, der weder dem alten committeten Stand noch dem aktuellen Dateiinhalt auf der Festplatte entspricht, sondern exakt der von Hand ausgewählten Zwischenversion. Genau das ermöglicht fokussierte, thematisch saubere Commits aus einer einzigen, unaufgeräumten Arbeitssitzung.
$ git add -p app/code/Mironsoft/SeoSuite/Model/MetaGenerator.php
diff --git a/.../MetaGenerator.php b/.../MetaGenerator.php
@@ -12,6 +12,9 @@ class MetaGenerator
public function generate(string $sku): string
{
+ // Bug fix: trim whitespace before length check
+ $sku = trim($sku);
+
Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -12,3 +12,4 @@ class MetaGenerator
public function generate(string $sku): string
{
+ $sku = trim($sku);
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -20,6 +21,9 @@ class MetaGenerator
return $meta;
}
+
+ // New feature: separate log entry, unrelated to the bug fix above
+ private function logGeneration(string $sku): void { /* ... */ }
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? n
$ git diff --staged --stat
MetaGenerator.php | 1 +
1 file changed, 1 insertion(+)
8. Der Index während Merge-Konflikten: Stage 1, 2 und 3
Während eines Merge-Konflikts verlässt der Index kurzzeitig sein normales Schema. Statt eines einzigen Eintrags pro Pfad mit Stage 0 enthält er für jeden konfliktbehafteten Pfad bis zu drei Einträge gleichzeitig: Stage 1 für die gemeinsame Vorgängerversion (Merge-Base), Stage 2 für "ours", die Version des aktuellen Branches, und Stage 3 für "theirs", die Version des zu mergenden Branches. Erst wenn ein Konflikt aufgelöst und die Datei erneut mit git add erfasst wird, verschwinden die Stage-1-bis-3-Einträge zugunsten eines einzigen Stage-0-Eintrags.
git ls-files -u macht genau diese drei Versionen sichtbar, jeweils mit eigenem Blob-Hash. Darauf bauen git checkout --ours <datei> und git checkout --theirs <datei> auf: Sie kopieren gezielt den Blob aus Stage 2 beziehungsweise Stage 3 zurück ins Working Directory, ohne den Konflikt im Index selbst aufzulösen. Wer stattdessen den Inhalt manuell zusammenführt, muss am Ende trotzdem git add aufrufen, denn erst das reduziert die drei konkurrierenden Stage-Einträge wieder auf den regulären Stage-0-Zustand.
9. Index-Operationen im direkten Vergleich
Die folgende Tabelle stellt die in diesem Artikel behandelten Index-Operationen nebeneinander: was jeder Befehl liest, was er tatsächlich verändert, und wofür er sich im Entwickleralltag eignet. Sie macht sichtbar, dass die meisten Verwirrungen rund um den Index letztlich eine Frage der Lese- und Schreibrichtung sind.
| Befehl | Was gelesen wird | Was geschrieben wird | Typischer Einsatz |
|---|---|---|---|
| git add <pfad> | Working Directory | Neuer Blob + Index-Eintrag (Stage 0) | Änderungen für den nächsten Commit vormerken |
| git status | Working Directory, Index, HEAD | Nichts | Gesamtüberblick über alle drei Bereiche |
| git diff | Working Directory vs. Index | Nichts | Vorschau, was ein add erfassen würde |
| git diff --staged | Index vs. HEAD | Nichts | Vorschau, was ein commit speichern würde |
| git ls-files -s | Nur der Index | Nichts | Rohdaten aller Index-Einträge inspizieren |
| git update-index --cacheinfo | Vorhandener Blob-Hash | Index-Eintrag direkt | Skriptgesteuerte Index-Manipulation |
| git add -p | Working Directory (hunkweise) | Neuer Teil-Blob + Index-Eintrag | Fokussierte, thematisch saubere Commits bauen |
| git checkout --ours <pfad> | Stage-2-Eintrag im Index | Working Directory | Merge-Konflikt zugunsten der eigenen Version lösen |
Wer diese Tabelle verinnerlicht, erkennt ein wiederkehrendes Muster: Lesende Befehle wie status, diff und ls-files verändern niemals den Index selbst, sie machen nur seinen aktuellen Zustand sichtbar. Schreibende Befehle wie add, add -p und update-index sind dagegen die einzigen Wege, wie sich der Inhalt von .git/index tatsächlich ändert, unabhängig davon, ob die Änderung über das Working Directory, einen Patch oder einen direkt angegebenen Hash erfolgt.
Mironsoft
Git-Workflows, Code-Reviews und CI/CD-Pipelines für PHP- und Magento-Teams
Sauberere Commits durch echtes Index-Verständnis?
Wir helfen Entwicklerteams, fokussierte Commit-Historien, saubere Merge-Konflikt-Resolution und Index-basierte Pre-Commit-Hooks aufzubauen, statt Git-Befehle nach Gefühl einzusetzen.
Git-Internals-Schulung
Index, Objekte und Trees praxisnah für Entwicklerteams erklärt
Workflow-Audit
Bestehende Staging- und Commit-Praxis analysieren und optimieren
CI/CD-Integration
Index-basierte Pre-Commit-Hooks und Linting-Pipelines aufsetzen
10. Zusammenfassung
Der Git-Index ist keine Metapher, sondern eine reale, binäre Datei unter .git/index mit dokumentiertem Format, Prüfsumme und sortierten Einträgen aus Pfad, Modus, Blob-Hash und Stage-Nummer. git add schreibt dabei tatsächlich einen neuen Blob nach .git/objects und aktualisiert den passenden Index-Eintrag inklusive Stat-Cache, lange bevor überhaupt ein Commit existiert. git commit verwandelt diesen Index-Zustand direkt in ein Tree-Objekt, ohne Zwischenschritt und ohne für unveränderte Dateien neue Blobs zu erzeugen.
git status und git diff sind letztlich nur Vergleichsoperationen zwischen Working Directory, Index und HEAD, während git ls-files und git update-index direkten Lese- und Schreibzugriff auf den Index selbst erlauben, unabhängig vom Working Directory. Genau diese Entkopplung macht git add -p möglich und erklärt, warum der Index während eines Merge-Konflikts kurzzeitig bis zu drei Versionen desselben Pfads gleichzeitig verwalten kann, adressiert über die Stage-Nummern 1, 2 und 3.
Der Git-Index auf einen Blick
Index-Format
.git/index ist eine binäre, versionierte Datei mit DIRC-Signatur, Prüfsumme und sortierten Einträgen aus Pfad, Modus, Hash und Stage.
git add
Schreibt einen neuen Blob nach .git/objects und aktualisiert den passenden Index-Eintrag inklusive Stat-Cache.
Inspektion
git ls-files -s/-u/-m/-o und git update-index machen den Index direkt sichtbar und manipulierbar.
Merge-Konflikte
Stage 1 bis 3 speichern Basis, ours und theirs gleichzeitig, bis git add sie auf Stage 0 reduziert.