byteleaf logo
← zurück zum Blog

Squash, Rebase und wiederkehrende Merge-Konflikte in Git

Um Konflikte zwischen Branches zu beheben, bietet Git verschiedene Möglichkeiten. Bei falscher Handhabung können diese Konflikte wiederholt auftreten. Dies kann aber vermieden werden, wenn wir einfache Prinzipien befolgen.
Jakob Hirscheider
Jakob Hirscheider
28. August 2022

Das Versionsverwaltungstool Git

Laut dem Developer Survey von StackOverflow verwendeten im Jahr 2021 über 90 % der Entwickler Git, womit es das mit Abstand am meisten verwendete Versionsverwaltungstool ist. Eingesetzt wird es dort, wo mehrere Entwickler an der gleichen Codebasis zusammenarbeiten. Für Kollaborationsplattformen wie GitHub oder Azure DevOps bietet es die Grundlage der Codeverwaltung mittels Pull Requests.

Arbeiten mehrere Entwickler an den gleichen Codezeilen, kann es vorkommen, dass Git diese nicht mehr automatisiert zusammenführen kann. In so einem Fall spricht man von einem Merge-Konflikt. Als Entwickler müssen wir jetzt entscheiden, welche Änderungen aus den verschiedenen Codeständen wir behalten wollen.

Bei richtiger Handhabung muss jeder Konflikt nur einmal gelöst werden, und ist dann permanenter Bestandteil der Git-Historie. Insbesondere bei Pull Requests, die Squash-Merges und Rebases erlauben, kann es aber vorkommen, dass Konflikte wiederholt auftreten. Woher das kommt und wie wir das vermeiden können, wird im Folgenden erklärt.

Wie funktioniert Git?

Die komplette Git-Historie besteht aus einer Reihe von Commits, die sich auf Branches befinden:

Main-Branch mit Commits C1, C2, C3, C4
Abbildung 1: Git Commits auf dem Main-Branch

Die Commits C1 bis C4 sind in diesem Fall alle auf dem main-Branch. Die Commits bestehen aus einer Commit-Message, den Änderungen, die im Quellcode vorgenommen wurden, und einer generierten Commit-ID. Wenn mehr als ein Entwicklungsstrang gleichzeitig existieren soll, erstellen wir dazu einen neuen Branch:

Main-Branch mit Commits C1, C2, C3, C4. Feature Branch zweigt von Main im Commit C2 ab und enthält die Commits F1, F2.
Abbildung 2: Commits auf mehreren Branches

Der Branch feature zweigt von dem Commit C2 ab. Der neue Branch setzt sich zusammen aus den Commits F1 und F2, und stellt damit einen abweichenden Stand des Repositories dar.

Wie können Änderungen auf den Ursprungs-Branch zurückgeführt werden?

Um Änderungen wieder auf den ursprünglichen Branch zurückzubringen, können wir in Git eine der drei folgenden Möglichkeiten anwenden: Merge-Commit, Rebase oder Squash-Commit. Beim einfachen Merge wird ein sogenannter Merge-Commit erstellt, der beide Branches als Vorgänger besitzt:

Main-Branch mit Commits C1, C2, C3, C4. Feature-Branch zweigt von Main ab und enthält Commits F1, F2. Merge-Commit auf Main-Branch hat Vorgänger C4 und F2.
Abbildung 3: Mergen zweier Branches mit Merge-Commit

Damit sind in der Git-Historie beide Branches und deren Commit bis zum Merge-Commit verewigt. Der Merge-Commit enthält die Information, welche zwei Branches gemerged wurden, sowie Konfliktlösungen zwischen den beiden Branches.

Beim Rebase werden alle Änderungen des zweiten Branches an der Spitze des ersten Branches erneut ausgeführt. Damit wird auf einen Merge-Commit verzichtet. Da es aber Konflikte zwischen den Commits geben kann, wird in jedem einzelnen Commit des Feature-Branches Konfliktlösung ausgeführt. Diese Commits sind nun neue Commits mit einer neuen Commit-ID:

Main-Branch mit Commits C1, C2, C3, C4. Feature-Branch zweigt ab von Main-Branch und enthält Commits F1*, F2*.
Abbildung 4: Zusammenführen zweier Branches mit Rebase

Das obige Abbild entspricht dem Befehl git switch feature && git rebase main oder einem Pull Request mit Rebase als Merge-Strategie. Der Vorteil des Rebase ist, dass die Git-Historie linear bleibt und damit leichter zurückzuverfolgen ist.

