Kapitel 1: Git-Interna verstehen – Wie Git wirklich funktioniert

Einleitung: Im Einsteigerkurs hast du gelernt, Git zu benutzen – jetzt ist es Zeit zu verstehen, wie Git unter der Haube funktioniert. Git ist im Kern eine inhaltsadressierte Datenbank, die auf vier fundamentalen Objekttypen basiert: Blobs, Trees, Commits und Tags. Wenn du verstehst, wie diese zusammenhängen, kannst du Git-Probleme schneller diagnostizieren und fortgeschrittene Operationen selbstbewusst durchführen. Du wirst lernen, dass ein Commit nicht einfach „eine Änderung“ ist, sondern ein kompletter Schnappschuss mit Zeiger auf seinen Vorgänger. Dieses Wissen wird dir helfen, Befehle wie rebase, cherry-pick und reflog wirklich zu verstehen, anstatt sie nur auswendig zu lernen.

Die vier fundamentalen Objekttypen in Git 🔍

Git ist im Kern nichts anderes als eine inhaltsadressierte Datenbank – ein elegantes System, das auf nur vier fundamentalen Objekttypen basiert. Wenn du verstehst, wie diese Objekte funktionieren und zusammenhängen, wirst du Git nie wieder als „magische Black Box" wahrnehmen, sondern als das durchdachte, transparente System, das es wirklich ist.


Das Fundament: Gits Objektmodell

Jedes Git-Repository speichert seine gesamte Geschichte in Form von Objekten im Verzeichnis .git/objects/. Jedes Objekt wird durch einen SHA-1-Hash identifiziert – eine 40-stellige hexadezimale Zeichenkette, die aus dem Inhalt des Objekts berechnet wird. Das bedeutet: Gleicher Inhalt ergibt immer den gleichen Hash, egal auf welchem Computer oder in welchem Repository.

flowchart TB
    subgraph Objektdatenbank[".git/objects/"]
        B1["Blob\nfa3c2..."]
        B2["Blob\n8b137..."]
        B3["Blob\ne69de..."]
        T1["Tree\na1b2c..."]
        T2["Tree\nd4e5f..."]
        C1["Commit\n7f8d9..."]
        C2["Commit\n3a4b5..."]
        TAG["Tag\nc6d7e..."]
    end
    
    C1 --> T1
    C2 --> T2
    C2 -.->|parent| C1
    T1 --> B1
    T1 --> B2
    T1 --> T2
    T2 --> B3
    TAG --> C2
    
    style B1 fill:#e8f4e8,color:#000000
    style B2 fill:#e8f4e8,color:#000000
    style B3 fill:#e8f4e8,color:#000000
    style T1 fill:#fff3e0,color:#000000
    style T2 fill:#fff3e0,color:#000000
    style C1 fill:#e3f2fd,color:#000000
    style C2 fill:#e3f2fd,color:#000000
    style TAG fill:#fce4ec,color:#000000

1. Blob – Der Dateiinhalt 📄

Ein Blob (Binary Large Object) ist das einfachste Git-Objekt. Es speichert ausschließlich den Inhalt einer Datei – nichts weiter. Kein Dateiname, keine Berechtigungen, keine Metadaten. Nur der pure Inhalt.

Eigenschaften des Blob-Objekts

Das Letztere ist besonders clever: Wenn du dieselbe Datei in zehn verschiedenen Verzeichnissen hast, speichert Git sie nur einmal als Blob. Alle Trees verweisen dann auf denselben Blob.

Blob mit git cat-file untersuchen

Lass uns das praktisch ausprobieren. Erstelle zunächst ein Test-Repository:

# Neues Repository erstellen
mkdir git-objekte-demo && cd git-objekte-demo
git init

# Eine einfache Datei erstellen
echo "Hallo Git-Welt!" > hallo.txt
git add hallo.txt

Mit git add wurde die Datei in die Staging Area aufgenommen – und dabei wurde ein Blob-Objekt erstellt. Finde den Hash heraus:

# Hash des Blobs anzeigen (aus dem Index/Staging Area)
git ls-files -s

Ausgabe:

100644 d6a96ae3b442218a91512b9e1c57b9578b8998e8 0	hallo.txt

Die Zahl 100644 sind die Dateiberechtigungen, d6a96ae... ist der SHA-1-Hash des Blobs. Jetzt können wir den Blob untersuchen:

# Typ des Objekts anzeigen
git cat-file -t d6a96ae3b442218a91512b9e1c57b9578b8998e8

Ausgabe:

blob
# Inhalt des Blobs anzeigen
git cat-file -p d6a96ae3b442218a91512b9e1c57b9578b8998e8

Ausgabe:

Hallo Git-Welt!
# Größe des Blobs in Bytes
git cat-file -s d6a96ae3b442218a91512b9e1c57b9578b8998e8

Ausgabe:

16

💡 Tipp: Du musst nicht den vollständigen Hash eingeben. Git akzeptiert auch eindeutige Präfixe, z.B. git cat-file -p d6a96ae oder sogar git cat-file -p d6a96.


2. Tree – Das Verzeichnis 🌳

Ein Tree repräsentiert ein Verzeichnis in Git. Er ist quasi das „Inhaltsverzeichnis", das Dateinamen mit Blobs verknüpft. Ein Tree enthält eine Liste von Einträgen, wobei jeder Eintrag aus folgenden Komponenten besteht:

Tree-Struktur visualisiert

flowchart TB
    ROOT["Tree: Wurzelverzeichnis\na1b2c3d..."]
    
    ROOT --> E1["100644 blob fa3c2...\nhallo.txt"]
    ROOT --> E2["100644 blob 8b137...\nREADME.md"]
    ROOT --> E3["040000 tree d4e5f...\nsrc/"]
    
    SRC["Tree: src/\nd4e5f6g..."]
    E3 -.-> SRC
    
    SRC --> E4["100644 blob e69de...\nindex.php"]
    SRC --> E5["100755 blob 12345...\nscript.sh"]
    
    style ROOT fill:#fff3e0,color:#000000
    style SRC fill:#fff3e0,color:#000000
    style E1 fill:#e8f4e8,color:#000000
    style E2 fill:#e8f4e8,color:#000000
    style E3 fill:#fff3e0,color:#000000
    style E4 fill:#e8f4e8,color:#000000
    style E5 fill:#e8f4e8,color:#000000

Tree praktisch untersuchen

Erweitern wir unser Demo-Repository und erstellen einen Commit, damit wir Trees untersuchen können:

# Weitere Dateien und ein Verzeichnis erstellen
echo "# Mein Projekt" > README.md
mkdir src
echo "<?php echo 'Hello World';" > src/index.php
git add .
git commit -m "Erster Commit mit Projektstruktur"

Jetzt können wir den Tree des letzten Commits anschauen:

# Den Tree-Hash des letzten Commits finden
git cat-file -p HEAD

Ausgabe:

tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
author Max Mustermann <max@example.com> 1703847600 +0100
committer Max Mustermann <max@example.com> 1703847600 +0100

Erster Commit mit Projektstruktur

Den Tree-Hash (hier beginnt er mit 4b825dc..., dein Hash wird anders sein) können wir nun untersuchen:

# Tree-Inhalt anzeigen (ersetze den Hash durch deinen)
git cat-file -p 4b825dc642cb6eb9a060e54bf8d69288fbee4904

Ausgabe:

