Git im Alltag

Git-Merge-Konflikte auflösen, ohne fremde Arbeit zu überschreiben

Die Markierungen im Text sind der leichte Teil. Der schwierige ist die Frage, ob nach dem Auflösen noch alles da ist, was die andere Seite beigetragen hat.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Der Konflikt ist schnell verschwunden, die verlorene Änderung fällt erst Wochen später auf

Der übliche Ablauf unter Zeitdruck sieht so aus: Git meldet einen Konflikt, in der Datei stehen Markierungen, und weil die eigene Fassung vertraut aussieht und die andere nicht, wird die andere gelöscht. Die Markierungen sind weg, es lässt sich wieder committen, und die Sache gilt als erledigt. Was dabei passiert ist, merkt niemand: Die Änderung des Kollegen ist nicht mehr im Code, sie steht aber weiterhin in seiner Historie, und deshalb meldet Git sie beim nächsten Mal auch nicht wieder an.

Genau das macht diese Fehlerklasse so unangenehm. Ein überschriebener Konflikt hinterlässt keine Spur, die jemand sucht. Der Fehler taucht später als Rückschritt auf, also als eine Korrektur, die schon einmal drin war und plötzlich fehlt, oder als ein Test, der ohne erkennbaren Grund wieder rot ist. Bis jemand den Zusammenhang zum Merge von vor drei Wochen herstellt, vergeht ein Vielfaches der Zeit, die das saubere Auflösen gekostet hätte.

Der zweite häufige Fall ist der Griff zu einer pauschalen Regel. git checkout --ours oder ein Merge mit der Strategieoption ours löst zwar jeden Konflikt sofort auf, aber eben auf Dateiebene und ohne hinzusehen. Das ist in bestimmten Situationen richtig, etwa bei einer Datei, die ohnehin neu erzeugt wird. Als Gewohnheit ist es der zuverlässigste Weg, Arbeit zu verlieren, und es sieht im Verlauf aus wie ein sauberer Merge.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Symptom, Ursache, Lösung

Die Fälle, die im Alltag wirklich auflaufen

Sortiert nach dem, was du siehst, und nicht nach der Systematik dahinter. Zu jedem Fall steht, woher er kommt und welcher Handgriff ihn erledigt, ohne dass danach etwas fehlt.

Symptom

Git meldet einen Inhaltskonflikt, und in der Datei stehen die bekannten Markierungen mit deinem Stand oben und dem fremden unten.

Ursache

Beide Seiten haben denselben Bereich derselben Datei verändert. Git führt zeilenweise zusammen und kann nicht entscheiden, welche der beiden Änderungen gelten soll, also legt es beide nebeneinander und übergibt.

Lösung

Schalt die Anzeige des gemeinsamen Ausgangsstands ein, damit du siehst, was jede Seite tatsächlich getan hat. Schreib danach die Fassung, die beide Absichten enthält, entfern die Markierungen vollständig und füg die Datei mit git add hinzu. Abgeschlossen wird der Vorgang mit git merge --continue, das den Abschluss nur zulässt, wenn keine offenen Pfade mehr übrig sind.

Symptom

Die Meldung spricht davon, dass eine Datei auf der einen Seite geändert und auf der anderen gelöscht wurde. Markierungen gibt es keine.

Ursache

Meistens steckt eine Umbenennung oder eine Umstrukturierung dahinter: Eine Seite hat die Datei verschoben oder ersetzt, die andere hat in der alten Datei weitergearbeitet. Git kann Umbenennungen zwar erkennen, bei größeren Änderungen am Inhalt aber nicht mehr sicher.

Lösung

Erst nachsehen, wohin die Datei gewandert ist, etwa mit git log --follow auf den alten Pfad. Danach entscheidest du bewusst: Gehört die Änderung in die neue Datei, überträgst du sie dorthin und entfernst die alte mit git rm. Soll die Datei bestehen bleiben, holst du sie mit git add zurück in den Stand. Ein automatisches Auflösen gibt es hier nicht, und das ist auch gut so.

Symptom

Der Konflikt sitzt in einer Datei, die niemand von Hand pflegt, etwa in einem Lockfile des Paketwerkzeugs oder in einer erzeugten Projektdatei.

Ursache

Es handelt sich um ein Erzeugnis, das im Repository liegt. Solche Dateien ändern sich bei jedem Lauf an vielen Stellen gleichzeitig, deshalb kollidieren sie fast immer, und von Hand zusammengeführt ergeben sie oft einen Zustand, den kein Werkzeug je erzeugt hätte.

Lösung

