Nachdem wir verstanden haben, wie Branches organisatorisch genutzt werden, betrachten wir nun die technischen Mechanismen, mit denen Änderungen zwischen Branches integriert werden. Die Wahl der Merge-Strategie hat fundamentale Auswirkungen auf die Struktur der Repository-Historie – und damit auf die Lesbarkeit, Wartbarkeit und praktische Nutzbarkeit des Projekts.
Es gibt drei primäre Merge-Strategien: Fast-Forward, Merge Commit und Squash Merge. Dazu kommt Rebase als alternative Integration-Methode. Jede dieser Strategien erzeugt eine andere Commit-Graph-Struktur, und die Entscheidung zwischen ihnen ist nicht nur technisch, sondern auch philosophisch: Wie wichtig ist eine vollständige, unveränderliche Historie versus eine saubere, lesbare Darstellung der Entwicklung?
Bevor wir in die Strategien eintauchen, ein fundamentales Konzept, das in allen Git-Operationen relevant ist: Wie adressiert man Commits?
In Git existieren viele Wege, auf einen Commit zu verweisen. Diese Addressierungsmechanismen sind nicht spezifisch für Merge, Rebase oder andere Operationen – sie sind ein grundlegendes Feature, das überall in Git funktioniert.
SHA-1 Hash (vollständig): Die eindeutigste Referenz.
git show 7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6SHA-1 Hash (verkürzt): Git akzeptiert Präfixe, solange sie eindeutig sind (meist 7-8 Zeichen).
git show 7e8f9a0Branch-Name: Verweist auf den Commit, auf den der Branch zeigt.
git show main
git log feature/new-uiTag-Name: Verweist auf den getaggten Commit.
git show v1.2.0
git diff v1.1.0 v1.2.0HEAD: Der aktuelle Commit (bzw. der Commit, auf den der aktuelle Branch zeigt).
git show HEAD
git diff HEADGit bietet zwei Operatoren für relative Referenzen: ^
(Caret) und ~ (Tilde). Sie navigieren durch den
Commit-Graph, haben aber unterschiedliche Semantik.
^ wählt Parents aus. Bei Commits mit einem Parent ist
^ simpel:
HEAD^ # Der Parent von HEAD (ein Commit zurück)
HEAD^^ # Der Parent des Parents (zwei Commits zurück)
HEAD^^^ # Drei Commits zurückBei Merge-Commits (mit mehreren Parents) wird
^ interessant:
HEAD^1 # Erster Parent (explizit)
HEAD^2 # Zweiter Parent
HEAD^3 # Dritter Parent (bei Octopus-Merges)Ohne Zahl bedeutet ^ immer den ersten Parent:
HEAD^ = HEAD^1.
Beispiel:
# Zeige den Commit, der gemerged wurde (zweiter Parent)
git show HEAD^2
# Zeige den Commit vor dem gemergten Commit
git show HEAD^2^
# Kombinationen sind möglich
git show HEAD^1^2 # Erster Parent, dann dessen zweiter Parent~ folgt immer dem ersten Parent und zählt
Generationen:
HEAD~1 # Ein Commit zurück (= HEAD^)
HEAD~2 # Zwei Commits zurück (= HEAD^^)
HEAD~5 # Fünf Commits zurück (= HEAD^^^^^)~ ist praktischer für “N Commits zurück” ohne sich um
Merge-Struktur zu kümmern:
# Letzten 3 Commits anzeigen
git log HEAD~3..HEAD
# Diff zwischen vor 5 Commits und jetzt
git diff HEAD~5 HEAD# Letzten Commit rückgängig machen
git reset HEAD~1
# Letzten 3 Commits interaktiv rebasen
git rebase -i HEAD~3
# Diff zwischen aktuellem Branch und main
git diff main..HEAD
# Log der Commits, die in feature sind, aber nicht in main
git log main..feature
# Zeige Änderungen im gemergten Branch
git log HEAD^2
# Checkout eines spezifischen Commits relativ
git checkout HEAD~5
# Cherry-pick eines Commits aus anderem Branch
git cherry-pick feature~2Die Operatoren können kombiniert werden:
HEAD~2^2 # Zwei Commits zurück, dann zweiter Parent
main^^2^ # main, zwei erste Parents zurück, dann zweiter Parent, dann noch ein Parent
HEAD~3^2~2 # Drei zurück, zweiter Parent, zwei weitere zurückDiese Syntax ist universell und funktioniert mit
jedem Git-Befehl: - git show,
git log, git diff - git reset,
git revert, git cherry-pick -
git rebase, git merge,
git checkout - Jedes Tool, das Commit-Referenzen
akzeptiert
Diese Addressierungsmechanismen sind nicht
rebase-spezifisch, merge-spezifisch oder sonst wie spezialisiert. Sie
sind fundamentale Navigation im Commit-Graph – so grundlegend wie
cd .. im Dateisystem.
@ # Synonym für HEAD
@{-1} # Vorheriger Branch (vor checkout)
@{-2} # Zwei Branches zuvor
HEAD@{2.days.ago} # HEAD vor 2 Tagen (via reflog)
main@{yesterday} # main gestern
HEAD@{5} # HEAD vor 5 Operationen (reflog)
branchname@{upstream} # Upstream-Branch (tracking branch)
branchname@{push} # Push-DestinationDiese Addressierung ist mächtig und wird in fortgeschrittenen Workflows unverzichtbar. Sie ist die Sprache, in der wir mit dem Commit-Graph kommunizieren.
Ein Fast-Forward Merge tritt auf, wenn der Ziel-Branch keine neuen Commits seit der Erstellung des Feature-Branches hat. Technisch: Es existiert ein direkter Pfad durch Parent-Referenzen vom Ziel-Branch zum Feature-Branch.
Ausgangssituation:
main feature
↓ ↓
C1 → C2 → C3
main zeigt auf C1, feature auf C3. C3 ist
direkt von C1 erreichbar (C3 → C2 → C1). Es gibt keine Divergenz, keine
parallele Entwicklung.
Bei git merge feature (von main aus): 1.
Git prüft: Ist C3 von C1 erreichbar? Ja. 2. Git verschiebt einfach den
main-Pointer auf C3. 3. Kein neuer Commit wird
erstellt.
$ git checkout main
$ git merge feature
Updating a1b2c3d..7e8f9a0
Fast-forward
file.txt | 10 ++++++++++
1 file changed, 10 insertions(+)
$ cat .git/refs/heads/main
7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6
$ cat .git/refs/heads/feature
7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6Beide Branches zeigen auf denselben Commit. Die Historie bleibt linear:
Keine neuen Commits: Die Commit-Objekte bleiben unverändert. Nur Branch-Pointer bewegen sich.
Lineare Historie: git log zeigt eine
gerade Linie. Keine Merge-Commits, keine Verzweigungen.
Vollständige Informationsbewahrung: Alle Original-Commits bleiben erhalten, mit ihren Messages, Timestamps, Authoren.
Keine Merge-Signatur: Man sieht nicht, dass hier ein Merge stattfand. Die Historie sieht aus, als wäre alles sequenziell auf einem Branch entwickelt worden.
Fast-Forward ist Gits Default-Verhalten, wenn möglich. Man kann es aber steuern:
# Fast-Forward erzwingen (Merge schlägt fehl, wenn FF nicht möglich)
git merge --ff-only feature
# Fast-Forward verhindern (immer Merge-Commit erstellen)
git merge --no-ff featureWarum --no-ff? Um explizit zu markieren, dass hier ein
Feature-Branch integriert wurde, auch wenn technisch ein Fast-Forward
möglich wäre. Dies erhält die Information “dieser Bereich war ein
Feature”.
Wenn beide Branches neue Commits haben, ist Fast-Forward unmöglich. Git erstellt einen Merge-Commit – ein Commit mit zwei Parents.
Ausgangssituation:
C3 (feature)
/
C1 → C2
\
C4 (main)
main hat C4 (von C2 abgezweigt), feature
hat C3 (ebenfalls von C2). Common ancestor: C2.
Bei git merge feature (von main aus): 1.
Git findet common ancestor (C2) 2. Git erstellt Three-Way-Merge: Base
(C2), Ours (C4), Theirs (C3) 3. Git erstellt neuen Commit (C5) mit: -
parent1: C4 (aktueller main) - parent2: C3
(gemerged feature) - tree: Kombinierte Änderungen aus
beiden Branches
$ git merge feature
Merge made by the 'ort' strategy.
file.txt | 5 +++++
1 file changed, 5 insertions(+)
$ git cat-file -p HEAD
tree 9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0
parent a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 # C4 (main)
parent 7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6 # C3 (feature)
author John Doe <john@example.com> 1704122400 +0100
committer John Doe <john@example.com> 1704122400 +0100
Merge branch 'feature'Der Merge-Commit C5 hat zwei Parents:
Die Struktur im Graph:
C3 (feature)
/ \
C1 → C2 C5 (main, Merge Commit)
\ /
C4
Zwei Parents: Der Merge-Commit verbindet beide Historien. Von C5 aus sind sowohl C3 als auch C4 erreichbar.
Verzweigte Historie: git log --graph
zeigt die Verzweigung deutlich. Die parallele Entwicklung ist
sichtbar.
Explizite Integration-Punkte: Jeder Merge-Commit markiert einen Punkt, wo Branches zusammengeführt wurden. Dies ist nachvollziehbar.
Vollständige Historie: Alle Commits beider Branches bleiben erhalten, in ihrer originalen Reihenfolge und Struktur.
Die Commit-Message eines Merge-Commits dokumentiert die Integration:
Merge branch 'feature/new-login'
Adds new authentication system with:
- OAuth2 integration
- Password reset flow
- 2FA support
Closes #123
Diese Message ist wertvoll: Sie fasst zusammen, was integriert wurde, ohne dass man alle einzelnen Commits durchgehen muss.
Squash Merge nimmt alle Commits eines Feature-Branches und kondensiert sie zu einem einzigen Commit auf dem Ziel-Branch.
Ausgangssituation:
C3 → C4 → C5 (feature)
/
C1 → C2 (main)
Der Feature-Branch hat drei Commits (C3, C4, C5), jeder mit kleinen Schritten.
Bei git merge --squash feature (von main
aus): 1. Git berechnet die kumulative Änderung von C2 zu C5 2. Git
wendet diese Änderung auf main an (Working Directory +
Index) 3. Git erstellt keinen Commit automatisch 4.
Entwickler muss committen:
$ git merge --squash feature
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
$ git status
On branch main
Changes to be committed:
(all changes from feature branch)
$ git commit -m "Add new login system
Implements OAuth2, password reset, and 2FA.
Closes #123"
[main 8c7d6e5] Add new login systemDas Ergebnis:
C1 → C2 → C6 (main)
C6 ist ein normaler Commit (ein Parent: C2) mit
allen Änderungen aus C3, C4, C5 kombiniert. Die Original-Commits C3-C5
bleiben im Graph, aber sind nicht mehr von main
erreichbar:
Ein Commit: Die gesamte Feature-Arbeit wird als atomare Unit dargestellt.
Lineare Historie auf main: Der Haupt-Branch bleibt eine gerade Linie. Keine Verzweigungen.
Feature-Historie im Feature-Branch: Die Detail-Historie bleibt im Feature-Branch erhalten (bis dieser gelöscht wird).
Kein Merge-Commit: C6 ist ein normaler Commit mit einem Parent. Git “weiß” nicht, dass hier ein Branch integriert wurde.
Verlust von Zwischenschritten: Auf main
sind die einzelnen Schritte C3-C5 nicht sichtbar. Nur das
Endergebnis.
Squash ist ideal, wenn der Feature-Branch “dirty” ist – viele kleine Commits wie:
Fix typo
WIP - testing approach
Revert "testing approach"
Actually fix the thing
Fix lint errors
Respond to review comments
Diese Granularität ist während der Entwicklung nützlich, aber in der
main-Historie Noise. Ein squashed Commit:
Add user authentication
Implements login, logout, and session management.
Includes unit tests and documentation.
ist klarer und informativer.
Es gibt zwei Philosophien zur Git-Historie, die sich fundamental unterscheiden.
Perspektive: Git-Historie sollte die wahre Entwicklungsgeschichte abbilden. Jeder Commit, jede Verzweigung, jeder Merge zeigt, wie das Projekt wirklich entstand. Historie ist ein Audit-Trail, der nicht verändert werden sollte.
Praktiken: - Merge Commits bevorzugen (keine Rebases) - Alle Feature-Branches bleiben sichtbar - Auch “schlechte” Commits bleiben erhalten (Teil der Realität)
Argument: “Was passiert ist, ist passiert. Historie zu ändern verfälscht die Wahrheit.”
Beispiel-Historie:
Diese Historie zeigt genau, wann welcher Branch erstellt, wie lange daran gearbeitet und wann gemerged wurde. Vollständige Transparenz.
Perspektive: Git-Historie sollte klar und lesbar sein. Sie dient zukünftigen Entwicklern, die verstehen wollen, wie das System aufgebaut ist. Historie ist Dokumentation, die kuratiert werden sollte.
Praktiken: - Rebase bevorzugen (lineare Historie) - Squash Merge für Feature-Branches - “Schlechte” Commits werden umgeschrieben - Interactive Rebase für Cleanup
Argument: “Die rohe Entwicklungsgeschichte ist Noise. Was zählt, ist eine verständliche Darstellung.”
Beispiel-Historie:
Diese Historie ist eine klare, lineare Story. Jeder Commit ist ein sinnvolles, in sich geschlossenes Feature. Lesbar wie ein Buch.
Die meisten Teams liegen irgendwo dazwischen. Einige pragmatische Perspektiven:
Argument für lineare Historie:
git log --oneline zeigt
eine Story, keine Spaghetti.Argument für vollständige Historie:
Für Feature-Branches vor Integration: Alles ist erlaubt. Committe häufig, sei messy, experimentiere. Diese Historie ist privat.
Bei Integration in main: Kuratiere. Entweder: - Interactive Rebase, um Commits zu cleanen, zu squashen, Messages zu verbessern - Squash Merge, um ein sauberes Commit zu erstellen - Merge Commit mit aussagekräftiger Message
Für main selbst: Halte es sauber. Jeder Commit auf
main sollte: - In sich geschlossen sein (funktioniert,
testet, buildet) - Eine klare Message haben - Ideally ein Feature/Fix
repräsentieren, nicht “WIP” oder “fix typo”
Die “Angst vor Rebase”:
Viele Entwickler haben unbegründete Angst vor Rebase, basierend auf Missverständnissen:
Mythos: “Rebase löscht Commits und verliert
Historie.” Realität: Rebase erstellt neue Commits mit
anderen Hashes. Die alten Commits bleiben in .git/objects/
(bis GC). Via git reflog sind sie wiederherstellbar. Nichts
ist “verloren”, nur nicht mehr von Branches referenziert.
Mythos: “Rebase ist gefährlich und kann alles zerstören.” Realität: Rebase ist gefährlich, wenn bereits gepushte und von anderen genutzte Commits rebased werden. Für lokale Commits oder private Feature-Branches ist Rebase harmlos und empfohlen.
Mythos: “Merge-Commits sind wichtig für die Historie.” Realität: Merge-Commits dokumentieren Integration-Punkte. Aber bei trivialen Features ist diese Information Noise. Ein gut geschriebenes Feature-Commit dokumentiert besser als ein generischer “Merge branch ‘feature-123’”.
Die Scheu vor Rebase ist oft institutionelles Wissen, basierend auf schlechten Erfahrungen (jemand hat mal production rebased und chaos verursacht). Aber korrekt angewendet, ist Rebase ein mächtiges Tool für lesbare Historie.
| Kontext | Empfohlene Strategie | Begründung |
|---|---|---|
| Feature-Branch → main (small team) | Rebase + FF oder Squash | Saubere, lineare Historie |
| Feature-Branch → main (large team) | Merge Commit | Zeigt parallele Arbeit, Credit für alle |
| Hotfix → main | Fast-Forward (wenn möglich) | Dringend, sollte linear sein |
| Release-Branch → main | Merge Commit mit Tag | Markiert wichtigen Integration-Punkt |
| Long-Running Feature | Regelmäßig Rebase auf main | Verhindert massive Konflikte später |
| Experiment-Branch | Squash oder gar nicht mergen | Experimentelle Historie ist irrelevant |
# Feature-Branch erstellen und arbeiten
git checkout -b feature/new-api
# ... viele Commits, experimentieren, WIP commits ...
git commit -m "WIP - trying approach A"
git commit -m "Revert - approach A doesn't work"
git commit -m "Implement approach B"
git commit -m "Fix typo"
git commit -m "Respond to linter"
# Vor dem Merge: Historie aufräumen
git rebase -i main
# Interactive Rebase Editor öffnet sich:
# pick abc123 Implement approach B
# squash def456 Fix typo
# squash ghi789 Respond to linter
# drop earlier WIP commits
# Resultat: Ein sauberer Commit
git log --oneline
abc123 Implement new REST API endpoint
# Merge (Fast-Forward möglich, da rebased auf main)
git checkout main
git merge feature/new-apiDie finale main-Historie ist sauber. Die messy
Entwicklungs-Historie ist weg (aber via git reflog noch
erreichbar, wenn nötig).
# Feature-Branch mit mehreren logischen Commits
git checkout -b feature/authentication
git commit -m "Add user model and database schema"
git commit -m "Implement password hashing"
git commit -m "Add login endpoint"
git commit -m "Add session management"
# Diese Commits sind alle wertvoll, behalten
git checkout main
git merge --no-ff feature/authentication -m "Add user authentication system
Implements:
- User model with secure password storage
- Login/logout endpoints
- Session-based authentication
Closes #234"Die main-Historie zeigt den Merge-Commit mit Kontext,
aber die Detail-Historie ist erreichbar via
git log --first-parent (zeigt nur Merge-Commits) oder
git log feature/authentication.
Git bietet verschiedene Merge-Strategien als Werkzeuge. Keine ist inhärent “besser”. Die Wahl hängt ab von: - Team-Größe und -Erfahrung - Projekt-Komplexität - Release-Prozessen - Persönlichen Präferenzen
Fast-Forward: Simpel, sauber, aber nur bei linearer Entwicklung möglich.
Merge Commit: Explizit, zeigt parallele Arbeit, kann historie unübersichtlicher machen.
Squash Merge: Komprimiert, ideal für messy Feature-Branches, verliert Detail-Historie.
Rebase: Reorganisiert Historie, macht sie linear, sollte nur für nicht-gepushte Commits verwendet werden.
Die “richtige” Strategie ist die, die für dein Team funktioniert. Aber bitte: Hab keine irrationale Angst vor Rebase oder Squash. Sie sind mächtige Tools für lesbare Historie. Und lesbare Historie macht Code-Maintainance, Debugging und Onboarding neuer Entwickler drastisch einfacher.
Git-Historie ist nicht heilig. Sie ist ein Tool, das uns dient. Wie ein gutes Buch sollte sie eine klare Story erzählen, nicht ein ungefiltertes Protokoll jeden Tippfehlers sein. Kuratiere deine Historie. Dein zukünftiges Selbst (und deine Kollegen) werden es dir danken.
Und: Nutze die Commit-Addressierung. Sie ist fundamental und mächtig.
HEAD~5, main^2,
feature@{yesterday} – diese Mechanismen funktionieren
überall in Git. Sie sind nicht spezifisch für Merge oder Rebase. Sie
sind die Sprache, in der wir mit dem Commit-Graph reden. Beherrsche sie,
und Git wird von kryptisch zu intuitiv.