Der Foxtrott-Merge ist wie Tanzen mit Git – nur dass hier nicht die Füße stolpern, sondern die Commits. Beide starten synchron, drehen sich elegant umeinander, bis einer plötzlich im falschen Takt mitschreibt. Während Tänzer das charmant überspielen, verursacht es in Git eine chaotische Merge-Historie. Der Foxtrott bringt Schwung ins Parkett, der Foxtrott-Merge dagegen Unordnung ins Repository.
Und das Beste daran: Ich kann zwar kein Foxtrott tanzen, aber diesen Merge krieg ich jedes Mal hin – leider. In diesem Sinne mal eine Anleitung mit Lösung.
Ein Foxtrot Merge ist ein problematischer Merge-Commit in Git, bei dem die Reihenfolge der Parent-Commits vertauscht ist und dadurch die First-Parent-Historie des Hauptzweigs durcheinanderbringt.
Konkret entsteht ein Foxtrot Merge, wenn du lokale Änderungen committed hast, dann git pull ausführst (was automatisch einen Merge erstellt), und dieser Merge-Commit den Remote-Branch als zweiten statt als ersten Parent referenziert.
Das Problem wollen wir mal auf einem Raspberry PI 2 W Zero mit Kali (jedes andere Betriebssystem geht auch 😉 nachstellen und zum Schluss eine Lösung zum verhindern aufzeigen. Wir machen grob das:
Das Problem
Bei einem Merge-Commit sind die Eltern geordnet: Der erste Parent ist HEAD (der Commit, auf dem du warst), der zweite Parent ist der Commit, den du mergst.
Ein Foxtrot Merge macht origin/master zum zweiten Parent, wodurch die First-Parent-Historie – die Git in vielen Befehlen wie git log –first-parent verwendet – verfälscht wird. Dies erschwert die Nachvollziehbarkeit der Haupt-Entwicklungslinie erheblich.
Hier eine Schritt für Schritt Anleitung, oder auch hier ein Script auf GitLab, welches alles automatisch erstellt.
1. Neues Repository initialisieren
Ein neues leeres Repository wird angelegt und initialisiert.
mkdir foxtrot-demo
cd foxtrot-demo
git init
2. Erster Commit
Eine erste Datei wird angelegt und commitet.
echo "1. Initial content" > file.txt
git add file.txt
# Optional, wenn noch nicht gemacht
# git config --global user.email "you@example.com"
# git config --global user.name "Your Name"
git commit -m "1. Initial commit"
git log --oneline --graph --all --decorate
Graph:
# * 144b377 (HEAD -> master) 1. Initial commit
Der master
-Branch zeigt auf den ersten Commit.
3. Remote anlegen und verbinden
Das lokale Repository wird als Bare Repository geklont und als Remote eingerichtet.
cd ..
git clone --bare foxtrot-demo foxtrot-remote.git
cd foxtrot-demo
git remote add origin ../foxtrot-remote.git
git remote -v
# Ergebnis:
# origin ../foxtrot-remote.git/ (fetch)
# origin ../foxtrot-remote.git/ (push)
git push -u origin master
git log --oneline --graph --all --decorate
Keine Veränderung im Graph — er ist nur gespiegelt.
Graph:
* 144b377 (HEAD -> master, origin/master) 1. Initial commit
4. Parallelen Klon als zweites Repository erstellen
cd ..
git clone foxtrot-remote.git foxtrot-other
cd foxtrot-other
git log --oneline --graph --all --decorate
Graph: identisch mit dem ersten Repository.
# * 144b377 (HEAD -> master, origin/master, origin/HEAD) 1. Initial commit
5. Änderung im Remote-Klon durchführen
Eine neue Zeile wird hinzugefügt, commitet und ins Remote gepusht.
echo "2. Remote change" >> file.txt
git add file.txt
git commit -m "2. Remote commit"
git push origin master
git log --oneline --graph --all --decorate
Graph:
* 59cdc2d (HEAD -> master, origin/master, origin/HEAD) 2. Remote commit
* 144b377 1. Initial commit
Jetzt enthält das Remote einen zusätzlichen Commit.
6. Änderung im ersten Repository lokal durchführen
Zurück im Haupt-Repository entsteht ein lokaler Commit ohne vorheriges Pull.
cd ../foxtrot-demo
echo "3. Local change" >> file.txt
git add file.txt
git commit -m "3. Local commit"
git log --oneline --graph --all --decorate
Graph:
* 5f8b0f4 (HEAD -> master) 3. Local commit
* 144b377 (origin/master) 1. Initial commit
Das lokale Repository weiß noch nichts vom Remote-Commit. Die beiden Verläufe haben sich getrennt.
7. Foxtrot-Merge erzeugen durch Pull
Ein normales git pull
wird ausgeführt.
git pull origin master
# Es wird diese ausführliche Meldung ausgegeben:
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 264 bytes | 52.00 KiB/s, done.
From ../foxtrot-remote
* branch master -> FETCH_HEAD
144b377..59cdc2d master -> origin/master
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
# wir erzwingen nun nur einmalig, für die Erzeugung des Foxtrot merge, man könnte es auch wie oben beschrieben mit
# git config pull.rebase false
# dauerhaft für das Repo einstellen oder auch Global mit
# git config --global pull.rebase false
# also nur einmalig mit
git pull --no-rebase origin master
# Merge conflikt auflösen
git add file.txt
git commit -m "änderungen wegen merge conflict"
git log --oneline --graph --all --decorate
Graph:
* 7469ab1 (HEAD -> master) änderungen wegen merge conflict
|\
| * 59cdc2d (origin/master) 2. Remote commit
* | 5f8b0f4 3. Local commit
|/
* 144b377 1. Initial commit
Hier entsteht der eigentliche Foxtrot-Merge.
-Commit-ID 7469ab1 ist der Merge-Commit, der durch das Zusammenführen des lokalen und des Remote-Zweigs entstanden ist.
-Commit-ID 5f8b0f4 ist der lokale Commit auf deinem Branch vor dem Merge.
-Commit-ID 59cdc2d ist der Remote-Commit, der parallel entstanden ist und im Remote-Repository liegt.
-Commit-ID 144b377 ist der ursprüngliche Initial-Commit, von dem sowohl die lokalen als auch die Remote-Änderungen ausgegangen sind.
Der Merge-Commit 7469ab1 hat zuerst den lokalen Commit 5f8b0f4 als Parent und zweiten den Remote-Commit 59cdc2d.
Dadurch wird die Hauptlinie der Historie (first parent) in die falsche Richtung geführt: Die erste Elternlinie folgt dem lokalen Commit, nicht dem Remote-Commit.
Der Remote-Commit hängt nur noch seitlich am Verlauf und wird in der First-Parent-Historie nicht als direkter Fortschritt des Branches betrachtet.
Dieses Muster ist charakteristisch für einen Foxtrot-Merge.
Infolgedessen führt eine Historienbetrachtung, die nur der ersten Elternlinie folgt (z. B. mit git log –first-parent ), dazu, dass wichtige Remote-Commits im Verlauf verschwinden und die Nachvollziehbarkeit der Entwicklung erschwert wird.
Oder auch mit:
git log --all --date-order --pretty="%h|%p|%d"
7469ab1|5f8b0f4 59cdc2d| (HEAD -> master)
5f8b0f4|144b377|
59cdc2d|144b377| (origin/master)
144b377||
Ein:
git log --first-parent
# liefert
commit 7469ab165b778faff283e480a5980dab821b08ef (HEAD -> master)
Merge: 5f8b0f4 59cdc2d
Author: Thomas Wenzlaff
Date: Thu Oct 16 18:56:30 2025 +0200
änderungen wegen merge conflict
commit 5f8b0f431bda9ed15bcd4feb9df48a0cf9c0182f
Author: Thomas Wenzlaff
Date: Thu Oct 16 18:43:12 2025 +0200
3. Local commit
commit 144b37719658c67291ad14552c51f9767dcf6f72
Author: Thomas Wenzlaff
Date: Thu Oct 16 18:29:51 2025 +0200
1. Initial commit
8. Merge ins Remote übertragen
Damit wird der fehlerhafte Merge dauerhaft im Remote verankert.
git push origin master
git log --oneline --graph --all --decorate
Graph:
* 7469ab1 (HEAD -> master, origin/master) änderungen wegen merge conflict
|\
| * 59cdc2d 2. Remote commit
* | 5f8b0f4 3. Local commit
|/
* 144b377 1. Initial commit
Jetzt befindet sich der Foxtrot-Merge auch auf dem Remote.
Die Commitreihenfolge ist invertiert – origin/master verliert seine klare First-Parent-Linie.
9. Kontrolle und Bewertung
Der Graph zeigt nun deutlich die Verzweigung mit verkehrter Merge-Reihenfolge.
Wenn man den Verlauf mit --first-parent
betrachtet, ist der Remote-Commit 59cdc2d nicht mehr sichtbar.
git log --first-parent
commit 7469ab165b778faff283e480a5980dab821b08ef (HEAD -> master, origin/master)
Merge: 5f8b0f4 59cdc2d
Author: Thomas Wenzlaff
Date: Thu Oct 16 18:56:30 2025 +0200
änderungen wegen merge conflict
commit 5f8b0f431bda9ed15bcd4feb9df48a0cf9c0182f
Author: Thomas Wenzlaff
Date: Thu Oct 16 18:43:12 2025 +0200
3. Local commit
commit 144b37719658c67291ad14552c51f9767dcf6f72
Author: Thomas Wenzlaff
Date: Thu Oct 16 18:29:51 2025 +0200
1. Initial commit
Dadurch wird die Analyse von Release-Historien oder automatischen Deployments schwierig.
Mit dieser visualisierten Schrittfolge ist nachvollziehbar, wie ein Foxtrot-Merge entsteht –
durch gleichzeitige lokale Commits und nachfolgendes einfaches git pull
, das automatisch einen Merge in falscher Elternreihenfolge erzeugt.
Was kann man aber nun tun? Wie kann man Foxrot-Merge verhindern?
Die zuverlässigste Methode ist ein Pre-Receive-Hook auf dem Git-Server, der Foxtrot-Merges beim Push automatisch blockiert.
GitLab z.B. hat ein offenes Feature-Request für native Foxtrot-Merge-Prävention.
Um Foxtrot-Merges in Teams zu vermeiden, benötigst du klare Richtlinien, die technische Maßnahmen mit organisatorischen Best Practices kombinieren.
Rebase-Workflow etablieren, als Team-Richtlinie: Immer rebase verwenden, wie auch oben von git ausgegeben wird.
git config –global pull.rebase true
# Oder pro Repository
git config pull.rebase true
Dies erzwingt automatisch git pull –rebase statt dem gefährlichen git pull.
Oder
# Im Repository konfigurieren
git config merge.ff only
Dies erlaubt nur Fast-Forward-Merges, wodurch Foxtrot-Merges unmöglich werden.
Best Practices:
– Niemals git pull ohne –rebase auf Hauptbranches verwenden
– Niemals git merge master in lokale Branches ausführen, wenn diese auf master gemerged werden sollen
– Regelmäßig git fetch gefolgt von git rebase origin/master verwenden
– Pull-Request-Workflow konsequent einhalten
Grundlegende Team-Regeln
Niemals direkt auf Master/Main arbeiten: Entwickler sollten ausschließlich auf Feature-Branches arbeiten und nie direkt Commits auf master oder main erstellen. Dies verhindert die Hauptursache von Foxtrot-Merges – lokale Änderungen auf dem Hauptbranch gefolgt von einem git pull .
Pull-Request-Workflow durchsetzen: Alle Änderungen müssen über Pull Requests in den Hauptbranch gelangen. Konfiguriere Branch-Protection-Rules, die direkte Pushes auf master blockieren und Code-Reviews erzwingen.
Rebase statt Merge für Feature-Branch-Updates:
Wenn Feature-Branches mit dem aktuellen master synchronisiert werden müssen, verwende ausschließlich git rebase , nicht git merge :
git fetch origin
git rebase origin/master
Diese Regel hält die Historie linear und verhindert unnötige Merge-Commits.
Hier im Repo das Script, das alle Schritte automatisch ausführt.