Nicht zeilenweise auflösen. Nimm die Datei von einer Seite vollständig, lass das erzeugende Werkzeug danach neu laufen und commit das Ergebnis. Für den Dauerzustand gibt es zwei Wege: die Datei über eine Angabe in .gitattributes als binär behandeln, damit Git gar nicht erst zusammenzuführen versucht, oder die Frage stellen, ob sie überhaupt ins Repository gehört.

Symptom

Beim Rebase läuft derselbe Konflikt bei mehreren Commits hintereinander auf, und du löst dreimal dasselbe.

Ursache

Ein Rebase setzt jeden deiner Commits einzeln auf den neuen Ausgangspunkt auf. Wenn eine Stelle in mehreren deiner Commits berührt wurde, entsteht der Konflikt bei jedem davon erneut.

Lösung

Aktivier die Aufzeichnung von Auflösungen, dann wendet Git deine erste Entscheidung bei den folgenden Commits automatisch an. Wenn das nicht reicht, gibt es zwei Auswege: den Rebase abbrechen und stattdessen zusammenführen, weil dabei nur eine Auflösung anfällt, oder die eigenen Commits vorher mit git rebase -i zusammenfassen, sodass nur ein Aufsetzvorgang übrig bleibt.

Symptom

Der Merge ist durchgelaufen, gemeldet wurde nichts, und trotzdem fehlt später eine Änderung, die vorher da war.

Ursache

Fast immer wurde eine Seite pauschal genommen, entweder mit git checkout --ours auf Dateiebene oder mit der Strategieoption ours für den gesamten Merge. Beides führt zu einem Ergebnis ohne Konflikt und ohne Hinweis darauf, dass etwas ausgelassen wurde.

Lösung

Solange der Merge-Commit noch nicht geteilt ist, bringt dich git reset --hard ORIG_HEAD zurück auf den Stand davor, und du kannst es sauber wiederholen. Ist er schon auf dem Server, nimm ihn mit git revert -m 1 zurück, führ danach richtig zusammen und denk daran, dass ein erneutes Zusammenführen desselben Branches erst wirkt, wenn die Rücknahme selbst wieder zurückgenommen wurde.

Symptom

Bei jedem Merge kollidieren dieselben Dateien in Zeilen, an denen inhaltlich niemand etwas geändert hat.

Ursache

Unterschiedliche Zeilenenden zwischen Windows und den übrigen Systemen, unterschiedliche Formatierungseinstellungen in den Entwicklungsumgebungen oder ein Formatter, der bei jedem Speichern eine andere Meinung hat. Git vergleicht Text, und für Git ist eine geänderte Zeile eine geänderte Zeile.

Lösung

Leg in .gitattributes fest, wie mit Zeilenenden umgegangen wird, und einige dich auf einen Formatierer mit einer Konfigurationsdatei im Repository, die alle benutzen. Die einmalige Umformatierung des Bestands kommt als eigener Commit, und dessen ID trägst du in eine Datei mit auszublendenden Commits ein, damit die Zuordnung von Zeilen zu Autoren nicht darüber stolpert.

Fünf Schritte durch einen Konflikt

  1. 01 git status zeigt die betroffenen Dateien, bevor du irgendetwas öffnest.
  2. 02 Der eingeblendete Ausgangsstand verrät, welche Seite was geändert hat.
  3. 03 Das Ergebnis enthält beide Absichten, nicht eine gewählte Seite.
  4. 04 Suche nach Resten der Markierungen im gesamten Projekt.
  5. 05 git diff gegen beide Seiten und ein Testlauf, erst danach der Commit.
Was du mitnimmst

Was du nach dieser Seite in Minuten statt Stunden erledigst

Sechs Handgriffe decken den Alltag ab. Zwei davon sind Einstellungen, die du einmal setzt und danach nie wieder anfasst, und sie sind es, die den größten Unterschied machen.

Erst den Überblick, dann die Datei

git status listet unter den nicht zusammengeführten Pfaden genau die Dateien, um die es geht. Mit git diff --name-only --diff-filter=U bekommst du dieselbe Liste ohne Beiwerk. Damit weißt du vor dem ersten Eingriff, ob es um zwei Zeilen oder um vierzig Dateien geht, und ob ein Abbruch die bessere Entscheidung ist.

Die gemeinsame Basis mit anzeigen lassen

Setz einmalig git config --global merge.conflictStyle zdiff3. Danach zeigt Git in einem Konflikt nicht nur die beiden Fassungen, sondern zusätzlich den gemeinsamen Ausgangsstand. Erst damit siehst du, was jede Seite tatsächlich geändert hat, statt zwei Textblöcke zu vergleichen, die beide plausibel aussehen.

