Schwierige Git-Merge-Konflikte Schritt für Schritt lösen

Lösen Sie schwierige Git-Merge-Konflikte, indem Sie unsere, ihre und die Basisversionen lesen, Umbenennungen, Rebases, Binärdateien und Tests handhaben.

Schwierige Git-Merge-Konflikte Schritt für Schritt lösen

Ein schwieriger Git-Merge-Konflikt ist selten wegen der Konfliktmarkierungen selbst schwierig. Er ist schwierig, weil Sie die Absicht von zwei verschiedenen Änderungen gleichzeitig bewahren müssen. Ein Branch hat eine Funktion umbenannt, während ein anderer ihr Verhalten geändert hat. Ein Branch hat eine Datei verschoben, während ein anderer sie bearbeitet hat. Ein Branch hat eine Datenbank-Migrationssequenz geändert. Git kann Ihnen die Überschneidung zeigen, aber es kann nicht entscheiden, was die Software danach bedeuten soll.

Wenn ein Merge mit Konflikten stoppt, beginnen Sie nicht sofort damit, Markierungen zu löschen. Orientieren Sie sich zuerst.

git status

Git listet nicht zusammengeführte Pfade auf. Es kann both modified, deleted by us, deleted by them, both added oder ähnliches sagen. Diese Phrasen sagen Ihnen die Form des Konflikts.

Wenn der Merge sich falsch anfühlt oder Sie nicht bereit sind, ihn zu lösen, brechen Sie ab, bevor Sie weitere Änderungen vornehmen:

git merge --abort

Für einen Rebase-Konflikt lautet das Äquivalent:

git rebase --abort

Das ist kein Fehler. Es ist ein sauberer Reset auf den Zustand vor der Operation, was oft der klügste Schritt ist, wenn Sie feststellen, dass Sie mehr Kontext benötigen.

Lesen Sie den Konflikt als drei Versionen

Eine normale Konfliktmarkierung sieht so aus:

<<<<<<< HEAD
aktuelle Branch-Version
=======
eingehende Branch-Version
>>>>>>> feature-branch

Während eines Merges ist HEAD der Branch, den Sie ausgecheckt hatten, als Sie git merge ausgeführt haben. Die untere Seite ist der Branch, der zusammengeführt wird.

Bei schwierigen Konflikten verwenden Sie die drei Stufen, die Git im Index behält:

git show :1:path/to/file   # gemeinsamer Vorfahre
git show :2:path/to/file   # unsere Version
git show :3:path/to/file   # ihre Version

Der gemeinsame Vorfahre ist die Version, von der beide Branches ausgegangen sind. Er ist nützlich, weil er zeigt, was jeder Branch tatsächlich geändert hat. Ohne ihn könnten Sie zwei endgültige Versionen vergleichen und den Grund dahinter übersehen.

Sie können auch verwenden:

git diff
git diff --ours -- path/to/file
git diff --theirs -- path/to/file
git diff --base -- path/to/file

Hier gehen viele Leute zu schnell vor. Das Ziel ist nicht, "unsere" oder "ihre" Version als Team-Loyalitätsabstimmung zu wählen. Das Ziel ist, die korrekte endgültige Datei zu erstellen.

Ein sicherer manueller Arbeitsablauf

Verwenden Sie diese Routine für jede konfliktbehaftete Datei:

  1. Öffnen Sie die Datei und finden Sie jede Konfliktmarkierung.
  2. Lesen Sie den umgebenden Code, nicht nur die markierten Zeilen.
  3. Überprüfen Sie die Commits, die die Datei in beiden Branches berührt haben.
  4. Bearbeiten Sie die Datei in die endgültig beabsichtigte Version.
  5. Entfernen Sie alle Konfliktmarkierungen.
  6. Führen Sie den kleinsten relevanten Test oder Build-Check aus.
  7. Stagen Sie die Datei.

Nützliche Befehle dabei:

git log --oneline --left-right --merge -- path/to/file
git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

git diff --check fängt übrig gebliebene Leerzeichenprobleme. git grep fängt vergessene Konfliktmarkierungen, bevor sie CI erreichen.

Nach dem Auflösen einer Datei:

git add path/to/file

Wenn alle Konflikte gestaged sind:

git status
git commit

Während eines Rebases verwenden Sie:

git rebase --continue

Wenn beide Branches dieselbe Funktion geändert haben

