Konflikte lösen
Ein Konflikt bedeutet, dass Git zwei Änderungen an derselben Stelle gefunden hat und sich weigert zu raten. Eine Lösung ist eine Entscheidung über die Absicht, und der sichere Standard ist, die Absicht beider Seiten zu bewahren. Das Lösen durch Löschen einer Seite lässt die Markierungen zwar verschwinden, macht das Problem aber schlimmer: Der Build besteht trotzdem, und die Änderung eines Kollegen ist verschwunden.
1. Wissen, in welcher Operation Sie sich befinden – ours und theirs hängen davon ab
git status # „rebase in progress“ / „You have unmerged paths“ / „cherry-pick in progress“
| Operation | --ours / HEAD-Seite | --theirs-Seite |
|---|---|---|
merge | der Branch, auf dem Sie sind | der Branch, der gemerged wird |
rebase | der Branch, auf den Sie rebasen (Upstream) | Ihr eigener Commit, der wiederholt wird |
cherry-pick, revert, stash pop | der Branch, auf dem Sie sind | der Commit, der angewendet wird |
Die Zeile für Rebase ist die Falle. Während git rebase main ist „ours“ main und „theirs“ ist die eigene Arbeit des Benutzers, also das Gegenteil eines Merges. git checkout --theirs datei in einem Rebase behält Ihre Änderung und verwirft die des Upstreams. Lesen Sie diese Tabelle erneut, bevor Sie eines der Flags verwenden.
2. Den gesamten Konflikt sehen, nicht nur die Markierungen
git diff --name-only --diff-filter=U # nicht gemergte Dateien
git status # der Typ: both modified / deleted by us / both added
git config merge.conflictStyle zdiff3 # zeigt auch den gemeinsamen Vorfahren an (git 2.35+; bei älteren diff3 verwenden)
git checkout --conflict=zdiff3 -- <datei> # EINE Datei mit angezeigtem Basisinhalt neu rendern (verwirft bereits vorgenommene Bearbeitungen)
Mit sichtbarer Basis haben die Markierungen drei Teile: Ihre Seite, das Original, die andere Seite. Die meisten scheinbaren Konflikte werden offensichtlich, wenn Sie sehen können, von was jede Seite geändert hat.
Um zu verstehen, warum jede Seite geändert hat:
git log --merge -p -- <datei> # Commits von beiden Seiten, die die Datei betreffen (Merge oder Rebase läuft)
git show REBASE_HEAD # während eines Rebase: der Commit, der wiederholt wird
git show :1:<datei> :2:<datei> :3:<datei> # Basis, „ours“, „theirs“ als vollständige Dateien
Lesen Sie die Commit-Nachrichten. Eine Lösung, die Sie nicht in einem Satz pro Seite erklären können, ist eine Vermutung.
3. Durch Kombinieren lösen
- Beide Seiten haben dieselben Zeilen aus unterschiedlichen Gründen bearbeitet – schreiben Sie die Version, die beides tut. Zum Beispiel: Eine Seite hat einen Parameter
tax_ratehinzugefügt, die andere einen Parameterdiscount: Die gelöste Funktion nimmt beide entgegen und wendet beide an. - Eine Seite hat verschoben oder umbenannt, die andere hat bearbeitet – wenden Sie die Bearbeitung an der neuen Stelle an. Behalten Sie nicht die alte Datei und die neue.
- Von einer Seite gelöscht, von der anderen geändert – das ist eine echte Entscheidung, keine mechanische. Finden Sie heraus, warum es gelöscht wurde; wenn die Änderung noch relevant ist, muss sie übernommen werden, sonst gewinnt die Löschung. Fragen Sie den Benutzer, wenn es unklar ist.
- Generierte Dateien und Sperrdateien (
package-lock.json,poetry.lock,go.sum, Snapshots, kompilierte Ausgaben) – nicht von Hand mergen. Eine Seite nehmen, dann mit dem Tool neu generieren (npm install,poetry lock,go mod tidy, der Snapshot-Befehl). - Binärdateien – es gibt keinen Merge; wählen Sie bewusst eine Seite (beachten Sie die obige Tabelle) und sagen Sie dies.
Text kann sauber gemerged werden und trotzdem falsch sein: Wenn eine Seite eine Funktion umbenannt hat und die andere einen Aufruf des alten Namens hinzugefügt hat, sieht Git keinen Konflikt. Deshalb gehört der Build zu Schritt 4.
Pauschale Flags verstecken Entscheidungen. git checkout --ours ., -X ours / -X theirs (die jeden konfliktären Abschnitt zu einer Seite auflösen) und -s ours (die den anderen Branch komplett verwirft) werfen jedes Mal Arbeit weg, ohne sie anzusehen. Verwenden Sie sie nur, wenn der Benutzer genau das verlangt hat und weiß, was fallengelassen wird.
4. Verifizieren, dann fortfahren
git diff --check # schlägt fehl bei übriggebliebenen Konfliktmarkierungen und Whitespace-Fehlern
grep -rnE '^(<<<<<<<|=======|>>>>>>>)' . # zusätzliche Sicherheit in Dateien, die git diff-check nicht prüft
# Build und die schnellsten relevanten Tests ausführen
git add <gelöste-dateien>
Fahren Sie mit dem passenden Befehl fort. Fügen Sie GIT_EDITOR=true hinzu, wo ein Editor geöffnet würde, da ein Editor in einer nicht-interaktiven Shell hängt:
git merge --continue # oder: git commit --no-edit
GIT_EDITOR=true git rebase --continue
GIT_EDITOR=true git cherry-pick --continue
Ein Rebase spielt Commits einzeln wiederholt ab, daher kann er beim nächsten Commit erneut anhalten. Wiederholen Sie die Schleife. Lösen Sie den Konflikt beim frühesten Commit, in dem er auftritt; spätere können denselben Konflikt erneut darstellen.
Wenn Tests nach der Lösung fehlschlagen, ist der Fehler eine Information über die Lösung. Fahren Sie nicht darüber hinaus fort und „korrigieren“ Sie ihn nicht, indem Sie Tests bearbeiten, bis Sie wissen, welches Verhalten von welcher Seite der Test schützt.
5. Wann anhalten und fragen
Halten Sie an, anstatt zu raten, wenn:
- beide Seiten Geschäftslogik auf inkompatible Weise geändert haben (zwei unterschiedliche Steuerregeln, zwei unterschiedliche Wiederholungsstrategien);
- Sie nicht in einem Satz sagen können, warum jede Seite ihre Änderung vorgenommen hat;
- die Lösung einen Test fehlschlagen lässt und die Ursache nicht klar ist.
Geben Sie die beiden Absichten an, sagen Sie, was Sie wählen würden und warum, und lassen Sie den Benutzer entscheiden. Alles vor --continue ist umkehrbar: git merge --abort, git rebase --abort, git cherry-pick --abort stellen den Baum wieder auf den Ausgangszustand zurück.
Wiederholte Konflikte
Bei einem langen Rebase, bei dem derselbe Konflikt immer wieder auftritt, oder einem Branch, der wiederholt gemerged und aufgegeben wird:
git config rerere.enabled true # zeichnet jede Lösung auf und spielt sie beim nächsten Mal erneut ab
git rerere diff # was aufgezeichnet wurde
git rerere forget-path <datei> # eine aufgezeichnete Lösung verwerfen, die falsch war
rerere wendet eine Lösung ohne Nachfrage erneut an, daher breitet sich eine schlechte aus. Nach einem durch rerere gelösten Konflikt das Ergebnis dennoch überprüfen.
Wenn ein Branch langlebig und weit zurück ist, bedeutet das einmalige Mergen des Trunks in der Regel eine Runde Konflikte; Rebase wiederholt sie pro Commit. Diese Wahl bleibt dem Benutzer.
Leitplanken
- Nie lösen, indem Sie pauschal eine Seite übernehmen, um Konflikte verschwinden zu lassen. Die andere Seite ist die Arbeit von jemandem.
- Brechen Sie einen Rebase nicht ab, den der Benutzer zu Ende führen wollte, nur weil er mühsam ist; brechen Sie nur ab, wenn etwas schiefgeht, und sagen Sie es.
- Pushen Sie nicht nach dem Lösen, es sei denn, Sie werden darum gebeten. Ein Rebase erfordert einen Force-Push; folgen Sie
git-safe-history-rewrite. - Melden Sie jede konfliktäre Datei: was beide Seiten getan haben, was Sie behalten haben und alles, was Sie nicht verifizieren konnten.