100644 blob d6a96ae3b442218a91512b9e1c57b9578b8998e8	hallo.txt
100644 blob 7e59600739c96546163833214c36459e324bad0a	README.md
040000 tree 8f94139338f9404f26296befa88755fc2598c289	src

Du siehst: Der Tree enthält Verweise auf zwei Blobs (die Dateien) und einen weiteren Tree (das src-Verzeichnis). Lass uns auch den Unter-Tree anschauen:

# Unter-Tree (src/) untersuchen
git cat-file -p 8f94139338f9404f26296befa88755fc2598c289

Ausgabe:

100644 blob 8b137891791fe96927ad78e64b0aad7bded08bdc	index.php

Praktischer Shortcut mit git ls-tree

Für die Untersuchung von Trees gibt es auch den spezialisierten Befehl git ls-tree:

# Tree des aktuellen Commits rekursiv anzeigen
git ls-tree -r HEAD

Ausgabe:

100644 blob d6a96ae3b442218a91512b9e1c57b9578b8998e8	hallo.txt
100644 blob 7e59600739c96546163833214c36459e324bad0a	README.md
100644 blob 8b137891791fe96927ad78e64b0aad7bded08bdc	src/index.php

3. Commit – Der Schnappschuss 📸

Ein Commit ist das Herzstück von Git. Er repräsentiert einen Schnappschuss deines gesamten Projekts zu einem bestimmten Zeitpunkt. Wichtig zu verstehen: Ein Commit speichert nicht die Änderungen (Diffs), sondern verweist auf einen Tree, der den kompletten Zustand aller Dateien beschreibt.

Bestandteile eines Commit-Objekts

Feld Beschreibung
tree SHA-1-Hash des Root-Trees (Projekt-Schnappschuss)
parent SHA-1-Hash des Vorgänger-Commits (kann fehlen beim ersten Commit, oder mehrfach vorhanden sein bei Merge-Commits)
author Name, E-Mail und Zeitstempel der Person, die die Änderung ursprünglich erstellt hat
committer Name, E-Mail und Zeitstempel der Person, die den Commit erstellt hat (kann bei Cherry-Picks/Rebases abweichen)
Leerzeile Trennt Header von der Nachricht
Commit-Message Die Beschreibung der Änderung

Commit praktisch untersuchen

# Einen zweiten Commit erstellen
echo "Neue Zeile" >> hallo.txt
git add hallo.txt
git commit -m "hallo.txt erweitert"

# Die letzten beiden Commits anzeigen
git log --oneline -2

Ausgabe:

a7b3c9d (HEAD -> main) hallo.txt erweitert
f1e2d3c Erster Commit mit Projektstruktur

Jetzt untersuchen wir den neuesten Commit:

git cat-file -p HEAD

Ausgabe:

tree 9f8e7d6c5b4a3210fedcba9876543210abcdef12
parent f1e2d3c4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0
author Max Mustermann <max@example.com> 1703851200 +0100
committer Max Mustermann <max@example.com> 1703851200 +0100

hallo.txt erweitert

Du siehst hier den parent-Eintrag, der auf den vorherigen Commit verweist. Diese Verkettung durch Parent-Referenzen bildet die Commit-Historie – eine einfach verkettete Liste (oder bei Merges: ein gerichteter azyklischer Graph).

Die Commit-Kette visualisiert

flowchart RL
    C3["Commit C3\na7b3c9d\n\nhallo.txt erweitert"]
    C2["Commit C2\nf1e2d3c\n\nErster Commit"]
    C1["Commit C1\n0a1b2c3\n\nInitial commit"]
    
    C3 -->|parent| C2
    C2 -->|parent| C1
    
    HEAD["HEAD"]
    MAIN["main"]
    
    HEAD -.-> MAIN
    MAIN -.-> C3
    
    style C3 fill:#e3f2fd,color:#000000
    style C2 fill:#e3f2fd,color:#000000
    style C1 fill:#e3f2fd,color:#000000
    style HEAD fill:#ffecb3,color:#000000
    style MAIN fill:#c8e6c9,color:#000000

Merge-Commits haben mehrere Parents

Bei einem Merge-Commit gibt es mehrere parent-Einträge:

# Beispiel eines Merge-Commits (nach einem Merge erstellt)
git cat-file -p <merge-commit-hash>

Ausgabe:

tree 1234567890abcdef...
parent aaaaaaa111111...   <-- erster Parent (der Branch, IN den gemerged wurde)
parent bbbbbbb222222...   <-- zweiter Parent (der Branch, DER gemerged wurde)
author Max Mustermann <max@example.com> 1703854800 +0100
committer Max Mustermann <max@example.com> 1703854800 +0100

Merge branch 'feature/login'

4. Tag – Die benannte Markierung 🏷️

Ein Tag ist ein benannter Zeiger auf einen bestimmten Commit – typischerweise verwendet für Release-Versionen wie v1.0.0 oder v2.3.1. Git kennt zwei Arten von Tags:

Lightweight Tags vs. Annotated Tags

Eigenschaft Lightweight Tag Annotated Tag
Speicherung Nur eine Referenz in .git/refs/tags/ Eigenes Tag-Objekt in .git/objects/
Metadaten Keine Tagger, Datum, Nachricht
GPG-Signatur Nicht möglich Möglich
Empfehlung Für temporäre/lokale Markierungen Für Releases und wichtige Meilensteine

Annotated Tag erstellen und untersuchen

# Annotated Tag erstellen
git tag -a v1.0.0 -m "Erste stabile Version"

# Tag-Objekt untersuchen
git cat-file -t v1.0.0

Ausgabe:

tag
git cat-file -p v1.0.0

Ausgabe:

object a7b3c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
type commit
tag v1.0.0
tagger Max Mustermann <max@example.com> 1703858400 +0100

Erste stabile Version

Das Tag-Objekt enthält:

Lightweight Tag zum Vergleich

# Lightweight Tag erstellen
git tag v1.0.0-beta

# Typ prüfen - zeigt direkt auf den Commit!
git cat-file -t v1.0.0-beta

Ausgabe:

commit

Ein Lightweight Tag ist kein eigenes Objekt, sondern nur eine Referenz-Datei. Wenn du git cat-file -p v1.0.0-beta ausführst, siehst du direkt den Commit-Inhalt, nicht ein Tag-Objekt.


Wie alles zusammenhängt 🔗

Jetzt, wo du alle vier Objekttypen kennst, lass uns das große Ganze betrachten. Ein typisches Git-Repository mit zwei Commits könnte intern so aussehen:

flowchart TB
    subgraph Referenzen
        HEAD["HEAD\n→ refs/heads/main"]
        MAIN["refs/heads/main\n→ commit 2"]
        TAG["refs/tags/v1.0.0\n→ tag-objekt"]
    end
    
    subgraph Objekte
        TAGO["Tag-Objekt\nv1.0.0\n→ commit 2"]
        
        C2["Commit 2\ntree: T2\nparent: C1"]
        C1["Commit 1\ntree: T1\nparent: keine"]
        
        T2["Tree T2\nhallo.txt → B2\nREADME.md → B3\nsrc/ → T3"]
        T1["Tree T1\nhallo.txt → B1\nREADME.md → B3"]
        T3["Tree T3\nindex.php → B4"]
        
        B1["Blob B1\nHallo Git-Welt!"]
        B2["Blob B2\nHallo Git-Welt!\nNeue Zeile"]
        B3["Blob B3\n# Mein Projekt"]
        B4["Blob B4\nPHP-Code..."]
    end
    
    HEAD -.-> MAIN
    MAIN -.-> C2
    TAG -.-> TAGO
    TAGO --> C2
    
    C2 --> T2
    C2 -->|parent| C1
    C1 --> T1
    
    T2 --> B2
    T2 --> B3
    T2 --> T3
    T1 --> B1
    T1 --> B3
    T3 --> B4
    
    style HEAD fill:#ffecb3,color:#000000
    style MAIN fill:#c8e6c9,color:#000000
    style TAG fill:#c8e6c9,color:#000000
    style TAGO fill:#fce4ec,color:#000000
    style C1 fill:#e3f2fd,color:#000000
    style C2 fill:#e3f2fd,color:#000000
    style T1 fill:#fff3e0,color:#000000
    style T2 fill:#fff3e0,color:#000000
    style T3 fill:#fff3e0,color:#000000
    style B1 fill:#e8f4e8,color:#000000
    style B2 fill:#e8f4e8,color:#000000
    style B3 fill:#e8f4e8,color:#000000
    style B4 fill:#e8f4e8,color:#000000

Beachte die Effizienz: Die Datei README.md hat sich zwischen Commit 1 und Commit 2 nicht geändert. Daher verweisen beide Trees auf denselben Blob (B3). Git speichert den Inhalt nur einmal!


Die wichtigsten git cat-file-Optionen im Überblick

Option Beschreibung Beispiel
-t Zeigt den Typ des Objekts git cat-file -t HEADcommit
-p Pretty-Print: Zeigt den Inhalt formatiert an git cat-file -p HEAD
-s Zeigt die Größe in Bytes git cat-file -s HEAD
-e Existenz-Check: Exit-Code 0 wenn Objekt existiert git cat-file -e abc123

Nützliche Kombinationen und Shortcuts

# Alle Objekte im Repository auflisten
find .git/objects -type f | head -20

# Typ und Größe aller Objekte eines Commits anzeigen
git rev-list --objects HEAD | while read hash name; do
  echo "$hash $(git cat-file -t $hash) $(git cat-file -s $hash) $name"
done

# Direkter Zugriff auf Tree eines Commits
git cat-file -p HEAD^{tree}

# Direkter Zugriff auf eine Datei in einem Commit
git cat-file -p HEAD:src/index.php

Praktische Übung zum Selbermachen 🎯

Um das Gelernte zu festigen, probiere Folgendes in einem Test-Repository:

  1. Erstelle ein neues Repository mit einigen Dateien und Unterverzeichnissen

  2. Finde die Blob-Hashes deiner Dateien mit git ls-files -s

  3. Untersuche den Tree deines letzten Commits mit git cat-file -p HEAD^{tree}

  4. Vergleiche zwei Commits: Erstelle eine kleine Änderung, committe sie, und vergleiche die Tree-Hashes beider Commits. Welche Blobs haben sich geändert, welche sind gleich geblieben?

  5. Erstelle einen Annotated Tag und untersuche sein Objekt

Mit diesem Wissen über Git-Interna bist du bestens vorbereitet für die fortgeschrittenen Themen wie Rebase, Reflog und Troubleshooting – denn du verstehst jetzt, was Git eigentlich tut, wenn du diese Befehle ausführst! 🚀

Git als inhaltsadressierte Datenbank 🔐

Git unterscheidet sich fundamental von anderen Versionskontrollsystemen durch seine Architektur als inhaltsadressierte Datenbank (content-addressable storage). Dieses Konzept ist der Schlüssel, um zu verstehen, warum Git so effizient, zuverlässig und schnell ist – und warum bestimmte Operationen überhaupt möglich sind.


Was „inhaltsadressiert" bedeutet

Bei einer inhaltsadressierten Datenbank wird der Speicherort eines Objekts nicht durch einen Namen, einen Pfad oder eine fortlaufende Nummer bestimmt, sondern direkt durch den Inhalt selbst. Genauer gesagt: Git berechnet aus dem Inhalt einer Datei (oder eines anderen Objekts) einen eindeutigen kryptografischen Hash und verwendet diesen Hash als „Adresse" oder Identifikator.

Das steht im Gegensatz zu herkömmlichen Dateisystemen:

Eigenschaft Klassisches Dateisystem Git (inhaltsadressiert)
Identifikation Pfad und Dateiname Hash des Inhalts
Umbenennung Ändert den Identifikator Hash bleibt gleich
Gleicher Inhalt, zwei Orte Zwei separate Speicherungen Nur einmal gespeichert
Inhalt geändert Gleicher Name, neuer Inhalt Neuer Hash = neues Objekt

Der entscheidende Gedanke ist: Der Inhalt ist die Adresse. Wenn du den Inhalt kennst, kennst du auch, wo und wie er gespeichert ist. Und umgekehrt: Wenn du den Hash hast, weißt du exakt, welcher Inhalt dahintersteckt.


Das SHA-1-Hash-System im Detail

Was ist ein Hash?

Ein Hash (auch „Prüfsumme" oder „Fingerabdruck") ist das Ergebnis einer mathematischen Funktion, die eine beliebig große Eingabe auf eine Ausgabe fester Länge abbildet. Git verwendet dafür den SHA-1-Algorithmus (Secure Hash Algorithm 1), der einen 160-Bit-Hash erzeugt, dargestellt als 40-stellige Hexadezimalzahl.

Beispiel eines Git-Hash:

a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

Eigenschaften von SHA-1

SHA-1 hat mehrere wichtige Eigenschaften, die es für Git ideal machen:

  1. Deterministisch: Derselbe Inhalt erzeugt immer denselben Hash – auf jedem Computer, zu jeder Zeit, in jedem Repository weltweit.

  2. Kollisionsresistent: Es ist praktisch unmöglich, zwei verschiedene Inhalte zu finden, die denselben Hash erzeugen. Bei 160 Bit gibt es $2^{160}$ mögliche Hashes, das sind etwa $1{,}46 \times 10^{48}$ verschiedene Werte.

  3. Einweg-Funktion: Aus dem Hash kann man den ursprünglichen Inhalt nicht rekonstruieren. Der Hash verrät nichts über den Inhalt außer seiner Identität.

  4. Lawineneffekt: Selbst kleinste Änderungen am Inhalt führen zu einem völlig anderen Hash:

    Inhalt: "Hello World"
    Hash:   0a4d55a8d778e5022fab701977c5d840bbc486d0
    
    Inhalt: "Hello World!" (nur ein Ausrufezeichen hinzugefügt)
    Hash:   2ef7bde608ce5404e97d5f042f95f89f1c232871
    

Wie Git den Hash berechnet

Git berechnet den Hash nicht einfach nur vom reinen Dateiinhalt, sondern fügt einen Header hinzu, der den Objekttyp und die Größe enthält:

<typ> <größe>\0<inhalt>

Für eine Datei mit dem Inhalt „Hello World" (11 Bytes) würde Git also hashen:

blob 11\0Hello World

Du kannst das selbst überprüfen:

# Manuell den Hash berechnen
echo -n "Hello World" | git hash-object --stdin
# Ergebnis: 5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689

# Zum Vergleich mit Unix-Tools
printf "blob 11\0Hello World" | sha1sum
# Gleiches Ergebnis!

Der Übergang zu SHA-256

