11 Die Staging Area: Gits unsichtbare Zwischenschicht

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.

11.1 Die Drei-Ebenen-Architektur

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.

11.2 Der Index als Datei: Anatomie von .git/index

Die Staging Area ist keine abstrakte Konzeption, sondern eine konkrete Binärdatei: .git/index. Schauen wir sie uns an.

11.2.1 Erstellen eines Beispiel-Repositories

# 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/index

Die Datei .git/index existiert erst nach dem ersten git add. Sie ist binär, aber wir können sie mit Git-Tools inspizieren.

11.2.2 Index-Inspektion mit git ls-files

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.txt

Die 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.

11.2.3 Was git add tatsächlich tut

Wenn wir git add file1.txt ausführen, passieren mehrere Dinge:

  1. Hash-Berechnung: Git liest file1.txt und berechnet den SHA-1-Hash des Inhalts (mit Blob-Header).
  2. Objekt-Speicherung: Ein Blob-Objekt wird in .git/objects/ erstellt (wenn nicht bereits vorhanden).
  3. Index-Update: Der Eintrag für 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
557db03de997c86a4a028e1ebd3a1ceb225be238

Der Hash stimmt überein! git add hat den Blob erstellt und den Index aktualisiert – alles vor dem Commit.

11.2.4 Multiple Dateien im Index

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.txt

Der Index speichert eine flache Liste aller Dateien, auch aus Unterverzeichnissen. Die hierarchische Struktur (Trees) wird erst beim Commit aus dieser flachen Liste rekonstruiert.

11.2.5 Index-Metadaten: Mehr als nur Hashes

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.txt

Diese 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.

11.3 Die Binärstruktur des Index

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.

11.4 Working Directory, Staging Area und Repository im Vergleich

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 2

Drei 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 2

Der 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.

11.5 Partial Staging: Die Macht des Index

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
EOF

Wir 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,?]? y

Wir 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.

11.6 Der Index bei Merge-Konflikten

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.txt

Drei 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.txt

Der Index ist wieder “normal” – bereit für den Merge-Commit.

11.7 git diff: Verschiedene Perspektiven

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ände

Diese verschiedenen Perspektiven ergeben sich direkt aus der Drei-Ebenen-Architektur.

11.8 Von Index zu Tree: Der Commit-Prozess

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.py

Git gruppiert Pfade nach Verzeichnis und erstellt Trees:

  1. Root-Tree: Enthält README.md, Tree für src/, Tree für tests/
  2. src/-Tree: Enthält main.py, Tree für utils/
  3. src/utils/-Tree: Enthält helper.py
  4. tests/-Tree: Enthält test_main.py

Wir 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.py

Der 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)

11.9 Index-Manipulation: Low-Level-Befehle

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 HEAD

Diese Low-Level-Befehle sind selten nötig, zeigen aber die Flexibilität: Der Index ist manipulierbar, der Tree-Erstellungsprozess ist kontrollierbar.

11.10 Praktische Implikationen und Best Practices

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.

11.11 Der Index als Schaltzentrale

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.