Git Merge, Rebase und Konflikte

Tobias Janssen

Backend Software-Developer und GIT Enthusiast bei traperto GmbH
GIT Logo

Jeder Entwickler, der mit GIT arbeitet, stand schon mal vor der Entscheidung ob Änderungen zwischen zwei Branches über einen Merge oder einen Rebase zusammen gefasst werden sollen.
Aber wann sollte was verwendet werden und welche Besonderheiten bietet ein Rebase oder ein Merge?
Um die Unterschiede genau aufzeigen zu können, brauchen wir eine einheitliche Basis:
In einem Repository mit einem Master Branch existieren mehrere Branches, die als Quelle den Master Branch verwenden.

GIT Quelle

Nehmen wir folgendes an:
Beide Branches haben an dem Feature Telefonbuch gearbeitet und die Änderungen von Branch A sind bereits im Master Branch. Wie werden nun die Änderungen von Branch B in den Master Branch überführt?

Merge

Bei der Methode Branch B in den Master Branch zu mergen werden die Änderungen in den Master Branch überführt und ein extra Commit erzeugt. Dieser extra Commit wird Merge Commit genannt.

$ git checkout master
$ git merge branch-b

Wenn bei jedem Merge ein Merge Commit erzeugt wird, kann das GIT Log schnell unübersichtlich werden. Hier ein kleines Beispiel wie verwirrend der Log aussehen kann:

* 70bd00f (WeirdBranch-8) Merge branch 'WeirdBranch-4' into WeirdBranch-8
|\
| * a89ccbf (WeirdBranch-4) Merge branch 'WeirdBranch-3' into WeirdBranch-4
| |\
| | * 7ce75e5 (WeirdBranch-3) Merge branch 'WeirdBranch-2' into WeirdBranch-3
| | |\
| | | * 15587c0 (WeirdBranch-2) Weird Branch File 2
| | * | 27cee09 Weird Branch File 3
| | |/
| * / ae2539e Weird Branch File 4
| |/
* | ecfbdde Merge branch 'WeirdBranch-1' into WeirdBranch-8
|\ \
* \ \ dad7411 Merge branch 'WeirdBranch-9' into WeirdBranch-8
|\ \ \
* | | | a025320 Weird Branch File 8
| |_|/
|/| |
| | | * 34117fb (HEAD -> WeirdBranch-1, WeirdBranch-9) Merge branch 'WeirdBranch-1' into WeirdBranch-9
| | |/|
| |/|/
| | * 42a7fe8 Weird Branch File 1
| |/
|/|
| * 6090155 Merge branch 'WeirdBranch-10' into WeirdBranch-9
| |\
| | * f836c34 (WeirdBranch-10) Weird Branch File 10
| |/
|/|
| * c05db36 Weird Branch File 9
|/

Rebase

Bei einem Rebase wird kein extra Commit erzeugt, sondern die Quelle des Branches angepasst. Re-Base ist das umbiegen/anpassen der Basis.

$ git checkout branch-b
$ git rebase master

Vorteil des Rebase ist, dass kein extra Commit erzeugt wird und somit der Log sehr übersichtlich bleibt. Ein Nachteil ist, dass, sobald mit Remote Repositories gearbeitet wird, die Historie des Branches angepasst wird und so ein Force Push getätigt werden muss. Ein Force Push überschreibt den aktuellen Stand der Historie und ist deshalb mit Vorsicht zu verwenden.

$ git push -f

Konflikte

Egal für welche Zusammenführungsstrategie man sich entscheidet, bei beiden Verfahren kann es zu Konflikten kommen. Konflikte entstehen, wenn bei Branches (Branch A und Branch B) die gleiche Basis verwenden wird, die Änderungen von Branch A jedoch schon im Master Branch sind und im Branch B zufällig an der gleichen Datei & Zeile gearbeitet wurde. Wenn Branch B in den Master Branch überführt werden soll, kommt es so zu einem Konflikt:

automatischer Merge von Telefonbuch.txt
KONFLIKT (Inhalt): Merge-Konflikt in Telefonbuch.txt
Automatischer Merge fehlgeschlagen; beheben Sie die Konflikte und committen Sie dann das Ergebnis.

GIT weiß an dieser Stelle nicht Was ist jetzt richtig? – verwende ich die Änderung aus Branch B oder behalte ich meine Änderung.
Über den aktuellen Status können wir genau erfahren um welche Datei es sich hierbei handelt:

Auf Branch master
Sie haben nicht zusammengeführte Pfade.
 (beheben Sie die Konflikte und führen Sie "git commit" aus)
  (benutzen Sie "git merge --abort", um den Merge abzubrechen)

Nicht zusammengeführte Pfade:
  (benutzen Sie "git add/rm <Datei>...", um die Auflösung zu markieren)
	von beiden geändert:    Telefonbuch.txt

keine Änderungen zum Commit vorgemerkt (benutzen Sie "git add" und/oder "git commit -a")

Sehen wir uns jetzt die Datei genauer an, sehen wir, wie GIT ein Konflikt markiert.

 1 <<<<<<< HEAD
  2 Tobias Janssen 0123 - 1234
  3 Fabian Grebe - 999 - 1111
  4 =======
  5 Tobias Janssen 0123 - 1231
  6 >>>>>>> branch-b

