Git für Fortgeschrittene

Du beherrschst die Grundlagen von Git und möchtest jetzt verstehen, wie Git wirklich funktioniert – nicht nur, welche Befehle du tippst, sondern was unter der Haube passiert.

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.

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

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! 🚀

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

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

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

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! 🎯

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

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! 🛟

Kapitel 2: Fortgeschrittenes Branching – Strategien für professionelle Entwicklung

Einleitung: Im Einsteigerkurs hast du gelernt, Branches zu erstellen und zu mergen. In der professionellen Entwicklung reicht das nicht – du brauchst eine durchdachte Branching-Strategie, die zu deinem Projekt und Team passt. Git Flow, GitHub Flow, Trunk-Based Development – jede Strategie hat ihre Vor- und Nachteile. Außerdem wirst du lernen, wie du mit Long-Running Branches umgehst, Release-Branches verwaltest und Hotfixes in mehrere Branches gleichzeitig einpflegst. Dieses Kapitel verwandelt dich vom Branch-Nutzer zum Branch-Strategen.

Kapitel 2: Fortgeschrittenes Branching – Strategien für professionelle Entwicklung

Branching-Strategien im Vergleich: Git Flow, GitHub Flow und Trunk-Based Development

Die Wahl der richtigen Branching-Strategie ist eine der wichtigsten architektonischen Entscheidungen für dein Projekt. Sie beeinflusst, wie dein Team zusammenarbeitet, wie schnell ihr Features ausliefern könnt und wie stabil eure Releases sind. Lass mich dir die drei populärsten Strategien im Detail vorstellen.


🌳 Git Flow – der strukturierte Klassiker

Git Flow wurde 2010 von Vincent Driessen vorgestellt und war lange Zeit der Standard für professionelle Softwareentwicklung. Es ist eine ausgeklügelte, aber komplexe Strategie mit klar definierten Branch-Typen und strengen Regeln.

Die Branch-Struktur

gitGraph
    commit id: "Initial"
    branch develop
    checkout develop
    commit id: "Setup"
    branch feature/login
    checkout feature/login
    commit id: "Login UI"
    commit id: "Login Logic"
    checkout develop
    merge feature/login id: "Merge Login"
    branch release/1.0
    checkout release/1.0
    commit id: "Bugfix"
    commit id: "Version bump"
    checkout main
    merge release/1.0 id: "Release 1.0" tag: "v1.0"
    checkout develop
    merge release/1.0 id: "Back-merge"