Denkaufgabe: Wie würden die Commits andersrum lauten, also mit git switch main && git rebase feature ?*

Der Squash-Commit geht einen Schritt weiter, indem alle Änderungen eines Branches in einem Commit zusammengefasst werden, und dieser dem Ziel-Branch hinzugefügt wird:

Main-Branch mit Commits C1, C2, C3, C4, Squash-Commit. Feature-Branch zweigt von Main im Commit C2 ab und enthält Commits F1, F2.
Abbildung 5: Übernehmen von Änderungen eines Feature-Branches mit Squash-Commit

Der Feature-Branch wird dabei nicht angefasst, und ist Vorgänger von keinem Commit des Main Branches.

Warum können Konflikte erneut auftreten?

Beim Rebase und Squash-Commit werden neue Commits erstellt, und die Git-Historie ändert sich. Sobald sich die Git-Historie ändert und die IDs nicht mehr dieselben sind wie zuvor, kann Git nicht mehr erkennen, welche Änderungen aus anderen Branches wir schon übernommen haben.

Wie das zu einem Problem führen kann, wird an folgendem Beispiel veranschaulicht: Wir gehen aus von Abbildung 2 mit Master und Feature-Branch. Da es sich bei dem Feature-Branch um einen länger laufenden Entwicklungsstrang handelt und um eine zu große Divergenz zu vermeiden, mergen wir die Änderungen des Master-Branches regelmäßig in den Feature-Branch.

Main-Branch mit Commits C1, C2, C3, C4. Feature-Branch zweigt von Main  im Commit C2 ab und enthält Commits F1, F2, Squash C3/C4.
Abbildung 6: Integration des Main-Branches auf den Feature-Branch durch einen Squash-Commit

Wie hier abgebildet, haben wir die Änderungen auf des Master-Branches mittels Squash-Commit auf den Feature-Branch gebracht. Der Squash-Commit beinhaltet dabei die Commits C3 und C4, und die Konfliktlösungen zwischen den beiden Branches. Die ID wurde neu generiert und ist unterschiedlich zu C3 und C4. Versuchen wir zu einem späteren Zeitpunkt erneut, die Änderungen des Master-Branches in den Feature-Branch zu integrieren, treten genau diese Merge-Konflikte erneut auf (genau genommen kann der gleiche Squash-Merge zweimal in Folge ausgeführt werden und es müssen zweimal dieselben Merge-Konflikte gelöst werden). Das geschieht, weil sich die Commits C3 und C4 nicht in der Historie des Feature-Branches befinden. Git erkennt damit nicht, dass diese Änderungen bereits in dem Feature-Branch aufgenommen wurden. Wie können wiederkehrende Konflikte vermieden werden? Wiederkehrende Konflikte in Git können leicht vermieden werden, wenn man sich an zwei Grundsätze hält:

  • Squash-Commits nur dann verwenden, wenn der Ursprungsbranch nach dem Merge gelöscht wird
  • Für alle anderen Fälle Merge-Commits verwenden

Wird ein Branch nach dem Squash gelöscht, kann es offensichtlich nicht mehr zu Merge-Konflikten kommen, da dessen Historie ebenfalls verschwunden ist. Beim Merge-Commit bleibt die Historie beider Branches unverändert erhalten. Dadurch erkennt Git immer, welche Änderungen bereits im aktuellen Stand integriert sind.

Ist es im eigenen Team-Setup nicht möglich, diese zwei Grundsätze durchzusetzen, sollten wir uns zuerst selbst hinterfragen, ob unsere Strategie zur Verwendung von Git wirklich die richtige ist. Gerade bei großen Projekten und lang laufenden Feature-Branches kann das Mergen von Branches sehr viel Zeit beanspruchen. Diese Zeit einzusparen und in Kauf zu nehmen, dass die Git-Historie nicht perfekt den eigenen Vorstellungen entspricht, hört sich für mich nach einem fairen Tausch an.

* Die Lösung lautet C1-C2-F1-F2-C3*-C4*

Kontakt

E-Mail

info@byteleaf.de

Telefon

+49 89 21527370

Links

Code

GitHub

Wo wir sind

Adresse

byteleaf GmbH
Barthstraße 16
80339 München

ImpressumDatenschutzDatenschutz BewerbungsverfahrenCookie-Einstellungen
© 2022 - Code: byteleaf - Design: Michael Motzek