Dies ist der häufige Fall. Angenommen, ein Branch fügt Validierung hinzu und der andere benennt einen Parameter um:

<<<<<<< HEAD
function createUser(email) {
  return db.users.insert({ email });
}
=======
function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}
>>>>>>> normalize-email

Die richtige Antwort könnte beide kombinieren:

function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}

Aber nur, wenn die Aufrufer aktualisiert wurden, um rawEmail zu übergeben, und nur, wenn die Normalisierung noch gewünscht ist. Suchen Sie nach der Funktion:

git grep -n 'createUser'

Schwierige Konflikte erfordern oft die Überprüfung benachbarter Dateien. Ein Funktionssignaturkonflikt in einer Datei kann Aktualisierungen in Tests, Routen, Typen, Mocks oder Dokumentation erfordern.

Umbenennungs- und Bearbeitungskonflikte

Umbenennungskonflikte sind ärgerlich, weil die gewünschte Datei möglicherweise nicht dort ist, wo Sie sie erwarten. Beginnen Sie mit dem Status:

git status --short

Überprüfen Sie dann die Namensstatus-Informationen:

git diff --name-status --diff-filter=R

Wenn ein Branch src/user.js in src/account.js umbenannt hat und der andere src/user.js bearbeitet hat, möchten Sie normalerweise den bearbeiteten Inhalt auf den neuen Pfad anwenden. Ein visuelles Merge-Tool kann helfen, aber das Konzept ist einfach: Bewahren Sie die Umbenennung und bewahren Sie die sinnvollen Bearbeitungen.

Nachdem Sie den endgültigen Pfad entschieden haben, entfernen Sie den veralteten Pfad, falls nötig, und stagen Sie den endgültigen:

git rm old/path.js
git add new/path.js

Stagen Sie nicht beide Dateien, es sei denn, das endgültige Projekt sollte wirklich beide enthalten.

Von uns gelöscht oder von ihnen gelöscht

Ein Lösch-/Änderungskonflikt bedeutet, dass ein Branch eine Datei gelöscht hat, während der andere sie geändert hat. Git kann nicht wissen, ob die Löschung die Änderung irrelevant gemacht hat.

Wenn die Datei gelöscht bleiben soll:

git rm path/to/file

Wenn die Datei erhalten bleiben soll, wählen Sie die gewünschte Version und stagen Sie sie:

git checkout --theirs path/to/file
git add path/to/file

oder:

git checkout --ours path/to/file
git add path/to/file

Seien Sie vorsichtig mit --ours und --theirs während eines Rebases. Bei einem Rebase können die Bezeichnungen umgekehrt wirken, weil Git Ihre Commits auf eine andere Basis wiederholt. Wenn Sie unsicher sind, überprüfen Sie die Stufen:

git show :2:path/to/file
git show :3:path/to/file

Binärdateikonflikte

Git kann die meisten Binärdateien nicht zusammenführen. Wenn zwei Branches dasselbe Bild, Archiv, Dokument oder kompilierte Asset geändert haben, müssen Sie eine Version wählen oder manuell eine neue Datei erstellen.

Um unsere Version zu nehmen:

git checkout --ours path/to/file.bin
git add path/to/file.bin

Um ihre Version zu nehmen:

git checkout --theirs path/to/file.bin
git add path/to/file.bin

Wenn die Binärdatei generiert ist, ist die beste Antwort möglicherweise, sie nach dem Auflösen der Textdateien aus der Quelle neu zu generieren. Wenn die Binärdatei ein Design-Asset oder Dokument ist, sprechen Sie mit der Person, die die andere Seite geändert hat. Raten kann Arbeit zerstören.

Verwenden Sie ein Merge-Tool, wenn die Datei zu schwer zu lesen ist

Ein gutes Merge-Tool zeigt vier Dinge: die Basisversion, Ihre Version, ihre Version und das Ergebnis. Konfigurieren Sie eines, das Ihnen tatsächlich gefällt. Visual Studio Code ist üblich:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

Führen Sie dann aus:

git mergetool

Andere Teams bevorzugen Meld, KDiff3, Beyond Compare oder IDE-integrierte Tools. Das Tool ist weniger wichtig als das Verständnis der drei Versionen. Klicken Sie nicht "eingehend akzeptieren" durch einen komplexen Konflikt, nur um die roten Markierungen verschwinden zu lassen.

Nach der Verwendung eines Merge-Tools überprüfen Sie auf Backup-Dateien wie .orig:

