Git Rebase und das Multiversum 

Als Software Entwickler kommt man früher oder später in den Kontakt mit einem Versionskontrollsystem (VCS) und landet sehr wahrscheinlich bei Git. Ein Feature von Git, welches vielen Entwicklern Kopfzerbrechen bereitet, ist git rebase und sein nerviger kleiner Bruder git push --force.

Das Konzept von Branches und Merges ist in der Regel schnell erlernt, doch was genau ist dieses Rebase und wann sollte ich es nutzen? Und vor allem, wann nicht?

Meines Erachtens ist dieses Feature die größte Ursache für Probleme beim Arbeiten mit Git im Team. Deshalb möchte ich nun im Folgenden versuchen es anschaulich zu erklären, da es richtig angewandt durchaus nützlich ist.

Es wächst zusammen, was zusammengehört

Rebase ist eine von mehreren Varianten, Änderungen von einem Branch in einen anderen zu übertragen. Bei einem konventionellen Merge werden (vereinfacht gesagt) Referenzen von Commits aus einem Quell-Branch in einen Ziel-Branch übertragen, und ein neuer Merge-Commit mit Meta-Informationen dazu hinzugefügt.

Die Commits bleiben dabei mehr oder weniger in der gleichen zeitlichen Reihenfolge, in der sie erstellt wurden. Bei der Entwicklung könnte ich nun regelmäßig Änderungen des Hauptstrangs in meinen davon abgezweigten Feature-Branchmergen, um auch dort immer auf dem aktuellen Stand zu sein.

Am Ende der Entwicklung des Features würde ich den Feature-Branch in den Hauptstrang mergen und die Arbeit ist abgeschlossen. Das Einzige was schief gehen kann, ist dass ein Konflikt auftritt, wenn beide Branches Änderungen an den gleichen Code-Zeilen vorgenommen haben und manuell entschieden werden muss, welche Variante nun bevorzugt wird.

Die unendliche Geschichte

Bei der Verwendung von Merges entstehen mitunter sehr viele dieser Merge-Commits, die am Ende kaum einen Mehrwert bieten, da sich die eigentlichen Code Änderungen in den jeweiligen normalen Commits befinden. Zusätzlich kann es als unschön empfunden werden, dass mehrere Commits, die innerhalb eines Feature-Branches erstellt und ge-merged wurden, chronologisch über die Historie verteilt, statt thematisch schön gruppiert aufgelistet werden.

Letzteres könnte man durch ein git merge --squash beheben, der beim Merge alle Commits des Feature-Branches auf einen einzigen Commit zusammenstaucht. Dann hätte man allerdings bei großen Features einen riesigen Commit, was effektiv, aber auch unbequem bei einer Fehlereingrenzung sein kann.

Eine Zeitmaschine namens Rebase

Für Code-Archäologen mit ästhetischen Ansprüchen ist der Zustand der Git-Historie, den ein ständiges Mergen hinterlässt, nicht hinnehmbar. Rebase wurde deshalb so konzipiert, dass die Historie eines Zeitstrahls (Branch) lediglich pure und thematisch zusammenhängende Commits enthält.

Um dies zu erreichen führt der Aufruf von git rebase <quelle> dazu, dass der Zustand des Feature-Branches auf den aktuellsten Stand des Quell-Branches zurückgespult (rewind) wird. Anschließend werden die Commits, die originär nur im Feature-Branch erstellt wurden, wieder sauber ans Ende der Historie gereiht.

Dies führt dazu, dass sich im Feature-Branch alle neuen Entwicklungen schön säuberlich am Ende des Feature-Branches befinden und nach dem Mergen zurück in den Hauptstrang schick in einer Reihe stehen.

Wenn man dies konsequent durchzieht, gibt es keine unnötigen Merge-Commits im Hauptstrang, der seinen Zustand einfach auf den letzten Stand des Feature-Branches vorspulen (fast-forward) kann.

Butterfly Effect

Wie bei einem Science-Fiction Film kann es zu unerwünschten Komplikationen kommen, wenn man mit Git in der Zeit umherspringt und Zustände verändert. Wenn ich alleine an einem Projekt mit mehreren Feature-Branches arbeite, befinde ich mich in einem einzigen Universum und ich kann jeder Zeit bestimmen, welcher Zustand meine Realität bzw. Wahrheit sein soll.

Bei der Verwendung von Git Rebase sind allerdings Multiversen das Problem. Bei der Arbeit im Team hat jeder Entwickler eine eigene Kopie der Realität bzw. des Zustands des Projektes auf seinem Rechner. Wenn nun mehrere Entwickler am gleichen Feature-Branch arbeiten, ist die Versuchung groß irgendwann einen Rebase des Quell-Branches durchzuführen.

Dies bewirkt nun einen Zeitsprung dieses Entwicklers, der kurz darauf die Geschichte verändert, indem er seine Commits schön säuberlich erneut ans Ende der Geschichte anhängt. Das wäre eigentlich kein Problem, wenn diese Commits nicht (wie bei einem git cherry-pick) neue IDs erhalten würden, obwohl die Inhalte (der Code) in der Regel unberührt bleiben.

Zerstörer der Welten

Dieser Vorgang hat nun unweigerlich zu der Entstehung eines weiteren Universums geführt. Auf den ersten Blick sieht es gleich aus wie das Universum der anderen Entwickler und es verhält sich auch gleich. Allerdings gibt es Abweichungen in der Geschichte, da die IDs der Commits nun nicht mehr kompatibel sind.

Diese Divergenz wird allerdings erst zum Problem, wenn dieser Entwickler versucht, seine Realität als die einzig Wahre durchzusetzen. Glücklicherweise bietet Git einen Abwehrmechanismus, der die Inkompatibilität erkennt und einen Push verhindert.

Dies kann jedoch leicht durch ein kühnes git push --force umgangen werden. Ein Force-Push führt nun dazu, dass der Zustand des Feature-Branches im gemeinsam genutzten Git Remote Repository ausgelöscht und vollständig durch den Zustand des pushenden Entwicklers ersetzt wird. Die Universen der anderen Entwickler kollabieren, sobald sie erneut den aktuellsten Stand aus dem Remote Repository abgleichen.

Dies kann natürlich vollkommen OK und erwünscht sein, vorausgesetzt, der Entwickler hat sein Vorhaben ordentlich kommuniziert und die anderen haben ihre lokalen Änderungen rechtzeitig in Sicherheit gebracht (z.B. durch git stash).

Was lernen wir daraus?

Rebase ist ungefährlich, wenn

  • ich alleine an einem Projekt arbeite
  • ich alleine an einem Feature-Branch arbeite (und Force-Pushes niemand anderes beeinflussen)
  • der Feature-Branch neu ist und noch garnicht nach Remote gepushed wurde
  • das Feature fertig entwickelt ist, und kurz vor dem Merge die Historie neu geordnet werden soll
  • ich meinem Team kommuniziert habe, dass ich einen Rebase und Force-Push vorhabe

Merges sind die bessere Wahl, wenn

  • mir die Ästhetik der Commit-Historie nicht wichtig ist
  • ich aktiv mit mehreren Entwickler an einem Feature-Branch arbeite
  • ich weniger obskure Probleme mit Konflikten haben möchte

TL;DR

Benutzt einfach immer Merges falls ihr im Team an einem Projekt arbeitet und euch nicht gegenseitig den Tag und die Laune vermiesen möchtet 😉