In der ersten Zeile steht HEAD, betrachten wir HEAD als eine Art Zeiger auf den aktuellen Branch. In dem HEAD sind zwei Zeilen mit der Telefonnummer von Tobias und Fabian hinterlegt. Die Änderung aus Branch B zeigt, dass sich die Telefonnummer von Tobias geändert hat.
Der Konflikt kann jetzt händisch gelöst und durch ein Commit in den Master Branch überführt werden. Es gibt auch hilfreiche Tools, die ein Konflikt besser darstellen. Zum einen macht das Visual Studio Code oder Kdiff3.
Welches Tool bei einem Konflikt verwendet wird, kann in der .gitconfig hinterlegt werden. In diesem Fall verwende ich das Tool Kdiff3. Mit git mergetool wird das Tool ausgeführt. Die Konfigdatei ist eine versteckte Datei (~/.gitconfig) und kann mit jedem beliebigen Texteditor angepasst werden. 

# This is Git's per-user configuration file.
[user]
# Please adapt and uncomment the following lines:
        name = Tobias Janssen
        email = example@example.com
[mergetool "kdiff3"]
        path = <path>/kdiff3

Zum einen sind hier die Benutzerinformationen hinterlegt, die z.B. bei einem Commit verwendet werden und zum anderen der Pfad des Kdiff3 Tools.

Interactive Rebase

Ein Interactive Rebase ist ein Rebase, bei dem eine Veränderung eines jeden Commits möglich ist. Mit dem Interactive Rebase können unter anderem Commits zusammengefasst, geändert, neu dokumentiert, oder auch ganz entfernt werden.
Mit anderen Worten: Ein Interactive Rebase ist ein Werkzeug um sein Repository aufzuräumen und zu sortieren.
Sehen wir uns einmal den GIT Log an:

*   39526a0 (HEAD -> master) Anpassung der Telefonnumer von Tobias
|\
| * de20bd8 (branch-b) Anpassung der Telefonnumer von Tobias
* | 4283bc1 (branch-a) Fabian hinzugefügt
|/
* de4d379 Telefonbuch

Durch die Ähnlichkeiten der Commit Nachrichten aus Commit de20bd8 und Commit 4283bc1 kann man erahnen, dass diese Commits gut zusammengefasst werden können. Um herauszufinden, welche Änderungen genau in diesen Commits getätigt wurden, kann der git show Befehl verwendet werden:

$ git show 4283bc1

commit 4283bc1cd768b838271e0a9f348d6ae961f2f3ab (branch-a)
Author: Tobias Janssen <example@example.com>
Date:   Thu Jul 30 21:15:41 2020 +0200

    Fabian hinzugefügt

diff --git a/Telefonbuch.txt b/Telefonbuch.txt
index 34ee513..403c890 100644
--- a/Telefonbuch.txt
+++ b/Telefonbuch.txt
@@ -1 +1,2 @@
 Tobias Janssen 0123 - 1234
+Fabian Grebe - 999 - 1111

Der Show Befehl zeigt die genauen Änderungen innerhalb des Commits an. Verwende ich den gleichen Befehl für den anderen Commit, stellen wir fest, dass sich die Änderungen ähneln und somit zusammengefasst werden können. Mit folgendem Befehl wird der Interactive Rebase für die letzten zwei Commits gestartet:

$ git rebase -i HEAD~2

Anschließend öffnet sich folgendes Menü:

pick 4283bc1 Fabian hinzugefügt
pick de20bd8 Anpassung der Telefonnumer von Tobias

# Rebase von de4d379..39526a0 auf de4d379 (2 Kommandos)
#
# Befehle:
# p, pick <Commit> = Commit verwenden
# r, reword <Commit> = Commit verwenden, aber Commit-Beschreibung bearbeiten
# e, edit <Commit> = Commit verwenden, aber zum Nachbessern anhalten
# s, squash <Commit> = Commit verwenden, aber mit vorherigem Commit vereinen
# f, fixup <Commit> = wie "squash", aber diese Commit-Beschreibung verwerfen
# x, exec <Commit> = Befehl (Rest der Zeile) mittels Shell ausführen
# b, break = hier anhalten (Rebase später mit 'git rebase --continue' fortsetzen)
# d, drop <Commit> = Commit entfernen
# l, label <Label> = aktuellen HEAD mit Label versehen
# t, reset <Label> = HEAD zu einem Label umsetzen
# m, merge [-C <Commit> | -c <Commit>] <Label> [# <eineZeile>]
# .       Merge-Commit mit der originalen Merge-Commit-Beschreibung erstellen
# .       (oder die eine Zeile, wenn keine originale Merge-Commit-Beschreibung
# .       spezifiziert ist). Benutzen Sie -c <Commit> zum Bearbeiten der
# .       Commit-Beschreibung.

Wenn bei dem zweiten Commit anstatt pick ein s (kurzschreibweise für squash) verwendet wird, werden die Commits zusammengefasst. GIT fasst zwar die Commits zusammen, weiß aber nicht, welche Commit Nachricht verwendet werden soll. Standardmäßig werden die Nachrichten zusammengefasst. GIT fragt aber vorher nochmal wie letztendlich die Commit Nachricht aussehen soll.
Hier wird auch wieder die Historie angepasst und es bedarf eines Force Push, sollte auf einem Remote Repository gearbeitet werden.
Diesen neu erstellten Commit sehen wir im Log:

* ae5c5a8 (HEAD -> master) Anpassung und neue Telefonnummern
* de4d379 Telefonbuch

Dadurch wird der Log übersichtlicher und lesbarer.