git status --short

Sie können die Backup-Dateien des Merge-Tools global deaktivieren, wenn Sie sie nicht möchten:

git config --global mergetool.keepBackup false

Strategieoptionen sind keine Magie

Sie könnten Ratschläge sehen wie:

git merge -X theirs feature

Das bedeutet nicht "ersetze meinen Branch durch feature." Es bedeutet, dass, wenn Gits Merge-Strategie widersprüchliche Stücke sieht, sie die andere Seite für diese Stücke bevorzugen soll. Nicht-konfliktreiche Änderungen von beiden Branches werden weiterhin zusammengeführt. Das kann nützlich sein für generierte Lockfiles oder mechanische Formatierungskonflikte, ist aber riskant für Geschäftslogik.

-X ours und -X theirs sind Strategieoptionen. Die ours Merge-Strategie ist anders:

git merge -s ours old-branch

Das zeichnet einen Merge auf, während der aktuelle Baum erhalten bleibt. Es ist ein spezialisiertes Werkzeug, das oft verwendet wird, um einen Branch als zusammengeführt zu markieren, ohne seinen Inhalt zu übernehmen. Verwenden Sie es nicht für normale Konfliktlösung, es sei denn, Sie sind sich sehr sicher.

Rebase-Konflikte

Während eines Rebases wiederholt Git Commits einzeln. Das bedeutet, dass Sie mehrere kleinere Konflikte anstelle eines großen Merge-Konflikts lösen können.

Die Schleife ist:

git status
# Dateien bearbeiten
git add resolved-file
git rebase --continue

Wenn ein wiederholter Commit nicht mehr benötigt wird, weil die neue Basis die Änderung bereits enthält, verwenden Sie:

git rebase --skip

Verwenden Sie Skip vorsichtig. Es verwirft diesen Commit aus dem rebasierten Branch. Lesen Sie zuerst den Commit:

git show

Auch hier können --ours und --theirs bei Rebase verwirrend sein. Überprüfen Sie :2: und :3:, wenn Sie unsicher sind.

Testen Sie die Lösung, nicht nur den Merge

Ein Merge kann syntaktisch gelöst sein und trotzdem falsch sein. Nach dem Stagen von Dateien führen Sie Tests aus, die den geänderten Bereich betreffen. Für einen Frontend-Konflikt könnte das ein Typtest und ein fokussierter Komponententest sein. Für einen Backend-Konflikt könnte es ein Servicetest oder Migrationscheck sein. Für einen Lockfile-Konflikt installieren Sie Abhängigkeiten neu und führen Sie den Verifikationsbefehl des Paketmanagers aus.

Mindestens:

git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

Führen Sie dann den projektspezifischen Check aus, der eine schlechte Kombination erkannt hätte.

Zukünftige Konflikte reduzieren

Der beste Konflikt ist der, den Sie nie erstellen. Halten Sie Branches kurzlebig, rebasen oder mergen Sie regelmäßig vom Hauptbranch, und vermeiden Sie es, mechanische Änderungen mit Feature-Änderungen zu mischen. Ein reiner Formatierungs-PR sollte nicht auch Logik ändern. Ein Dateiverschieben sollte die Datei nicht auch umschreiben, wenn Sie es vermeiden können.

Für Dateien, die immer schmerzhaft sind, ziehen Sie Änderungen an der Zuständigkeit oder Struktur in Betracht. Große Konfigurationsdateien, generierte Snapshots, Lockfiles, Migrationslisten und zentrale Routenregister erstellen oft wiederholte Konflikte, weil jeder denselben Bereich bearbeitet. Manchmal ist die Lösung ein Prozess. Manchmal ist die Lösung, die Datei aufzuteilen oder aus kleineren Quellen zu generieren.

Verwenden Sie .gitattributes für Dateien, die spezielles Merge-Verhalten benötigen. Zum Beispiel können einige generierte Lockfiles paketmanagerspezifische Merge-Treiber haben. Erfinden Sie nicht beiläufig einen, aber überprüfen Sie, ob Ihr Ökosystem einen empfohlenen Treiber hat.

Merge-Konflikte sind teils technische Arbeit und teils Kommunikation. Wenn Sie die Absicht des anderen Branches nicht verstehen, fragen Sie nach. Zehn Minuten mit dem Autor sind billiger, als stillschweigend Code zu mergen, der Tests besteht, aber das Feature entfernt, das sie gebaut haben.