Git Flow definiert fünf Branch-Typen mit jeweils spezifischen Aufgaben:

  1. main (früher master)

    Der heilige Gral deines Repositories. Dieser Branch enthält ausschließlich produktionsreifen Code. Jeder Commit auf main repräsentiert eine Release-Version und wird typischerweise mit einem Tag versehen (z.B. v1.0.0, v1.1.0). Direktes Committen auf main ist streng verboten – Änderungen kommen nur durch Merges von Release- oder Hotfix-Branches.

  2. develop

    Der Integrations-Branch für alle neuen Entwicklungen. Hier fließen alle abgeschlossenen Features zusammen. develop repräsentiert den aktuellen Entwicklungsstand und ist die Basis für neue Feature-Branches. Man könnte sagen: main ist „was der Kunde sieht", develop ist „was als nächstes kommt".

  3. feature/* (z.B. feature/user-authentication, feature/shopping-cart)

    Für jedes neue Feature wird ein eigener Branch von develop abgezweigt. Hier findet die eigentliche Entwicklungsarbeit statt. Feature-Branches können beliebig viele Commits enthalten und existieren so lange, bis das Feature fertig ist. Nach Abschluss wird der Branch zurück in develop gemergt und gelöscht.

  4. release/* (z.B. release/1.2.0)

    Wenn develop genügend Features für eine neue Version angesammelt hat, wird ein Release-Branch abgezweigt. Ab diesem Zeitpunkt kommen keine neuen Features mehr hinzu – nur noch Bugfixes, Dokumentation und Release-Vorbereitungen (Versionsnummern aktualisieren, Changelog schreiben). Nach Fertigstellung wird der Release-Branch sowohl in main als auch zurück in develop gemergt.

  5. hotfix/* (z.B. hotfix/1.0.1-security-patch)

    Für kritische Bugs in der Produktion, die nicht auf den nächsten regulären Release warten können. Hotfix-Branches werden direkt von main abgezweigt, enthalten nur die minimale Änderung zur Fehlerbehebung und werden nach Fertigstellung sowohl in main (mit neuem Tag) als auch in develop gemergt.

Der typische Workflow in Git Flow

Ein neues Feature durchläuft folgenden Weg:

develop → feature/xyz → develop → release/1.0 → main + develop
                                       ↑
                                  (Bugfixes)

Und ein Hotfix:

main → hotfix/1.0.1 → main + develop

Vor- und Nachteile von Git Flow

✅ Vorteile ❌ Nachteile
Klare Trennung zwischen Entwicklung und Produktion Hohe Komplexität durch viele Branch-Typen
Parallele Arbeit an Release und neuen Features möglich Lange Feature-Branches führen zu schwierigen Merges
Gut geeignet für versionierte Software mit Releases Langsamerer Entwicklungszyklus
Stabile main-Branch garantiert Erfordert Disziplin und Tooling
Hotfixes können unabhängig eingespielt werden Overkill für kleine Teams oder Web-Apps

Wann Git Flow verwenden?

Git Flow eignet sich besonders für:


🚀 GitHub Flow – schlank und kontinuierlich

GitHub Flow wurde von GitHub selbst als Reaktion auf die Komplexität von Git Flow entwickelt. Es ist radikal einfacher: Es gibt nur main und Feature-Branches. Die Philosophie ist „ship early, ship often" – kontinuierliche Auslieferung kleiner Änderungen.

Die Branch-Struktur

gitGraph
    commit id: "Initial"
    branch feature/login
    checkout feature/login
    commit id: "Login UI"
    commit id: "Login Tests"
    checkout main
    merge feature/login id: "PR #1" tag: "deployed"
    branch feature/dashboard
    checkout feature/dashboard
    commit id: "Dashboard"
    checkout main
    merge feature/dashboard id: "PR #2" tag: "deployed"
    branch bugfix/header
    checkout bugfix/header
    commit id: "Fix Header"
    checkout main
    merge bugfix/header id: "PR #3" tag: "deployed"

Die sechs Regeln von GitHub Flow

  1. main ist immer deploybar

    Der main-Branch muss zu jedem Zeitpunkt in Produktion deployt werden können. Das bedeutet: Nur getesteter, funktionierender Code kommt hinein.

  2. Für jede Änderung einen Branch erstellen

    Egal ob Feature, Bugfix oder Experiment – erstelle einen beschreibend benannten Branch von main (z.B. add-user-profile, fix-login-timeout, experiment/new-checkout).

  3. Regelmäßig committen und pushen

    Kleine, häufige Commits mit klaren Nachrichten. Push regelmäßig zu GitHub, damit andere den Fortschritt sehen und du ein Backup hast.

  4. Pull Request erstellen, wenn bereit für Review

    Sobald deine Änderung bereit für Feedback ist (oder du Diskussion brauchst), öffne einen Pull Request. Dies ist der zentrale Ort für Code Review und Diskussion.

  5. Nach Review und Approval: Merge in main

    Sobald der PR approved ist und alle Tests grün sind, wird er in main gemergt. Bei GitHub Flow gibt es keine Zwischenstationen.

  6. Sofort nach dem Merge deployen

    Der Merge in main triggert (idealerweise automatisch) ein Deployment. Das bedeutet, dass jeder Merge innerhalb von Minuten in Produktion ist.

Der typische Workflow in GitHub Flow

main → feature-branch → Pull Request → Code Review → main → Deploy
           ↑                               ↓
      (entwickeln)              (automatische Tests)

Vor- und Nachteile von GitHub Flow

✅ Vorteile ❌ Nachteile
Extrem einfach zu verstehen und umzusetzen Keine native Unterstützung für versionierte Releases
Fördert kontinuierliche Integration Erfordert sehr gute Testabdeckung und CI/CD
Schnelle Iteration und Feedback-Zyklen Hotfixes sind „normal" – keine spezielle Behandlung
Pull Requests als zentrale Review-Plattform Weniger geeignet, wenn mehrere Versionen unterstützt werden müssen
Ideal für Web-Applikationen und SaaS Kann bei großen Features unübersichtlich werden

Wann GitHub Flow verwenden?

GitHub Flow eignet sich besonders für:


🎯 Trunk-Based Development – der radikale Ansatz

Trunk-Based Development (TBD) geht noch einen Schritt weiter als GitHub Flow. Die Idee: Alle Entwickler committen direkt in einen einzigen Branch (den „Trunk", typischerweise main). Feature-Branches existieren entweder gar nicht oder sind extrem kurzlebig (maximal 1–2 Tage).

Die Branch-Struktur

gitGraph
    commit id: "Feature A - Part 1"
    commit id: "Feature A - Part 2"
    commit id: "Bugfix"
    commit id: "Feature B" tag: "deployed"
    commit id: "Refactoring"
    commit id: "Feature A - Part 3"
    commit id: "Feature A complete" tag: "deployed"

Die Kernprinzipien

  1. Ein einziger Branch für alle

    Es gibt nur main (den „Trunk"). Alle Entwickler integrieren ihre Änderungen hier. Lange lebende Feature-Branches sind verboten.

  2. Sehr kleine, sehr häufige Commits

    Statt eines großen Commits am Ende eines Features gibt es viele kleine Commits während der Entwicklung. Jeder Commit muss den Build grün halten.

  3. Feature Flags für unfertige Features

    Wie bringt man Code für ein halbfertiges Feature in main, ohne dass Nutzer es sehen? Durch Feature Flags (auch Feature Toggles genannt):

    if ($featureFlags->isEnabled('new-checkout-process')) {
        return $this->newCheckoutProcess($cart);
    } else {
        return $this->legacyCheckout($cart);
    }
    

    Das Feature wird erst „eingeschaltet", wenn es fertig ist – obwohl der Code schon lange in Produktion ist.

  4. Kurzlebige Feature-Branches (optional)

    Manche Teams erlauben Feature-Branches, aber mit strikter Regel: Sie dürfen maximal 1–2 Tage existieren und müssen dann gemergt werden. Das erzwingt kleine, inkrementelle Änderungen.

  5. Exzellente CI/CD ist Pflicht

    Da jeder Commit potenziell in Produktion geht, muss die Test-Pipeline schnell und zuverlässig sein. Typischerweise < 10 Minuten für den kompletten Build.

Branch by Abstraction

Für größere Refactorings, die nicht in 1–2 Tagen erledigt sind, nutzt TBD das Pattern „Branch by Abstraction":

  1. Erstelle eine Abstraktionsschicht (Interface) vor dem zu ändernden Code
  2. Implementiere die neue Version hinter dem Interface
  3. Schalte schrittweise um (Feature Flag oder schrittweise Migration)
  4. Entferne die alte Implementierung, sobald die neue stabil ist

So bleibt der Code immer lauffähig, obwohl du parallel zwei Implementierungen hast.

Vor- und Nachteile von Trunk-Based Development

✅ Vorteile ❌ Nachteile
Keine Merge-Hölle durch lang lebende Branches Erfordert erfahrene, disziplinierte Entwickler
Maximale Kontinuierliche Integration Feature Flags erhöhen Code-Komplexität
Sehr schnelles Feedback Sehr gute Testabdeckung ist Voraussetzung
Fördert kleine, fokussierte Änderungen Kann für Junior-Entwickler überfordernd sein
Von Google, Facebook und anderen Tech-Giganten verwendet Weniger Review-Möglichkeiten vor dem Merge

Wann Trunk-Based Development verwenden?

Trunk-Based Development eignet sich besonders für:


📊 Der große Vergleich

Aspekt Git Flow GitHub Flow Trunk-Based
Komplexität Hoch Niedrig Mittel*
Branch-Typen 5 (main, develop, feature, release, hotfix) 2 (main, feature) 1 (main)
Lebensdauer Feature-Branch Tage bis Wochen Stunden bis Tage Keine oder < 2 Tage
Release-Modell Geplante Versionen Kontinuierlich Kontinuierlich
Merge-Frequenz Selten, dafür größer Häufig Sehr häufig (mehrmals täglich)
CI/CD-Anforderung Optional Empfohlen Pflicht
Feature Flags nötig? Nein Selten Ja
Parallele Versionen Ja Nein Nein
Team-Größe Mittel bis groß Klein bis mittel Klein bis mittel (erfahren)
Lernkurve Steil Flach Mittel

*Die Komplexität bei TBD liegt nicht im Branching, sondern in den Praktiken (Feature Flags, kleine Commits, CI/CD).


💡 Meine Empfehlung für dein PHP-Webprojekt

Du beschreibst ein mittelgroßes PHP-Webprojekt mit regelmäßigen Releases. Lass mich die Faktoren analysieren:

Analyse deiner Situation

Meine Empfehlung: GitHub Flow mit Release-Tags 🏆

Für dein Szenario empfehle ich GitHub Flow, aber mit einer kleinen Erweiterung für die Versionierung:

flowchart LR
    subgraph Entwicklung
        A["Feature-Branch\nerstellen"] --> B["Entwickeln\nund testen"]
        B --> C["Pull Request\nerstellen"]
        C --> D["Code Review"]
        D --> E["Merge in main"]
    end
    
    subgraph Release
        E --> F{"Release\nfällig?"}
        F -->|Ja| G["Tag erstellen\nv1.2.0"]
        F -->|Nein| H["Weiter entwickeln"]
        G --> I["Deployment"]
    end
    
    style G fill:#90EE90,color:#000000
    style I fill:#87CEEB,color:#000000

Warum GitHub Flow für dich?

  1. Einfachheit: Du und dein Team müsst nicht fünf verschiedene Branch-Typen jonglieren. Die Regeln passen auf einen Bierdeckel.

  2. Web-Applikation: PHP-Webprojekte werden deployt, nicht installiert. Du brauchst keine parallelen Versionen zu unterstützen – es gibt nur „was gerade live ist".

  3. Pull Requests als Qualitätssicherung: Jede Änderung durchläuft einen Review-Prozess. Das ist Gold wert für die Code-Qualität.

  4. Flexibilität bei Releases: Du kannst so oft oder selten releasen, wie du möchtest. Ein Release ist einfach ein Tag auf main.

  5. Einfacher Einstieg: Falls Teammitglieder noch nicht so Git-erfahren sind, ist GitHub Flow viel leichter zu erlernen als Git Flow.

Wie du „regelmäßige Releases" mit GitHub Flow umsetzt

Anstatt Release-Branches zu nutzen, arbeitest du mit Tags und einem Release-Rhythmus:

# Nach dem Merge aller Features für Version 1.2.0
git tag -a v1.2.0 -m "Release 1.2.0: User Dashboard, Performance Improvements"
git push origin v1.2.0

In GitHub kannst du dann einen formalen Release erstellen, der auf diesen Tag verweist – mit Changelog, Release Notes und ggf. Assets.

Dein angepasster Workflow

  1. Für jede Aufgabe: Erstelle einen Branch von main

    git checkout main
    git pull
    git checkout -b feature/user-dashboard
    
  2. Entwickle und committe regelmäßig mit aussagekräftigen Nachrichten

  3. Erstelle einen Pull Request auf GitHub, wenn bereit für Review

  4. Nach Approval: Merge in main (ich empfehle „Squash and Merge" für eine saubere Historie)

  5. Für Releases: Wenn genügend Features gemergt sind, erstelle einen Tag und deploye:

    git checkout main
    git pull
    git tag -a v1.2.0 -m "Release 1.2.0"
    git push origin v1.2.0
    # Deployment triggern (manuell oder automatisch via GitHub Actions)
    
  6. Für Hotfixes: Behandle sie wie normale Features – Branch erstellen, fixen, PR, mergen. Danach ggf. Patch-Version taggen (v1.2.1).

Wann du doch Git Flow in Betracht ziehen solltest

Überdenke meine Empfehlung, wenn einer dieser Punkte zutrifft:

Wann Trunk-Based Development interessant wäre


🎓 Zusammenfassung

Strategie Motto Ideal für
Git Flow „Struktur und Kontrolle" Versionierte Software, große Teams, lange Zyklen
GitHub Flow „Ship early, ship often" Web-Apps, kleine-mittlere Teams, kontinuierliche Delivery
Trunk-Based „Immer integriert" Hochperformante Teams, mehrfach tägliches Deployment

Für dein mittelgroßes PHP-Webprojekt ist GitHub Flow der Sweet Spot: Einfach genug, um schnell produktiv zu sein, aber strukturiert genug für professionelle Zusammenarbeit. Mit Tags und GitHub Releases bekommst du die Versionierung, die du für „regelmäßige Releases" brauchst – ohne die Komplexität von Git Flow.

Starte mit GitHub Flow, und wenn du merkst, dass du mehr Struktur brauchst, kannst du immer noch zu Git Flow wechseln. Der umgekehrte Weg (von komplex zu einfach) ist erfahrungsgemäß schwieriger! 😉

Kapitel 2: Fortgeschrittenes Branching – Strategien für professionelle Entwicklung

Long-Running Branches verstehen und synchron halten

Was sind Long-Running Branches?

Long-Running Branches (auch langlebige oder permanente Branches genannt) sind Branches, die über die gesamte Lebensdauer eines Projekts oder zumindest über längere Zeiträume hinweg bestehen bleiben – im Gegensatz zu kurzlebigen Feature-Branches, die nach dem Merge gelöscht werden.

Die typischen Long-Running Branches

In den meisten professionellen Projekten findest du folgende Struktur:

Branch Zweck Lebensdauer
main (oder master) Produktionscode – enthält nur stabilen, deploybaren Code Permanent
develop Integrationsbranch für die nächste Version – hier fließen Feature-Branches zusammen Permanent
release/* Vorbereitung einer neuen Version, nur noch Bugfixes erlaubt Mittelfristig (Wochen)
hotfix/* Kritische Fixes für Produktion Kurzfristig (Tage)
gitGraph
    commit id: "Initial"
    branch develop
    commit id: "Setup"
    branch feature/login
    commit id: "Login-Form"
    commit id: "Validation"
    checkout develop
    merge feature/login id: "Merge Login"
    branch release/1.0
    commit id: "Version bump"
    commit id: "Bugfix"
    checkout main
    merge release/1.0 id: "Release 1.0" tag: "v1.0.0"
    checkout develop
    merge release/1.0 id: "Sync Release"

Warum ist Synchronisation so wichtig? ⚠️

Stell dir folgendes Szenario vor:

  1. Du erstellst am Montag einen Feature-Branch von main
  2. Du arbeitest zwei Wochen an deinem Feature
  3. In der Zwischenzeit wurden 50 Commits in main gemergt (von anderen Entwicklern oder anderen Features)
  4. Dein Feature-Branch ist jetzt zwei Wochen und 50 Commits hinter main

Die Probleme, die entstehen können:

Die Lösung: Regelmäßige Synchronisation – idealerweise täglich oder zumindest bei jedem größeren Meilenstein in main.


Die zwei Strategien: Merge vs. Rebase

Es gibt zwei grundlegend verschiedene Ansätze, um Änderungen aus main in deinen Feature-Branch zu integrieren. Beide haben ihre Berechtigung, und die Wahl hängt von deinem Workflow und deinen Team-Konventionen ab.

Strategie 1: Merge (main → Feature-Branch)

Bei einem Merge werden die Änderungen aus main in deinen Feature-Branch integriert, wobei ein Merge-Commit entsteht, der beide Historien verbindet.

gitGraph
    commit id: "A"
    commit id: "B"
    branch feature/shop
    commit id: "F1"
    commit id: "F2"
    checkout main
    commit id: "C"
    commit id: "D"
    checkout feature/shop
    merge main id: "Merge main" type: HIGHLIGHT
    commit id: "F3"

Was passiert hier?

Strategie 2: Rebase (Feature-Branch auf main)

Bei einem Rebase werden deine Feature-Commits „abgelöst" und auf die Spitze von main neu aufgesetzt. Es entsteht eine lineare Historie ohne Merge-Commits.

gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "C"
    commit id: "D"
    branch feature/shop
    commit id: "F1 neu" type: HIGHLIGHT
    commit id: "F2 neu" type: HIGHLIGHT
    commit id: "F3"

Was passiert hier?


Ausführliche Anleitung: Synchronisation mit Merge

Szenario

Du arbeitest an einem Feature-Branch feature/newsletter und möchtest die neuesten Änderungen aus main integrieren.

Schritt 1: Sicherstellen, dass du einen sauberen Arbeitsstand hast

Bevor du irgendetwas synchronisierst, sollte dein Working Directory „sauber" sein – also keine uncommitteten Änderungen enthalten.

In PhpStorm prüfen:

Falls du unfertige Änderungen hast, nutze Stash:

# Terminal-Variante
git stash push -m "WIP: Newsletter-Formular"

In PhpStorm:

  1. Menü: Git → Uncommitted Changes → Stash Changes...
  2. Gib eine aussagekräftige Nachricht ein
  3. Klicke Create Stash

Schritt 2: Den neuesten Stand von main holen (Fetch)

Zuerst musst du sicherstellen, dass dein lokales Repository die neuesten Informationen vom Remote hat.

In PhpStorm:

  1. Klicke auf das blaue ↓ Pfeil-Symbol in der Toolbar (oder Ctrl+T / Cmd+T)
  2. Oder: Menü Git → Fetch

Dies aktualisiert origin/main mit dem Stand auf GitHub, ohne deinen lokalen main-Branch zu verändern.

Schritt 3: main in deinen Feature-Branch mergen

Methode A: Über das Git-Log in PhpStorm (empfohlen)

  1. Öffne das Git-Log: Klicke unten auf den Tab „Git" und dann auf „Log"
  2. Stelle sicher, dass du auf deinem Feature-Branch bist (sichtbar unten rechts in der Statusleiste)
  3. Im Log siehst du alle Branches – finde origin/main
  4. Rechtsklick auf den neuesten Commit von origin/main
  5. Wähle „Merge 'origin/main' into 'feature/newsletter'"
flowchart TD
    A["1. Git-Log offnen\n Ansicht: Git - Log"] --> B["2. Branch prufen\n Bist du auf feature/newsletter?"]
    B --> C["3. origin/main finden\n Im Log-Graphen"]
    C --> D["4. Rechtsklick auf\n neuesten Commit"]
    D --> E["5. Merge origin/main\n into feature/newsletter"]
    E --> F{Konflikte?}
    F -->|Ja| G["Konflikte losen\n 3-Way-Merge-Editor"]
    F -->|Nein| H["Fertig!\n Merge-Commit erstellt"]
    G --> H
    
    style A fill:#e1f5fe,color:#000000
    style E fill:#fff3e0,color:#000000
    style H fill:#e8f5e9,color:#000000

Methode B: Über das Branch-Menü

  1. Klicke unten rechts auf den aktuellen Branch-Namen (z.B. feature/newsletter)
  2. Im Dropdown: Finde „origin/main" unter „Remote Branches"
  3. Klicke darauf und wähle „Merge into Current"

Methode C: Über das Hauptmenü

  1. Menü: Git → Merge...
  2. Wähle origin/main aus der Liste
  3. Klicke Merge

Schritt 4: Konflikte lösen (falls vorhanden)

Wenn es Konflikte gibt, öffnet PhpStorm automatisch ein Dialogfenster „Files Merged with Conflicts":

  1. Klicke auf „Merge..." neben der konfliktbehafteten Datei
  2. Der 3-Way-Merge-Editor öffnet sich:
    • Links: Deine Version (feature/newsletter)
    • Rechts: Ihre Version (main)
    • Mitte: Das Ergebnis, das du zusammenbaust
  3. Nutze die >> und << Buttons, um Änderungen zu übernehmen
  4. Wenn du fertig bist: Apply
  5. Nachdem alle Konflikte gelöst sind: Der Merge-Commit wird automatisch erstellt

Schritt 5: Ergebnis überprüfen

Nach dem Merge solltest du kurz prüfen:

  1. Schau ins Git-Log: Du siehst jetzt einen Merge-Commit mit zwei Eltern-Commits
  2. Führe deine Tests aus: Stelle sicher, dass alles noch funktioniert
  3. Pushe deinen Branch: Ctrl+Shift+K oder Git → Push

Terminal-Befehle (zur Referenz)

# 1. Auf Feature-Branch wechseln (falls nicht schon dort)
git checkout feature/newsletter

# 2. Neueste Daten vom Remote holen
git fetch origin

# 3. main in Feature-Branch mergen
git merge origin/main

# 4. Bei Konflikten: Nach dem manuellen Lösen
git add .
git commit  # Merge-Commit abschließen

# 5. Pushen
git push origin feature/newsletter

Ausführliche Anleitung: Synchronisation mit Rebase

Wichtiger Hinweis vorab ⚠️

Rebase schreibt die Historie um – deine Commits bekommen neue SHA-Hashes. Das bedeutet:

Szenario

Du arbeitest an feature/checkout und möchtest deine Commits auf den aktuellen Stand von main neu aufsetzen.

Schritt 1: Vorbereitung (wie beim Merge)

Schritt 2: Rebase in PhpStorm starten

Methode A: Über das Git-Menü (empfohlen für Einsteiger)

  1. Stelle sicher, dass du auf deinem Feature-Branch bist
  2. Menü: Git → Rebase...
  3. Im Dialog „Rebase":
    • „Onto": Wähle origin/main
    • Die anderen Optionen kannst du auf Standard lassen
  4. Klicke Rebase

Methode B: Über das Branch-Popup

  1. Klicke unten rechts auf deinen Branch-Namen
  2. Finde origin/main unter „Remote Branches"
  3. Klicke darauf und wähle „Rebase Current onto Selected"

Methode C: Über das Git-Log

  1. Öffne das Git-Log
  2. Rechtsklick auf den neuesten Commit von origin/main
  3. Wähle „Rebase 'feature/checkout' onto 'origin/main'"

Schritt 3: Konflikte während des Rebases lösen

Beim Rebase werden deine Commits einzeln auf main neu angewendet. Das bedeutet: Wenn es Konflikte gibt, musst du sie für jeden betroffenen Commit einzeln lösen.

Der Ablauf bei Konflikten:

flowchart TD
    A["Rebase starten"] --> B["Commit 1 anwenden"]
    B --> C{Konflikt?}
    C -->|Nein| D["Commit 2 anwenden"]
    C -->|Ja| E["Konflikt losen"]
    E --> F["Git - Rebase - Continue"]
    F --> D
    D --> G{Konflikt?}
    G -->|Nein| H["Commit 3 anwenden"]
    G -->|Ja| I["Konflikt losen"]
    I --> J["Git - Rebase - Continue"]
    J --> H
    H --> K["Rebase abgeschlossen!"]
    
    style A fill:#e1f5fe,color:#000000
    style E fill:#ffcccc,color:#000000
    style I fill:#ffcccc,color:#000000
    style K fill:#e8f5e9,color:#000000

Konkret in PhpStorm:

  1. Bei einem Konflikt zeigt PhpStorm eine Notification: „Rebase stopped due to conflicts"
  2. Löse die Konflikte im 3-Way-Merge-Editor (wie beim Merge)
  3. Nach dem Lösen: Git → Rebase → Continue
  4. PhpStorm wendet den nächsten Commit an
  5. Wiederhole, bis alle Commits neu angewendet wurden

Wenn du abbrechen möchtest:

Schritt 4: Force-Push (falls bereits gepusht)

Da Rebase neue Commits erstellt, musst du mit --force pushen, wenn der Branch bereits auf GitHub existiert.

In PhpStorm:

  1. Ctrl+Shift+K (oder Git → Push)
  2. PhpStorm zeigt dir, dass ein Force-Push nötig ist
  3. Klicke auf den kleinen Pfeil neben „Push" und wähle „Force Push"

Besser: Verwende --force-with-lease:

git push --force-with-lease origin feature/checkout

Dies ist sicherer als --force, weil es prüft, ob jemand anderes in der Zwischenzeit gepusht hat.

In PhpStorm aktivieren:

  1. Settings → Version Control → Git
  2. Aktiviere „Use --force-with-lease for force push"

Terminal-Befehle (zur Referenz)

# 1. Auf Feature-Branch wechseln
git checkout feature/checkout

# 2. Fetch
git fetch origin

# 3. Rebase starten
git rebase origin/main

# 4a. Bei Konflikten: Nach dem Lösen weitermachen
git add .
git rebase --continue

# 4b. Oder abbrechen
git rebase --abort

# 5. Force-Push (nur wenn schon gepusht)
git push --force-with-lease origin feature/checkout

Merge vs. Rebase: Direkter Vergleich

Aspekt Merge Rebase
Historie Bewahrt die komplette, echte Historie mit allen Verzweigungen Erzeugt eine lineare, „saubere" Historie
Merge-Commits Ja, ein Merge-Commit pro Synchronisation Nein, keine zusätzlichen Commits
Commit-Hashes Bleiben unverändert Ändern sich (neue Commits)
Konfliktlösung Alle Konflikte auf einmal Konflikte pro Commit einzeln
Force-Push nötig? Nein Ja, wenn Branch bereits gepusht
Sicher bei Team-Branches? ✅ Ja ⚠️ Nur mit Vorsicht
Nachvollziehbarkeit Sehr gut (wann wurde was integriert?) Geschichte wird „umgeschrieben"
Komplexität Einfacher Komplexer

Visuelle Gegenüberstellung

Nach mehreren Merge-Synchronisationen:

main:     A───B───C───D───E───F───G
               \       \       \
feature:        X───Y───M───Z───M───W
                       ↑       ↑
                  Merge-Commits

Nach Rebase (vor dem finalen Merge):

main:     A───B───C───D───E───F───G
                                   \
feature:                            X'──Y'──Z'──W'
                                    ↑
                               Neu aufgesetzte Commits

Welche Strategie solltest du wählen?

Wähle Merge, wenn...

Wähle Rebase, wenn...

Der Kompromiss: „Rebase lokal, Merge für Integration"

Viele Teams nutzen eine hybride Strategie:

  1. Während der Feature-Entwicklung: Rebase nutzen, um den Feature-Branch aktuell zu halten (solange nicht gepusht oder alleine am Branch)
  2. Für den finalen Merge: Einen normalen Merge (oder Squash-Merge) in main machen
flowchart LR
    A["Feature-Branch\n erstellen"] --> B["Entwickeln"]
    B --> C{"Neue Commits\n in main?"}
    C -->|Ja| D["Rebase auf main\n lokal"]
    D --> B
    C -->|Nein| E{"Feature\n fertig?"}
    E -->|Nein| B
    E -->|Ja| F["Pull Request\n erstellen"]
    F --> G["Code Review"]
    G --> H["Merge PR\n in main"]
    
    style D fill:#fff3e0,color:#000000
    style H fill:#e8f5e9,color:#000000

Praktische Tipps für die tägliche Arbeit 💡

Tipp 1: Synchronisiere regelmäßig

Mache es dir zur Gewohnheit, mindestens einmal täglich (oder bei jedem PR, der in main gemergt wird) zu synchronisieren:

# Schneller Check am Morgen
git fetch origin
git log HEAD..origin/main --oneline
# Zeigt dir, wie viele Commits du "hinterher" bist

In PhpStorm siehst du das im Git-Log: Wenn origin/main weiter ist als der Verzweigungspunkt deines Branches, ist es Zeit zu synchronisieren.

Tipp 2: Nutze PhpStorms „Update Project"

Der schnellste Weg für den täglichen Workflow:

  1. Ctrl+T (oder Git → Update Project)
  2. Wähle Rebase oder Merge (du kannst eine Standardeinstellung setzen)
  3. PhpStorm führt Fetch + Merge/Rebase in einem Schritt aus

Tipp 3: Konfiguriere deine Standard-Strategie

In PhpStorm:

Global in Git:

# Für Rebase als Standard
git config --global pull.rebase true

# Für Merge als Standard (default)
git config --global pull.rebase false

Tipp 4: Erstelle einen Alias für den Workflow

Wenn du häufig synchronisierst, erstelle einen Git-Alias:

# In ~/.gitconfig oder via Befehl:
git config --global alias.sync-main '!git fetch origin && git rebase origin/main'

# Nutzung:
git sync-main

Tipp 5: Kommuniziere mit deinem Team

Die wichtigste „Technik" ist eigentlich keine technische: Sprich mit deinem Team!


Zusammenfassung 📋

Long-Running Branches sind permanente Branches wie main und develop, die als Integrationspunkte dienen und niemals gelöscht werden.

Regelmäßige Synchronisation ist essentiell, um:

Zwei Strategien stehen zur Verfügung:

  1. Merge (git merge origin/main):

    • Erzeugt Merge-Commits
    • Bewahrt die echte Historie
    • Sicher für Team-Branches
  2. Rebase (git rebase origin/main):

    • Erzeugt lineare Historie
    • Ändert Commit-Hashes (Force-Push nötig)
    • Nur für Branches, an denen du alleine arbeitest

Die beste Strategie ist die, auf die sich dein Team einigt und die konsistent angewendet wird.

Kapitel 2: Fortgeschrittenes Branching – Strategien für professionelle Entwicklung

Der Hotfix-Workflow: Kritische Bugfixes sicher in alle Branches bringen 🚨

Ein Hotfix ist eine der stressigsten Situationen in der Softwareentwicklung: Ein kritischer Bug ist in der Produktion aufgetaucht und muss sofort behoben werden – ohne dabei die laufende Entwicklungsarbeit zu gefährden oder den Fix irgendwo zu „vergessen". In dieser Lektion zeige ich dir, wie du einen professionellen Hotfix-Workflow umsetzt und dabei sicherstellst, dass der Fix in allen relevanten Branches ankommt.


Das Grundproblem verstehen

Stell dir folgendes Szenario vor:

main (v1.2.0) ──●──●──●  ← Produktionsversion, hier ist der Bug!
                    \
develop ─────────────●──●──●──●  ← Hier wird an v1.3.0 gearbeitet
                          \
feature/neues-login ───────●──●  ← Feature in Entwicklung

Der Bug existiert in main (der Produktionsversion), aber auch in develop und allen Feature-Branches, die von main oder develop abgezweigt wurden. Wenn du den Fix nur in main einspielst, wird er bei der nächsten Entwicklung wieder überschrieben. Spielst du ihn nur in develop ein, dauert es bis zum nächsten Release, bis er in Produktion kommt.

Die Lösung: Ein dedizierter Hotfix-Branch, der von main abzweigt und nach der Behebung in alle relevanten Branches gemergt wird.


Der klassische Hotfix-Workflow (Schritt für Schritt)

1. Hotfix-Branch von main erstellen

Der Hotfix-Branch wird immer von main (oder dem Branch, der die Produktion repräsentiert) erstellt – niemals von develop! So stellst du sicher, dass du exakt den Code-Stand der Produktion als Basis hast.

In PhpStorm:

  1. Öffne das Git-Tool-Window (Alt+9 / Cmd+9)
  2. Stelle sicher, dass du auf main bist (Rechtsklick auf mainCheckout)
  3. Führe einen Pull durch, um sicherzustellen, dass du den aktuellen Stand hast
  4. Klicke auf New Branch (oder Ctrl+Shift+` / Cmd+Shift+`)
  5. Benenne den Branch nach dem Schema: hotfix/kurze-beschreibung oder hotfix/v1.2.1

Auf der Kommandozeile:

# Sicherstellen, dass main aktuell ist
git checkout main
git pull origin main

# Hotfix-Branch erstellen
git checkout -b hotfix/kritischer-login-bug

Wichtige Namenskonventionen:


2. Den Bug beheben und committen

Jetzt behebst du den Bug. Dabei gelten einige wichtige Regeln:

Nur den Bug fixen – nichts anderes! Ein Hotfix sollte so minimal wie möglich sein. Jede zusätzliche Änderung erhöht das Risiko, neue Probleme einzuführen. Widerstehe der Versuchung, „schnell noch" andere kleine Dinge zu korrigieren.

Commit-Message mit Kontext:

git add src/Auth/LoginController.php
git commit -m "fix: SQL-Injection-Schwachstelle im Login behoben

- Prepared Statements statt String-Konkatenation
- Betrifft Login und Passwort-Reset
- Fixes #247"

In PhpStorm:

  1. Mache deine Änderungen im Code
  2. Öffne das Commit-Tool-Window (Ctrl+K / Cmd+K)
  3. Wähle nur die relevanten Dateien aus
  4. Schreibe eine aussagekräftige Commit-Message
  5. Nutze optional Amend wenn du nachbessern musst

3. Hotfix testen

Bevor du den Hotfix irgendwohin mergst, teste ihn gründlich:

In PhpStorm kannst du Tests direkt ausführen:


4. Hotfix in main mergen und taggen

Jetzt kommt der kritische Teil: Der Fix muss zurück nach main, damit er in die Produktion deployed werden kann.

In PhpStorm:

  1. Wechsle zu main (Rechtsklick → Checkout)
  2. Rechtsklick auf deinen Hotfix-Branch → Merge into Current
  3. PhpStorm zeigt dir den Merge-Dialog – überprüfe die Änderungen
  4. Bestätige den Merge

Auf der Kommandozeile:

# Zu main wechseln
git checkout main

# Hotfix mergen (mit --no-ff für einen expliziten Merge-Commit)
git merge --no-ff hotfix/kritischer-login-bug -m "Merge hotfix/kritischer-login-bug: SQL-Injection behoben"

Warum --no-ff? Die Option --no-ff (no fast-forward) erzwingt einen Merge-Commit, auch wenn ein Fast-Forward möglich wäre. Das hat zwei Vorteile:

  1. Der Hotfix bleibt in der Historie als eigenständiger „Block" erkennbar
  2. Du hast einen klaren Merge-Commit, der dokumentiert, wann der Hotfix eingespielt wurde

Version-Tag erstellen:

Nach einem Hotfix solltest du einen neuen Version-Tag erstellen:

# Tag erstellen
git tag -a v1.2.1 -m "Hotfix: SQL-Injection-Schwachstelle behoben"

# Tag auf GitHub pushen
git push origin v1.2.1

In PhpStorm:


5. Hotfix in develop mergen ⚠️

Das ist der Schritt, der am häufigsten vergessen wird! Wenn du den Hotfix nicht auch in develop mergst, wird der Bug beim nächsten Release wieder auftauchen, weil develop dann ohne den Fix nach main gemergt wird.

In PhpStorm:

  1. Wechsle zu develop (Rechtsklick → Checkout)
  2. Rechtsklick auf main (oder den Hotfix-Branch) → Merge into Current
  3. Löse eventuelle Konflikte im Merge-Tool

Auf der Kommandozeile:

git checkout develop
git pull origin develop  # Sicherstellen, dass develop aktuell ist
git merge --no-ff hotfix/kritischer-login-bug -m "Merge hotfix in develop: SQL-Injection behoben"

Oder alternativ – den Tag mergen:

git checkout develop
git merge --no-ff v1.2.1 -m "Merge v1.2.1 hotfix in develop"

6. Alles pushen

Jetzt müssen alle Änderungen auf GitHub landen:

# main mit dem Fix pushen
git push origin main

# develop mit dem Fix pushen
git push origin develop

# Tags pushen (falls noch nicht geschehen)
git push origin --tags

In PhpStorm:


7. Hotfix-Branch aufräumen

Nach erfolgreichem Merge in beide Branches kann der Hotfix-Branch gelöscht werden:

Lokal löschen:

git branch -d hotfix/kritischer-login-bug

Auf GitHub löschen:

git push origin --delete hotfix/kritischer-login-bug

In PhpStorm:


Visualisierung des Workflows

flowchart TD
    subgraph Ausgangssituation
        A["main\n v1.2.0 - Bug vorhanden"]
        B["develop\n Aktive Entwicklung"]
        A -.->|"abgezweigt"| B
    end
    
    subgraph Hotfix-Prozess
        C["1. Hotfix-Branch erstellen\n von main"]
        D["2. Bug beheben\n und committen"]
        E["3. Testen"]
        F["4. Merge in main\n Tag: v1.2.1"]
        G["5. Merge in develop"]
        H["6. Push und Cleanup"]
    end
    
    A --> C
    C --> D
    D --> E
    E --> F
    F --> G
    G --> H
    
    style C fill:#ffcccc,color:#000000
    style F fill:#ccffcc,color:#000000
    style G fill:#ccffcc,color:#000000

Die Commit-Historie nach dem Hotfix:

main:    ──●──●──●─────────────●── (v1.2.1)
                 \            /
hotfix:           ●──●──●────┘
                        \
develop: ──●──●──●──●────●── (enthält den Fix)

Umgang mit Konflikten beim Merge in develop

Es ist sehr wahrscheinlich, dass beim Merge des Hotfixes in develop Konflikte auftreten – schließlich wurde in develop weiterentwickelt, während der Hotfix auf dem älteren main-Stand basiert.

Typisches Konflikt-Szenario:

Der Hotfix hat eine Funktion in LoginController.php geändert, aber in develop wurde dieselbe Funktion für ein neues Feature erweitert.

Konfliktlösung in PhpStorm:

  1. Nach dem Merge-Versuch zeigt PhpStorm die konfligierenden Dateien an
  2. Doppelklick auf eine Datei öffnet den 3-Way-Merge-Editor
  3. Du siehst drei Spalten:
    • Links (Yours): Der aktuelle Stand von develop
    • Rechts (Theirs): Der Hotfix
    • Mitte (Result): Das gewünschte Ergebnis
  4. Übernimm die Sicherheitsänderungen aus dem Hotfix
  5. Behalte die neuen Features aus develop
  6. Stelle sicher, dass beides zusammen funktioniert

Wichtig: Nach dem Lösen von Konflikten immer testen! Ein schlecht gelöster Konflikt kann den Fix unwirksam machen.


Was ist mit Feature-Branches?

Wenn zum Zeitpunkt des Hotfixes Feature-Branches existieren, die von develop (oder main) abgezweigt wurden, enthalten auch diese den Bug. Hier gibt es mehrere Strategien:

Option A: Feature-Branches auf develop rebasen

Nachdem der Hotfix in develop gemergt wurde, können Feature-Branch-Entwickler ihren Branch auf den neuen develop-Stand rebasen:

git checkout feature/neues-login
git fetch origin
git rebase origin/develop

Vorteil: Der Feature-Branch enthält automatisch den Fix.
Nachteil: Erfordert einen Force-Push, wenn der Branch bereits gepusht wurde.

Option B: develop in Feature-Branches mergen

Alternativ können die Feature-Branch-Entwickler develop in ihren Branch mergen:

git checkout feature/neues-login
git fetch origin
git merge origin/develop

Vorteil: Kein Force-Push nötig.
Nachteil: Zusätzliche Merge-Commits.

Option C: Nichts tun und beim PR lösen

Wenn der Feature-Branch ohnehin bald gemergt wird, kann der Konflikt auch beim finalen Merge/PR gelöst werden. Der Hotfix landet dann automatisch im Feature-Branch, sobald dieser nach develop gemergt wurde.

Empfehlung: Für sicherheitskritische Hotfixes ist Option A oder B besser, damit alle aktiv entwickelten Branches sofort geschützt sind.


Der Hotfix-Workflow mit Pull Requests

In professionellen Teams wird der Hotfix-Workflow oft mit Pull Requests kombiniert, um Code Reviews auch für kritische Fixes sicherzustellen:

Workflow mit PRs

  1. Hotfix-Branch erstellen und pushen:

    git checkout -b hotfix/kritischer-bug main
    # Fix implementieren
    git push -u origin hotfix/kritischer-bug
    
  2. Ersten PR erstellen: Hotfix → main

    • Auf GitHub: New Pull Request
    • Base: main, Compare: hotfix/kritischer-bug
    • Als Critical oder Urgent markieren
    • Reviewer zuweisen (idealerweise jemand, der sofort verfügbar ist)
  3. Review und Merge in main

    • Schnelles, fokussiertes Review
    • Nach Approval: Merge (nicht Squash, damit der Commit-Hash erhalten bleibt)
    • Tag erstellen
  4. Zweiten PR erstellen: main → develop (oder Hotfix → develop)

    • Base: develop, Compare: main
    • Dieser PR dokumentiert, dass der Hotfix auch in develop übernommen wurde

Automatisierung mit GitHub Actions

Du kannst einen GitHub Actions Workflow erstellen, der automatisch einen PR von main nach develop erstellt, sobald ein Hotfix-Tag gepusht wird:

name: Create Hotfix Backport PR

on:
  push:
    tags:
      - 'v*.*.*'  # Triggert bei Version-Tags

jobs:
  create-backport-pr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          
      - name: Create Pull Request to develop
        uses: peter-evans/create-pull-request@v5
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          branch: backport/${{ github.ref_name }}-to-develop
          base: develop
          title: "Backport ${{ github.ref_name }} to develop"
          body: |
            Automatischer Backport-PR für Hotfix ${{ github.ref_name }}.
            
            **Bitte prüfen und mergen, damit der Fix in develop landet!**
          labels: hotfix, backport

Checkliste für Hotfixes ✅

Hier ist eine praktische Checkliste, die du bei jedem Hotfix abarbeiten solltest:

Vor dem Hotfix

Während des Hotfixes

Nach dem Hotfix


Häufige Fehler und wie du sie vermeidest

Fehler 1: Hotfix von develop statt main erstellen

Problem: Du erstellst den Hotfix von develop, das bereits neue, ungetestete Features enthält. Wenn du jetzt nach main mergst, landen diese Features ungewollt in der Produktion.

Lösung: Immer zuerst git checkout main und dann den Hotfix-Branch erstellen.

Fehler 2: Vergessen, den Hotfix in develop zu mergen

Problem: Der Fix ist in Produktion, aber bei der nächsten großen Release-Integration wird der Bug wieder eingeführt, weil develop den Fix nicht enthält.

Lösung: Nutze die Checkliste oder automatisiere den Backport mit GitHub Actions.

Fehler 3: Zu viele Änderungen im Hotfix

Problem: Du nutzt den Hotfix als Gelegenheit, „schnell noch" andere Dinge zu fixen. Der Hotfix wird groß und schwer zu reviewen, das Risiko für neue Bugs steigt.

Lösung: Disziplin! Andere Fixes kommen in reguläre Feature-Branches. Ein Hotfix ist nur für den kritischen Bug.

Fehler 4: Keinen Tag erstellen

Problem: Ohne Tag ist später schwer nachzuvollziehen, welcher exakte Code-Stand in Produktion deployed wurde.

Lösung: Immer einen semantischen Version-Tag erstellen (v1.2.1 für einen Hotfix auf v1.2.0).


Hotfixes in PhpStorm – Die wichtigsten Shortcuts

Aktion Windows/Linux macOS
Git-Tool-Window öffnen Alt+9 Cmd+9
Neuen Branch erstellen Ctrl+Shift+`` Cmd+Shift+``
Commit-Dialog öffnen Ctrl+K Cmd+K
Push-Dialog öffnen Ctrl+Shift+K Cmd+Shift+K
Pull (Update Project) Ctrl+T Cmd+T
Branches anzeigen Ctrl+Shift+`` Cmd+Shift+``
Git-Log anzeigen Alt+9, dann Tab Log Cmd+9, dann Tab Log

Zusammenfassung

Der Hotfix-Workflow folgt einem klaren Muster:

  1. Branch von main – Nicht von develop!
  2. Minimal fixen – Nur den Bug, nichts anderes
  3. Testen – Automatisiert und manuell
  4. Merge in main – Mit --no-ff und Version-Tag
  5. Merge in develop – Der kritische Schritt, der oft vergessen wird
  6. Aufräumen – Branch löschen, Team informieren

Mit diesem Workflow stellst du sicher, dass kritische Bugfixes schnell in die Produktion gelangen und in allen Entwicklungszweigen ankommen, sodass der Bug nicht bei der nächsten Release-Integration wieder auftaucht.

Kapitel 2: Fortgeschrittenes Branching – Strategien für professionelle Entwicklung

Protected Branches auf GitHub – Dein Sicherheitsnetz für kritische Branches

Protected Branches sind eines der wichtigsten Features, um die Integrität deines Codes zu schützen. Sie verhindern, dass versehentlich (oder absichtlich) schädliche Änderungen direkt in wichtige Branches wie main oder develop gelangen. Stell dir Protected Branches als Türsteher vor, die genau prüfen, wer unter welchen Bedingungen Änderungen einbringen darf.


Was sind Protected Branches? 🛡️

Ein Protected Branch ist ein Branch, für den du auf GitHub spezielle Regeln definierst, die eingehalten werden müssen, bevor Änderungen akzeptiert werden. Ohne Schutzregeln kann jeder mit Schreibrechten direkt auf main pushen – ein einziger falscher Befehl wie git push --force origin main könnte die gesamte Projekthistorie zerstören.

Mit Protected Branches kannst du unter anderem festlegen:

Warum sind sie so wichtig?

Szenario ohne Schutz Mögliche Konsequenz
Entwickler pusht ungetesteten Code direkt auf main Produktionssystem fällt aus
Jemand führt versehentlich git push --force aus Commit-Historie geht verloren
Merge ohne Code Review Bugs und Sicherheitslücken gelangen unbemerkt in die Produktion
Commits von unverifizierten Accounts Supply-Chain-Angriffe werden möglich

Die wichtigsten Schutzregeln im Detail

GitHub bietet eine Vielzahl von Schutzregeln, die du kombinieren kannst. Hier sind die wichtigsten mit Erklärungen und Empfehlungen:

1. „Require a pull request before merging"

Diese Regel ist das Herzstück der Branch Protection. Sie verhindert, dass irgendjemand direkt auf den geschützten Branch pushen kann. Alle Änderungen müssen über einen Pull Request laufen.

Unteroptionen:

2. „Require status checks to pass before merging"

Diese Regel stellt sicher, dass automatisierte Prüfungen (wie Tests oder Linting) erfolgreich durchlaufen müssen, bevor ein Merge möglich ist. Das ist dein Qualitätstor.

Unteroptionen:

# Beispiel: Workflow, der als Required Check verwendet werden kann
name: Tests
on: [push, pull_request]
jobs:
  phpunit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run PHPUnit
        run: vendor/bin/phpunit

3. „Require conversation resolution before merging"

Wenn Reviewer Kommentare oder Änderungswünsche hinterlassen, müssen diese als „resolved" markiert werden, bevor der PR gemerged werden kann. Das stellt sicher, dass kein Feedback ignoriert wird.

Empfehlung: Aktivieren. Es ist frustrierend als Reviewer, wenn deine Anmerkungen einfach übergangen werden.

4. „Require signed commits"

Mit dieser Regel müssen alle Commits kryptografisch signiert sein (GPG, SSH oder S/MIME). Signierte Commits zeigen auf GitHub ein grünes „Verified"-Badge und beweisen, dass der Commit wirklich von der angegebenen Person stammt.

Empfehlung: Für Open-Source-Projekte oder sicherheitskritische Anwendungen empfohlen. Für kleine, interne Projekte optional. Die Einrichtung erfordert etwas Aufwand (siehe Kapitel 10 des Kurses).

5. „Require linear history"

Diese Regel erzwingt eine lineare Commit-Historie, indem sie nur Squash-Merges oder Rebase-Merges erlaubt – keine regulären Merge-Commits mit zwei Parents.

Empfehlung: Geschmackssache. Eine lineare Historie ist übersichtlicher, aber manche Teams bevorzugen explizite Merge-Commits, weil sie zeigen, wann welcher Feature-Branch integriert wurde. Ich empfehle es für Projekte, die eine saubere, leicht lesbare Historie priorisieren.

6. „Do not allow bypassing the above settings"

Normalerweise können Repository-Administratoren alle Schutzregeln umgehen. Mit dieser Option wird auch Admins das Umgehen verboten.

Empfehlung: Für die meisten Projekte nicht aktivieren, da Admins manchmal legitime Gründe haben, Regeln zu umgehen (z.B. bei Notfall-Hotfixes). Bei sehr kritischen Projekten oder aus Compliance-Gründen kann es sinnvoll sein.

7. „Restrict who can push to matching branches"

Du kannst explizit definieren, welche Personen, Teams oder Apps überhaupt auf den Branch pushen dürfen (selbst über PRs). Das ist nützlich, wenn du z.B. nur bestimmten Maintainern erlauben möchtest, PRs zu mergen.

8. „Allow force pushes" und „Allow deletions"

Diese Optionen sind standardmäßig deaktiviert bei Protected Branches – und das sollte auch so bleiben!

Empfehlung: Niemals aktivieren für main oder andere kritische Branches.


Schritt-für-Schritt-Anleitung: Branch Protection einrichten

So richtest du die Schutzregeln für deinen main-Branch ein:

  1. Navigiere zu den Repository-Einstellungen

    Öffne dein Repository auf GitHub und klicke auf „Settings" (Zahnrad-Symbol) in der oberen Navigationsleiste.

  2. Öffne die Branch-Einstellungen

    In der linken Seitenleiste findest du unter „Code and automation" den Punkt „Branches". Klicke darauf.

  3. Füge eine Branch Protection Rule hinzu

    Unter „Branch protection rules" klickst du auf „Add branch protection rule" (oder „Add rule" bei neueren Rulesets).

  4. Definiere das Branch-Muster

    Im Feld „Branch name pattern" gibst du den Namen des zu schützenden Branches ein. Du kannst:

    • Einen exakten Namen eingeben: main
    • Wildcards verwenden: release/* schützt alle Branches, die mit „release/" beginnen
  5. Wähle deine Schutzregeln

    Aktiviere die gewünschten Optionen (siehe detaillierte Beschreibungen oben).

  6. Speichere die Regel

    Scrolle nach unten und klicke auf „Create" oder „Save changes".


Empfohlene Konfiguration für verschiedene Szenarien

🧑‍💻 Solo-Entwickler (persönliches Projekt)

Auch als Solo-Entwickler können Protected Branches sinnvoll sein – sie schützen dich vor dir selbst!

Regel Empfehlung
Require pull request ❌ Optional (kann Workflow verlangsamen)
Require status checks ✅ Aktivieren, wenn du CI/CD hast
Require signed commits ❌ Optional
Allow force pushes ❌ Deaktiviert lassen
Allow deletions ❌ Deaktiviert lassen

Als Minimalkonfiguration empfehle ich: Keine Force Pushes, keine Deletions, Status Checks falls vorhanden.

👥 Kleines Team (2–5 Entwickler)

Regel Empfehlung
Require pull request ✅ Aktivieren
Require approvals ✅ 1 Approval
Dismiss stale approvals ✅ Aktivieren
Require status checks ✅ Aktivieren
Require branch up to date ✅ Aktivieren
Require conversation resolution ✅ Aktivieren
Allow force pushes ❌ Deaktiviert
Allow deletions ❌ Deaktiviert

🏢 Größeres Team oder Open-Source-Projekt

Regel Empfehlung
Require pull request ✅ Aktivieren
Require approvals ✅ 2 Approvals
Dismiss stale approvals ✅ Aktivieren
Require review from Code Owners ✅ Aktivieren
Require status checks ✅ Aktivieren (mehrere Checks)
Require branch up to date ✅ Aktivieren
Require conversation resolution ✅ Aktivieren
Require signed commits ✅ Aktivieren
Require linear history ⚠️ Team-Entscheidung
Do not allow bypassing ⚠️ Bei hohen Compliance-Anforderungen

CODEOWNERS – Automatische Review-Zuweisung 📋

Die CODEOWNERS-Datei ist ein mächtiges Feature, das perfekt mit Protected Branches zusammenarbeitet. Du definierst darin, wer für welche Teile des Codes verantwortlich ist. Diese Personen werden automatisch als Reviewer zu PRs hinzugefügt, die „ihre" Dateien betreffen.

So erstellst du eine CODEOWNERS-Datei

Die Datei muss an einem dieser Orte liegen:

Syntax und Beispiele

# Dies ist ein Kommentar

# Standard-Owner für alles, was nicht anders definiert ist
* @standard-reviewer

# Bestimmte Dateien/Ordner einem Team zuweisen
/src/api/           @backend-team
/src/frontend/      @frontend-team

# Bestimmte Dateitypen
*.js                @javascript-expert
*.css               @design-team

# Kritische Konfigurationsdateien
/config/            @team-lead @senior-developer
.github/workflows/  @devops-team

# Bestimmte Dateien mehreren Reviewern zuweisen (alle müssen reviewen)
/security/          @security-team @team-lead

CODEOWNERS in Kombination mit Branch Protection

Wenn du in den Branch Protection Rules „Require review from Code Owners" aktivierst, müssen die definierten Code Owners den PR genehmigen. Das stellt sicher, dass z.B.:


Rulesets – Die moderne Alternative 🆕

GitHub hat 2023 Rulesets eingeführt, eine modernere und flexiblere Alternative zu den klassischen Branch Protection Rules. Rulesets bieten einige Vorteile:

Rulesets vs. klassische Branch Protection

Feature Branch Protection Rulesets
Repository-spezifisch
Organisations-weit
Tag Protection
Bypass-Ausnahmen Eingeschränkt Flexibel
API-Steuerung

Empfehlung: Für einzelne Repositories sind die klassischen Branch Protection Rules einfacher. Für Organisationen mit vielen Repositories lohnt es sich, Rulesets zu evaluieren.


Visualisierung: Typischer PR-Workflow mit Branch Protection

flowchart TD
    A["Entwickler erstellt\nFeature-Branch"] --> B["Entwickler pusht\nCommits"]
    B --> C["Pull Request\nwird erstellt"]
    C --> D{"Status Checks\nerfolgreich?"}
    D -->|Nein| E["Entwickler fixt\nProbleme"]
    E --> B
    D -->|Ja| F{"Code Review\nbestanden?"}
    F -->|Nein| G["Entwickler arbeitet\nFeedback ein"]
    G --> B
    F -->|Ja| H{"Branch\naktuell?"}
    H -->|Nein| I["Branch mit main\naktualisieren"]
    I --> D
    H -->|Ja| J{"Alle Conversations\nresolved?"}
    J -->|Nein| K["Offene Diskussionen\nklaeren"]
    K --> J
    J -->|Ja| L["Merge in main\nmoeglich"]
    L --> M["Feature-Branch\nloeschen"]

    style A fill:#e1f5fe,color:#000000
    style L fill:#c8e6c9,color:#000000
    style M fill:#c8e6c9,color:#000000
    style E fill:#ffcdd2,color:#000000
    style G fill:#ffcdd2,color:#000000

Häufige Fragen und Probleme 🤔

„Ich bin Admin, kann aber nicht pushen – warum?"

Wenn „Do not allow bypassing the above settings" aktiviert ist, gelten die Regeln auch für dich. Du musst dann ebenfalls einen PR erstellen.

„Mein Status Check taucht nicht in der Auswahlliste auf"

Status Checks erscheinen erst, nachdem sie mindestens einmal gelaufen sind. Erstelle einen Test-PR oder pushe auf einen Testbranch, um den Workflow auszulösen.

„Wie kann ich in einem Notfall trotzdem direkt pushen?"

Wenn du Admin bist und „Do not allow bypassing" nicht aktiviert ist, kannst du die Regeln umgehen. Alternativ:

  1. Die Regel temporär deaktivieren
  2. Den Notfall-Push machen
  3. Die Regel sofort wieder aktivieren
  4. Dokumentieren, warum das nötig war

Besser: Auch Notfall-Hotfixes über PRs, aber mit minimaler Review-Zeit und einem speziellen hotfix-Label.

„Wie schütze ich mehrere Branches gleichzeitig?"

Nutze Wildcards im Branch-Pattern:


Checkliste für deine Branch Protection 📝

Nutze diese Checkliste, um deine main-Branch-Protection einzurichten:


Fazit

Protected Branches sind kein bürokratisches Hindernis, sondern ein essentielles Werkzeug für professionelle Softwareentwicklung. Sie schützen nicht nur vor böswilligen Änderungen, sondern vor allem vor Versehen und menschlichen Fehlern. Die initiale Einrichtung dauert nur wenige Minuten, kann aber stundenlange Debugging-Sessions oder sogar Datenverlust verhindern.

Selbst als Solo-Entwickler empfehle ich dir, zumindest Force Pushes und Deletions für main zu verbieten. Sobald du im Team arbeitest, sollten Pull Requests mit mindestens einem Review zur Pflicht werden. Die Zeit, die du in Reviews „verlierst", gewinnst du mehrfach zurück durch früh entdeckte Bugs und bessere Code-Qualität.

Kapitel 3: Rebase meistern – Geschichte umschreiben wie ein Profi

Einleitung: git rebase ist einer der mächtigsten und gleichzeitig gefürchtetsten Befehle in Git. Richtig eingesetzt ermöglicht er dir eine saubere, lineare Commit-Historie ohne unnötige Merge-Commits. Mit Interactive Rebase kannst du sogar die Geschichte umschreiben: Commits zusammenfassen, aufteilen, umbenennen oder neu ordnen. Aber Vorsicht: Mit großer Macht kommt große Verantwortung! Das Umschreiben von bereits gepushter Historie kann zu ernsthaften Problemen führen. Dieses Kapitel lehrt dich, Rebase sicher und effektiv einzusetzen.

Kapitel 3: Rebase meistern – Geschichte umschreiben wie ein Profi

Merge vs. Rebase – Der fundamentale Unterschied

Der Unterschied zwischen git merge und git rebase ist eines der wichtigsten Konzepte, das du als fortgeschrittener Git-Nutzer wirklich verstanden haben solltest. Beide Befehle dienen demselben Zweck – Änderungen aus einem Branch in einen anderen zu integrieren – aber sie tun dies auf völlig unterschiedliche Weise, mit unterschiedlichen Auswirkungen auf deine Commit-Historie.


Die Ausgangssituation

Stellen wir uns folgendes Szenario vor: Du arbeitest an einem Feature-Branch, während dein Team weiterhin Commits auf main macht. Nach einiger Zeit sieht deine Repository-Struktur so aus:

gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    commit id: "X"
    commit id: "Y"
    checkout main
    commit id: "C"
    commit id: "D"

Du hast auf deinem feature-Branch die Commits X und Y erstellt, während auf main in der Zwischenzeit C und D hinzugekommen sind. Jetzt möchtest du die Änderungen von main in deinen Feature-Branch integrieren (oder umgekehrt deinen Feature-Branch in main einbringen). Hier kommen Merge und Rebase ins Spiel.


Was passiert bei einem Merge?

Ein Merge erstellt einen neuen Merge-Commit, der die beiden Entwicklungslinien zusammenführt. Dieser Merge-Commit hat zwei Eltern-Commits – er verbindet buchstäblich die beiden Branches miteinander.

Der Merge-Befehl

# Du bist auf feature und möchtest main integrieren
git checkout feature
git merge main

In PhpStorm: Git → Merge... → main auswählen

Das Ergebnis nach dem Merge

gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    commit id: "X"
    commit id: "Y"
    checkout main
    commit id: "C"
    commit id: "D"
    checkout feature
    merge main id: "M" tag: "Merge-Commit"

Der neue Commit M ist der Merge-Commit. Er enthält keine eigenen Code-Änderungen (außer eventuell Konfliktlösungen), sondern dient als „Knotenpunkt", der die beiden Historien verbindet.

Eigenschaften eines Merge

Aspekt Beschreibung
Commit-Historie Bleibt vollständig erhalten, zeigt aber eine verzweigte Struktur
Originalcommits X und Y behalten ihre ursprünglichen SHA-Hashes
Neuer Commit Ein Merge-Commit mit zwei Eltern wird erstellt
Nicht-destruktiv Die bestehende Historie wird niemals verändert

Was passiert bei einem Rebase?

Ein Rebase nimmt deine Commits und „verpflanzt" sie auf eine neue Basis. Statt eines Merge-Commits werden deine Commits neu erstellt – sie werden auf den aktuellen Stand des Ziel-Branches aufgebaut, als hättest du erst jetzt mit der Arbeit begonnen.

Der Rebase-Befehl

# Du bist auf feature und möchtest auf main "umbasieren"
git checkout feature
git rebase main

In PhpStorm: Git → Rebase... → main auswählen

Was genau passiert beim Rebase?

Der Rebase-Prozess läuft in mehreren Schritten ab:

  1. Git identifiziert die Commits, die nur auf feature existieren (X und Y)
  2. Git speichert diese Commits temporär (als Patches)
  3. Git setzt den feature-Branch auf die Spitze von main (Commit D)
  4. Git wendet die gespeicherten Commits nacheinander wieder an

Das Ergebnis sind neue Commits X' und Y', die denselben Inhalt haben wie die Originale, aber:

Das Ergebnis nach dem Rebase

gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "C"
    commit id: "D"
    commit id: "X'" tag: "neu erstellt"
    commit id: "Y'" tag: "neu erstellt"

Beachte: Die ursprünglichen Commits X und Y existieren technisch noch im Repository (erreichbar über git reflog), aber kein Branch zeigt mehr auf sie. Sie werden bei der nächsten Garbage Collection entfernt.


Visueller Vergleich: Merge vs. Rebase

Um den Unterschied noch deutlicher zu machen, hier beide Ergebnisse im direkten Vergleich:

flowchart TB
    subgraph vorher["Ausgangssituation"]
        direction LR
        A1["A"] --> B1["B"]
        B1 --> C1["C"] --> D1["D"]
        B1 --> X1["X"] --> Y1["Y"]
    end
    
    subgraph merge["Nach git merge main"]
        direction LR
        A2["A"] --> B2["B"]
        B2 --> C2["C"] --> D2["D"]
        B2 --> X2["X"] --> Y2["Y"]
        D2 --> M["M"]
        Y2 --> M
    end
    
    subgraph rebase["Nach git rebase main"]
        direction LR
        A3["A"] --> B3["B"] --> C3["C"] --> D3["D"]
        D3 --> X3["X'"] --> Y3["Y'"]
    end
    
    vorher --> merge
    vorher --> rebase
    
    style M fill:#90EE90,color:#000000
    style X3 fill:#FFD700,color:#000000
    style Y3 fill:#FFD700,color:#000000

Die tiefgreifenden Unterschiede im Detail

1. Integrität der Historie vs. Sauberkeit

Merge bewahrt die exakte historische Wahrheit: Es zeigt, dass parallel entwickelt wurde und wann die Zusammenführung stattfand. Das kann wertvoll sein, wenn du später nachvollziehen möchtest, wie die Entwicklung tatsächlich ablief.

Rebase erzeugt eine saubere, lineare Historie, als hätte es nie parallele Entwicklung gegeben. Das macht die Historie leichter lesbar, „lügt" aber technisch gesehen über den tatsächlichen Entwicklungsverlauf.

2. Commit-Identität

Dieser Punkt ist entscheidend für das Verständnis:

# Vor dem Rebase
git log --oneline feature
# abc1234 Y - Feature fertiggestellt
# def5678 X - Feature begonnen

# Nach dem Rebase
git log --oneline feature
# 111aaaa Y' - Feature fertiggestellt  ← NEUER HASH!
# 222bbbb X' - Feature begonnen        ← NEUER HASH!

Die Commits nach dem Rebase sind komplett neue Git-Objekte. Sie haben:

3. Konfliktbehandlung

Bei einem Merge musst du Konflikte einmal lösen – im Merge-Commit. Die Konfliktlösung wird Teil dieses einen Commits.

Bei einem Rebase musst du Konflikte für jeden Commit einzeln lösen, während Git die Commits nacheinander auf die neue Basis anwendet. Das kann aufwändiger sein, führt aber dazu, dass jeder Commit für sich genommen „funktioniert".

# Rebase mit Konflikten
git rebase main
# CONFLICT in datei.php
# Konflikt lösen, dann:
git add datei.php
git rebase --continue
# Möglicherweise nächster Konflikt im nächsten Commit...

4. Auswirkungen auf andere Entwickler

Hier liegt der kritischste Unterschied:

Szenario Merge Rebase
Commits noch nicht gepusht ✅ Sicher ✅ Sicher
Commits bereits gepusht ✅ Sicher ⚠️ Gefährlich!
Andere arbeiten auf dem Branch ✅ Sicher Sehr problematisch!

Wenn du Commits rebasst, die bereits gepusht wurden, änderst du deren SHA-Hashes. Wenn ein Kollege diese Commits bereits hat, entstehen Duplikate in der Historie, weil Git die alten und neuen Commits als unterschiedlich betrachtet.


Wann sollte ich Merge verwenden?

Merge ist die richtige Wahl, wenn:

  1. Du öffentliche Historie integrierst – Wenn du Änderungen aus main in deinen Feature-Branch holst und dieser Branch bereits gepusht wurde, ist Merge sicherer.

  2. Mehrere Entwickler am gleichen Branch arbeiten – Ein Merge verändert keine bestehenden Commits, sodass niemand Probleme bekommt.

  3. Du die komplette Entwicklungshistorie bewahren möchtest – Für Auditing oder Nachvollziehbarkeit kann die „echte" Historie wichtig sein.

  4. Du größere Feature-Branches in main integrierst – Viele Teams bevorzugen hier einen Merge-Commit als „Markierung", dass ein Feature abgeschlossen wurde.

  5. Du unsicher bist – Merge ist die sicherere Option. Im Zweifel: Merge verwenden.

Beispiel: Feature in main mergen

git checkout main
git merge feature --no-ff  # --no-ff erzwingt einen Merge-Commit

Das --no-ff (no fast-forward) Flag ist wichtig, wenn du explizit einen Merge-Commit möchtest, auch wenn ein Fast-Forward möglich wäre.


Wann sollte ich Rebase verwenden?

Rebase ist die richtige Wahl, wenn:

  1. Du deine lokale Arbeit aufräumen möchtest, bevor du sie teilst – Vor dem ersten Push ist Rebase völlig sicher und macht deine Commits präsentabler.

  2. Du deinen Feature-Branch auf den neuesten Stand von main bringen möchtest (und der Branch noch nicht gepusht wurde oder nur du daran arbeitest).

  3. Du eine lineare, saubere Historie bevorzugst – Manche Teams haben die Konvention, dass Feature-Branches vor dem Merge rebased werden müssen.

  4. Du mit Interactive Rebase Commits zusammenfassen oder aufteilen möchtest – Das ist nur mit Rebase möglich.

Beispiel: Feature-Branch aktualisieren

git checkout feature
git fetch origin
git rebase origin/main
# Jetzt ist dein Feature-Branch auf dem neuesten Stand

In PhpStorm kannst du das über Git → Rebase... machen oder über das Git-Log-Fenster (Rechtsklick auf origin/main → „Rebase Current onto Selected").


Der kombinierte Workflow: Das Beste aus beiden Welten

Viele professionelle Teams nutzen einen kombinierten Workflow, der die Vorteile beider Ansätze vereint:

flowchart TD
    A["Feature-Branch erstellen"] --> B["Lokal entwickeln\nmit kleinen Commits"]
    B --> C{"Bereit zum Teilen?"}
    C -->|Nein| B
    C -->|Ja| D["Interactive Rebase:\nCommits aufräumen"]
    D --> E["Rebase auf aktuellen main"]
    E --> F["Push Feature-Branch"]
    F --> G["Pull Request erstellen"]
    G --> H["Code Review"]
    H --> I{"Änderungen nötig?"}
    I -->|Ja| J["Änderungen committen"]
    J --> H
    I -->|Nein| K["Merge oder Squash-Merge\nin main"]
    
    style D fill:#FFD700,color:#000000
    style E fill:#FFD700,color:#000000
    style K fill:#90EE90,color:#000000

Die Schritte im Detail

  1. Während der Entwicklung: Mache so viele kleine Commits wie nötig, ohne dir über die „Schönheit" der Historie Gedanken zu machen.

  2. Vor dem Push: Nutze Interactive Rebase (git rebase -i), um deine Commits aufzuräumen – Work-in-Progress-Commits zusammenfassen, Commit-Messages verbessern.

  3. Vor dem Pull Request: Rebase auf den aktuellen main, um sicherzustellen, dass dein Branch sauber auf dem neuesten Stand aufbaut.

  4. Beim Mergen: Der Pull Request wird entweder mit einem Merge-Commit oder einem Squash-Merge in main integriert.


Die „goldene Regel" des Rebasings

🚨 Rebase niemals Commits, die bereits gepusht wurden und an denen andere arbeiten könnten!

Diese Regel ist so wichtig, dass sie einen eigenen Abschnitt verdient. Hier ist, was passiert, wenn du sie brichst:

Das Katastrophen-Szenario

sequenceDiagram
    participant Du
    participant GitHub
    participant Kollege
    
    Du->>GitHub: Push commits X, Y
    GitHub->>Kollege: Pull commits X, Y
    Note over Kollege: Kollege arbeitet<br/>basierend auf Y
    Du->>Du: Rebase X, Y → X', Y'
    Du->>GitHub: Force Push X', Y'
    Note over GitHub: X, Y ersetzt durch X', Y'
    Kollege->>GitHub: Push neuer Commit Z
    Note over GitHub: KONFLIKT!<br/>Z basiert auf Y,<br/>aber Y existiert nicht mehr
    GitHub-->>Kollege: Rejected!
    Note over Kollege: Muss jetzt manuell<br/>reparieren 😱

Dein Kollege hat jetzt ein ernsthaftes Problem: Seine Arbeit basiert auf Commits, die in der offiziellen Historie nicht mehr existieren.

Was tun, wenn es doch passiert ist?

Falls du versehentlich gepushte Commits rebased hast:

  1. Kommuniziere sofort mit deinem Team – Informiere alle, die betroffen sein könnten.

  2. Koordiniere die Reparatur – Alle müssen ihre lokalen Branches aktualisieren:

    git fetch origin
    git reset --hard origin/feature
    # ACHTUNG: Lokale Änderungen gehen verloren!
    
  3. Lerne daraus – Richte Protected Branches ein, die Force-Push verbieten.


Merge und Rebase in PhpStorm

Merge durchführen

  1. Stelle sicher, dass du auf dem Ziel-Branch bist (z.B. feature)
  2. Gehe zu Git → Merge...
  3. Wähle den Branch, den du einmergen möchtest (z.B. main)
  4. Klicke auf Merge
  5. Bei Konflikten öffnet sich der Merge-Dialog

Rebase durchführen

  1. Stelle sicher, dass du auf dem Branch bist, den du rebasen möchtest (z.B. feature)
  2. Gehe zu Git → Rebase...
  3. Wähle den Branch, auf den du rebasen möchtest (z.B. main)
  4. Klicke auf Rebase
  5. Bei Konflikten:
    • Löse jeden Konflikt im Editor
    • Klicke auf Continue Rebase in der Notification

Tipp: Rebase über das Git-Log-Fenster

Eine besonders intuitive Methode in PhpStorm:

  1. Öffne das Git-Tool-Window (Alt+9 / Cmd+9)
  2. Sieh dir das Commit-Log an
  3. Rechtsklicke auf den Commit, auf den du rebasen möchtest
  4. Wähle Rebase Current onto Selected

Praktisches Beispiel: Ein Tag im Leben eines Entwicklers

Lass mich einen typischen Workflow durchspielen:

Morgens: Feature-Branch erstellen

git checkout main
git pull
git checkout -b feature/user-profile

Während des Tages: Entwickeln mit vielen kleinen Commits

git commit -m "WIP: Grundstruktur angelegt"
git commit -m "WIP: Formular hinzugefügt"
git commit -m "Fix Typo"
git commit -m "WIP: Validierung"
git commit -m "Fertig mit Validierung"

Vor dem Feierabend: Aufräumen mit Interactive Rebase

git rebase -i HEAD~5
# Im Editor: squash die WIP-Commits zusammen
# Ergebnis: 2 saubere Commits statt 5 chaotische

Nächster Morgen: Auf aktuellen main rebasen

git fetch origin
git rebase origin/main
# Konflikte lösen, falls nötig

Push und Pull Request

git push -u origin feature/user-profile
# Pull Request auf GitHub erstellen

Nach dem Review: Merge in main

Auf GitHub: Squash and Merge oder Merge Commit – je nach Team-Konvention.


Zusammenfassung: Merge vs. Rebase auf einen Blick

Kriterium Merge Rebase
Historie Verzweigt, zeigt parallele Entwicklung Linear, als wäre sequentiell entwickelt worden
Neue Commits Ein Merge-Commit Keine neuen Commits (außer neu erstellten)
Original-Commits Bleiben unverändert Werden neu erstellt (neue SHA-Hashes)
Sicherheit Immer sicher Nur sicher für nicht-gepushte Commits
Konfliktlösung Einmal im Merge-Commit Für jeden Commit einzeln
Lesbarkeit Kann unübersichtlich werden Sehr übersichtlich
Empfohlen für Öffentliche Branches, Team-Arbeit Lokale Arbeit, Branch-Aktualisierung

Meine Empfehlung: Starte mit Merge als Standard – es ist sicherer und verzeihender. Nutze Rebase gezielt für lokale Aufräumarbeiten und Branch-Aktualisierungen, bevor du pushst. Mit der Zeit wirst du ein Gefühl dafür entwickeln, wann welcher Ansatz am besten passt. 🎯

Kapitel 3: Rebase meistern – Geschichte umschreiben wie ein Profi

Interactive Rebase meistern – Commits aufräumen wie ein Profi 🧹

Der Interactive Rebase ist eines der mächtigsten Werkzeuge in Git, um deine Commit-Historie nachträglich zu bearbeiten. Stell dir vor, du hättest eine Zeitmaschine für deine Commits: Du kannst sie umbenennen, zusammenfassen, aufteilen, neu ordnen oder komplett entfernen. Das Ergebnis ist eine saubere, verständliche Historie, die anderen (und deinem zukünftigen Ich) das Leben leichter macht.


Was ist Interactive Rebase und wann nutze ich ihn?

Beim normalen git rebase wird dein Branch auf einen anderen Basis-Commit „umgepflanzt". Der Interactive Rebase (git rebase -i) geht einen Schritt weiter: Er öffnet einen Editor, in dem du für jeden einzelnen Commit entscheiden kannst, was damit passieren soll.

Typische Anwendungsfälle:

⚠️ Wichtig: Interactive Rebase schreibt die Historie um. Das bedeutet, dass alle betroffenen Commits neue SHA-Hashes bekommen. Nutze Interactive Rebase nur für Commits, die du noch nicht gepusht hast, oder sei dir der Konsequenzen bewusst (Force-Push erforderlich, Probleme für andere Team-Mitglieder).


Die Grundlagen: Interactive Rebase auf der Kommandozeile

Bevor wir zu PhpStorm kommen, ist es wichtig, die Grundlagen auf der Kommandozeile zu verstehen. Das hilft dir, zu verstehen, was PhpStorm im Hintergrund macht.

Den Interactive Rebase starten

Um die letzten n Commits zu bearbeiten, verwendest du:

git rebase -i HEAD~n

Zum Beispiel, um die letzten 4 Commits zu bearbeiten:

git rebase -i HEAD~4

Alternativ kannst du auch einen spezifischen Commit-Hash angeben – dann werden alle Commits nach diesem Commit bearbeitet:

git rebase -i abc1234

Was passiert nach dem Start?

Git öffnet deinen konfigurierten Editor (z.B. Vim, VS Code, oder den von PhpStorm) mit einer Liste aller betroffenen Commits. Diese Liste sieht ungefähr so aus:

pick a1b2c3d Feature: Login-Formular hinzugefügt
pick e4f5g6h WIP: Validierung angefangen
pick i7j8k9l Tippfehler korrigiert
pick m0n1o2p Validierung fertiggestellt

# Rebase abc1234..m0n1o2p onto abc1234 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's message
# d, drop = remove commit

Wichtig zu verstehen: Die Commits werden in chronologischer Reihenfolge angezeigt (ältester oben, neuester unten) – das ist umgekehrt zur Anzeige in git log!


Die sechs Rebase-Befehle im Detail

1. pick (p) – Commit unverändert übernehmen ✅

pick a1b2c3d Feature: Login-Formular hinzugefügt

Was es macht: Der Commit wird genau so übernommen, wie er ist – gleicher Inhalt, gleiche Message. Das ist die Standardoption.

Wann verwenden: Wenn ein Commit bereits perfekt ist und keine Änderung braucht.

2. reword (r) – Commit-Message ändern ✏️

reword a1b2c3d Feature: Login-Formular hinzugefügt

Was es macht: Der Commit-Inhalt bleibt unverändert, aber nach dem Speichern der Rebase-Liste öffnet Git einen Editor, in dem du die Commit-Message bearbeiten kannst.

Wann verwenden:

Beispiel-Workflow:

  1. Du änderst pick zu reword in der Liste
  2. Du speicherst und schließt die Liste
  3. Git öffnet einen neuen Editor mit der alten Commit-Message
  4. Du bearbeitest die Message und speicherst
  5. Git setzt den Rebase fort

3. edit (e) – Commit anhalten und bearbeiten 🛠️

edit a1b2c3d Feature: Login-Formular hinzugefügt

Was es macht: Git wendet den Commit an und pausiert dann. Du befindest dich in einem Zustand, in dem du:

Nach deinen Änderungen setzt du den Rebase mit git rebase --continue fort.

Wann verwenden:

Beispiel: Einen Commit in zwei aufteilen:

# Nach dem Pausieren beim edit-Commit:

# Schritt 1: Den Commit "aufmachen", aber Änderungen behalten
git reset HEAD~1

# Schritt 2: Selektiv committen
git add src/Login/Form.php
git commit -m "Feature: Login-Formular HTML-Struktur"

git add src/Login/Validation.php
git commit -m "Feature: Login-Formular Validierung"

# Schritt 3: Rebase fortsetzen
git rebase --continue

4. squash (s) – Mit vorherigem Commit verschmelzen 🔗

pick a1b2c3d Feature: Login-Formular hinzugefügt
squash e4f5g6h WIP: Validierung angefangen
squash i7j8k9l Validierung fertiggestellt

Was es macht: Der Commit wird mit dem vorherigen Commit (dem darüber in der Liste) verschmolzen. Die Änderungen beider Commits werden kombiniert. Git öffnet dann einen Editor, in dem du eine neue, kombinierte Commit-Message schreiben kannst – dabei siehst du die Messages aller beteiligten Commits.

Wann verwenden:

Der kombinierte Message-Editor sieht so aus:

# This is a combination of 3 commits.
# This is the 1st commit message:

Feature: Login-Formular hinzugefügt

# This is the commit message #2:

WIP: Validierung angefangen

# This is the commit message #3:

Validierung fertiggestellt

# Please enter the commit message for your changes.

Du löschst oder kommentierst die nicht benötigten Zeilen aus und schreibst eine saubere, neue Message:

Feature: Login-Formular mit Validierung hinzugefügt

- Formular-HTML-Struktur erstellt
- Client-seitige Validierung implementiert
- Server-seitige Validierung hinzugefügt

5. fixup (f) – Verschmelzen ohne Message 🧽

pick a1b2c3d Feature: Login-Formular hinzugefügt
fixup e4f5g6h WIP
fixup i7j8k9l Tippfehler

Was es macht: Genau wie squash, aber die Commit-Message des fixup-Commits wird automatisch verworfen. Es wird nur die Message des Basis-Commits (des pick-Commits darüber) verwendet. Kein Editor öffnet sich für die Message-Bearbeitung.

Wann verwenden:

Tipp: Du kannst im Voraus Commits erstellen, die für Fixup gedacht sind, mit:

git commit --fixup=abc1234

Git erstellt dann automatisch einen Commit mit der Message fixup! <Original-Message>. Später kannst du mit git rebase -i --autosquash diese Fixup-Commits automatisch an die richtige Stelle sortieren lassen!

6. drop (d) – Commit komplett entfernen 🗑️

pick a1b2c3d Feature: Login-Formular hinzugefügt
drop e4f5g6h Debug-Code versehentlich committed
pick i7j8k9l Validierung fertiggestellt

Was es macht: Der Commit wird komplett aus der Historie entfernt. Die Änderungen dieses Commits sind danach nicht mehr vorhanden – weder die Dateien noch die Commit-Message.

Wann verwenden:

Alternativ: Du kannst auch einfach die Zeile mit dem Commit aus der Liste löschen – das hat denselben Effekt wie drop.

⚠️ Vorsicht: Wenn spätere Commits von den Änderungen des gelöschten Commits abhängen, wirst du Konflikte bekommen!


Commits umsortieren

Eine oft übersehene Funktion: Du kannst die Reihenfolge der Zeilen in der Rebase-Liste einfach ändern, um Commits umzusortieren!

Vorher:

pick a1b2c3d Feature A
pick e4f5g6h Feature C
pick i7j8k9l Feature B

Nachher:

pick a1b2c3d Feature A
pick i7j8k9l Feature B
pick e4f5g6h Feature C

Wann verwenden:

⚠️ Vorsicht: Wenn Commits voneinander abhängen (Commit B ändert Code, den Commit A eingeführt hat), kann das Umsortieren zu Konflikten führen.


Übersicht: Alle Befehle auf einen Blick

Befehl Kurzform Änderungen Message Wann verwenden
pick p behalten behalten Commit ist perfekt
reword r behalten ändern Message korrigieren
edit e pausieren & ändern ändern möglich Commit aufteilen/erweitern
squash s mit vorherigem vereinen kombinieren Commits zusammenfassen
fixup f mit vorherigem vereinen verwerfen Kleine Korrekturen
drop d entfernen entfernen Commit löschen

Interactive Rebase in PhpStorm 🛠️

PhpStorm bietet eine komfortable grafische Oberfläche für Interactive Rebase, die besonders für Einsteiger viel zugänglicher ist als die Kommandozeile.

Methode 1: Über das Git-Log-Fenster

  1. Git-Log öffnen: Gehe zu View → Tool Windows → Git oder klicke unten auf den Reiter „Git"

  2. Den Basis-Commit finden: Scrolle im Log zu dem Commit, ab dem du die Historie bearbeiten möchtest (der Commit selbst wird nicht bearbeitet, nur alle danach)

  3. Kontextmenü öffnen: Rechtsklick auf diesen Commit

  4. Rebase starten: Wähle „Interactively Rebase from Here..."

    ┌─────────────────────────────────────────┐
    │  Commit abc1234                         │
    │  ─────────────────────────              │
    │  → Show Diff                            │
    │  → Copy Revision Number                 │
    │  → Create Branch...                     │
    │  → Checkout Revision                    │
    │  → Interactively Rebase from Here... ◄──│
    │  → Reset Current Branch to Here...      │
    └─────────────────────────────────────────┘
    

Methode 2: Über das Hauptmenü

  1. Gehe zu Git → Rebase...
  2. Wähle im Dialog die Option „Interactive"
  3. Wähle den Basis-Commit oder Branch aus

Methode 3: Über den aktuellen Branch

  1. Klicke unten rechts auf den Branch-Namen in der Statusleiste
  2. Rechtsklick auf deinen aktuellen Branch
  3. Wähle „Rebase Current onto Selected..." und dann die Interactive-Option

Der PhpStorm Interactive Rebase Dialog

Nach dem Starten öffnet sich ein übersichtlicher Dialog:

┌─────────────────────────────────────────────────────────────────────┐
│  Rebasing 4 commits                                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ ▼ Action  │ Commit          │ Message                       │   │
│  ├───────────┼─────────────────┼───────────────────────────────┤   │
│  │ pick    ▼ │ a1b2c3d         │ Feature: Login-Formular       │   │
│  │ squash  ▼ │ e4f5g6h         │ WIP: Validierung              │   │
│  │ fixup   ▼ │ i7j8k9l         │ typo                          │   │
│  │ pick    ▼ │ m0n1o2p         │ Validierung fertig            │   │
│  └───────────┴─────────────────┴───────────────────────────────┘   │
│                                                                     │
│  [↑ Move Up]  [↓ Move Down]  [Show Diff]                           │
│                                                                     │
├─────────────────────────────────────────────────────────────────────┤
│                              [Cancel]  [Start Rebasing]             │
└─────────────────────────────────────────────────────────────────────┘

Aktionen in PhpStorm ausführen

Action ändern: Klicke auf das Dropdown-Menü in der „Action"-Spalte neben jedem Commit. Du siehst alle verfügbaren Optionen:

Commits umsortieren: Wähle einen Commit aus und nutze die Buttons „Move Up" und „Move Down" oder ziehe die Commits per Drag & Drop an die gewünschte Position

Commit-Diff ansehen: Wähle einen Commit und klicke auf „Show Diff", um zu sehen, welche Änderungen dieser Commit enthält – sehr hilfreich, um zu entscheiden, was damit passieren soll

Commit-Message vorab bearbeiten: Bei reword kannst du in manchen PhpStorm-Versionen die neue Message direkt im Dialog eingeben, ohne dass später ein separater Editor öffnet

Schritt-für-Schritt: Commits in PhpStorm squashen

Hier ein konkretes Beispiel – du hast diese Commits und möchtest sie aufräumen:

pick a1b2c3d Feature: User-Profil-Seite erstellt
pick e4f5g6h WIP
pick i7j8k9l noch mehr WIP
pick m0n1o2p Profil-Seite fertig

Ziel: Alle vier Commits zu einem einzigen, sauberen Commit zusammenfassen.

  1. Rebase starten: Rechtsklick auf den Commit vor a1b2c3d → „Interactively Rebase from Here..."

  2. Actions konfigurieren:

    Commit Aktion Warum
    a1b2c3d pick Basis-Commit, behält die Message
    e4f5g6h fixup Soll zu a1b2c3d hinzugefügt werden, Message verwerfen
    i7j8k9l fixup Soll auch hinzugefügt werden, Message verwerfen
    m0n1o2p fixup Letzter Teil, Message verwerfen
  3. „Start Rebasing" klicken

  4. Ergebnis: Ein einziger Commit a1b2c3d mit der Message „Feature: User-Profil-Seite erstellt", der alle Änderungen enthält

Wenn du squash statt fixup verwendest: PhpStorm öffnet nach dem Rebase einen Editor, in dem du eine neue, kombinierte Message schreiben kannst.

Schritt-für-Schritt: Commit-Message in PhpStorm ändern

  1. Rebase starten

  2. Action auf reword setzen: Ändere die Action für den gewünschten Commit zu „reword"

  3. „Start Rebasing" klicken

  4. Message bearbeiten: PhpStorm öffnet einen Commit-Message-Editor für jeden reword-Commit

    ┌─────────────────────────────────────────────────┐
    │  Edit Commit Message                            │
    ├─────────────────────────────────────────────────┤
    │  ┌─────────────────────────────────────────┐    │
    │  │ Feature: Login-Formular mit Validierung │    │
    │  │                                         │    │
    │  │ - Email-Validierung hinzugefügt         │    │
    │  │ - Passwort-Stärke-Check implementiert   │    │
    │  │ - Fehlerbehandlung verbessert           │    │
    │  └─────────────────────────────────────────┘    │
    │                                                 │
    │                    [Cancel]  [Save Message]     │
    └─────────────────────────────────────────────────┘
    
  5. Speichern: Klicke auf „Save Message" oder drücke Ctrl+Enter

Den edit-Befehl in PhpStorm nutzen

Der edit-Befehl ist etwas spezieller, weil er den Rebase pausiert und dir Kontrolle gibt:

  1. Action auf edit setzen und Rebase starten

  2. PhpStorm-Benachrichtigung: Du siehst eine Notification:

    „Interactive rebase stopped. You can amend the commit or continue rebasing."

  3. Änderungen vornehmen:

    • Dateien bearbeiten
    • Neue Dateien zur Staging Area hinzufügen
    • Commit (ohne Push!) um den aktuellen Commit zu erweitern
    • Oder im Terminal: git commit --amend
  4. Rebase fortsetzen:

    • Über die Notification: Klicke auf „Continue Rebasing"
    • Über das Menü: Git → Continue Rebasing
    • Im Terminal: git rebase --continue

Konflikte während des Interactive Rebase

Beim Umsortieren oder Squashen kann es zu Konflikten kommen, besonders wenn Commits voneinander abhängen.

Konflikte erkennen

PhpStorm zeigt dir eine klare Meldung:

┌─────────────────────────────────────────────────────────────┐
│  ⚠ Rebasing conflict                                        │
│                                                             │
│  Conflicts in the following files:                          │
│  • src/Login/Form.php                                       │
│  • src/Login/Validation.php                                 │
│                                                             │
│  [Resolve Conflicts]  [Abort Rebase]                        │
└─────────────────────────────────────────────────────────────┘

Konflikte lösen

  1. „Resolve Conflicts" klicken: Der Merge-Dialog öffnet sich

  2. 3-Way-Merge nutzen: PhpStorm zeigt dir:

    • Left (Yours/HEAD): Der aktuelle Stand nach den bisherigen Rebase-Schritten
    • Right (Theirs): Die Änderungen aus dem Commit, der gerade angewendet wird
    • Center (Result): Das Ergebnis, das du zusammenbaust
  3. Konflikte einzeln lösen: Nutze die Buttons „Accept Left", „Accept Right" oder bearbeite manuell

  4. Alle Konflikte gelöst: Klicke auf „Apply"

  5. Rebase fortsetzen: Klicke auf „Continue Rebasing" in der Notification

Rebase abbrechen

Wenn du merkst, dass der Rebase in eine Sackgasse führt:

Das stellt den exakten Zustand vor dem Rebase wieder her – als wäre nichts passiert.


Praktisches Beispiel: Kompletter Workflow

Hier ein realistisches Szenario, das alles zusammenbringt:

Ausgangssituation

Du hast an einem Feature gearbeitet und folgende Commits erstellt:

1. abc1234 - WIP: Angefangen mit der API
2. def5678 - API Endpunkt erstellt  
3. ghi9012 - FIXME: Debug-Ausgaben drin!!!
4. jkl3456 - API fertig, aber Typo in Message
5. mno7890 - Dokumentation hinzugefügt

Ziel

Bevor du pushst, möchtest du:

Schritt für Schritt in PhpStorm

  1. Rebase starten: Rechtsklick auf den Commit vor abc1234 → „Interactively Rebase from Here..."

  2. Dialog konfigurieren:

    Original Aktion Neue Position Ergebnis
    mno7890 pick ↑ nach oben verschoben bleibt
    abc1234 pick wird Basis für squash
    def5678 squash verschmilzt mit abc1234
    ghi9012 drop wird entfernt
    jkl3456 reword Message wird korrigiert

    Die neu sortierte Liste sieht so aus:

    pick    mno7890  Dokumentation hinzugefügt
    pick    abc1234  WIP: Angefangen mit der API
    squash  def5678  API Endpunkt erstellt
    reword  jkl3456  API fertig, aber Typo in Message
    
  3. „Start Rebasing" klicken

  4. Squash-Message bearbeiten: PhpStorm öffnet einen Editor für die kombinierte Message:

    Feature: REST-API implementiert
    
    - Endpunkt /api/users erstellt
    - Authentifizierung hinzugefügt
    - Response-Format definiert
    
  5. Reword-Message bearbeiten: Danach öffnet sich der Editor für den Typo-Fix

  6. Ergebnis: Saubere Historie mit drei aussagekräftigen Commits:

    1. Dokumentation hinzugefügt
    2. Feature: REST-API implementiert
    3. API-Tests und finale Korrekturen
    

Best Practices für Interactive Rebase 📋

Vor dem Rebase:

Während des Rebase:

Commit-Messages nach dem Squash:

Wenn etwas schiefgeht:


Zusammenfassung

Interactive Rebase ist ein unverzichtbares Werkzeug für professionelle Git-Nutzung. Mit den sechs Befehlen pick, reword, edit, squash, fixup und drop kannst du deine Commit-Historie perfekt aufräumen, bevor du sie mit der Welt teilst. PhpStorm macht diesen Prozess durch seine grafische Oberfläche besonders zugänglich – du siehst auf einen Blick alle Commits, kannst Actions per Dropdown wählen und Commits per Drag & Drop umsortieren.

Die wichtigsten Takeaways:

Kapitel 3: Rebase meistern – Geschichte umschreiben wie ein Profi

Commits zusammenfassen (Squash) in PhpStorm 🔧

Du hast also fleißig gearbeitet und dabei mehrere kleine Commits erstellt – vielleicht mit Nachrichten wie „WIP", „Fix typo", „Noch ein Fix" oder „Jetzt aber wirklich fertig". Bevor du diese „Commits des Schreckens" auf GitHub pushst, möchtest du sie zu einem einzigen, sauberen Commit zusammenfassen. Das ist nicht nur guter Stil, sondern macht die Historie für dich und dein Team deutlich lesbarer. Lass mich dich Schritt für Schritt durch den gesamten Prozess führen.


Warum überhaupt Commits squashen?

Bevor wir in die Praxis einsteigen, kurz zum Warum: Eine saubere Commit-Historie ist wie ein gut geschriebenes Tagebuch deines Projekts. Wenn du später (oder ein Kollege) nachvollziehen möchtest, welche Änderung wann und warum gemacht wurde, helfen aussagekräftige Commits enorm. Niemand möchte sich durch zehn „WIP"-Commits wühlen, um zu verstehen, was eigentlich passiert ist.

Vorteile des Squashens:


Die Ausgangssituation

Nehmen wir an, du hast in den letzten Stunden an einem Login-Formular gearbeitet und dabei folgende Commits erstellt (vom ältesten zum neuesten):

a1b2c3d  WIP: Login-Formular angefangen
d4e5f6g  Passwort-Feld hinzugefügt
h7i8j9k  Typo im Label gefixt
l0m1n2o  CSS angepasst
p3q4r5s  Validation hinzugefügt

Diese fünf Commits sollen zu einem einzigen werden:

x9y8z7w  feat: Login-Formular mit Validierung implementiert

Methode 1: Der komfortable Weg über das Git-Log in PhpStorm

Dies ist die empfohlene Methode für die meisten Situationen, da sie visuell und intuitiv ist.

  1. Öffne das Git-Tool-Window

    Navigiere zu View → Tool Windows → Git oder nutze die Tastenkombination Alt + 9 (Windows/Linux) bzw. ⌘ + 9 (macOS). Du siehst jetzt das Git-Log mit allen Commits deines Repositories.

  2. Filtere auf deinen aktuellen Branch

    In der Toolbar des Git-Fensters siehst du ein Dropdown-Menü für Branches. Stelle sicher, dass du nur die Commits deines Feature-Branches siehst. Du kannst auch im Suchfeld nach deinem Branch-Namen filtern.

  3. Identifiziere den Commit-Bereich

    Scrolle durch die Liste und identifiziere die Commits, die du zusammenfassen möchtest. In unserem Beispiel sind das die fünf Commits von „WIP: Login-Formular angefangen" bis „Validation hinzugefügt".

  4. Wähle den Basis-Commit aus

    Hier wird es wichtig: Du musst den Commit vor deinem ersten zu squashenden Commit finden. Das ist der Commit, auf dem deine Arbeit aufbaut – typischerweise der letzte Commit, bevor du mit dem Feature angefangen hast. Dieser Commit selbst wird nicht verändert, er dient nur als Referenzpunkt.

  5. Starte den Interactive Rebase

    Klicke mit der rechten Maustaste auf diesen Basis-Commit und wähle im Kontextmenü: Interactively Rebase from Here... (auf Deutsch: „Interaktiv Rebase von hier...").

  6. Das Interactive Rebase-Fenster

    PhpStorm öffnet jetzt ein Fenster mit dem Titel „Rebasing Commits" oder „Git Interactive Rebase". Du siehst eine Liste aller Commits, die nach deinem gewählten Basis-Commit kommen – also genau die Commits, die du bearbeiten möchtest. Die Liste zeigt:

    • Die Aktion (standardmäßig „pick" für jeden Commit)
    • Den Commit-Hash (verkürzt)
    • Die Commit-Nachricht
  7. Commits zum Squashen markieren

    Jetzt kommt der entscheidende Schritt. Du hast mehrere Möglichkeiten, die Aktion für jeden Commit zu ändern:

    • Doppelklick auf die Aktion öffnet ein Dropdown-Menü
    • Rechtsklick auf einen Commit zeigt alle verfügbaren Aktionen
    • Wähle mehrere Commits mit Strg + Klick (Windows/Linux) oder ⌘ + Klick (macOS) und ändere die Aktion für alle gleichzeitig

    Für das Squashen gehst du so vor:

    • Der erste Commit (der älteste, auf den die anderen aufbauen) behält die Aktion „pick"
    • Alle folgenden Commits, die du zusammenfassen möchtest, änderst du auf „squash" oder „fixup"
  8. Der Unterschied zwischen „squash" und „fixup"

    Aktion Beschreibung
    squash Kombiniert den Commit mit dem vorherigen. Die Commit-Nachricht wird beibehalten, und du kannst sie im nächsten Schritt bearbeiten.
    fixup Kombiniert den Commit mit dem vorherigen, aber verwirft die Commit-Nachricht. Nutze das für Commits wie „Typo gefixt", deren Nachricht nicht erhalten bleiben muss.

    Für unser Beispiel könntest du es so konfigurieren:

    pick    a1b2c3d  WIP: Login-Formular angefangen
    squash  d4e5f6g  Passwort-Feld hinzugefügt
    fixup   h7i8j9k  Typo im Label gefixt
    squash  l0m1n2o  CSS angepasst
    squash  p3q4r5s  Validation hinzugefügt
    
  9. Starte den Rebase

    Klicke auf „Start Rebasing" (oder den entsprechenden Button in deiner PhpStorm-Version). PhpStorm beginnt jetzt, die Commits neu zu schreiben.

  10. Bearbeite die kombinierte Commit-Nachricht

    Nach dem Rebase öffnet PhpStorm ein Fenster, in dem du die neue, kombinierte Commit-Nachricht eingeben kannst. Du siehst dort alle Nachrichten der Commits, die mit „squash" markiert waren (nicht die mit „fixup" – deren Nachrichten wurden verworfen). Das sieht ungefähr so aus:

    # This is a combination of 5 commits.
    # The first commit's message is:
    WIP: Login-Formular angefangen
    
    # This is the 2nd commit message:
    Passwort-Feld hinzugefügt
    
    # This is the 4th commit message:
    CSS angepasst
    
    # This is the 5th commit message:
    Validation hinzugefügt
    

    Lösche diesen ganzen Text und schreibe stattdessen eine saubere, aussagekräftige Commit-Nachricht:

    feat: Login-Formular mit Validierung implementiert
    
    - Login-Formular mit Benutzername- und Passwort-Feld erstellt
    - Client-seitige Validierung für beide Felder hinzugefügt
    - CSS-Styling für konsistentes Design angepasst
    
  11. Bestätige die Nachricht

    Klicke auf „OK" oder „Commit", um den Prozess abzuschließen.


Methode 2: Der schnelle Weg mit „Squash Commits" direkt in PhpStorm

PhpStorm bietet auch eine direkte Squash-Funktion, die noch schneller ist, wenn du weißt, welche Commits du zusammenfassen möchtest.

  1. Öffne das Git-Log wie in Methode 1 beschrieben (Alt + 9 oder ⌘ + 9).

  2. Wähle die Commits aus, die du zusammenfassen möchtest. Halte Strg (Windows/Linux) oder (macOS) gedrückt und klicke auf jeden Commit, den du squashen möchtest. Die Commits müssen aufeinanderfolgend sein.

  3. Rechtsklick auf die Auswahl und wähle im Kontextmenü: Squash Commits... (auf Deutsch: „Commits zusammenfassen...").

  4. Bearbeite die Commit-Nachricht im erscheinenden Dialog und bestätige mit „OK".

Diese Methode ist schneller, bietet aber weniger Kontrolle als der Interactive Rebase – du kannst zum Beispiel nicht einzelne Commits mit „fixup" statt „squash" markieren.


Methode 3: Über das Terminal in PhpStorm

Falls du lieber mit dem Terminal arbeitest oder die grafischen Methoden aus irgendeinem Grund nicht funktionieren, kannst du den Interactive Rebase auch direkt über die Kommandozeile durchführen.

  1. Öffne das Terminal in PhpStorm

    Navigiere zu View → Tool Windows → Terminal oder nutze Alt + F12 (Windows/Linux) bzw. ⌥ + F12 (macOS).

  2. Finde heraus, wie viele Commits du zusammenfassen möchtest

    Mit git log --oneline siehst du eine kompakte Liste deiner letzten Commits:

    git log --oneline -10
    

    Das zeigt dir die letzten 10 Commits. Zähle, wie viele du squashen möchtest – in unserem Beispiel sind es 5.

  3. Starte den Interactive Rebase

    git rebase -i HEAD~5
    

    Damit sagst du Git: „Ich möchte die letzten 5 Commits interaktiv bearbeiten."

  4. Bearbeite die Commit-Liste im Editor

    Git öffnet einen Text-Editor (in PhpStorm normalerweise der integrierte Editor oder der von dir konfigurierte). Du siehst:

    pick a1b2c3d WIP: Login-Formular angefangen
    pick d4e5f6g Passwort-Feld hinzugefügt
    pick h7i8j9k Typo im Label gefixt
    pick l0m1n2o CSS angepasst
    pick p3q4r5s Validation hinzugefügt
    

    Ändere es zu:

    pick a1b2c3d WIP: Login-Formular angefangen
    squash d4e5f6g Passwort-Feld hinzugefügt
    fixup h7i8j9k Typo im Label gefixt
    squash l0m1n2o CSS angepasst
    squash p3q4r5s Validation hinzugefügt
    

    Du kannst auch die Kurzformen verwenden: p für pick, s für squash, f für fixup.

  5. Speichere und schließe den Editor

    In PhpStorms integriertem Editor klickst du einfach auf den Bestätigungs-Button. Bei Vim-Editoren: :wq und Enter.

  6. Bearbeite die kombinierte Commit-Nachricht wie bei Methode 1 beschrieben.


Was passiert, wenn etwas schief geht? 🆘

Keine Panik! Ein Rebase kann abgebrochen werden, solange er noch läuft:

Im Terminal:

git rebase --abort

In PhpStorm: Wenn der Rebase fehlschlägt (z.B. wegen Konflikten), zeigt PhpStorm eine Benachrichtigung an. Du kannst dort „Abort Rebase" wählen, um zum Zustand vor dem Rebase zurückzukehren.

Wenn du den Rebase schon abgeschlossen hast und merkst, dass etwas nicht stimmt, hilft dir das Reflog:

git reflog

Dort siehst du alle Zustände deines Repositories, auch die vor dem Rebase. Mit git reset --hard HEAD@{n} (wobei n die Nummer aus dem Reflog ist) kannst du zu einem früheren Zustand zurückkehren.


Wichtige Hinweise und Warnungen ⚠️

  1. Nur unpushte Commits squashen!

    Die goldene Regel des Rebasings: Schreibe niemals die Historie um, die bereits gepusht wurde (es sei denn, du bist dir absolut sicher, was du tust, und hast das mit deinem Team abgesprochen). Wenn du bereits gepushte Commits squashst, müsstest du mit git push --force pushen, was die Historie für alle anderen Entwickler kaputt macht.

  2. Force-Push nur bei Feature-Branches

    Wenn du an einem Feature-Branch arbeitest, den nur du nutzt, und du bereits gepusht hast, kannst du nach dem Squash einen Force-Push machen:

    git push --force-with-lease origin feature/login-formular
    

    --force-with-lease ist sicherer als --force, weil es prüft, ob jemand anders in der Zwischenzeit gepusht hat.

  3. Mache vorher ein Backup (optional)

    Wenn du unsicher bist, erstelle vorher einen temporären Branch als Backup:

    git branch backup-vor-squash
    

    Falls etwas schief geht, kannst du immer zu diesem Branch zurückkehren.


Ein Beispiel-Workflow zusammengefasst

flowchart TD
    A["Du hast 5 WIP-Commits\nim Feature-Branch"] --> B["Oeffne Git-Log\nAlt+9 oder Cmd+9"]
    B --> C["Finde den Commit VOR\ndeiner Arbeit"]
    C --> D["Rechtsklick:\nInteractively Rebase from Here"]
    D --> E["Markiere Commits\nmit squash oder fixup"]
    E --> F["Klicke: Start Rebasing"]
    F --> G["Schreibe eine saubere\nkombinierte Commit-Nachricht"]
    G --> H["Bestaetigen"]
    H --> I["Fertig: 1 sauberer Commit\nstatt 5 WIP-Commits"]
    
    style A fill:#ffcccc,color:#000000
    style I fill:#ccffcc,color:#000000

Zusammenfassung

Schritt Aktion
1 Git-Log öffnen (Alt + 9 / ⌘ + 9)
2 Basis-Commit identifizieren (der Commit vor deiner Arbeit)
3 Rechtsklick → „Interactively Rebase from Here..."
4 Commits mit „squash" oder „fixup" markieren
5 „Start Rebasing" klicken
6 Neue, saubere Commit-Nachricht schreiben
7 Bestätigen – fertig! ✅

Mit etwas Übung wird das Squashen zur Routine und deine Commit-Historie wird es dir danken. Dein zukünftiges Ich (und deine Kollegen) werden dich dafür lieben, wenn sie durch eine saubere, nachvollziehbare Historie scrollen können, statt sich durch zwanzig „WIP"- und „Fix"-Commits zu kämpfen! 🎉

Kapitel 3: Rebase meistern – Geschichte umschreiben wie ein Profi

Die „goldene Regel" des Rebasings 🔒

Die goldene Regel des Rebasings lässt sich in einem Satz zusammenfassen:

„Schreibe niemals die Historie von Commits um, die bereits gepusht und mit anderen geteilt wurden."

Diese Regel ist so fundamental, dass sie in der Git-Community fast schon als Gesetz gilt. Um zu verstehen, warum diese Regel so wichtig ist und was passiert, wenn man sie bricht, müssen wir zunächst verstehen, was beim Rebase technisch passiert.


Was passiert beim Rebase technisch?

Wenn du git rebase ausführst, erstellt Git neue Commits – auch wenn der Inhalt identisch bleibt. Das liegt daran, wie Git-Commits aufgebaut sind:

Ein Commit ist durch seinen SHA-1-Hash eindeutig identifiziert, und dieser Hash wird berechnet aus:

Beim Rebase änderst du den Parent-Commit – deine Commits bekommen einen neuen „Vorgänger". Selbst wenn sich sonst nichts ändert, erzeugt das einen komplett neuen Hash. Für Git sind das völlig neue Commits!

flowchart TD
    subgraph vorher["Vor dem Rebase"]
        A1["Commit A\nHash: abc123"] --> B1["Commit B\nHash: def456"]
        B1 --> C1["Dein Commit X\nHash: 111aaa\nParent: def456"]
        C1 --> D1["Dein Commit Y\nHash: 222bbb\nParent: 111aaa"]
    end
    
    subgraph nachher["Nach dem Rebase"]
        A2["Commit A\nHash: abc123"] --> B2["Commit B\nHash: def456"]
        B2 --> E2["Commit C - NEU\nHash: ghi789"]
        E2 --> C2["Dein Commit X - NEU\nHash: 333ccc\nParent: ghi789"]
        C2 --> D2["Dein Commit Y - NEU\nHash: 444ddd\nParent: 333ccc"]
    end
    
    vorher -.->|"git rebase"| nachher
    
    style C1 fill:#ffcccc,color:#000000
    style D1 fill:#ffcccc,color:#000000
    style C2 fill:#ccffcc,color:#000000
    style D2 fill:#ccffcc,color:#000000

Die roten Commits (alte Hashes) existieren nach dem Rebase nicht mehr in deinem Branch – stattdessen gibt es die grünen Commits mit neuen Hashes. Für Git sind das völlig unterschiedliche Commits, auch wenn sie inhaltlich identisch sein können.


Warum ist das Umschreiben öffentlicher Historie so problematisch?

1. Divergierende Historien im Team 🔀

Stell dir folgendes Szenario vor:

  1. Du hast Commits A → B → C gepusht
  2. Dein Kollege hat diese Commits bereits gepullt
  3. Du machst einen Rebase und pusht mit --force
  4. Jetzt hat dein Kollege A → B → C, aber auf dem Server liegt A → B' → C'

Für Git sind das zwei völlig unterschiedliche Branches, die zufällig einen gemeinsamen Vorfahren haben. Wenn dein Kollege jetzt pullt oder pusht, passiert Chaos:

flowchart TD
    subgraph server["GitHub - nach deinem Force-Push"]
        S1["A"] --> S2["B'"] --> S3["C'"]
    end
    
    subgraph kollege["Dein Kollege - lokales Repo"]
        K1["A"] --> K2["B"] --> K3["C"] --> K4["D - seine neue Arbeit"]
    end
    
    subgraph problem["Das Problem beim Pull/Push"]
        P1["Git sieht zwei divergierende\nBranches mit gleichem Namen"]
        P2["Merge-Konflikte oder\nduplizierte Commits"]
        P1 --> P2
    end
    
    server -.->|"git pull"| problem
    kollege -.->|"betrifft"| problem
    
    style S2 fill:#ccffcc,color:#000000
    style S3 fill:#ccffcc,color:#000000
    style K2 fill:#ffcccc,color:#000000
    style K3 fill:#ffcccc,color:#000000
    style P2 fill:#ffdddd,color:#000000

2. Duplizierte Commits 👯

Wenn dein Kollege nach deinem Force-Push versucht, seine Arbeit zu synchronisieren, kann es passieren, dass dieselben Änderungen zweimal in der Historie auftauchen – einmal mit dem alten Hash, einmal mit dem neuen. Die Historie wird unverständlich und aufgebläht.

3. Verlorene Arbeit 💀

Noch schlimmer: Wenn jemand auf den „alten" Commits aufgebaut hat und dann deine neue Historie überschreibt, kann dessen Arbeit komplett verloren gehen – oder es entstehen massive Merge-Konflikte, die sehr schwer aufzulösen sind.

4. Kaputte Referenzen überall 🔗

Commits werden an vielen Stellen referenziert:

Nach einem Force-Push zeigen all diese Referenzen ins Leere – die Commits mit diesen Hashes existieren nicht mehr (oder nur noch im Reflog einzelner Entwickler).

5. Vertrauensverlust in die Historie 📜

Die Git-Historie ist nicht nur ein technisches Artefakt – sie ist auch Dokumentation. Teams verlassen sich darauf, dass die Historie korrekt widerspiegelt, was passiert ist. Wenn regelmäßig umgeschrieben wird, verliert die Historie ihren Wert als verlässliche Quelle der Wahrheit.


Was genau bedeutet „öffentlich" oder „geteilt"?

Die Frage ist nicht, ob ein Commit gepusht wurde, sondern ob andere darauf aufbauen könnten:

Situation Rebase erlaubt? Begründung
Lokale Commits, nie gepusht ✅ Ja Niemand kennt diese Commits
Gepusht auf deinen eigenen Feature-Branch, noch kein PR ⚠️ Mit Vorsicht Wenn du sicher bist, dass niemand anders den Branch gepullt hat
Feature-Branch mit offenem PR ❌ Besser nicht Reviewer haben möglicherweise den Branch ausgecheckt
Feature-Branch, der bereits gemerged wurde ❌ Nein Die Commits sind jetzt Teil der Haupthistorie
main, develop oder andere geteilte Branches ❌ Niemals Hier baut das ganze Team drauf auf

Die Ausnahme: Force-Push auf eigene Feature-Branches

Es gibt eine legitime Situation für Force-Push, die in vielen Teams akzeptiert oder sogar erwünscht ist:

Das Aufräumen deines eigenen Feature-Branches vor dem Merge.

Viele Teams praktizieren folgenden Workflow:

  1. Du arbeitest auf einem Feature-Branch und machst viele kleine „WIP"-Commits
  2. Du pushst regelmäßig als Backup
  3. Vor dem finalen Review räumst du auf: Commits zusammenfassen, Messages verbessern
  4. Du machst einen Force-Push auf deinen Feature-Branch
  5. Dann erst wird der PR final reviewed und gemerged

Voraussetzungen dafür:

In PhpStorm findest du die Force-Push-Option über Git → Push und dann den Haken bei Force Push (⚠️ nur setzen, wenn du dir sicher bist!).


Was tun, wenn du versehentlich öffentliche Historie umgeschrieben hast? 🚨

Okay, es ist passiert. Du hast einen Force-Push auf einen Branch gemacht, auf dem andere aufbauen. Keine Panik – es gibt Lösungsansätze, aber handle schnell!

Sofortmaßnahme: Team informieren 📢

Kommuniziere sofort mit deinem Team über Slack, Teams, oder wie auch immer ihr kommuniziert:

„⚠️ Achtung: Ich habe versehentlich einen Force-Push auf [branch-name] gemacht. Bitte noch nicht pullen/pushen, bis wir das geklärt haben!"

Je schneller du kommunizierst, desto weniger Leute sind betroffen.

Option 1: Alten Zustand wiederherstellen (beste Option, wenn möglich)

Wenn noch niemand (oder fast niemand) die neue Historie gepullt hat, ist die sauberste Lösung, den alten Zustand wiederherzustellen:

  1. Finde den alten Commit-Hash – Der alte Zustand existiert noch im Reflog (lokal) oder vielleicht bei einem Kollegen:

    # In deinem lokalen Repository:
    git reflog show origin/main
    

    Du siehst eine Liste wie:

    abc1234 origin/main@{0}: update by push --force
    def5678 origin/main@{1}: update by push
    

    Der Hash def5678 ist der Zustand vor deinem Force-Push.

  2. Setze den Branch zurück auf den alten Zustand:

    git push --force origin def5678:main
    

    Damit überschreibst du den Branch wieder mit dem ursprünglichen Zustand.

  3. In PhpStorm:

    • Öffne Git → Show Git Log
    • Aktiviere Show All Branches und suche im Reflog
    • Rechtsklick auf den gewünschten Commit → Reset Current Branch to Here
    • Dann Git → Push mit Force-Push-Option

Option 2: Kollegen haben bereits gepullt – Koordiniertes Recovery

Wenn einige Teammitglieder bereits die neue Historie gepullt haben, wird es komplizierter. Hier ist ein koordinierter Ansatz:

  1. Einigt euch auf den „korrekten" Zustand – Welche Version soll gelten? Die alte oder die neue?

  2. Wenn die alte Version gilt:

    • Du stellst den alten Zustand wieder her (siehe Option 1)
    • Jeder, der die neue Version hat, muss seinen lokalen Branch zurücksetzen:
      git fetch origin
      git reset --hard origin/main
      
    • Achtung: Lokale Änderungen, die auf der neuen Historie aufbauen, müssen vorher gesichert werden!
  3. Wenn die neue Version gilt:

    • Die neue Historie bleibt auf dem Server
    • Jeder muss seinen lokalen Branch an die neue Historie anpassen:
      git fetch origin
      git reset --hard origin/main
      
    • Wer eigene Commits auf der alten Historie gemacht hat, muss diese cherry-picken oder rebasen

Option 3: Der „Merge-Workaround" für komplizierte Situationen

Wenn das Chaos zu groß ist und ein Reset nicht praktikabel, gibt es einen pragmatischen Workaround:

  1. Erstelle einen Branch vom alten Zustand (aus dem Reflog eines Kollegen)
  2. Merge diesen Branch in den aktuellen Zustand
  3. Damit sind beide Historien zusammengeführt

Das Ergebnis ist keine saubere Historie, aber alle Änderungen sind erhalten und jeder kann normal weiterarbeiten.

Option 4: Arbeit von betroffenen Kollegen retten

Wenn ein Kollege bereits neue Commits auf der alten Historie gemacht hat, müssen diese gerettet werden:

flowchart TD
    subgraph problem["Ausgangssituation"]
        A1["origin/main - neue Historie\nA → B' → C'"]
        A2["Kollege lokal\nA → B → C → D → E"]
        A3["D und E sind seine neue Arbeit\nauf der alten Historie"]
    end
    
    subgraph loesung["Lösung"]
        B1["1. Kollege notiert sich die Hashes\nvon D und E"]
        B2["2. git fetch origin"]
        B3["3. git reset --hard origin/main"]
        B4["4. git cherry-pick D-hash E-hash"]
        B1 --> B2 --> B3 --> B4
    end
    
    problem --> loesung
    
    style A3 fill:#ffdddd,color:#000000
    style B4 fill:#ccffcc,color:#000000

Der Kollege führt aus:

# 1. Hashes der eigenen Commits notieren
git log --oneline  # z.B. abc1234 (E) und def5678 (D)

# 2. Aktuelle Remote-Historie holen
git fetch origin

# 3. Auf die neue Historie wechseln (ACHTUNG: lokale Änderungen gehen verloren!)
git reset --hard origin/main

# 4. Eigene Commits wieder anwenden
git cherry-pick def5678 abc1234  # D und E

In PhpStorm:

  1. Git → Fetch ausführen
  2. Im Git-Log die eigenen Commits finden und deren Hashes notieren
  3. Rechtsklick auf origin/mainReset Current Branch to Here (Hard)
  4. Im Git-Log die notierten Commits finden → Rechtsklick → Cherry-Pick

Präventivmaßnahmen: Wie vermeide ich diesen Fehler in Zukunft? 🛡️

1. GitHub Branch Protection aktivieren

Für wichtige Branches wie main oder develop solltest du Force-Pushes verbieten:

  1. Gehe zu Settings → Branches → Add rule
  2. Branch name pattern: main
  3. Aktiviere: ✅ Require a pull request before merging
  4. Aktiviere: ✅ Do not allow force pushes (unter „Rules applied to everyone")

Damit kann niemand (auch keine Admins) einen Force-Push auf diesen Branch machen.

2. Git-Konfiguration anpassen

Du kannst Git so konfigurieren, dass Force-Pushes schwieriger werden:

# Statt --force nutze --force-with-lease
# Dieser Befehl schlägt fehl, wenn jemand anders in der Zwischenzeit gepusht hat
git config --global push.default simple
git config --global alias.pushf "push --force-with-lease"

--force-with-lease ist eine „sicherere" Version von --force: Sie prüft, ob der Remote-Branch noch dem entspricht, was du erwartest. Wenn jemand in der Zwischenzeit gepusht hat, schlägt der Push fehl.

3. Lokale Git-Hooks nutzen

Du kannst einen pre-push Hook einrichten, der dich warnt:

Erstelle die Datei .git/hooks/pre-push:

#!/bin/bash

# Warnung bei Force-Push auf geschützte Branches
protected_branches="main master develop"

for branch in $protected_branches; do
    if echo "$@" | grep -q "$branch"; then
        echo "⚠️  WARNUNG: Du versuchst auf den geschützten Branch '$branch' zu pushen!"
        echo "   Bist du sicher? (y/n)"
        read -r answer
        if [ "$answer" != "y" ]; then
            echo "Push abgebrochen."
            exit 1
        fi
    fi
done

4. Workflow-Regel etablieren

Etabliere im Team eine klare Regel:

„Rebase nur für lokale Commits oder eigene, noch nicht gemergte Feature-Branches."

Für alles andere: Merge verwenden.


Zusammenfassung: Die wichtigsten Punkte ✨

Aspekt Empfehlung
Goldene Regel Niemals gepushte/geteilte Historie umschreiben
Lokale Commits Rebase erlaubt und empfohlen für saubere Historie
Eigene Feature-Branches Mit Vorsicht, wenn niemand anders darauf arbeitet
Geteilte Branches Niemals rebasen – immer mergen
Wenn es passiert ist Schnell kommunizieren, alten Zustand aus Reflog wiederherstellen
Prävention Branch Protection, --force-with-lease, Team-Kommunikation

Die goldene Regel schützt nicht nur dein Team vor Chaos, sondern auch dich selbst: Nichts ist frustrierender, als Stunden damit zu verbringen, ein Repository-Chaos zu entwirren, das durch einen unbedachten Force-Push entstanden ist. Im Zweifel: Merge statt Rebase! 🎯