Die Absicht beider Seiten herstellen, nicht eine Seite wählen

In den meisten Konflikten ist die richtige Fassung keine der beiden, sondern eine dritte, in der beide Änderungen enthalten sind. Frag dich je Seite, welche Absicht dahintersteckt, und schreib das Ergebnis. Wenn sich die Absichten tatsächlich widersprechen, ist das keine Git-Frage mehr, sondern eine kurze Rückfrage beim Autor.

Nach den Markierungen suchen, bevor du hinzufügst

Ein Rest einer Markierung im Code ist der peinlichste und häufigste Fehler. Such vor dem Hinzufügen im gesamten Projekt nach den Zeichenfolgen aus sieben Kleiner- und Größer-Zeichen. Wer will, hinterlegt dieselbe Suche als Prüfung vor dem Commit, dann kann sie niemand vergessen.

Das Ergebnis gegen beide Seiten prüfen

git diff HEAD zeigt, was gegenüber deinem Stand herauskommt, und git diff MERGE_HEAD, was gegenüber der anderen Seite. Wenn im zweiten Vergleich Zeilen verschwinden, die niemand entfernen wollte, hast du genau den Fall gefunden, um den es hier geht. Ein Testlauf gehört danach trotzdem dazu.

Wiederkehrende Auflösungen einmal aufzeichnen

git config --global rerere.enabled true merkt sich, wie du einen Konflikt aufgelöst hast, und wendet dieselbe Auflösung beim nächsten identischen Konflikt automatisch an. Das ist vor allem beim Rebase über mehrere Commits die Einstellung, die aus einer halben Stunde ein paar Minuten macht.

Den Ausstieg kennen, bevor du ihn brauchst

git merge --abort und git rebase --abort bringen dich zurück auf den Stand vor dem Versuch, solange du noch mitten im Vorgang bist. Wer weiß, dass dieser Weg offensteht, arbeitet deutlich ruhiger und probiert eine zweite Herangehensweise aus, statt sich durch eine schlechte durchzukämpfen.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Was Git beim Zusammenführen tatsächlich vergleicht

Git führt nicht zwei Stände zusammen, sondern drei. Neben deiner Fassung und der fremden Fassung sucht es den letzten gemeinsamen Vorfahren beider Linien, den du dir mit git merge-base auch selbst anzeigen lassen kannst. Erst mit diesem dritten Punkt lässt sich eine Frage beantworten, die mit zwei Ständen unbeantwortbar ist: Hat eine Seite etwas hinzugefügt, oder hat die andere Seite es entfernt?

Solange die beiden Seiten unterschiedliche Bereiche einer Datei angefasst haben, erledigt Git das ohne Rückfrage. Ein Konflikt entsteht erst, wenn beide denselben Bereich verändert haben, und Bereich heißt dabei: dieselben oder unmittelbar benachbarte Zeilen. Deshalb kollidieren Änderungen, die inhaltlich gar nichts miteinander zu tun haben, wenn sie zufällig übereinanderliegen, und deshalb hilft es nicht, im Konflikt nach einem inhaltlichen Zusammenhang zu suchen.

Der praktische Nutzen dieser Einsicht ist die Einstellung zur Anzeige. Voreingestellt zeigt Git nur die beiden Endstände. Mit der Anzeige des gemeinsamen Ausgangsstands, also der Einstellung diff3 oder der neueren Variante zdiff3, siehst du zusätzlich, wovon beide ausgegangen sind. Aus einem Vergleich zweier Behauptungen wird damit die Rekonstruktion zweier Absichten, und das ist eine völlig andere Aufgabe.

Die Markierungen richtig lesen

Der obere Block, eingeleitet von sieben Kleiner-Zeichen und dem Wort HEAD, ist der Stand, auf dem du gerade stehst. Bei einem Merge ist das dein Branch. Der untere Block, abgeschlossen von sieben Größer-Zeichen und dem Namen der anderen Seite, ist das, was hereinkommt. Getrennt werden beide von einer Reihe von sieben Gleichheitszeichen.

Bei einem Rebase dreht sich diese Zuordnung um, und genau daran verlieren sich die meisten. Während eines Rebase steht Git auf dem Ziel, auf das aufgesetzt wird, deshalb ist der obere Block der fremde Stand und der untere dein eigener Commit. Wer die Zuordnung aus dem Merge übernimmt, wählt zuverlässig die falsche Seite. Die Regel dagegen ist einfach: nicht nach oben und unten entscheiden, sondern lesen, welcher Name in der Zeile steht.