Lockfiles, Migrationen und andere reibungsintensive Dateien

Einige Dateien geraten häufiger in Konflikt, weil viele Branches denselben kleinen Bereich bearbeiten. Abhängigkeits-Lockfiles sind ein häufiges Beispiel. Wenn zwei Branches Pakete hinzufügen, kann der Lockfile-Konflikt technisch groß, aber konzeptionell einfach sein: Regenerieren Sie ihn mit dem Paketmanager, nachdem Sie die Manifestdatei gelöst haben.

Für ein Node-Projekt könnte das bedeuten, package.json zu lösen und dann den Paketmanager auszuführen, der das Lockfile besitzt:

npm install
# oder pnpm install
# oder yarn install

Stagen Sie dann sowohl das Manifest als auch das Lockfile. Bearbeiten Sie ein komplexes Lockfile nicht von Hand, es sei denn, Sie verstehen sein Format. Der Paketmanager macht weniger wahrscheinlich einen subtilen Fehler im Abhängigkeitsgraphen.

Datenbankmigrationen erfordern mehr Sorgfalt. Wenn zwei Branches Migrationen mit Ordnungsannahmen erstellen, reicht es möglicherweise nicht, beide Dateien zu akzeptieren. Überprüfen Sie die Migrationszeitstempel, Sequenznummern, Abhängigkeiten und ob beide Migrationen dieselbe Tabelle oder Daten ändern. Manchmal ist die richtige Lösung eine neue Folge-Migration, die die beiden Branches in Einklang bringt.

Generierte Snapshots und Golden Files haben dasselbe Muster: Lösen Sie zuerst die Quelländerung, regenerieren Sie die Ausgabe und überprüfen Sie dann das generierte Diff. Wenn das generierte Diff enorm ist, fragen Sie, ob es in denselben Merge-Commit gehört. Riesige generierte Änderungen können eine schlechte manuelle Lösung verbergen.

Wenn ein Konflikt sich über Dateien erstreckt, notieren Sie das beabsichtigte endgültige Verhalten, bevor Sie bearbeiten. Eine kurze Notiz wie "die neue Validierung von Feature A behalten, den umbenannten Service von Feature B behalten, Client-Typen regenerieren" verhindert, dass Sie jede Datei lokal lösen, während Sie das Gesamtdesign verlieren.

Für besonders riskante Merges erstellen Sie vor dem Start einen temporären Branch:

git switch -c merge-test/main-with-feature
git merge feature

Wenn die Lösung unübersichtlich wird, können Sie den temporären Branch aufgeben, ohne Ihren ursprünglichen Branch zu stören. Diese kleine Gewohnheit macht schwierige Konflikte weniger stressig, weil Sie immer einen sauberen Rückweg haben.

Überprüfen Sie den endgültigen Merge wie eine eigene Änderung

Eine Konfliktlösung ist neue Arbeit. Behandeln Sie sie in der Überprüfung so. Das endgültige Diff sollte nicht nur die Änderungen beider Branches zeigen, sondern auch jeden Klebecode, den Sie geschrieben haben, um sie zusammenarbeiten zu lassen. Wenn der Merge-Commit groß ist, erklären Sie die Lösung in der Commit-Nachricht oder im Pull-Request-Kommentar. Prüfer sollten nicht rückentwickeln müssen, warum eine Seite gewählt wurde.

Vergleichen Sie vor dem Pushen das endgültige Ergebnis mit beiden Eltern, wenn möglich:

git diff HEAD^1..HEAD -- path/to/file
git diff HEAD^2..HEAD -- path/to/file

Für einen nicht committeten Merge überprüfen Sie gestagte Änderungen:

git diff --cached

Achten Sie auf versehentliches Löschen von Tests, nicht mehr verwendete Imports, doppelte Konfigurationseinträge und Codepfade, bei denen beide Branches ähnliche Logik unter verschiedenen Namen hinzugefügt haben. Dies sind die Fehler, die Git nicht für Sie identifizieren kann.

Wenn der Konflikt Verhalten betraf, fügen Sie einen Test hinzu oder aktualisieren Sie einen, der fehlschlagen würde, wenn Sie die falsche Seite gewählt hätten. Dieser Test tut mehr, als den heutigen Merge zu beweisen. Er schützt die Entscheidung davor, im nächsten Refaktor rückgängig gemacht zu werden.