Während Commits und Branches zu den sichtbaren, konzeptionell greifbaren Teilen von Git gehören, operiert die Staging Area – auch als Index bezeichnet – im Hintergrund. Sie ist die Zwischenschicht zwischen dem Working Directory (wo du Dateien editierst) und dem Repository (wo Commits gespeichert werden). Diese Dreistufenarchitektur ist ein Alleinstellungsmerkmal von Git und ermöglicht präzise Kontrolle darüber, was in einen Commit aufgenommen wird.
Viele Git-Nutzer arbeiten jahrelang mit der Staging Area, ohne ihre
technische Natur zu verstehen. Dieses Kapitel öffnet den Vorhang: Wir
werden den Index direkt inspizieren, seine Binärstruktur untersuchen und
verstehen, wie git add, git commit und
verwandte Befehle diese zentrale Datenstruktur manipulieren.
Git organisiert Daten in drei distinkten Bereichen:
Working Directory: Dein normales Dateisystem. Hier editierst du Dateien mit deinem Editor, Compiler, IDE. Git beobachtet dieses Verzeichnis, manipuliert es aber nicht aktiv (außer bei Checkout-Operationen).
Staging Area (Index): Eine Binärdatei
.git/index, die den vorbereiteten Zustand für den nächsten
Commit enthält. Sie ist ein Snapshot zwischen Working Directory und
Repository – eine Art “Vorschau” auf den kommenden Commit.
Repository: Die Objekt-Datenbank in
.git/objects/, die alle Commits, Trees und Blobs enthält.
Unveränderlich und permanent (solange referenziert).
Diese Architektur erlaubt Workflows, die in Zwei-Ebenen-Systemen unmöglich sind: Du kannst selektiv Teile deiner Änderungen stagen, während andere im Working Directory bleiben. Du kannst den Index mehrfach adjustieren, bevor du committest. Du kannst Änderungen im Index haben, aber zurück zu einem sauberen Working Directory wechseln.
Die Staging Area ist keine abstrakte Konzeption, sondern eine
konkrete Binärdatei: .git/index. Schauen wir sie uns
an.
# Neues Repository
$ mkdir index-demo && cd index-demo
$ git init
# Erste Datei erstellen und stagen
$ echo "Hello World" > file1.txt
$ git add file1.txt
# Index wurde erstellt
$ ls -lh .git/index
-rw-r--r-- 1 user user 104 Jan 1 12:00 .git/indexDie Datei .git/index existiert erst nach dem ersten
git add. Sie ist binär, aber wir können sie mit Git-Tools
inspizieren.
Der primäre Befehl zur Index-Inspektion ist
git ls-files:
# Alle Dateien im Index
$ git ls-files
file1.txt
# Mit detaillierten Informationen
$ git ls-files --stage
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0 file1.txtDie Ausgabe von --stage (auch -s) folgt dem
Format:
<mode> <hash> <stage> <path>
Mode: Unix-Dateiberechtigungen in Oktal (identisch
zum Tree-Format) - 100644: Normale Datei -
100755: Ausführbare Datei - 120000: Symbolic
Link
Hash: Der SHA-1-Hash des Blob-Objekts, das den
Dateiinhalt repräsentiert. Dies ist entscheidend: Der Hash wird
berechnet, sobald git add ausgeführt wird – nicht erst bei
git commit.
Stage: Die Stage-Nummer, relevant bei
Merge-Konflikten: - 0: Normaler Eintrag (keine Konflikte) -
1: Common ancestor (base) bei Merge-Konflikt -
2: “Ours” (current branch) bei Merge-Konflikt -
3: “Theirs” (merging branch) bei Merge-Konflikt
Path: Dateipfad relativ zum Repository-Root.
Wenn wir git add file1.txt ausführen, passieren mehrere
Dinge:
file1.txt
und berechnet den SHA-1-Hash des Inhalts (mit Blob-Header)..git/objects/ erstellt (wenn nicht bereits vorhanden).file1.txt im Index wird aktualisiert oder hinzugefügt.Verifizieren wir dies:
# Hash im Index
$ git ls-files -s
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0 file1.txt
# Dieses Objekt existiert bereits in .git/objects/
$ git cat-file -t 557db03d
blob
$ git cat-file -p 557db03d
Hello World
# Manuell denselben Hash berechnen
$ echo "Hello World" | git hash-object --stdin
557db03de997c86a4a028e1ebd3a1ceb225be238Der Hash stimmt überein! git add hat den Blob erstellt
und den Index aktualisiert – alles vor dem Commit.
Fügen wir mehr Dateien hinzu:
$ echo "File 2 content" > file2.txt
$ mkdir subdir
$ echo "Nested file" > subdir/file3.txt
$ git add file2.txt subdir/file3.txt
$ git ls-files -s
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0 file1.txt
100644 8d1c8b69c3f5a8f5d3e7c2b1a4f6d8e5b7a9c3f2 0 file2.txt
100644 2f5a7c8d9e1b3f4a6c8d0e2b5a7c9f1d3e5b7a9 0 subdir/file3.txtDer Index speichert eine flache Liste aller Dateien, auch aus Unterverzeichnissen. Die hierarchische Struktur (Trees) wird erst beim Commit aus dieser flachen Liste rekonstruiert.
Der Index speichert nicht nur Hashes, sondern auch Metadaten. Wir können diese mit einem erweiterten Format sehen:
$ git ls-files --stage --debug
file1.txt
ctime: 1704110400:0
mtime: 1704110400:0
dev: 16777220 ino: 8605713
uid: 501 gid: 20
size: 12 flags: 0
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0 file1.txtDiese Metadaten umfassen: - ctime/mtime: Creation/Modification timestamps (Unix-Format) - dev/ino: Device und Inode (Dateisystem-Identifikatoren) - uid/gid: User/Group IDs - size: Dateigröße in Bytes - flags: Interne Git-Flags
Git nutzt diese Metadaten für Optimierungen. Wenn
git status prüft, ob Dateien geändert wurden, vergleicht es
zunächst Timestamps und Größe – nur bei Unterschieden wird der Hash neu
berechnet. Dies macht git status bei großen Repositories
schnell.
Die .git/index-Datei hat ein dokumentiertes Binärformat.
Wir können sie mit hexdump oder ähnlichen Tools
inspizieren:
$ hexdump -C .git/index | head -20
00000000 44 49 52 43 00 00 00 02 00 00 00 03 65 9f a4 00 |DIRC........e...|
00000010 00 00 00 00 65 9f a4 00 00 00 00 00 01 00 00 04 |....e...........|
00000020 00 83 63 d1 00 00 81 a4 00 00 01 f5 00 00 00 14 |..c.............|
00000030 00 00 00 0c 55 7d b0 3d e9 97 c8 6a 4a 02 8e 1e |....U}.=...jJ...|
00000040 bd 3a 1c eb 22 5b e2 38 00 09 66 69 6c 65 31 2e |.:.."[.8..file1.|
00000050 74 78 74 00 00 00 00 00 00 00 00 00 65 9f a4 10 |txt.........e...|
...Die Struktur beginnt mit: - Header:
DIRC (DIRectory Cache) – Magic Bytes -
Version: 00 00 00 02 – Version 2 des
Index-Formats - Entry Count: 00 00 00 03 –
3 Einträge im Index
Danach folgen die Einträge, jeder mit: - Timestamps (ctime/mtime) - Device, Inode, Mode, UID, GID, Size - SHA-1 Hash (20 Bytes) - Flags (Entry-Length, Stage-Nummer) - Pfad (null-terminiert, padded auf 8-Byte-Grenze)
Am Ende: Ein SHA-1-Checksum über den gesamten Index-Inhalt (für Integritätsprüfung).
Für normale Nutzung müssen wir diese Binärstruktur nicht verstehen –
git ls-files abstrahiert sie. Aber es ist erhellend zu
sehen, dass die Staging Area eine konkrete, wohldefinierte Datenstruktur
ist.
Die drei Ebenen können unterschiedliche Zustände haben. Ein praktisches Beispiel:
# Initial: Alles committed
$ echo "Version 1" > demo.txt
$ git add demo.txt
$ git commit -m "Initial commit"
# Datei im Working Directory ändern
$ echo "Version 2" > demo.txt
# Zustand prüfen
$ git status
On branch main
Changes not staged for commit:
modified: demo.txt
$ git ls-files -s
100644 b1946ac92492d2347c6235b4d2611184f3f0dc73 0 demo.txt
$ cat demo.txt
Version 2Drei verschiedene Versionen derselben Datei: 1. Repository
(letzter Commit): Hash b1946ac9... → “Version 1”
2. Staging Area (Index): Hash b1946ac9...
→ “Version 1” (noch nicht aktualisiert) 3. Working
Directory: “Version 2”
git status erkennt den Unterschied zwischen Working
Directory und Staging Area. Jetzt stagen wir:
$ git add demo.txt
$ git ls-files -s
100644 0c1e7391ca4e59584f8b773ecdbbb9467eba1547 0 demo.txt
$ git cat-file -p 0c1e7391
Version 2Der Hash im Index hat sich geändert! Jetzt sind Staging Area und Working Directory synchron, aber unterscheiden sich vom Repository:
Nach git commit sind alle drei Ebenen wieder
synchron.
Die Staging Area ermöglicht granulare Kontrolle. Wir können Teile einer Datei stagen, während andere unstaged bleiben.
# Datei mit mehreren Änderungen
$ cat > code.py << 'EOF'
def function_a():
# Bug fix
return corrected_value
def function_b():
# New feature
return new_functionality
def function_c():
# Another bug fix
return another_correction
EOF
$ git add code.py
$ git commit -m "Initial version"
# Jetzt ändern wir mehrere Funktionen
$ cat > code.py << 'EOF'
def function_a():
# Bug fix applied
return fixed_value
def function_b():
# Feature expanded
return enhanced_functionality
def function_c():
# Bug fix applied
return corrected_value
EOFWir haben drei Änderungen, wollen aber nur die Bug-Fixes committen,
nicht das Feature. Mit git add -p (patch mode) können wir
interaktiv stagen:
$ git add -p code.py
diff --git a/code.py b/code.py
index a1b2c3d..e5f6a7b 100644
--- a/code.py
+++ b/code.py
@@ -1,5 +1,5 @@
def function_a():
- # Bug fix
- return corrected_value
+ # Bug fix applied
+ return fixed_value
Stage this hunk [y,n,q,a,d,s,e,?]? y
@@ -6,5 +6,5 @@
def function_b():
- # New feature
- return new_functionality
+ # Feature expanded
+ return enhanced_functionality
Stage this hunk [y,n,q,a,d,s,e,?]? n
@@ -11,5 +11,5 @@
def function_c():
- # Another bug fix
- return another_correction
+ # Bug fix applied
+ return corrected_value
Stage this hunk [y,n,q,a,d,s,e,?]? yWir haben selektiv gestaged: function_a und function_c ja, function_b nein. Der Index enthält jetzt eine Version der Datei, die nicht im Working Directory existiert – eine Mischung aus alten und neuen Änderungen.
# Index-Version (staged)
$ git diff --staged
# Zeigt nur Änderungen in function_a und function_c
# Working Directory vs. Index
$ git diff
# Zeigt nur Änderungen in function_b (unstaged)Diese Fähigkeit ist einzigartig für Git’s Drei-Ebenen-Architektur. Der Index fungiert als editierbarer Puffer.
Wenn ein Merge fehlschlägt, speichert der Index mehrere Versionen der konfliktierenden Dateien. Die Stage-Nummern repräsentieren verschiedene Versionen:
# Simuliere Merge-Konflikt
$ git checkout -b feature
$ echo "Feature change" > conflict.txt
$ git add conflict.txt
$ git commit -m "Feature commit"
$ git checkout main
$ echo "Main change" > conflict.txt
$ git add conflict.txt
$ git commit -m "Main commit"
$ git merge feature
Auto-merging conflict.txt
CONFLICT (content): Merge conflict in conflict.txt
Automatic merge failed; fix conflicts and then commit the result.
# Index-Zustand prüfen
$ git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 conflict.txt
100644 a4e2b1c5d3f6e8a7b9c0d1e2f3a4b5c6d7e8f9a0 2 conflict.txt
100644 b5c3d2e1f4a6c8d7e9b0f1a2c3b4d5e6f7a8b9c0 3 conflict.txtDrei Einträge für dieselbe Datei! - Stage 1: Common ancestor (Base) - Stage 2: Current branch (Ours / main) - Stage 3: Merging branch (Theirs / feature)
Diese drei Versionen ermöglichen fortgeschrittene Merge-Tools, alle
Perspektiven zu zeigen. Nach Konfliktlösung und git add
bleibt nur Stage 0:
# Konflikt manuell lösen
$ echo "Resolved version" > conflict.txt
$ git add conflict.txt
$ git ls-files -s
100644 c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 0 conflict.txtDer Index ist wieder “normal” – bereit für den Merge-Commit.
git diff zeigt unterschiedliche Vergleiche, je nach
Kontext:
# Working Directory vs. Staging Area
$ git diff
# Zeigt unstaged Änderungen
# Staging Area vs. Repository (letzter Commit)
$ git diff --staged
# (Oder --cached, Synonym)
# Zeigt staged Änderungen
# Working Directory vs. Repository
$ git diff HEAD
# Zeigt alle lokalen Änderungen (staged + unstaged)
# Zwischen zwei Commits
$ git diff commit1 commit2
# Vergleicht Repository-ZuständeDiese verschiedenen Perspektiven ergeben sich direkt aus der Drei-Ebenen-Architektur.
Wenn git commit ausgeführt wird, konvertiert Git den
flachen Index in eine hierarchische Tree-Struktur. Dieser Prozess ist
algorithmisch interessant:
# Index mit hierarchischen Pfaden
$ git ls-files -s
100644 hash1 0 README.md
100644 hash2 0 src/main.py
100644 hash3 0 src/utils/helper.py
100644 hash4 0 tests/test_main.pyGit gruppiert Pfade nach Verzeichnis und erstellt Trees:
README.md, Tree für
src/, Tree für tests/main.py, Tree für
utils/helper.pytest_main.pyWir können den Low-Level-Befehl git write-tree nutzen,
um dies zu demonstrieren:
# Index zu Tree konvertieren (ohne Commit)
$ git write-tree
4d5f2e1c3b6a8f7d9e2b5c1a4f8d6e3b7a9c2d1f
# Diesen Tree inspizieren
$ git cat-file -p 4d5f2e1c
100644 blob hash1 README.md
040000 tree hash5 src
040000 tree hash6 tests
# Subtrees inspizieren
$ git cat-file -p hash5 # src/
100644 blob hash2 main.py
040000 tree hash7 utils
$ git cat-file -p hash7 # src/utils/
100644 blob hash3 helper.pyDer Root-Tree 4d5f2e1c ist exakt das, was als
tree-Referenz im Commit-Objekt gespeichert würde.
git commit ist konzeptuell:
# Pseudo-Code von git commit
tree_hash = write_tree_from_index()
parent_hash = get_current_commit()
commit_hash = create_commit_object(tree_hash, parent_hash, message, author, ...)
update_branch_ref(current_branch, commit_hash)Git bietet Plumbing-Befehle zur direkten Index-Manipulation:
# Index leeren (ohne Working Directory zu ändern)
$ git rm --cached -r .
# Entfernt alle Einträge aus Index, Dateien bleiben
# Einzelne Datei aus Index entfernen
$ git rm --cached file.txt
# Direkt in Index schreiben
$ echo "content" | git hash-object -w --stdin
557db03de997c86a4a028e1ebd3a1ceb225be238
$ git update-index --add --cacheinfo 100644 557db03de997c86a4a028e1ebd3a1ceb225be238 newfile.txt
# Index-Eintrag aktualisieren
$ git update-index file.txt
# Index komplett neu aus Tree bauen
$ git read-tree HEADDiese Low-Level-Befehle sind selten nötig, zeigen aber die Flexibilität: Der Index ist manipulierbar, der Tree-Erstellungsprozess ist kontrollierbar.
Das Verständnis des Index hat praktische Konsequenzen:
Selective Staging: Nutze git add -p für
granulare Commits. Jeder Commit sollte eine logische Einheit sein – der
Index ermöglicht dies auch bei komplexen Änderungen.
Schnelles git status: Git’s Nutzung von
Index-Metadaten macht git status schnell, selbst bei
tausenden Dateien. Index-Corruption kann dies verlangsamen
(git update-index --refresh kann helfen).
Staging als Checkpoint: Du kannst Änderungen stagen,
weiter experimentieren, und bei Problemen zu gestagetem Zustand
zurückkehren (git restore --worktree).
CI/CD-Optimierung: Build-Systeme können Index-Diffs nutzen, um nur geänderte Komponenten zu rebuilden. Der Index zeigt exakt, was im nächsten Commit landet.
Merge-Tools: Merge-Konflikt-Resolution ist nur
möglich, weil der Index alle drei Versionen speichert. Tools wie
git mergetool nutzen Stage 1/2/3 für 3-Way-Merges.
Die Staging Area ist mehr als eine Zwischenschicht – sie ist die Schaltzentrale von Git’s Flexibilität. Sie ermöglicht:
Die Binärdatei .git/index ist klein (oft nur Kilobytes),
aber fundamental. Sie ist der Vermittler zwischen der veränderlichen
Welt des Working Directory und der unveränderlichen Historie des
Repositories. Verstehen, wie der Index funktioniert, bedeutet verstehen,
wie Git funktioniert.
Das nächste Mal, wenn du git add ausführst, weißt du:
Git berechnet einen Hash, speichert ein Blob-Objekt, aktualisiert
Metadaten in einer Binärdatei – und bereitet den perfekten Snapshot für
deinen nächsten Commit vor. Diese scheinbar simple Operation ist der
Kern von Git’s Eleganz.