Git arbeitet seit einiger Zeit an einer Migration zu SHA-256, da SHA-1 theoretische Schwächen zeigt (2017 wurde die erste praktische Kollision demonstriert). Für die allermeisten Anwendungsfälle ist SHA-1 aber weiterhin sicher, und das Konzept der Inhaltsadressierung bleibt identisch.


Warum identische Dateien denselben Hash haben

Hier liegt die Magie des Systems: Da der Hash ausschließlich vom Inhalt abhängt, erzeugen identische Inhalte garantiert identische Hashes – unabhängig davon:

Ein konkretes Beispiel

Stell dir vor, du und ein Kollege arbeiten an verschiedenen Projekten auf verschiedenen Kontinenten, ohne voneinander zu wissen:

# Dein Repository in Berlin
projekt-alpha/
└── src/
    └── utils.php    # Inhalt: "<?php echo 'Hello';"

# Repository deines Kollegen in Tokyo
projekt-beta/
└── lib/
    └── helper.php   # Inhalt: "<?php echo 'Hello';"

Obwohl die Dateien unterschiedliche Namen haben und in komplett unverbundenen Projekten liegen, haben sie exakt denselben Git-Hash, weil ihr Inhalt identisch ist.

Du kannst das selbst testen:

# In deinem Repository
echo "<?php echo 'Hello';" > test1.php
git hash-object test1.php
# Ergebnis: z.B. 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

# In einem komplett anderen Repository auf einem anderen Computer
echo "<?php echo 'Hello';" > andere_datei.php
git hash-object andere_datei.php
# Gleiches Ergebnis: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

Was nicht in den Hash einfließt

Es ist wichtig zu verstehen, was alles keinen Einfluss auf den Hash einer Datei hat:

Nur der reine Dateiinhalt (Byte für Byte) bestimmt den Hash.


Praktische Auswirkungen

Die Inhaltsadressierung hat weitreichende Konsequenzen für die tägliche Arbeit mit Git:

1. Automatische Deduplizierung 💾

Git speichert identischen Inhalt nur einmal, egal wie oft er vorkommt. Wenn du dieselbe Datei in 100 verschiedenen Commits hast und sie sich nie ändert, existiert sie physisch nur einmal im .git/objects-Verzeichnis.

# Beispiel: Eine Datei in 100 Commits
# Naiver Ansatz: 100 × 10 KB = 1 MB Speicher
# Git: 1 × 10 KB = 10 KB Speicher

Das gilt auch für Dateien, die kopiert oder umbenannt werden: Solange der Inhalt gleich bleibt, wird nichts doppelt gespeichert.

2. Extrem effizientes Klonen und Fetching 🚀

Wenn du ein Repository klonst oder Änderungen fetchst, muss Git nur die Objekte übertragen, die du noch nicht hast. Durch den Hash kann Git sofort feststellen: „Dieses Objekt habe ich schon" – ohne den Inhalt vergleichen zu müssen.

# Beim git fetch:
# Dein lokales Repo: "Ich habe Objekte a1b2c3, d4e5f6, g7h8i9"
# Remote antwortet: "Der neue Commit braucht a1b2c3, x9y8z7"
# Git: "a1b2c3 habe ich schon, nur x9y8z7 muss übertragen werden"

3. Integrität und Datenvalidierung ✅

Der Hash dient als kryptografische Prüfsumme. Wenn auch nur ein einziges Bit in einem Objekt verändert wird (durch Festplattenfehler, Übertragungsprobleme oder absichtliche Manipulation), stimmt der Hash nicht mehr. Git kann das sofort erkennen:

# Git prüft automatisch bei vielen Operationen
# Du kannst es auch manuell auslösen:
git fsck
# Überprüft die Integrität aller Objekte

# Mögliche Ausgabe bei Problemen:
# error: sha1 mismatch for objects/a1/b2c3...
# fatal: loose object a1b2c3... is corrupt

Diese Eigenschaft macht Git extrem robust gegen:

4. Verteilte Zusammenarbeit ohne zentrale Autorität 🌍

Da Hashes deterministisch sind, braucht Git keine zentrale Instanz, die Objekte nummeriert oder IDs vergibt. Zwei Entwickler können unabhängig voneinander arbeiten, und wenn sie später ihre Repositories zusammenführen, sind identische Objekte automatisch „dieselben" – ohne Konflikte.

flowchart TD
    subgraph Alice["Alice's Repository"]
        A1[("Commit abc123")]
        A2[("Blob def456")]
    end
    
    subgraph Bob["Bob's Repository"]
        B1[("Commit xyz789")]
        B2[("Blob def456")]
    end
    
    subgraph Merged["Nach dem Merge"]
        M1[("Commit abc123")]
        M2[("Commit xyz789")]
        M3[("Blob def456\nnur einmal!")]
    end
    
    Alice --> Merged
    Bob --> Merged
    
    style A2 fill:#90EE90,color:#000000
    style B2 fill:#90EE90,color:#000000
    style M3 fill:#90EE90,color:#000000

5. Sichere Referenzierung von History 🔒

Da jeder Commit einen Hash hat, der von seinem Inhalt und von seinen Parent-Commits abhängt, ist die gesamte Git-History kryptografisch verkettet – ähnlich wie eine Blockchain:

flowchart LR
    C1["Commit A\nhash: a1b2c3"]
    C2["Commit B\nhash: d4e5f6\nparent: a1b2c3"]
    C3["Commit C\nhash: g7h8i9\nparent: d4e5f6"]
    
    C1 --> C2 --> C3

Wenn jemand versucht, einen alten Commit zu verändern, ändert sich sein Hash. Da der nächste Commit auf diesen Hash verweist, müsste auch dieser Commit neu gehasht werden – und so weiter bis zum aktuellen Commit. Eine nachträgliche Manipulation der History ist sofort erkennbar.

6. Commits sind global eindeutig 🆔

Die Wahrscheinlichkeit einer Hash-Kollision (dass zwei verschiedene Inhalte denselben Hash erzeugen) ist astronomisch gering:

$$
P(\text{Kollision}) \approx \frac{n^2}{2 \times 2^{160}}
$$

Selbst bei einer Milliarde Objekte ($n = 10^9$) ist die Wahrscheinlichkeit etwa:

$$
P \approx \frac{(10^9)^2}{2 \times 2^{160}} \approx 3{,}4 \times 10^{-31}
$$

Das bedeutet: Commit-Hashes sind praktisch weltweit eindeutig. Du kannst einen Hash wie a1b2c3d4 als absoluten Identifikator verwenden, der niemals mit einem anderen Commit kollidieren wird.

7. Effizientes Branching und Tagging 🏷️

Branches und Tags in Git sind extrem „billig", weil sie nur 40-Byte-Zeiger auf einen Commit-Hash sind:

# Ein Branch ist nur eine Datei mit einem Hash
cat .git/refs/heads/main
# Ausgabe: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

# Ein Tag ist ähnlich simpel
cat .git/refs/tags/v1.0.0
# Ausgabe: f1e2d3c4b5a6978... (ein Hash)

Das Erstellen eines neuen Branches ist daher eine O(1)-Operation – es wird nur eine 40-Byte-Datei geschrieben, egal wie groß das Repository ist.

8. Rename Detection funktioniert automatisch 📁

Git speichert keine „Rename"-Operationen explizit. Wenn du eine Datei umbenennst, erkennt Git das beim Betrachten der History automatisch, weil der Blob-Hash identisch bleibt:

# Datei umbenennen
mv old_name.php new_name.php
git add -A
git commit -m "Rename file"