Mit eingeschalteter Anzeige des Ausgangsstands kommt ein dritter Block dazu, eingeleitet von sieben senkrechten Strichen. Er zeigt, wie die Stelle vor beiden Änderungen aussah. Damit lässt sich in Sekunden feststellen, ob eine Seite etwas ergänzt und die andere nur umformatiert hat, ob beide dasselbe Problem unterschiedlich gelöst haben oder ob tatsächlich zwei unvereinbare Entscheidungen aufeinandertreffen. Nur im letzten Fall braucht es eine Rückfrage.

Prüfen, bevor du abschließt

Das Auflösen ist erst fertig, wenn drei Dinge stimmen. Erstens: Es gibt keine Reste der Markierungen mehr, und zwar auch nicht in Dateien, die du für unbeteiligt hältst. Eine Suche über das gesamte Projekt kostet Sekunden. Zweitens: Der Vergleich gegen beide Seiten zeigt nichts, was ohne Absicht verschwunden ist. Drittens: Die Tests laufen, und zwar nicht nur die zur eigenen Änderung.

Der zweite Punkt verdient die meiste Aufmerksamkeit, weil er der einzige ist, der die stille Fehlerklasse aufdeckt. git diff HEAD zeigt dir das Ergebnis gegenüber deinem Ausgangsstand, git diff MERGE_HEAD gegenüber der anderen Seite. Wenn im zweiten Vergleich Löschungen auftauchen, die niemand vorgenommen hat, hast du eine fremde Änderung überschrieben. Bei größeren Merges lohnt zusätzlich der Blick auf die Liste der Commits, die von der anderen Seite hereinkommen.

Für das Review gilt derselbe Grundsatz. Ein Merge-Commit mit Änderungen, die in keiner der beiden Seiten stehen, ist ein Warnzeichen, denn genau dort sitzen entweder eine unabsichtliche Auslassung oder Anpassungen, die eigentlich einen eigenen Commit verdient hätten. Manche Teams verlangen deshalb, dass bei einer nicht trivialen Auflösung im Beitrag ein kurzer Satz dazu steht, welche Entscheidung getroffen wurde und warum.

Konflikte vermeiden statt auflösen

Die Menge an Konfliktarbeit hängt nicht vom Können ab, sondern von zwei organisatorischen Größen. Die erste ist die Lebensdauer eines Branches: Konflikte wachsen mit dem Abstand zwischen den Linien, und dieser Abstand wächst mit der Zeit. Ein Branch, der zwei Tage lebt, erzeugt kleine, verständliche Konflikte. Ein Branch, der sechs Wochen lebt, erzeugt eine Auflösung, die niemand mehr überblickt und die deshalb schlecht gemacht wird.

Die zweite Größe ist die Häufigkeit, mit der ihr den Hauptstand in laufende Branches holt. Wer das täglich tut, verteilt dieselbe Konfliktmenge auf viele kleine Portionen, und jede einzelne ist in Minuten erledigt. Wer bis zum Schluss wartet, bekommt sie am Stück, und zwar genau dann, wenn der Termin drückt. Das ist derselbe Aufwand, nur zum ungünstigsten Zeitpunkt und mit dem höchsten Risiko, etwas zu übersehen.

Daneben helfen ein paar unspektakuläre Absprachen. Große Umbenennungen und Umformatierungen kommen als eigener Commit und werden im Team angekündigt, damit niemand parallel in denselben Dateien arbeitet. Erzeugte Dateien gehören nach Möglichkeit nicht ins Repository. Und wenn zwei Personen gleichzeitig an derselben Datei arbeiten müssen, ist ein Satz im Chat billiger als jede Auflösung hinterher.

Wenn es schiefgegangen ist: der Weg zurück

Solange du noch mitten im Vorgang steckst, ist die Sache harmlos. git merge --abort stellt den Stand von vor dem Merge wieder her, git rebase --abort denselben vor dem Rebase, und beide funktionieren auch dann, wenn du schon einige Dateien bearbeitet hast. Ein einzelner Fehlgriff innerhalb eines laufenden Konflikts lässt sich mit git checkout --merge auf die betroffene Datei zurücknehmen, danach stehen die Markierungen wieder wie zu Beginn.

Ist der Merge bereits abgeschlossen, aber noch nicht geteilt, hilft ORIG_HEAD. Git merkt sich darin, wo dein Branch vor dem Merge stand, und git reset --hard ORIG_HEAD bringt dich dorthin zurück. Falls dieser Zeiger schon überschrieben wurde, weil zwischendurch etwas anderes lief, findest du denselben Stand in der Ausgabe von git reflog, die für jede Bewegung deines Branches einen Eintrag enthält.

