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
- Die vier fundamentalen Objekttypen in Git 🔍
- Git als inhaltsadressierte Datenbank 🔐
- Git-Referenzen, Branches und HEAD – Was wirklich passiert 🔍
- Das Reflog – deine Lebensversicherung bei Git-Unfällen 🛟
- Kapitel 2: Fortgeschrittenes Branching – Strategien für professionelle Entwicklung
- Branching-Strategien im Vergleich: Git Flow, GitHub Flow und Trunk-Based Development
- Long-Running Branches verstehen und synchron halten
- Der Hotfix-Workflow: Kritische Bugfixes sicher in alle Branches bringen 🚨
- Protected Branches auf GitHub – Dein Sicherheitsnetz für kritische Branches
- Kapitel 3: Rebase meistern – Geschichte umschreiben wie ein Profi
Kapitel 1: Git-Interna verstehen – Wie Git wirklich funktioniert
Einleitung: Im Einsteigerkurs hast du gelernt, Git zu benutzen – jetzt ist es Zeit zu verstehen, wie Git unter der Haube funktioniert. Git ist im Kern eine inhaltsadressierte Datenbank, die auf vier fundamentalen Objekttypen basiert: Blobs, Trees, Commits und Tags. Wenn du verstehst, wie diese zusammenhängen, kannst du Git-Probleme schneller diagnostizieren und fortgeschrittene Operationen selbstbewusst durchführen. Du wirst lernen, dass ein Commit nicht einfach „eine Änderung“ ist, sondern ein kompletter Schnappschuss mit Zeiger auf seinen Vorgänger. Dieses Wissen wird dir helfen, Befehle wie rebase, cherry-pick und reflog wirklich zu verstehen, anstatt sie nur auswendig zu lernen.
Die vier fundamentalen Objekttypen in Git 🔍
Git ist im Kern nichts anderes als eine inhaltsadressierte Datenbank – ein elegantes System, das auf nur vier fundamentalen Objekttypen basiert. Wenn du verstehst, wie diese Objekte funktionieren und zusammenhängen, wirst du Git nie wieder als „magische Black Box" wahrnehmen, sondern als das durchdachte, transparente System, das es wirklich ist.
Das Fundament: Gits Objektmodell
Jedes Git-Repository speichert seine gesamte Geschichte in Form von Objekten im Verzeichnis .git/objects/. Jedes Objekt wird durch einen SHA-1-Hash identifiziert – eine 40-stellige hexadezimale Zeichenkette, die aus dem Inhalt des Objekts berechnet wird. Das bedeutet: Gleicher Inhalt ergibt immer den gleichen Hash, egal auf welchem Computer oder in welchem Repository.
flowchart TB
subgraph Objektdatenbank[".git/objects/"]
B1["Blob\nfa3c2..."]
B2["Blob\n8b137..."]
B3["Blob\ne69de..."]
T1["Tree\na1b2c..."]
T2["Tree\nd4e5f..."]
C1["Commit\n7f8d9..."]
C2["Commit\n3a4b5..."]
TAG["Tag\nc6d7e..."]
end
C1 --> T1
C2 --> T2
C2 -.->|parent| C1
T1 --> B1
T1 --> B2
T1 --> T2
T2 --> B3
TAG --> C2
style B1 fill:#e8f4e8,color:#000000
style B2 fill:#e8f4e8,color:#000000
style B3 fill:#e8f4e8,color:#000000
style T1 fill:#fff3e0,color:#000000
style T2 fill:#fff3e0,color:#000000
style C1 fill:#e3f2fd,color:#000000
style C2 fill:#e3f2fd,color:#000000
style TAG fill:#fce4ec,color:#000000
1. Blob – Der Dateiinhalt 📄
Ein Blob (Binary Large Object) ist das einfachste Git-Objekt. Es speichert ausschließlich den Inhalt einer Datei – nichts weiter. Kein Dateiname, keine Berechtigungen, keine Metadaten. Nur der pure Inhalt.
Eigenschaften des Blob-Objekts
- Speichert rohen Dateiinhalt (Text oder Binärdaten)
- Enthält keinen Dateinamen – der Name wird im Tree gespeichert
- Enthält keine Zeitstempel oder andere Metadaten
- Identische Dateien erzeugen identische Blobs (und damit denselben Hash)
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 d6a96aeoder sogargit 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:
- Modus (Dateiberechtigungen, z.B.
100644für normale Dateien,100755für ausführbare Dateien,040000für Unterverzeichnisse) - Objekttyp (blob oder tree)
- SHA-1-Hash des referenzierten Objekts
- Name der Datei oder des Unterverzeichnisses
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:
- object: Der SHA-1-Hash des Commits, auf den der Tag zeigt
- type: Der Typ des referenzierten Objekts (normalerweise
commit) - tag: Der Name des Tags
- tagger: Wer den Tag wann erstellt hat
- Nachricht: Die Tag-Beschreibung
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 HEAD → commit |
-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:
-
Erstelle ein neues Repository mit einigen Dateien und Unterverzeichnissen
-
Finde die Blob-Hashes deiner Dateien mit
git ls-files -s -
Untersuche den Tree deines letzten Commits mit
git cat-file -p HEAD^{tree} -
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?
-
Erstelle einen Annotated Tag und untersuche sein Objekt
Mit diesem Wissen über Git-Interna bist du bestens vorbereitet für die fortgeschrittenen Themen wie Rebase, Reflog und Troubleshooting – denn du verstehst jetzt, was Git eigentlich tut, wenn du diese Befehle ausführst! 🚀
Git als inhaltsadressierte Datenbank 🔐
Git unterscheidet sich fundamental von anderen Versionskontrollsystemen durch seine Architektur als inhaltsadressierte Datenbank (content-addressable storage). Dieses Konzept ist der Schlüssel, um zu verstehen, warum Git so effizient, zuverlässig und schnell ist – und warum bestimmte Operationen überhaupt möglich sind.
Was „inhaltsadressiert" bedeutet
Bei einer inhaltsadressierten Datenbank wird der Speicherort eines Objekts nicht durch einen Namen, einen Pfad oder eine fortlaufende Nummer bestimmt, sondern direkt durch den Inhalt selbst. Genauer gesagt: Git berechnet aus dem Inhalt einer Datei (oder eines anderen Objekts) einen eindeutigen kryptografischen Hash und verwendet diesen Hash als „Adresse" oder Identifikator.
Das steht im Gegensatz zu herkömmlichen Dateisystemen:
| Eigenschaft | Klassisches Dateisystem | Git (inhaltsadressiert) |
|---|---|---|
| Identifikation | Pfad und Dateiname | Hash des Inhalts |
| Umbenennung | Ändert den Identifikator | Hash bleibt gleich |
| Gleicher Inhalt, zwei Orte | Zwei separate Speicherungen | Nur einmal gespeichert |
| Inhalt geändert | Gleicher Name, neuer Inhalt | Neuer Hash = neues Objekt |
Der entscheidende Gedanke ist: Der Inhalt ist die Adresse. Wenn du den Inhalt kennst, kennst du auch, wo und wie er gespeichert ist. Und umgekehrt: Wenn du den Hash hast, weißt du exakt, welcher Inhalt dahintersteckt.
Das SHA-1-Hash-System im Detail
Was ist ein Hash?
Ein Hash (auch „Prüfsumme" oder „Fingerabdruck") ist das Ergebnis einer mathematischen Funktion, die eine beliebig große Eingabe auf eine Ausgabe fester Länge abbildet. Git verwendet dafür den SHA-1-Algorithmus (Secure Hash Algorithm 1), der einen 160-Bit-Hash erzeugt, dargestellt als 40-stellige Hexadezimalzahl.
Beispiel eines Git-Hash:
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Eigenschaften von SHA-1
SHA-1 hat mehrere wichtige Eigenschaften, die es für Git ideal machen:
-
Deterministisch: Derselbe Inhalt erzeugt immer denselben Hash – auf jedem Computer, zu jeder Zeit, in jedem Repository weltweit.
-
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.
-
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.
-
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:
- Wer den Hash berechnet
- Wann der Hash berechnet wird
- Wo (in welchem Repository) der Hash berechnet wird
- Wie die Datei heißt
- In welchem Verzeichnis die Datei liegt
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:
- ❌ Der Dateiname
- ❌ Der Verzeichnispfad
- ❌ Die Dateiberechtigungen (außer dem Executable-Bit)
- ❌ Der Zeitstempel der Datei
- ❌ Der Autor oder Committer
- ❌ Das Repository, in dem die Datei liegt
- ❌ Die Git-Version
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:
- Festplattenfehler
- Netzwerkprobleme beim Push/Pull
- Versehentliche oder absichtliche Manipulation
- Bitrot (schleichende Datenkorruption)
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 🏷️
# Ein Branch ist nur eine Datei mit einem Hash
cat .git/refs/heads/main
# Ausgabe: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# Ein Tag ist ähnlich simpel
cat .git/refs/tags/v1.0.0
# Ausgabe: f1e2d3c4b5a6978... (ein Hash)
Das Erstellen eines neuen Branches ist daher eine O(1)-Operation – es wird nur eine 40-Byte-Datei geschrieben, egal wie groß das Repository ist.
8. Rename Detection funktioniert automatisch 📁
Git speichert keine „Rename"-Operationen explizit. Wenn du eine Datei umbenennst, erkennt Git das beim Betrachten der History automatisch, weil der Blob-Hash identisch bleibt:
# Datei umbenennen
mv old_name.php new_name.php
git add -A
git commit -m "Rename file"
# Git erkennt das Rename anhand des identischen Hashes
git log --follow --stat new_name.php
# Zeigt die komplette History, auch vor dem Rename!
Praktische Demonstration 🧪
Lass uns das Konzept in der Praxis erleben:
Schritt 1: Zwei Dateien mit gleichem Inhalt erstellen
# Neues Test-Repository
mkdir hash-demo && cd hash-demo
git init
# Zwei Dateien mit identischem Inhalt, aber verschiedenen Namen
echo "Identischer Inhalt" > datei1.txt
echo "Identischer Inhalt" > datei2.txt
# Hashes anzeigen (ohne zu committen)
git hash-object datei1.txt
# Ausgabe: z.B. 9c59e24b8b0c4c2a8d3e5f6a7b8c9d0e1f2a3b4c
git hash-object datei2.txt
# Ausgabe: 9c59e24b8b0c4c2a8d3e5f6a7b8c9d0e1f2a3b4c ← Identisch!
Schritt 2: Änderung demonstrieren
# Winzige Änderung an datei2
echo "Identischer Inhalt!" > datei2.txt # Ausrufezeichen hinzugefügt
git hash-object datei2.txt
# Ausgabe: 7f8e9d0c1b2a3456... ← Komplett anderer Hash!
Schritt 3: Objekte im Repository inspizieren
# Dateien zum Staging hinzufügen
git add datei1.txt
git add datei2.txt
git commit -m "Initial commit"
# Blobs im Repository anschauen
find .git/objects -type f
# Zeigt die gespeicherten Objekte
# Einen Blob inspizieren
git cat-file -t 9c59e24 # Typ anzeigen → "blob"
git cat-file -p 9c59e24 # Inhalt anzeigen → "Identischer Inhalt"
Zusammenfassung 📋
Die Inhaltsadressierung ist das architektonische Fundament von Git:
| Aspekt | Bedeutung |
|---|---|
| Identität | Inhalt = Adresse (Hash) |
| Effizienz | Automatische Deduplizierung |
| Integrität | Kryptografische Prüfsummen |
| Verteilung | Keine zentrale ID-Vergabe nötig |
| Performance | Schnelles Vergleichen via Hash |
| Zuverlässigkeit | Manipulation sofort erkennbar |
Wenn du dieses Konzept verinnerlicht hast, werden viele Git-Mechanismen plötzlich logisch: Warum Branches so billig sind, warum git gc Objekte aufräumen kann, warum Rebasing die History „umschreibt" und warum das Ändern alter Commits problematisch ist. Der Hash ist der Schlüssel zu allem. 🔑
Git-Referenzen, Branches und HEAD – Was wirklich passiert 🔍
Um Git wirklich zu verstehen, musst du begreifen, dass Branches keine Kopien deines Codes sind – sie sind lediglich winzige Textdateien, die auf einen Commit zeigen. Dieses Kapitel enthüllt die elegante Einfachheit, die hinter Gits scheinbar komplexem Branching-System steckt.
Das Konzept der Referenzen (refs)
Eine Referenz (kurz: ref) ist in Git nichts anderes als ein lesbarer Name für einen SHA-1-Hash. Anstatt dir kryptische Hashes wie a1b2c3d4e5f6... merken zu müssen, kannst du einfach main, feature/login oder v1.0.0 verwenden.
Was refs technisch sind
Jede Referenz ist eine simple Textdatei, die genau 40 Zeichen enthält – den SHA-1-Hash eines Commits. Das ist alles. Keine Magie, keine komplexe Datenstruktur, nur eine Zeile Text.
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
Die verschiedenen Arten von Referenzen
Git kennt mehrere Kategorien von Referenzen, die alle im .git/-Verzeichnis organisiert sind:
| Referenz-Typ | Speicherort | Beispiel | Zweck |
|---|---|---|---|
| Lokale Branches | .git/refs/heads/ |
refs/heads/main |
Deine lokalen Entwicklungszweige |
| Remote-Tracking-Branches | .git/refs/remotes/ |
refs/remotes/origin/main |
Abbild der Branches auf dem Remote |
| Tags | .git/refs/tags/ |
refs/tags/v1.0.0 |
Benannte Marker für Releases |
| HEAD | .git/HEAD |
– | Zeigt auf den aktuellen Branch |
| Stash | .git/refs/stash |
– | Zwischengespeicherte Änderungen |
Die Anatomie des .git/refs/-Verzeichnisses
Schauen wir uns an, wie ein typisches Repository strukturiert ist:
.git/
├── HEAD ← Zeigt auf aktuellen Branch
├── refs/
│ ├── heads/ ← Alle lokalen Branches
│ │ ├── main ← Der main-Branch (eine Textdatei!)
│ │ ├── develop ← Der develop-Branch
│ │ └── feature/
│ │ └── login ← Feature-Branch mit Unterordner
│ ├── remotes/
│ │ └── origin/ ← Remote-Tracking-Branches
│ │ ├── main
│ │ └── develop
│ └── tags/
│ ├── v1.0.0 ← Lightweight Tag
│ └── v2.0.0 ← Annotated Tag (zeigt auf Tag-Objekt)
└── packed-refs ← Optimierte Speicherung vieler Refs
Praktische Untersuchung in deinem Repository
Du kannst dir die Refs direkt im Dateisystem ansehen:
# Zeige den Inhalt des main-Branch (nur der Hash!)
cat .git/refs/heads/main
# Ausgabe: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
# Liste alle lokalen Branches als Dateien
ls -la .git/refs/heads/
# Zeige alle Referenzen mit git
git show-ref
Die packed-refs-Datei
Bei Repositories mit vielen Branches und Tags optimiert Git die Speicherung, indem es Referenzen in einer einzigen Datei zusammenfasst:
cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 refs/heads/main
b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5 refs/heads/develop
c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6 refs/tags/v1.0.0
Git sucht Referenzen zuerst in den einzelnen Dateien unter refs/ und dann in packed-refs. Wenn du einen Branch aktualisierst, wird eine neue Einzeldatei erstellt, die Vorrang vor dem Eintrag in packed-refs hat.
Branches – Nichts als bewegliche Zeiger
Ein Branch ist eine Referenz, die automatisch mitwandert, wenn du neue Commits erstellst. Das unterscheidet Branches von Tags, die fest an einem Commit verankert bleiben.
Was einen Branch ausmacht
gitGraph
commit id: "C1"
commit id: "C2"
branch feature/login
commit id: "C3"
commit id: "C4"
checkout main
commit id: "C5"
In diesem Diagramm sind main und feature/login keine Kopien des Codes – sie sind lediglich zwei 40-Byte-Dateien, die auf verschiedene Commits zeigen:
.git/refs/heads/main→ zeigt auf C5.git/refs/heads/feature/login→ zeigt auf C4
Was passiert, wenn du einen Branch erstellst?
Wenn du git branch feature/login ausführst, passiert erstaunlich wenig:
- Git ermittelt den aktuellen Commit – den Hash, auf den HEAD (über den aktuellen Branch) zeigt
- Git erstellt eine neue Datei unter
.git/refs/heads/feature/login - 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:
- Löscht Dateien, die im Ziel-Commit nicht existieren
- Erstellt Dateien, die im Ziel-Commit neu sind
- Aktualisiert Dateien, die sich unterscheiden
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:
-
Git erstellt ein neues Commit-Objekt mit:
- Tree-Objekt (Snapshot aller Dateien)
- Parent-Verweis (der vorherige Commit)
- Autor, Committer, Nachricht, Zeitstempel
-
Git ermittelt den aktuellen Branch durch Lesen von HEAD
-
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
- Klicke unten rechts in der Statusleiste auf den Branch-Namen (z.B. „main")
- Du siehst eine Liste aller lokalen und Remote-Branches
- Jeder Eintrag entspricht einer Datei in
.git/refs/
Branch erstellen in PhpStorm
- Rechtsklick auf einen Commit im Git-Log → New Branch...
- Oder: Branch-Menü → + New Branch
- PhpStorm erstellt die entsprechende Datei in
.git/refs/heads/
HEAD und aktuellen Branch sehen
- Der aktuelle Branch ist in der Statusleiste unten rechts sichtbar
- Im Git-Log (Alt+9 → Log-Tab) ist der aktuelle Branch fett markiert und HEAD wird angezeigt
- Bei einem „Detached HEAD"-Zustand zeigt PhpStorm eine Warnung
Refs direkt untersuchen
Du kannst das Terminal in PhpStorm (Alt+F12) nutzen, um die Ref-Dateien direkt zu untersuchen:
# In PhpStorms Terminal:
cat .git/HEAD
cat .git/refs/heads/main
git show-ref --heads
Zusammenfassung der wichtigsten Konzepte
| Konzept | Was es ist | Wo gespeichert | Verhalten |
|---|---|---|---|
| Referenz (ref) | Lesbarer Name für einen Commit-Hash | .git/refs/ |
Basis für Branches und Tags |
| Branch | Beweglicher Zeiger auf einen Commit | .git/refs/heads/<name> |
Wandert mit bei neuen Commits |
| Remote-Tracking-Branch | Abbild eines Remote-Branches | .git/refs/remotes/<remote>/<name> |
Wird bei fetch/pull aktualisiert |
| Tag | Fester Zeiger auf einen Commit | .git/refs/tags/<name> |
Bleibt immer am selben Commit |
| HEAD | Zeiger auf aktuellen Branch | .git/HEAD |
Bestimmt „wo bin ich jetzt" |
| Detached HEAD | HEAD zeigt direkt auf Commit | .git/HEAD (enthält Hash) |
Gefährlich – neue Commits können verloren gehen |
Praktische Übung 📝
Probiere folgende Befehle in einem Test-Repository aus, um das Gelernte zu festigen:
# 1. Untersuche dein aktuelles Repository
cat .git/HEAD
ls -la .git/refs/heads/
cat .git/refs/heads/main
# 2. Erstelle einen neuen Branch und beobachte die Änderungen
git branch test-branch
ls -la .git/refs/heads/
cat .git/refs/heads/test-branch
# 3. Wechsle den Branch und beobachte HEAD
git switch test-branch
cat .git/HEAD
# 4. Mache einen Commit und beobachte die Branch-Referenz
echo "test" > test.txt
git add test.txt
git commit -m "Test commit"
cat .git/refs/heads/test-branch # Neuer Hash!
# 5. Aufräumen
git switch main
git branch -d test-branch
Wenn du diese Übung durchgeführt hast, wirst du Gits Referenz-System nie wieder als mysteriös empfinden! 🎯
Das Reflog – deine Lebensversicherung bei Git-Unfällen 🛟
Das Reflog (Reference Log) ist eines der mächtigsten, aber am wenigsten bekannten Features von Git. Es ist buchstäblich dein Sicherheitsnetz, das dich vor fast jedem Git-Unfall retten kann – selbst wenn du denkst, du hättest Commits unwiederbringlich verloren.
Was genau ist das Reflog?
Das Reflog ist ein lokales Protokoll aller Bewegungen von Referenzen (insbesondere HEAD) in deinem Repository. Jedes Mal, wenn sich HEAD bewegt – sei es durch einen Commit, Checkout, Reset, Rebase, Merge oder irgendeinen anderen Befehl – wird ein Eintrag im Reflog erstellt.
Der entscheidende Unterschied zur normalen Historie
Die normale Git-Historie (die du mit git log siehst) zeigt dir nur die Commits, die von deinem aktuellen Branch aus erreichbar sind. Das Reflog hingegen protokolliert jede Zustandsänderung, unabhängig davon, ob die entsprechenden Commits noch von einem Branch referenziert werden oder nicht.
Stell dir das Reflog wie eine Zeitmaschine vor: Es merkt sich nicht nur, wo du jetzt bist, sondern auch jeden Ort, an dem du jemals warst – selbst wenn der Weg dorthin inzwischen „verschwunden" zu sein scheint.
Was wird im Reflog gespeichert?
Jeder Reflog-Eintrag enthält folgende Informationen:
- SHA-1-Hash des Commits, auf den HEAD zu diesem Zeitpunkt zeigte
- Aktion, die die Bewegung ausgelöst hat (commit, checkout, reset, rebase, merge, etc.)
- Zeitstempel der Aktion
- Nachricht mit zusätzlichen Details (z.B. Commit-Message oder Branch-Name)
Warum ist das Reflog deine „Lebensversicherung"? 🆘
In Git gibt es viele Situationen, in denen du denkst, du hättest Daten verloren:
- Harter Reset: Du führst
git reset --hard HEAD~5aus und merkst, dass du die falschen Commits gelöscht hast - Fehlgeschlagener Rebase: Ein Interactive Rebase geht schief und deine Commits scheinen verschwunden
- Branch gelöscht: Du löschst versehentlich einen Branch, der noch nicht gemergt war
- Falscher Checkout: Du wechselst den Branch und hast vergessen, Änderungen zu committen
- Amend-Unfall: Du machst
git commit --amendund ü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:
HEAD@{0}– der aktuelle Zustand von HEADHEAD@{1}– der vorherige Zustand von HEADHEAD@{2}– der Zustand davor- usw.
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
-
Öffne das Git-Tool-Fenster (unten in PhpStorm) oder über View → Tool Windows → Git
-
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
-
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
-
Öffne das Terminal in PhpStorm (View → Tool Windows → Terminal oder
Alt+F12) -
Führe dort
git reflogaus – du bekommst die gleiche Ausgabe wie im externen Terminal -
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:
- Rechtsklick auf den Commit im Log-Fenster
- Wähle „Reset Current Branch to Here..."
- 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:
- Finde den SHA-Hash im Reflog (Terminal:
git reflog) - In PhpStorm: Git → Uncommitted Changes → Cherry-Pick (oder über das Log-Fenster)
- Gib den SHA-Hash ein oder suche nach dem Commit
Methode 5: Branch aus Reflog-Commit erstellen
- Finde den SHA-Hash des gewünschten Commits
- Git → Branches → New Branch
- Im Dialog kannst du bei „Starting point" den SHA-Hash eingeben
- 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:
- Wenn du dein Repository neu klonst, ist das Reflog leer
- Teammitglieder haben jeweils ihr eigenes, unabhängiges Reflog
- Du kannst das Reflog nicht nutzen, um Commits wiederherzustellen, die du nie lokal hattest
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.
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:
-
main(frühermaster)Der heilige Gral deines Repositories. Dieser Branch enthält ausschließlich produktionsreifen Code. Jeder Commit auf
mainrepräsentiert eine Release-Version und wird typischerweise mit einem Tag versehen (z.B.v1.0.0,v1.1.0). Direktes Committen aufmainist streng verboten – Änderungen kommen nur durch Merges von Release- oder Hotfix-Branches. -
developDer Integrations-Branch für alle neuen Entwicklungen. Hier fließen alle abgeschlossenen Features zusammen.
developrepräsentiert den aktuellen Entwicklungsstand und ist die Basis für neue Feature-Branches. Man könnte sagen:mainist „was der Kunde sieht",developist „was als nächstes kommt". -
feature/*(z.B.feature/user-authentication,feature/shopping-cart)Für jedes neue Feature wird ein eigener Branch von
developabgezweigt. 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 indevelopgemergt und gelöscht. -
release/*(z.B.release/1.2.0)Wenn
developgenü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 inmainals auch zurück indevelopgemergt. -
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
mainabgezweigt, enthalten nur die minimale Änderung zur Fehlerbehebung und werden nach Fertigstellung sowohl inmain(mit neuem Tag) als auch indevelopgemergt.
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:
- Versionierte Software mit klaren Release-Zyklen (z.B. Desktop-Apps, Mobile Apps, Libraries)
- Projekte mit Support für mehrere Versionen gleichzeitig
- Größere Teams (5+ Entwickler) mit spezialisierten Rollen
- Regulierte Umgebungen, die Nachvollziehbarkeit und Audits erfordern
- Software mit längeren Release-Zyklen (Wochen bis Monate)
🚀 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
-
mainist immer deploybarDer
main-Branch muss zu jedem Zeitpunkt in Produktion deployt werden können. Das bedeutet: Nur getesteter, funktionierender Code kommt hinein. -
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). -
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.
-
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.
-
Nach Review und Approval: Merge in
mainSobald der PR approved ist und alle Tests grün sind, wird er in
maingemergt. Bei GitHub Flow gibt es keine Zwischenstationen. -
Sofort nach dem Merge deployen
Der Merge in
maintriggert (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:
- Web-Applikationen und SaaS, die kontinuierlich deployt werden
- Kleine bis mittlere Teams (1–10 Entwickler)
- Projekte mit guter CI/CD-Pipeline und automatisierten Tests
- Agile Teams mit kurzen Sprints und häufigen Releases
- Startups und schnell iterierende Produkte
🎯 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
-
Ein einziger Branch für alle
Es gibt nur
main(den „Trunk"). Alle Entwickler integrieren ihre Änderungen hier. Lange lebende Feature-Branches sind verboten. -
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.
-
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.
-
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.
-
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":
- Erstelle eine Abstraktionsschicht (Interface) vor dem zu ändernden Code
- Implementiere die neue Version hinter dem Interface
- Schalte schrittweise um (Feature Flag oder schrittweise Migration)
- 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:
- Erfahrene, hochperformante Teams mit starker Ingenieurskultur
- Unternehmen mit exzellenter CI/CD-Infrastruktur
- Projekte, die mehrmals täglich deployen wollen
- Teams, die Pair Programming praktizieren (ersetzt Code Review)
- Microservices-Architekturen mit kleinen, fokussierten Repositories
📊 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
- „Mittelgroß" deutet auf ein Team von 2–8 Entwicklern hin
- „PHP-Webprojekt" ist typischerweise eine Web-Applikation, die deployt wird (nicht installierte Software)
- „Regelmäßige Releases" können unterschiedlich interpretiert werden:
- Wöchentlich/zweiwöchentlich → GitHub Flow geeignet
- Monatlich mit festen Versionen → Git Flow könnte passen
- Kontinuierlich → Trunk-Based möglich
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?
-
Einfachheit: Du und dein Team müsst nicht fünf verschiedene Branch-Typen jonglieren. Die Regeln passen auf einen Bierdeckel.
-
Web-Applikation: PHP-Webprojekte werden deployt, nicht installiert. Du brauchst keine parallelen Versionen zu unterstützen – es gibt nur „was gerade live ist".
-
Pull Requests als Qualitätssicherung: Jede Änderung durchläuft einen Review-Prozess. Das ist Gold wert für die Code-Qualität.
-
Flexibilität bei Releases: Du kannst so oft oder selten releasen, wie du möchtest. Ein Release ist einfach ein Tag auf
main. -
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
-
Für jede Aufgabe: Erstelle einen Branch von
maingit checkout main git pull git checkout -b feature/user-dashboard -
Entwickle und committe regelmäßig mit aussagekräftigen Nachrichten
-
Erstelle einen Pull Request auf GitHub, wenn bereit für Review
-
Nach Approval: Merge in
main(ich empfehle „Squash and Merge" für eine saubere Historie) -
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) -
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:
- Du musst mehrere Versionen parallel unterstützen (z.B. v1.x und v2.x gleichzeitig pflegen)
- Du hast sehr lange Release-Zyklen (> 1 Monat) mit umfangreichen QA-Phasen
- Du arbeitest in einer regulierten Branche (Medizin, Finanzen) mit strengen Audit-Anforderungen
- Dein Team ist sehr groß (> 10 Entwickler) und braucht klare Abgrenzungen
Wann Trunk-Based Development interessant wäre
- Du möchtest mehrmals täglich deployen
- Dein Team ist sehr erfahren und praktiziert Pair Programming
- Du hast eine exzellente CI/CD-Pipeline mit < 10 Minuten Build-Zeit
- Du bist bereit, in Feature Flags zu investieren
🎓 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! 😉
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:
- Du erstellst am Montag einen Feature-Branch von
main - Du arbeitest zwei Wochen an deinem Feature
- In der Zwischenzeit wurden 50 Commits in
maingemergt (von anderen Entwicklern oder anderen Features) - Dein Feature-Branch ist jetzt zwei Wochen und 50 Commits hinter
main
Die Probleme, die entstehen können:
- Merge-Konflikte: Je länger du wartest, desto mehr Konflikte häufen sich an. Ein Merge nach zwei Wochen kann dutzende Konflikte haben, die du alle auf einmal lösen musst.
- Inkompatibilitäten: Vielleicht hat jemand eine Funktion geändert, die du verwendest. Dein Code funktioniert nicht mehr mit dem aktuellen Stand.
- Doppelte Arbeit: Möglicherweise wurde bereits Code geschrieben, den du gerade selbst implementierst.
- Veraltete Basis: Du testest dein Feature gegen eine veraltete Codebasis – es könnte in Produktion anders funktionieren.
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?
- Dein Feature-Branch enthält nach dem Merge sowohl deine Commits (F1, F2) als auch die neuen Commits aus
main(C, D) - Ein Merge-Commit dokumentiert, wann du synchronisiert hast
- Die ursprüngliche Historie bleibt vollständig erhalten
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?
- Deine Commits werden „verschoben" – sie basieren jetzt auf dem aktuellen Stand von
main - Es entstehen technisch gesehen neue Commits (mit neuen Hashes), auch wenn der Inhalt identisch ist
- Die Historie sieht aus, als hättest du erst jetzt mit deiner Arbeit begonnen
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:
- Schau in der linken unteren Ecke auf den „Git"-Tab
- Unter „Local Changes" sollten keine Dateien stehen
Falls du unfertige Änderungen hast, nutze Stash:
# Terminal-Variante
git stash push -m "WIP: Newsletter-Formular"
In PhpStorm:
- Menü: Git → Uncommitted Changes → Stash Changes...
- Gib eine aussagekräftige Nachricht ein
- 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:
- Klicke auf das blaue ↓ Pfeil-Symbol in der Toolbar (oder Ctrl+T / Cmd+T)
- 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)
- Öffne das Git-Log: Klicke unten auf den Tab „Git" und dann auf „Log"
- Stelle sicher, dass du auf deinem Feature-Branch bist (sichtbar unten rechts in der Statusleiste)
- Im Log siehst du alle Branches – finde
origin/main - Rechtsklick auf den neuesten Commit von
origin/main - 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ü
- Klicke unten rechts auf den aktuellen Branch-Namen (z.B.
feature/newsletter) - Im Dropdown: Finde „origin/main" unter „Remote Branches"
- Klicke darauf und wähle „Merge into Current"
Methode C: Über das Hauptmenü
- Menü: Git → Merge...
- Wähle
origin/mainaus der Liste - Klicke Merge
Schritt 4: Konflikte lösen (falls vorhanden)
Wenn es Konflikte gibt, öffnet PhpStorm automatisch ein Dialogfenster „Files Merged with Conflicts":
- Klicke auf „Merge..." neben der konfliktbehafteten Datei
- Der 3-Way-Merge-Editor öffnet sich:
- Links: Deine Version (feature/newsletter)
- Rechts: Ihre Version (main)
- Mitte: Das Ergebnis, das du zusammenbaust
- Nutze die
>>und<<Buttons, um Änderungen zu übernehmen - Wenn du fertig bist: Apply
- 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:
- Schau ins Git-Log: Du siehst jetzt einen Merge-Commit mit zwei Eltern-Commits
- Führe deine Tests aus: Stelle sicher, dass alles noch funktioniert
- Pushe deinen Branch:
Ctrl+Shift+Koder 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:
- Wenn du deinen Branch noch nicht gepusht hast: Kein Problem, rebase frei.
- Wenn du schon gepusht hast und alleine am Branch arbeitest: Möglich, aber du brauchst
git push --force-with-lease. - Wenn andere auch an deinem Branch arbeiten: 🚫 Kein Rebase! Du würdest die Arbeit der anderen durcheinanderbringen.
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)
- Sicherstellen, dass das Working Directory sauber ist
- Unfertige Änderungen ggf. stashen
git fetchausführen, umorigin/mainzu aktualisieren
Schritt 2: Rebase in PhpStorm starten
Methode A: Über das Git-Menü (empfohlen für Einsteiger)
- Stelle sicher, dass du auf deinem Feature-Branch bist
- Menü: Git → Rebase...
- Im Dialog „Rebase":
- „Onto": Wähle
origin/main - Die anderen Optionen kannst du auf Standard lassen
- „Onto": Wähle
- Klicke Rebase
Methode B: Über das Branch-Popup
- Klicke unten rechts auf deinen Branch-Namen
- Finde
origin/mainunter „Remote Branches" - Klicke darauf und wähle „Rebase Current onto Selected"
Methode C: Über das Git-Log
- Öffne das Git-Log
- Rechtsklick auf den neuesten Commit von
origin/main - 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:
- Bei einem Konflikt zeigt PhpStorm eine Notification: „Rebase stopped due to conflicts"
- Löse die Konflikte im 3-Way-Merge-Editor (wie beim Merge)
- Nach dem Lösen: Git → Rebase → Continue
- PhpStorm wendet den nächsten Commit an
- Wiederhole, bis alle Commits neu angewendet wurden
Wenn du abbrechen möchtest:
- Git → Rebase → Abort – Stellt den Zustand vor dem Rebase wieder her
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:
Ctrl+Shift+K(oder Git → Push)- PhpStorm zeigt dir, dass ein Force-Push nötig ist
- 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:
- Settings → Version Control → Git
- 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...
- 👥 Mehrere Personen am selben Branch arbeiten – Rebase würde deren Arbeit zerstören
- 📜 Du eine vollständige Audit-Trail brauchst – z.B. in regulierten Umgebungen
- 🆕 Du noch unsicher mit Git bist – Merge ist verzeihender
- 🔀 Dein Team Merge bevorzugt – Konsistenz ist wichtiger als persönliche Vorliebe
Wähle Rebase, wenn...
- 👤 Du alleine am Feature-Branch arbeitest
- ✨ Du eine saubere, lineare Historie bevorzugst
- 📖 Dein Team „Rebase before Merge" als Konvention hat
- 🧹 Du deine Commits vor dem PR aufräumen möchtest (Interactive Rebase)
Der Kompromiss: „Rebase lokal, Merge für Integration"
Viele Teams nutzen eine hybride Strategie:
- Während der Feature-Entwicklung: Rebase nutzen, um den Feature-Branch aktuell zu halten (solange nicht gepusht oder alleine am Branch)
- Für den finalen Merge: Einen normalen Merge (oder Squash-Merge) in
mainmachen
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:
Ctrl+T(oder Git → Update Project)- Wähle Rebase oder Merge (du kannst eine Standardeinstellung setzen)
- PhpStorm führt Fetch + Merge/Rebase in einem Schritt aus
Tipp 3: Konfiguriere deine Standard-Strategie
In PhpStorm:
- Settings → Version Control → Git
- Unter „Update Method": Wähle deine bevorzugte Methode
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!
- Einigt euch auf eine Strategie (Merge oder Rebase)
- Dokumentiert sie in der
CONTRIBUTING.md - Nutzt Branch Protection Rules, um die Strategie durchzusetzen
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:
- Merge-Konflikte klein zu halten
- Gegen aktuellen Code zu entwickeln
- Integrationsprobleme früh zu erkennen
Zwei Strategien stehen zur Verfügung:
-
Merge (
git merge origin/main):- Erzeugt Merge-Commits
- Bewahrt die echte Historie
- Sicher für Team-Branches
-
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.
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:
- Öffne das Git-Tool-Window (
Alt+9/Cmd+9) - Stelle sicher, dass du auf
mainbist (Rechtsklick aufmain→ Checkout) - Führe einen Pull durch, um sicherzustellen, dass du den aktuellen Stand hast
- Klicke auf New Branch (oder
Ctrl+Shift+` /Cmd+Shift+`) - Benenne den Branch nach dem Schema:
hotfix/kurze-beschreibungoderhotfix/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:
hotfix/beschreibung– z.B.hotfix/sql-injection-fixhotfix/vX.Y.Z– z.B.hotfix/v1.2.1(wenn du semantische Versionierung nutzt)hotfix/issue-123– wenn du ein Issue-Tracking-System verwendest
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:
- Mache deine Änderungen im Code
- Öffne das Commit-Tool-Window (
Ctrl+K/Cmd+K) - Wähle nur die relevanten Dateien aus
- Schreibe eine aussagekräftige Commit-Message
- Nutze optional Amend wenn du nachbessern musst
3. Hotfix testen
Bevor du den Hotfix irgendwohin mergst, teste ihn gründlich:
- Führe deine automatisierten Tests aus
- Teste den spezifischen Fix manuell
- Wenn möglich, deploye auf eine Staging-Umgebung
In PhpStorm kannst du Tests direkt ausführen:
- Rechtsklick auf den Test-Ordner → Run Tests
- Oder nutze die Run-Konfiguration für PHPUnit
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:
- Wechsle zu
main(Rechtsklick → Checkout) - Rechtsklick auf deinen Hotfix-Branch → Merge into Current
- PhpStorm zeigt dir den Merge-Dialog – überprüfe die Änderungen
- 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:
- Der Hotfix bleibt in der Historie als eigenständiger „Block" erkennbar
- 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:
- Menü: Git → New Tag
- Oder im Git-Log: Rechtsklick auf den Commit → New Tag
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:
- Wechsle zu
develop(Rechtsklick → Checkout) - Rechtsklick auf
main(oder den Hotfix-Branch) → Merge into Current - 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:
Ctrl+Shift+K/Cmd+Shift+Köffnet den Push-Dialog- Stelle sicher, dass Push Tags aktiviert ist
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:
- Im Git-Tool-Window: Rechtsklick auf den Branch → Delete
- Häkchen setzen bei Delete Tracking Branch um ihn auch remote zu löschen
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:
- Nach dem Merge-Versuch zeigt PhpStorm die konfligierenden Dateien an
- Doppelklick auf eine Datei öffnet den 3-Way-Merge-Editor
- Du siehst drei Spalten:
- Links (Yours): Der aktuelle Stand von
develop - Rechts (Theirs): Der Hotfix
- Mitte (Result): Das gewünschte Ergebnis
- Links (Yours): Der aktuelle Stand von
- Übernimm die Sicherheitsänderungen aus dem Hotfix
- Behalte die neuen Features aus
develop - 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
-
Hotfix-Branch erstellen und pushen:
git checkout -b hotfix/kritischer-bug main # Fix implementieren git push -u origin hotfix/kritischer-bug -
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)
-
Review und Merge in main
- Schnelles, fokussiertes Review
- Nach Approval: Merge (nicht Squash, damit der Commit-Hash erhalten bleibt)
- Tag erstellen
-
Zweiten PR erstellen: main → develop (oder Hotfix → develop)
- Base:
develop, Compare:main - Dieser PR dokumentiert, dass der Hotfix auch in
developübernommen wurde
- Base:
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
- Bug ist verifiziert und reproduzierbar
- Priorität ist klar (ist es wirklich ein kritischer Hotfix?)
- Alle Stakeholder sind informiert
Während des Hotfixes
- Hotfix-Branch von
mainerstellt (nicht vondevelop!) - Nur den Bug behoben, keine anderen Änderungen
- Aussagekräftige Commit-Message mit Issue-Referenz
- Tests geschrieben oder angepasst
- Alle Tests laufen durch
Nach dem Hotfix
- In
maingemergt - Version-Tag erstellt
- In
developgemergt ← Wird am häufigsten vergessen! - Alle Branches gepusht
- Tag gepusht
- Hotfix-Branch gelöscht (lokal und remote)
- Deployment in Produktion durchgeführt
- Fix in Produktion verifiziert
- Team informiert (welche Feature-Branches sollten aktualisiert werden?)
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:
- Branch von
main– Nicht vondevelop! - Minimal fixen – Nur den Bug, nichts anderes
- Testen – Automatisiert und manuell
- Merge in
main– Mit--no-ffund Version-Tag - Merge in
develop– Der kritische Schritt, der oft vergessen wird - 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.
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:
- Wer überhaupt auf den Branch pushen darf
- Ob direkte Pushes erlaubt sind oder nur über Pull Requests
- Welche Prüfungen (Tests, Reviews) bestanden sein müssen
- Ob die Branch-Historie überschrieben werden darf (Force Push)
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:
-
„Require approvals" – Legt fest, wie viele Personen den PR genehmigen müssen, bevor er gemerged werden kann. Du kannst 1 bis 6 erforderliche Approvals einstellen.
Empfehlung: Für kleine Teams (1–3 Entwickler) reicht 1 Approval. Für größere Teams oder kritische Projekte empfehle ich 2 Approvals.
-
„Dismiss stale pull request approvals when new commits are pushed" – Wenn diese Option aktiviert ist, werden bestehende Approvals ungültig, sobald neue Commits zum PR hinzugefügt werden. Das ist wichtig, weil ein Reviewer vielleicht Code genehmigt hat, der danach noch verändert wurde.
Empfehlung: Unbedingt aktivieren! Sonst könnte jemand nach dem Approval noch problematischen Code hinzufügen.
-
„Require review from Code Owners" – Wenn dein Repository eine
CODEOWNERS-Datei hat, müssen die dort definierten Verantwortlichen den PR genehmigen. Mehr dazu später. -
„Require approval of the most recent reviewable push" – Verhindert, dass der Autor des letzten Commits seinen eigenen Code genehmigt (relevant, wenn Maintainer selbst zum PR beitragen).
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:
-
„Require branches to be up to date before merging" – Der Feature-Branch muss auf dem aktuellen Stand des Ziel-Branches sein. Das verhindert, dass Code gemerged wird, der zwar mit einer alten Version von
mainfunktioniert, aber mit den neuesten Änderungen kollidieren könnte.Empfehlung: Aktivieren, auch wenn es manchmal nervig ist, den Branch aktualisieren zu müssen. Es verhindert das „aber auf meinem Branch hat es funktioniert"-Problem.
-
Status Checks auswählen – Du wählst aus, welche Checks bestanden sein müssen. Diese erscheinen erst in der Liste, nachdem sie mindestens einmal gelaufen sind (z.B. durch einen GitHub Actions Workflow).
# 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!
- Force Pushes können die gesamte Commit-Historie überschreiben
- Deletions würden erlauben, den Branch komplett zu löschen
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:
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:
- Repository-Root:
CODEOWNERS .github/CODEOWNERS(empfohlen)docs/CODEOWNERS
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.:
- Änderungen an der Datenbank-Schicht immer vom DBA geprüft werden
- Sicherheitskritischer Code vom Security-Team abgesegnet wird
- Frontend-Änderungen vom Frontend-Lead reviewed werden
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:
- Auf Organisations-Ebene definierbar – Eine Regel für alle Repositories
- Bessere Wildcards – Komplexere Muster möglich
- Tag Protection – Nicht nur Branches, auch Tags schützen
- Bypass-Listen – Feingranulare Kontrolle, wer Regeln umgehen darf
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:
- Die Regel temporär deaktivieren
- Den Notfall-Push machen
- Die Regel sofort wieder aktivieren
- 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:
release/*– Schützt alle Release-Branchesmainunddevelop– Erstelle zwei separate Regeln- Mit Rulesets kannst du auch
mainunddevelopin einer Regel kombinieren
Checkliste für deine Branch Protection 📝
Nutze diese Checkliste, um deine main-Branch-Protection einzurichten:
- Branch Protection Rule für
mainerstellt - „Require pull request before merging" aktiviert
- Anzahl der erforderlichen Approvals festgelegt
- „Dismiss stale approvals" aktiviert
- Status Checks definiert (Tests, Linting)
- „Require branch to be up to date" aktiviert
- „Require conversation resolution" aktiviert
- Force Pushes deaktiviert (Standard)
- Branch-Deletion deaktiviert (Standard)
- CODEOWNERS-Datei erstellt (optional)
- Team über die neuen Regeln informiert
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.
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:
- Git identifiziert die Commits, die nur auf
featureexistieren (X und Y) - Git speichert diese Commits temporär (als Patches)
- Git setzt den
feature-Branch auf die Spitze vonmain(Commit D) - Git wendet die gespeicherten Commits nacheinander wieder an
Das Ergebnis sind neue Commits X' und Y', die denselben Inhalt haben wie die Originale, aber:
- einen anderen Eltern-Commit (sie bauen jetzt auf D auf, nicht auf B)
- einen anderen SHA-Hash (weil sich der Eltern-Commit geändert hat)
- einen anderen Zeitstempel (optional, je nach Konfiguration)
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:
- denselben Autor
- dieselbe Commit-Message
- dieselben Code-Änderungen (Diffs)
- aber einen anderen SHA-Hash (weil der Parent-Commit anders ist)
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:
-
Du öffentliche Historie integrierst – Wenn du Änderungen aus
mainin deinen Feature-Branch holst und dieser Branch bereits gepusht wurde, ist Merge sicherer. -
Mehrere Entwickler am gleichen Branch arbeiten – Ein Merge verändert keine bestehenden Commits, sodass niemand Probleme bekommt.
-
Du die komplette Entwicklungshistorie bewahren möchtest – Für Auditing oder Nachvollziehbarkeit kann die „echte" Historie wichtig sein.
-
Du größere Feature-Branches in
mainintegrierst – Viele Teams bevorzugen hier einen Merge-Commit als „Markierung", dass ein Feature abgeschlossen wurde. -
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:
-
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.
-
Du deinen Feature-Branch auf den neuesten Stand von
mainbringen möchtest (und der Branch noch nicht gepusht wurde oder nur du daran arbeitest). -
Du eine lineare, saubere Historie bevorzugst – Manche Teams haben die Konvention, dass Feature-Branches vor dem Merge rebased werden müssen.
-
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
-
Während der Entwicklung: Mache so viele kleine Commits wie nötig, ohne dir über die „Schönheit" der Historie Gedanken zu machen.
-
Vor dem Push: Nutze Interactive Rebase (
git rebase -i), um deine Commits aufzuräumen – Work-in-Progress-Commits zusammenfassen, Commit-Messages verbessern. -
Vor dem Pull Request: Rebase auf den aktuellen
main, um sicherzustellen, dass dein Branch sauber auf dem neuesten Stand aufbaut. -
Beim Mergen: Der Pull Request wird entweder mit einem Merge-Commit oder einem Squash-Merge in
mainintegriert.
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:
-
Kommuniziere sofort mit deinem Team – Informiere alle, die betroffen sein könnten.
-
Koordiniere die Reparatur – Alle müssen ihre lokalen Branches aktualisieren:
git fetch origin git reset --hard origin/feature # ACHTUNG: Lokale Änderungen gehen verloren! -
Lerne daraus – Richte Protected Branches ein, die Force-Push verbieten.
Merge und Rebase in PhpStorm
Merge durchführen
- Stelle sicher, dass du auf dem Ziel-Branch bist (z.B.
feature) - Gehe zu Git → Merge...
- Wähle den Branch, den du einmergen möchtest (z.B.
main) - Klicke auf Merge
- Bei Konflikten öffnet sich der Merge-Dialog
Rebase durchführen
- Stelle sicher, dass du auf dem Branch bist, den du rebasen möchtest (z.B.
feature) - Gehe zu Git → Rebase...
- Wähle den Branch, auf den du rebasen möchtest (z.B.
main) - Klicke auf Rebase
- 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:
- Öffne das Git-Tool-Window (Alt+9 / Cmd+9)
- Sieh dir das Commit-Log an
- Rechtsklicke auf den Commit, auf den du rebasen möchtest
- 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. 🎯
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:
- Du hast mehrere kleine „WIP"-Commits gemacht und möchtest sie vor dem Push zu einem sauberen Commit zusammenfassen
- Eine Commit-Message enthält einen Tippfehler oder ist nicht aussagekräftig genug
- Du hast versehentlich etwas committed, das nicht in die Historie gehört
- Die Reihenfolge deiner Commits ergibt logisch keinen Sinn
- Du möchtest einen großen Commit nachträglich in kleinere, thematisch getrennte Commits aufteilen
⚠️ 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:
- Tippfehler in der Commit-Message korrigieren
- Eine aussagekräftigere Beschreibung nachträglich hinzufügen
- Ticket-Nummern oder Issue-Referenzen ergänzen
Beispiel-Workflow:
- Du änderst
pickzurewordin der Liste - Du speicherst und schließt die Liste
- Git öffnet einen neuen Editor mit der alten Commit-Message
- Du bearbeitest die Message und speicherst
- 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:
- Dateien ändern kannst
- Weitere Commits hinzufügen kannst
- Den bestehenden Commit mit
git commit --amendmodifizieren kannst - Den Commit sogar in mehrere Commits aufteilen kannst
Nach deinen Änderungen setzt du den Rebase mit git rebase --continue fort.
Wann verwenden:
- Einen großen Commit in mehrere kleinere aufteilen
- Eine vergessene Datei nachträglich zu einem Commit hinzufügen
- Eine Datei aus einem Commit entfernen (aber behalten)
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:
- Mehrere „Work in Progress"-Commits zu einem sauberen Commit zusammenfassen
- Commits, die logisch zusammengehören, vereinen
- Die Historie vor dem Push aufräumen
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:
- Kleine Korrekturen, die zu einem vorherigen Commit gehören
- „Oops, das habe ich vergessen"-Commits
- Commits mit wenig aussagekräftigen Messages wie „WIP", „fix", „typo"
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:
- Ein Commit enthält Testcode oder Debug-Ausgaben, die nicht in die Historie gehören
- Ein Commit war ein Fehler und sollte nie existiert haben
- Du möchtest Rauschen aus der Historie entfernen
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:
- Commits in eine logischere Reihenfolge bringen
- Verwandte Commits zusammenbringen, bevor du sie squashst
- Die Historie für Code-Reviews verständlicher machen
⚠️ 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
-
Git-Log öffnen: Gehe zu View → Tool Windows → Git oder klicke unten auf den Reiter „Git"
-
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)
-
Kontextmenü öffnen: Rechtsklick auf diesen Commit
-
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ü
- Gehe zu Git → Rebase...
- Wähle im Dialog die Option „Interactive"
- Wähle den Basis-Commit oder Branch aus
Methode 3: Über den aktuellen Branch
- Klicke unten rechts auf den Branch-Namen in der Statusleiste
- Rechtsklick auf deinen aktuellen Branch
- 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:
- pick
- edit
- reword (wird in PhpStorm manchmal als „Edit Message" bezeichnet)
- squash
- fixup
- drop (oder „Skip")
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.
-
Rebase starten: Rechtsklick auf den Commit vor
a1b2c3d→ „Interactively Rebase from Here..." -
Actions konfigurieren:
Commit Aktion Warum a1b2c3d pickBasis-Commit, behält die Message e4f5g6h fixupSoll zu a1b2c3d hinzugefügt werden, Message verwerfen i7j8k9l fixupSoll auch hinzugefügt werden, Message verwerfen m0n1o2p fixupLetzter Teil, Message verwerfen -
„Start Rebasing" klicken
-
Ergebnis: Ein einziger Commit
a1b2c3dmit 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
-
Rebase starten
-
Action auf
rewordsetzen: Ändere die Action für den gewünschten Commit zu „reword" -
„Start Rebasing" klicken
-
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] │ └─────────────────────────────────────────────────┘ -
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:
-
Action auf
editsetzen und Rebase starten -
PhpStorm-Benachrichtigung: Du siehst eine Notification:
„Interactive rebase stopped. You can amend the commit or continue rebasing."
-
Ä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
-
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
-
„Resolve Conflicts" klicken: Der Merge-Dialog öffnet sich
-
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
-
Konflikte einzeln lösen: Nutze die Buttons „Accept Left", „Accept Right" oder bearbeite manuell
-
Alle Konflikte gelöst: Klicke auf „Apply"
-
Rebase fortsetzen: Klicke auf „Continue Rebasing" in der Notification
Rebase abbrechen
Wenn du merkst, dass der Rebase in eine Sackgasse führt:
- In PhpStorm: Klicke auf „Abort Rebase" oder gehe zu Git → Abort Rebasing
- Im Terminal:
git rebase --abort
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:
- Die ersten zwei Commits zusammenfassen
- Den Debug-Commit entfernen
- Den Typo in Commit 4 korrigieren
- Die Dokumentation nach oben verschieben (vor die API-Commits)
Schritt für Schritt in PhpStorm
-
Rebase starten: Rechtsklick auf den Commit vor abc1234 → „Interactively Rebase from Here..."
-
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 -
„Start Rebasing" klicken
-
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 -
Reword-Message bearbeiten: Danach öffnet sich der Editor für den Typo-Fix
-
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:
- Stelle sicher, dass dein Working Directory „clean" ist (keine uncommitteten Änderungen)
- Erstelle ggf. einen Backup-Branch:
git branch backup-vor-rebase - Nutze Interactive Rebase vor dem Push, nicht danach
Während des Rebase:
- Arbeite von oben nach unten durch die Liste
- Squashe immer in den älteren Commit (der oben steht)
- Bei Unsicherheit: lieber abbrechen und neu starten
- Nutze
editsparsam – er ist mächtig, aber auch komplex
Commit-Messages nach dem Squash:
- Schreibe eine neue, zusammenfassende Message
- Lösche die alten WIP-Messages komplett
- Folge dem Conventional Commits Format, wenn dein Team es nutzt
Wenn etwas schiefgeht:
git rebase --abortist dein Freund- Das Reflog hat alles gespeichert:
git reflogzeigt dir alle Zustände - Mit
git reset --hard HEAD@{n}kannst du zu jedem vorherigen Zustand zurück
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:
- 🎯 Nutze Interactive Rebase nur für lokale, noch nicht gepushte Commits
- 🔗 Squash/Fixup sind deine Werkzeuge für saubere, atomare Commits
- ✏️ Reword ermöglicht nachträgliche Message-Korrekturen ohne Aufwand
- 🛑 Drop entfernt ungewollte Commits komplett aus der Historie
- 🔧 Edit gibt dir volle Kontrolle, ist aber auch am komplexesten
- ↩️ Im Zweifel:
git rebase --abortund von vorne beginnen
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:
- Lesbarkeit: Ein Commit pro logischer Änderung statt vieler Mikro-Commits
- Einfacheres Debugging: Mit
git bisectfindest du Bugs schneller, wenn jeder Commit einen sinnvollen Zustand repräsentiert - Saubere Pull Requests: Reviewer können die Änderungen besser nachvollziehen
- Professioneller Eindruck: Zeigt, dass du dir Gedanken über deine Arbeit machst
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.
-
Ö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. -
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.
-
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".
-
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.
-
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...").
-
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
-
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"
-
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 -
Starte den Rebase
Klicke auf „Start Rebasing" (oder den entsprechenden Button in deiner PhpStorm-Version). PhpStorm beginnt jetzt, die Commits neu zu schreiben.
-
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ügtLö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 -
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.
-
Öffne das Git-Log wie in Methode 1 beschrieben (
Alt + 9oder⌘ + 9). -
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. -
Rechtsklick auf die Auswahl und wähle im Kontextmenü: Squash Commits... (auf Deutsch: „Commits zusammenfassen...").
-
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.
-
Öffne das Terminal in PhpStorm
Navigiere zu View → Tool Windows → Terminal oder nutze
Alt + F12(Windows/Linux) bzw.⌥ + F12(macOS). -
Finde heraus, wie viele Commits du zusammenfassen möchtest
Mit
git log --onelinesiehst du eine kompakte Liste deiner letzten Commits:git log --oneline -10Das zeigt dir die letzten 10 Commits. Zähle, wie viele du squashen möchtest – in unserem Beispiel sind es 5.
-
Starte den Interactive Rebase
git rebase -i HEAD~5Damit sagst du Git: „Ich möchte die letzten 5 Commits interaktiv bearbeiten."
-
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ügtDu kannst auch die Kurzformen verwenden:
pfür pick,sfür squash,ffür fixup. -
Speichere und schließe den Editor
In PhpStorms integriertem Editor klickst du einfach auf den Bestätigungs-Button. Bei Vim-Editoren:
:wqund Enter. -
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 ⚠️
-
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 --forcepushen, was die Historie für alle anderen Entwickler kaputt macht. -
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-leaseist sicherer als--force, weil es prüft, ob jemand anders in der Zwischenzeit gepusht hat. -
Mache vorher ein Backup (optional)
Wenn du unsicher bist, erstelle vorher einen temporären Branch als Backup:
git branch backup-vor-squashFalls 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! 🎉
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:
- dem Inhalt aller Dateien (Tree)
- der Commit-Message
- dem Autor und Zeitstempel
- dem Parent-Commit (Vorgänger)
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:
- Du hast Commits A → B → C gepusht
- Dein Kollege hat diese Commits bereits gepullt
- Du machst einen Rebase und pusht mit
--force - 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:
- Pull Requests und Code Reviews verweisen auf spezifische Commit-Hashes
- Issue-Tracker und Commit-Messages referenzieren andere Commits
- CI/CD-Pipelines haben Build-Ergebnisse für bestimmte Commits gespeichert
- Deployment-Logs dokumentieren, welcher Commit deployed wurde
- Dokumentation und Changelogs können Commit-Hashes enthalten
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:
- Du arbeitest auf einem Feature-Branch und machst viele kleine „WIP"-Commits
- Du pushst regelmäßig als Backup
- Vor dem finalen Review räumst du auf: Commits zusammenfassen, Messages verbessern
- Du machst einen Force-Push auf deinen Feature-Branch
- Dann erst wird der PR final reviewed und gemerged
Voraussetzungen dafür:
- Der Branch gehört dir allein (niemand anders arbeitet darauf)
- Der PR wurde noch nicht approved/gemerged
- Das Team hat sich auf diesen Workflow geeinigt
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:
-
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/mainDu siehst eine Liste wie:
abc1234 origin/main@{0}: update by push --force def5678 origin/main@{1}: update by pushDer Hash
def5678ist der Zustand vor deinem Force-Push. -
Setze den Branch zurück auf den alten Zustand:
git push --force origin def5678:mainDamit überschreibst du den Branch wieder mit dem ursprünglichen Zustand.
-
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:
-
Einigt euch auf den „korrekten" Zustand – Welche Version soll gelten? Die alte oder die neue?
-
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!
-
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:
- Erstelle einen Branch vom alten Zustand (aus dem Reflog eines Kollegen)
- Merge diesen Branch in den aktuellen Zustand
- 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:
- Git → Fetch ausführen
- Im Git-Log die eigenen Commits finden und deren Hashes notieren
- Rechtsklick auf
origin/main→ Reset Current Branch to Here (Hard) - 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:
- Gehe zu Settings → Branches → Add rule
- Branch name pattern:
main - Aktiviere: ✅ Require a pull request before merging
- 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! 🎯