# Git erkennt das Rename anhand des identischen Hashes
git log --follow --stat new_name.php
# Zeigt die komplette History, auch vor dem Rename!

Praktische Demonstration 🧪

Lass uns das Konzept in der Praxis erleben:

Schritt 1: Zwei Dateien mit gleichem Inhalt erstellen

# Neues Test-Repository
mkdir hash-demo && cd hash-demo
git init

# Zwei Dateien mit identischem Inhalt, aber verschiedenen Namen
echo "Identischer Inhalt" > datei1.txt
echo "Identischer Inhalt" > datei2.txt

# Hashes anzeigen (ohne zu committen)
git hash-object datei1.txt
# Ausgabe: z.B. 9c59e24b8b0c4c2a8d3e5f6a7b8c9d0e1f2a3b4c

git hash-object datei2.txt
# Ausgabe: 9c59e24b8b0c4c2a8d3e5f6a7b8c9d0e1f2a3b4c  ← Identisch!

Schritt 2: Änderung demonstrieren

# Winzige Änderung an datei2
echo "Identischer Inhalt!" > datei2.txt  # Ausrufezeichen hinzugefügt

git hash-object datei2.txt
# Ausgabe: 7f8e9d0c1b2a3456...  ← Komplett anderer Hash!

Schritt 3: Objekte im Repository inspizieren

# Dateien zum Staging hinzufügen
git add datei1.txt
git add datei2.txt
git commit -m "Initial commit"

# Blobs im Repository anschauen
find .git/objects -type f
# Zeigt die gespeicherten Objekte

# Einen Blob inspizieren
git cat-file -t 9c59e24  # Typ anzeigen → "blob"
git cat-file -p 9c59e24  # Inhalt anzeigen → "Identischer Inhalt"

Zusammenfassung 📋

Die Inhaltsadressierung ist das architektonische Fundament von Git:

Aspekt Bedeutung
Identität Inhalt = Adresse (Hash)
Effizienz Automatische Deduplizierung
Integrität Kryptografische Prüfsummen
Verteilung Keine zentrale ID-Vergabe nötig
Performance Schnelles Vergleichen via Hash
Zuverlässigkeit Manipulation sofort erkennbar

Wenn du dieses Konzept verinnerlicht hast, werden viele Git-Mechanismen plötzlich logisch: Warum Branches so billig sind, warum git gc Objekte aufräumen kann, warum Rebasing die History „umschreibt" und warum das Ändern alter Commits problematisch ist. Der Hash ist der Schlüssel zu allem. 🔑

Git-Referenzen, Branches und HEAD – Was wirklich passiert 🔍

Um Git wirklich zu verstehen, musst du begreifen, dass Branches keine Kopien deines Codes sind – sie sind lediglich winzige Textdateien, die auf einen Commit zeigen. Dieses Kapitel enthüllt die elegante Einfachheit, die hinter Gits scheinbar komplexem Branching-System steckt.


Das Konzept der Referenzen (refs)

Eine Referenz (kurz: ref) ist in Git nichts anderes als ein lesbarer Name für einen SHA-1-Hash. Anstatt dir kryptische Hashes wie a1b2c3d4e5f6... merken zu müssen, kannst du einfach main, feature/login oder v1.0.0 verwenden.

Was refs technisch sind

Jede Referenz ist eine simple Textdatei, die genau 40 Zeichen enthält – den SHA-1-Hash eines Commits. Das ist alles. Keine Magie, keine komplexe Datenstruktur, nur eine Zeile Text.

a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4

Die verschiedenen Arten von Referenzen

Git kennt mehrere Kategorien von Referenzen, die alle im .git/-Verzeichnis organisiert sind:

Referenz-Typ Speicherort Beispiel Zweck
Lokale Branches .git/refs/heads/ refs/heads/main Deine lokalen Entwicklungszweige
Remote-Tracking-Branches .git/refs/remotes/ refs/remotes/origin/main Abbild der Branches auf dem Remote
Tags .git/refs/tags/ refs/tags/v1.0.0 Benannte Marker für Releases
HEAD .git/HEAD Zeigt auf den aktuellen Branch
Stash .git/refs/stash Zwischengespeicherte Änderungen

Die Anatomie des .git/refs/-Verzeichnisses

Schauen wir uns an, wie ein typisches Repository strukturiert ist:

.git/
├── HEAD                    ← Zeigt auf aktuellen Branch
├── refs/
│   ├── heads/              ← Alle lokalen Branches
│   │   ├── main            ← Der main-Branch (eine Textdatei!)
│   │   ├── develop         ← Der develop-Branch
│   │   └── feature/
│   │       └── login       ← Feature-Branch mit Unterordner
│   ├── remotes/
│   │   └── origin/         ← Remote-Tracking-Branches
│   │       ├── main
│   │       └── develop
│   └── tags/
│       ├── v1.0.0          ← Lightweight Tag
│       └── v2.0.0          ← Annotated Tag (zeigt auf Tag-Objekt)
└── packed-refs             ← Optimierte Speicherung vieler Refs

Praktische Untersuchung in deinem Repository

Du kannst dir die Refs direkt im Dateisystem ansehen:

# Zeige den Inhalt des main-Branch (nur der Hash!)
cat .git/refs/heads/main
# Ausgabe: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4

# Liste alle lokalen Branches als Dateien
ls -la .git/refs/heads/

# Zeige alle Referenzen mit git
git show-ref

Die packed-refs-Datei

Bei Repositories mit vielen Branches und Tags optimiert Git die Speicherung, indem es Referenzen in einer einzigen Datei zusammenfasst:

cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 refs/heads/main
b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5 refs/heads/develop
c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6 refs/tags/v1.0.0

Git sucht Referenzen zuerst in den einzelnen Dateien unter refs/ und dann in packed-refs. Wenn du einen Branch aktualisierst, wird eine neue Einzeldatei erstellt, die Vorrang vor dem Eintrag in packed-refs hat.


Branches – Nichts als bewegliche Zeiger

Ein Branch ist eine Referenz, die automatisch mitwandert, wenn du neue Commits erstellst. Das unterscheidet Branches von Tags, die fest an einem Commit verankert bleiben.

Was einen Branch ausmacht

gitGraph
    commit id: "C1"
    commit id: "C2"
    branch feature/login
    commit id: "C3"
    commit id: "C4"
    checkout main
    commit id: "C5"

In diesem Diagramm sind main und feature/login keine Kopien des Codes – sie sind lediglich zwei 40-Byte-Dateien, die auf verschiedene Commits zeigen:

Was passiert, wenn du einen Branch erstellst?

Wenn du git branch feature/login ausführst, passiert erstaunlich wenig:

  1. Git ermittelt den aktuellen Commit – den Hash, auf den HEAD (über den aktuellen Branch) zeigt
  2. Git erstellt eine neue Datei unter .git/refs/heads/feature/login
  3. Git schreibt den Hash in diese Datei

Das ist alles! Kein Kopieren von Dateien, kein Duplizieren von Historie. Ein Branch zu erstellen ist deshalb in Git instantan und kostet praktisch keinen Speicherplatz – egal wie groß dein Repository ist.

# Diese beiden Befehle sind funktional identisch:

# Der "offizielle" Weg:
git branch feature/login

# Was Git intern macht (vereinfacht):
git rev-parse HEAD > .git/refs/heads/feature/login

Demonstration: Branch manuell erstellen