Ist der Stand schon auf dem Server und haben andere ihn geholt, wird nichts mehr zurückgesetzt. Dann ist git revert -m 1 auf den Merge-Commit der richtige Weg, weil er die Historie unangetastet lässt und die Rücknahme sichtbar macht. Dazu gehört ein Merksatz, der später Zeit spart: Wenn ihr denselben Branch danach erneut zusammenführt, bringt er die Änderungen nicht von allein wieder mit, weil sie aus Sicht von Git bereits einmal übernommen wurden. Zuerst wird die Rücknahme zurückgenommen, dann wird zusammengeführt.

Dazu passende Kurse

Weil sich diese Handgriffe schlecht lesen und gut üben lassen, sind Git-Kurse, in denen das Auflösen von Konflikten geübt wird der direktere Weg als jede Anleitung im Wiki.

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Sehr umfangreicher Überblick über Zusammenhänge und Anwendungsbereiche. Sehr zu empfehlen.
Einführung in die Programmierung für Programmiereinsteiger
Effektives, gemeinsames Erarbeiten der Aufgaben mit vielen praktischen Tipps.
Groovy Grundkurs
Sehr guter Trainer und gute Aufbereitung der Inhalte und auch eingehen auf Fragen der Beteiligten.
Unreal Engine Kurs für Einsteiger

Häufige Fragen

Was bedeuten die Zeichenreihen mit den spitzen Klammern genau?
Die sieben Kleiner-Zeichen leiten den Stand ein, auf dem du gerade stehst, die sieben Gleichheitszeichen trennen die beiden Fassungen, und die sieben Größer-Zeichen schließen die hereinkommende Fassung ab. Hinter der ersten und der letzten Zeile steht jeweils der Name der Seite, und auf diesen Namen solltest du dich verlassen, weil sich die Reihenfolge beim Rebase umdreht.
Kann ich einen Konflikt einfach mit --ours auflösen?
Manchmal ja, als Gewohnheit nein. Richtig ist es bei Dateien, die ohnehin neu erzeugt werden, oder wenn du sicher weißt, dass die andere Seite an dieser Stelle nichts beitragen wollte. In allen anderen Fällen wählst du damit eine Seite auf Dateiebene, also inklusive aller Änderungen, die dort sonst noch drin sind, und genau so gehen fremde Beiträge spurlos verloren.
Wie mache ich einen Merge rückgängig, den ich schon gepusht habe?
Mit git revert -m 1 auf den Merge-Commit. Die Angabe bezeichnet den Elternteil, auf dem du bleiben willst, also üblicherweise den Stand deines Zielbranches. Zurücksetzen mit reset ist an dieser Stelle keine Option mehr, weil alle anderen den Stand bereits haben. Merk dir dazu, dass ein späteres erneutes Zusammenführen desselben Branches erst wirkt, wenn die Rücknahme selbst zurückgenommen wurde.
Warum kommt beim Rebase derselbe Konflikt mehrfach?
Weil jeder deiner Commits einzeln auf den neuen Ausgangspunkt gesetzt wird und jeder davon dieselbe Stelle berührt. Mit der Aufzeichnung von Auflösungen wendet Git deine erste Entscheidung automatisch wieder an. Alternativ fasst du deine Commits vorher zusammen, dann bleibt nur ein Aufsetzvorgang übrig, oder du führst zusammen statt aufzusetzen.
Lohnt sich ein grafisches Werkzeug zum Auflösen?
Bei Konflikten über mehrere Blöcke hinweg ja, weil die Dreispaltenansicht mit Ausgangsstand, eigener und fremder Fassung genau die Information zeigt, die im Text mühsam zusammenzusuchen ist. Eingerichtet wird es einmal in der Konfiguration und dann über git mergetool aufgerufen. Bei einem Konflikt über drei Zeilen ist der Editor schneller.
Kann ich vorher sehen, ob ein Merge Konflikte gibt?
Ja. Ein Probelauf mit den Optionen für keinen Commit und kein schnelles Vorspulen zeigt dir das Ergebnis im Arbeitsverzeichnis, ohne dass etwas festgeschrieben wird, und mit git merge --abort bist du danach wieder am Ausgangspunkt. Auf der Plattform macht die Anzeige im Beitrag dasselbe sichtbar, allerdings erst, nachdem ihr den Branch hochgeladen habt.
Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Programmierung-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

Konflikte einmal in Ruhe erzeugen statt unter Termindruck

Im Git-Kurs bei cmt legst du dir die typischen Konflikte selbst hin, löst sie auf und prüfst danach, ob wirklich beide Seiten enthalten sind. Genau dieser letzte Schritt ist der, den man im Alltag am ehesten überspringt.