Du kannst tatsächlich einen Branch „von Hand" erstellen:

# Aktuellen Commit-Hash ermitteln
git rev-parse HEAD
# Ausgabe: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4

# Branch manuell erstellen (nicht empfohlen, aber lehrreich!)
echo "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" > .git/refs/heads/mein-manueller-branch

# Überprüfen, ob es funktioniert hat
git branch
# Ausgabe enthält jetzt: mein-manueller-branch

HEAD – Der spezielle Zeiger auf „Wo bin ich jetzt?"

HEAD ist die wichtigste Referenz in Git. Sie beantwortet die Frage: „In welchem Branch arbeite ich gerade, und welchen Commit sehe ich in meinem Working Directory?"

HEAD ist (meistens) eine symbolische Referenz

Im Normalfall enthält .git/HEAD keinen direkten Commit-Hash, sondern einen Verweis auf einen Branch:

cat .git/HEAD
# Ausgabe: ref: refs/heads/main

Das bedeutet: „HEAD zeigt auf den Branch main, und main zeigt auf einen bestimmten Commit." Diese Indirektion ist wichtig, denn sie ermöglicht es Git, den Branch automatisch mitzubewegen, wenn du einen neuen Commit erstellst.

Die HEAD-Kette visualisiert

flowchart LR
    HEAD["HEAD\n.git/HEAD"]
    main["main\n.git/refs/heads/main"]
    commit["Commit\na1b2c3d4..."]
    
    HEAD -->|"ref: refs/heads/main"| main
    main -->|"a1b2c3d4..."| commit
    
    style HEAD fill:#e1f5fe,color:#000000
    style main fill:#fff3e0,color:#000000
    style commit fill:#f3e5f5,color:#000000

Detached HEAD – Wenn HEAD direkt auf einen Commit zeigt

Manchmal zeigt HEAD direkt auf einen Commit statt auf einen Branch. Das nennt man „Detached HEAD":

# Einen bestimmten Commit auschecken (nicht einen Branch)
git checkout a1b2c3d4

cat .git/HEAD
# Ausgabe: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4

Im Detached HEAD-Zustand zeigt HEAD nicht mehr auf ref: refs/heads/..., sondern enthält direkt den Hash. Neue Commits, die du jetzt machst, gehören zu keinem Branch und können verloren gehen, wenn du den Branch wechselst!

flowchart TB
    subgraph normal["Normaler Zustand"]
        HEAD1["HEAD"] --> main1["main"] --> C1["Commit C1"]
    end
    
    subgraph detached["Detached HEAD"]
        HEAD2["HEAD"] --> C2["Commit C2"]
        main2["main"] --> C3["Commit C3"]
    end
    
    style HEAD1 fill:#c8e6c9,color:#000000
    style HEAD2 fill:#ffcdd2,color:#000000
    style main1 fill:#fff3e0,color:#000000
    style main2 fill:#fff3e0,color:#000000

Was passiert beim Branch-Wechsel? 🔄

Wenn du git checkout feature/login oder git switch feature/login ausführst, passieren mehrere Dinge:

Schritt 1: Git aktualisiert HEAD

# Vorher:
cat .git/HEAD
# ref: refs/heads/main

# Nach git switch feature/login:
cat .git/HEAD
# ref: refs/heads/feature/login

Schritt 2: Git ermittelt den Ziel-Commit

Git liest den Hash aus .git/refs/heads/feature/login und weiß jetzt, welchen Zustand des Projekts es herstellen muss.

Schritt 3: Git aktualisiert das Working Directory

Git vergleicht den aktuellen Commit mit dem Ziel-Commit und:

Schritt 4: Git aktualisiert den Index (Staging Area)

Der Index wird aktualisiert, um den Zustand des Ziel-Commits widerzuspiegeln.

Visualisierung des gesamten Prozesses

flowchart TB
    subgraph vorher["Vor dem Wechsel"]
        HEAD_v["HEAD\nref: refs/heads/main"]
        main_v["main\na1b2c3d4"]
        feature_v["feature/login\nb2c3d4e5"]
        WD_v["Working Directory\nZustand von main"]
    end
    
    subgraph nachher["Nach git switch feature/login"]
        HEAD_n["HEAD\nref: refs/heads/feature/login"]
        main_n["main\na1b2c3d4"]
        feature_n["feature/login\nb2c3d4e5"]
        WD_n["Working Directory\nZustand von feature/login"]
    end
    
    vorher --> |"git switch feature/login"| nachher
    
    style HEAD_v fill:#fff3e0,color:#000000
    style HEAD_n fill:#c8e6c9,color:#000000
    style WD_v fill:#e3f2fd,color:#000000
    style WD_n fill:#e8f5e9,color:#000000

Warum Branch-Wechsel manchmal fehlschlägt

Git verhindert den Branch-Wechsel, wenn du uncommittete Änderungen hast, die beim Wechsel überschrieben würden:

git switch feature/login
# error: Your local changes to the following files would be overwritten by checkout:
#     src/Login.php
# Please commit your changes or stash them before you switch branches.

Dieses Verhalten schützt dich davor, Arbeit zu verlieren.


Was passiert beim Erstellen eines neuen Commits?

Wenn du git commit ausführst, passiert Folgendes mit den Referenzen:

  1. Git erstellt ein neues Commit-Objekt mit:

    • Tree-Objekt (Snapshot aller Dateien)
    • Parent-Verweis (der vorherige Commit)
    • Autor, Committer, Nachricht, Zeitstempel
  2. Git ermittelt den aktuellen Branch durch Lesen von HEAD

  3. Git aktualisiert die Branch-Referenz – die Datei unter refs/heads/ wird mit dem neuen Commit-Hash überschrieben

flowchart TB
    subgraph vorher["Vor dem Commit"]
        H1["HEAD\nref: refs/heads/main"]
        M1["main\na1b2c3d4"]
        C1["Commit\na1b2c3d4"]
    end
    
    subgraph nachher["Nach dem Commit"]
        H2["HEAD\nref: refs/heads/main"]
        M2["main\nx9y8z7w6"]
        C2["Neuer Commit\nx9y8z7w6"]
        C2_parent["Alter Commit\na1b2c3d4"]
        C2 -->|parent| C2_parent
    end
    
    vorher -->|"git commit"| nachher
    
    style C2 fill:#c8e6c9,color:#000000
    style M2 fill:#c8e6c9,color:#000000

Wichtig: HEAD selbst ändert sich nicht – es zeigt weiterhin auf refs/heads/main. Aber die Datei refs/heads/main enthält jetzt einen neuen Hash.


Alles zusammen in PhpStorm 🛠️

PhpStorm macht die Arbeit mit Branches komfortabel, aber es hilft zu wissen, was dahinter passiert:

Branches anzeigen und verstehen

  1. Klicke unten rechts in der Statusleiste auf den Branch-Namen (z.B. „main")
  2. Du siehst eine Liste aller lokalen und Remote-Branches
  3. Jeder Eintrag entspricht einer Datei in .git/refs/

Branch erstellen in PhpStorm

  1. Rechtsklick auf einen Commit im Git-Log → New Branch...
  2. Oder: Branch-Menü → + New Branch
  3. PhpStorm erstellt die entsprechende Datei in .git/refs/heads/

HEAD und aktuellen Branch sehen

Refs direkt untersuchen

Du kannst das Terminal in PhpStorm (Alt+F12) nutzen, um die Ref-Dateien direkt zu untersuchen:

# In PhpStorms Terminal:
cat .git/HEAD
cat .git/refs/heads/main
git show-ref --heads

Zusammenfassung der wichtigsten Konzepte

Konzept Was es ist Wo gespeichert Verhalten
Referenz (ref) Lesbarer Name für einen Commit-Hash .git/refs/ Basis für Branches und Tags
Branch Beweglicher Zeiger auf einen Commit .git/refs/heads/<name> Wandert mit bei neuen Commits
Remote-Tracking-Branch Abbild eines Remote-Branches .git/refs/remotes/<remote>/<name> Wird bei fetch/pull aktualisiert
Tag Fester Zeiger auf einen Commit .git/refs/tags/<name> Bleibt immer am selben Commit
HEAD Zeiger auf aktuellen Branch .git/HEAD Bestimmt „wo bin ich jetzt"
Detached HEAD HEAD zeigt direkt auf Commit .git/HEAD (enthält Hash) Gefährlich – neue Commits können verloren gehen

Praktische Übung 📝

Probiere folgende Befehle in einem Test-Repository aus, um das Gelernte zu festigen:

# 1. Untersuche dein aktuelles Repository
cat .git/HEAD
ls -la .git/refs/heads/
cat .git/refs/heads/main

# 2. Erstelle einen neuen Branch und beobachte die Änderungen
git branch test-branch
ls -la .git/refs/heads/
cat .git/refs/heads/test-branch

# 3. Wechsle den Branch und beobachte HEAD
git switch test-branch
cat .git/HEAD

# 4. Mache einen Commit und beobachte die Branch-Referenz
echo "test" > test.txt
git add test.txt
git commit -m "Test commit"
cat .git/refs/heads/test-branch  # Neuer Hash!

# 5. Aufräumen
git switch main
git branch -d test-branch

Wenn du diese Übung durchgeführt hast, wirst du Gits Referenz-System nie wieder als mysteriös empfinden! 🎯

Das Reflog – deine Lebensversicherung bei Git-Unfällen 🛟

Das Reflog (Reference Log) ist eines der mächtigsten, aber am wenigsten bekannten Features von Git. Es ist buchstäblich dein Sicherheitsnetz, das dich vor fast jedem Git-Unfall retten kann – selbst wenn du denkst, du hättest Commits unwiederbringlich verloren.


Was genau ist das Reflog?

Das Reflog ist ein lokales Protokoll aller Bewegungen von Referenzen (insbesondere HEAD) in deinem Repository. Jedes Mal, wenn sich HEAD bewegt – sei es durch einen Commit, Checkout, Reset, Rebase, Merge oder irgendeinen anderen Befehl – wird ein Eintrag im Reflog erstellt.

Der entscheidende Unterschied zur normalen Historie

Die normale Git-Historie (die du mit git log siehst) zeigt dir nur die Commits, die von deinem aktuellen Branch aus erreichbar sind. Das Reflog hingegen protokolliert jede Zustandsänderung, unabhängig davon, ob die entsprechenden Commits noch von einem Branch referenziert werden oder nicht.

Stell dir das Reflog wie eine Zeitmaschine vor: Es merkt sich nicht nur, wo du jetzt bist, sondern auch jeden Ort, an dem du jemals warst – selbst wenn der Weg dorthin inzwischen „verschwunden" zu sein scheint.

Was wird im Reflog gespeichert?

Jeder Reflog-Eintrag enthält folgende Informationen:


Warum ist das Reflog deine „Lebensversicherung"? 🆘

In Git gibt es viele Situationen, in denen du denkst, du hättest Daten verloren:

  1. Harter Reset: Du führst git reset --hard HEAD~5 aus und merkst, dass du die falschen Commits gelöscht hast
  2. Fehlgeschlagener Rebase: Ein Interactive Rebase geht schief und deine Commits scheinen verschwunden
  3. Branch gelöscht: Du löschst versehentlich einen Branch, der noch nicht gemergt war
  4. Falscher Checkout: Du wechselst den Branch und hast vergessen, Änderungen zu committen
  5. Amend-Unfall: Du machst git commit --amend und überschreibst versehentlich einen wichtigen Commit

In all diesen Fällen gilt: Die Commits sind nicht wirklich weg! Sie sind nur nicht mehr von einem Branch aus erreichbar. Das Reflog hat sie jedoch noch protokolliert und kennt ihre SHA-1-Hashes.

Die 90-Tage-Regel

Git behält Reflog-Einträge standardmäßig 90 Tage lang (für erreichbare Commits) bzw. 30 Tage (für nicht mehr erreichbare Commits). Erst danach werden sie bei einer Garbage Collection (git gc) endgültig entfernt. Du hast also ein großzügiges Zeitfenster, um Fehler zu korrigieren.


Das Reflog in der Praxis – Kommandozeile

Grundlegender Befehl

git reflog

Dieser Befehl zeigt dir die letzten Bewegungen von HEAD. Die Ausgabe sieht etwa so aus:

a1b2c3d (HEAD -> feature/login) HEAD@{0}: commit: Implementiere Login-Validierung
e4f5g6h HEAD@{1}: commit: Füge Login-Formular hinzu
i7j8k9l HEAD@{2}: checkout: moving from main to feature/login
m0n1o2p (main) HEAD@{3}: reset: moving to HEAD~3
q3r4s5t HEAD@{4}: commit: Wichtiger Commit den ich verloren glaubte
u6v7w8x HEAD@{5}: commit: Noch ein wichtiger Commit
y9z0a1b HEAD@{6}: commit: Der erste wichtige Commit

Die Reflog-Syntax verstehen

Die Notation HEAD@{n} ist eine spezielle Referenz-Syntax:

Du kannst diese Notation auch mit Zeitangaben verwenden:

git reflog HEAD@{1.hour.ago}
git reflog HEAD@{yesterday}
git reflog HEAD@{2024-01-15}

Detaillierte Reflog-Ansicht

Für mehr Details kannst du Optionen hinzufügen:

# Mit vollständigen Commit-Informationen
git reflog show --all

# Mit Datum und Uhrzeit
git reflog --date=iso

# Nur für einen bestimmten Branch
git reflog show feature/login

Praktische Rettungsszenarien mit dem Reflog

Szenario 1: Commits nach hartem Reset wiederherstellen

Du hast versehentlich einen harten Reset durchgeführt und wichtige Commits „verloren":

# Der fatale Befehl, der alles kaputt gemacht hat
git reset --hard HEAD~5

# Panik! Aber Ruhe bewahren – Reflog anschauen
git reflog

Die Ausgabe zeigt dir:

a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~5
f8e9d0c HEAD@{1}: commit: Letzter wichtiger Commit
b2c3d4e HEAD@{2}: commit: Vorletzter wichtiger Commit
...

Jetzt kannst du zu dem Commit vor dem Reset zurückkehren:

# Entweder mit der Reflog-Referenz
git reset --hard HEAD@{1}

# Oder direkt mit dem SHA-Hash
git reset --hard f8e9d0c

Szenario 2: Gelöschten Branch wiederherstellen

Du hast einen Branch gelöscht und merkst zu spät, dass er wichtige Arbeit enthielt:

# Der Fehler
git branch -D feature/wichtig

# Reflog zeigt den letzten Commit des Branches
git reflog | grep "feature/wichtig"
# Ausgabe: a1b2c3d HEAD@{5}: checkout: moving from feature/wichtig to main

# Branch wiederherstellen
git checkout -b feature/wichtig a1b2c3d

Szenario 3: Nach fehlgeschlagenem Rebase zurück zum Ursprung

Ein Interactive Rebase ist schiefgegangen:

# Du hast das hier gemacht
git rebase -i HEAD~10
# Und dabei alles vermasselt...

# Reflog zeigt den Zustand vor dem Rebase
git reflog
# Ausgabe: 
# x1y2z3w (HEAD -> feature) HEAD@{0}: rebase (finish): ...
# ...
# a1b2c3d HEAD@{7}: rebase (start): checkout HEAD~10
# f8e9d0c HEAD@{8}: commit: Mein letzter sauberer Commit

# Zurück zum Zustand vor dem Rebase
git reset --hard HEAD@{8}

Szenario 4: Versehentlich überschriebenen Commit nach Amend wiederherstellen

Du hast git commit --amend benutzt und dabei den ursprünglichen Commit überschrieben:

git reflog
# Ausgabe:
# a1b2c3d (HEAD -> main) HEAD@{0}: commit (amend): Neue Message
# f8e9d0c HEAD@{1}: commit: Ursprüngliche Message die ich behalten wollte

# Der ursprüngliche Commit existiert noch und kann wiederhergestellt werden
git cherry-pick f8e9d0c
# Oder wenn du komplett zurück willst:
git reset --hard f8e9d0c

Das Reflog in PhpStorm nutzen 🖥️

PhpStorm bietet eine grafische Oberfläche für das Reflog, die besonders für visuelle Lerntypen hilfreich ist.

Methode 1: Über das Git-Log-Fenster

  1. Öffne das Git-Tool-Fenster (unten in PhpStorm) oder über View → Tool Windows → Git

  2. Im Log-Tab siehst du normalerweise die Commit-Historie. Klicke auf das Uhren-Symbol (🕐) oder suche nach der Option „Show Reflog" in den Filter-Optionen

  3. Alternativ kannst du im Suchfeld des Log-Fensters nach spezifischen Commits suchen, wenn du deren SHA-Hash aus dem Terminal-Reflog kennst

Methode 2: Über das Terminal in PhpStorm

  1. Öffne das Terminal in PhpStorm (View → Tool Windows → Terminal oder Alt+F12)

  2. Führe dort git reflog aus – du bekommst die gleiche Ausgabe wie im externen Terminal

  3. Vorteil: Du kannst SHA-Hashes direkt kopieren und in PhpStorms Git-Funktionen verwenden

Methode 3: „Reset Current Branch to Here" nutzen

Wenn du im Git-Log einen bestimmten Commit gefunden hast (auch einen, der nur noch im Reflog existiert), kannst du ihn wiederherstellen:

  1. Rechtsklick auf den Commit im Log-Fenster
  2. Wähle „Reset Current Branch to Here..."
  3. Wähle den Reset-Modus:
    • Soft: Behält alle Änderungen staged
    • Mixed: Behält Änderungen, aber unstaged
    • Hard: Verwirft alle Änderungen (Vorsicht!)

Methode 4: Cherry-Pick für einzelne Commits

Wenn du nur einen bestimmten „verlorenen" Commit wiederhaben möchtest:

  1. Finde den SHA-Hash im Reflog (Terminal: git reflog)
  2. In PhpStorm: Git → Uncommitted Changes → Cherry-Pick (oder über das Log-Fenster)
  3. Gib den SHA-Hash ein oder suche nach dem Commit

Methode 5: Branch aus Reflog-Commit erstellen

  1. Finde den SHA-Hash des gewünschten Commits
  2. Git → Branches → New Branch
  3. Im Dialog kannst du bei „Starting point" den SHA-Hash eingeben
  4. Damit erstellst du einen neuen Branch, der auf den „verlorenen" Commit zeigt

Das Reflog für verschiedene Referenzen

Das Reflog existiert nicht nur für HEAD, sondern für alle Referenzen in deinem Repository:

# Reflog für HEAD (Standard)
git reflog show HEAD

# Reflog für einen bestimmten Branch
git reflog show main
git reflog show feature/login

# Reflog für alle Referenzen
git reflog show --all

Dies ist besonders nützlich, wenn du wissen möchtest, wie sich ein bestimmter Branch im Laufe der Zeit entwickelt hat, auch über Resets und Force-Pushes hinweg.


Wichtige Einschränkungen des Reflogs ⚠️

Trotz seiner Mächtigkeit hat das Reflog einige Grenzen, die du kennen solltest:

Das Reflog ist lokal

Das Reflog wird nicht mit Remote-Repositories synchronisiert. Es existiert nur auf deinem lokalen Rechner. Das bedeutet:

Zeitliche Begrenzung

Reflog-Einträge werden nach einer gewissen Zeit gelöscht:

# Standard-Einstellungen anzeigen
git config gc.reflogExpire          # Standard: 90 Tage
git config gc.reflogExpireUnreachable   # Standard: 30 Tage

# Falls gewünscht: Längere Aufbewahrung konfigurieren
git config gc.reflogExpire "180 days"
git config gc.reflogExpireUnreachable "90 days"

Garbage Collection beachten

Wenn git gc (Garbage Collection) läuft, können alte, nicht mehr referenzierte Commits tatsächlich gelöscht werden – aber nur, wenn sie auch aus dem Reflog abgelaufen sind:

# Manuelle Garbage Collection (normalerweise nicht nötig)
git gc

# Aggressive GC (kann alte Commits entfernen!)
git gc --aggressive --prune=now

Fortgeschrittene Reflog-Techniken

Commits in einem Zeitraum finden

# Alle Reflog-Einträge der letzten 2 Stunden
git reflog --since="2 hours ago"

# Reflog-Einträge zwischen zwei Zeitpunkten
git reflog --since="2024-01-01" --until="2024-01-15"

Reflog mit Diff kombinieren

Du kannst dir anschauen, was sich zwischen zwei Reflog-Zuständen geändert hat:

# Diff zwischen aktuellem Zustand und Zustand vor 5 Aktionen
git diff HEAD@{5} HEAD@{0}

# Welche Dateien haben sich geändert?
git diff --name-only HEAD@{5} HEAD@{0}

Reflog-Einträge als Grundlage für Branches

# Branch erstellen, der auf einen Reflog-Zustand zeigt
git branch recovery-branch HEAD@{10}

# Direkt zu einem Reflog-Zustand wechseln
git checkout HEAD@{10}  # Achtung: Detached HEAD!

Zusammenfassung – Dein Reflog-Notfall-Spickzettel 📋

Situation Lösung
Commits nach reset --hard verloren git reflog → SHA finden → git reset --hard <SHA>
Branch versehentlich gelöscht git reflog → letzten Commit des Branches finden → git checkout -b <branch-name> <SHA>
Rebase schiefgegangen git reflog → Zustand vor Rebase finden → git reset --hard HEAD@{n}
Commit nach Amend überschrieben git reflog → ursprünglichen Commit finden → git cherry-pick <SHA>
Wissen, was man vor X Stunden gemacht hat git reflog --since="X hours ago"

Das Wichtigste zum Schluss: Wenn etwas schiefgeht, keine Panik! Solange du nicht git gc --prune=now ausführst oder das Repository löschst, sind deine Commits mit sehr hoher Wahrscheinlichkeit noch da. Das Reflog ist dein Freund – lerne es kennen, bevor du es brauchst! 🛟