Git
Chapters ▾ 1st Edition

.6 Git Branching - Rebasing

Rebasing

Es gibt in Git zwei Wege, um Änderungen von einem Branch in einen anderen zu überführen: merge und rebase. In diesem Abschnitt wirst Du erfahren, was Rebasing ist, wie Du es anwendest, warum es ein verdammt abgefahrenes Werkzeug ist und bei welchen Gelegenheiten Du es besser nicht einsetzen solltest.

Der einfache Rebase

Wenn Du zu einem früheren Beispiel aus dem Merge-Kapitel zurückkehrst (siehe Abbildung 3-27), kannst Du sehen, dass Du Deine Arbeit aufgeteilt und Commits auf zwei unterschiedlichen Branches erstellt hast.


Abbildung 3-27. Deine initiale Commit-Historie zum Zeitpunkt der Aufteilung.

Der einfachste Weg, um Zweige zusammenzuführen, ist, wie bereits behandelt, die merge-Anweisung. Sie führt einen Drei-Wege-Merge durch zwischen den beiden letzten Branch-Zuständen (C3 und C4) und dem letzen gemeinsamen Vorgänger (C2) der beiden, erstellt einen neuen Schnappschuss (und einen Commit), wie in Abbildung 3-28 dargestellt.


Abbildung 3-28. Zusammenführen von Branches, um die verschiedenen Arbeitsfortschritte zu integrieren.

Allerdings gibt es noch einen anderen Weg: Du kannst den Patch der Änderungen, den wir in C3 eingeführt haben, nehmen und erneut anwenden auf C4. Dieses Vorgehen nennt man in Git rebasing. Mit der rebase-Anweisung kannst Du alle Änderungen, die an einem Branch vorgenommen wurden, auf einen anderen Branch erneut anwenden.

In unserem Beispiel würdest Du folgendes ausführen:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Dies funktioniert, indem Git zum letzten gemeinsamen Vorfahren der beiden Branches (der, auf dem Du arbeitest, und jener, auf den Du rebasen möchtest) geht, dann die Informationen zu den Änderungen (diffs) sammelt, welche seit dem bei jedem einzelen Commit des aktuellen Branches gemacht wurden, diese in temporären Dateien speichert, den aktuellen Branch auf den gleichen Commit setzt wie den Branch, auf den Du rebasen möchtest und dann alle Änderungen erneut durchführt. Die Abbildung 3-29 bildet diesen Prozess ab.


Abbildung 3-29. Rebasen der in C3 durchgeführten Änderungen auf C4.

An diesem Punkt kannst Du zurück zum Master-Branch wechseln und einen fast-forward Merge durchführen (siehe Abbildung 3-30).


Abbildung 3-30. Fast-forward des Master-Branches.

Nun ist der Schnappschuss, auf den C3' zeigt, exakt der gleiche, wie der auf den C5 in dem Merge-Beispiel gezeigt hat. Bei dieser Zusammenführung entsteht kein unterschiedliches Produkt, durch Rebasing ensteht allerdings ein sauberer Verlauf. Bei genauerer Betrachtung der Historie entpuppt sich der Rebased-Branch als linearer Verlauf – es scheint, als sei die ganze Arbeit in einer Serie entstanden, auch wenn sie in Wirklichkeit parallel stattfand.

Du wirst das häufig anwenden um sicherzustellen, dass sich Deine Commits sauber in einen Remote-Branch integrieren – möglicherweise in einem Projekt, bei dem Du Dich beteiligen möchtest, Du jedoch nicht der Verantwortliche bist. In diesem Fall würdest Du Deine Arbeiten in einem eigenen Branch erledigen und im Anschluss Deine Änderungen auf origin/master rebasen. Dann hätte der Verantwortliche nämlich keinen Aufwand mit der Integration – nur einen Fast-Forward oder eine saubere Integration (= Rebase?).

Beachte, dass der Schnappschuss, welche auf den letzten Commit zeigt, ob es nun der letzte der Rebase-Commits nach einem Rebase oder der finale Merge-Commit nach einem Merge ist, der selbe Schnappschuss ist, nur der Verlauf ist ein anderer. Rebasing wiederholt einfach die Änderungen einer Arbeitslinie auf einer anderen in der Reihenfolge, in der sie entstanden sind. Im Gegensatz hierzu nimmt Merging die beiden Endpunkte der Arbeitslinien und führt diese zusammen.

Mehr interessante Rebases

Du kannst Deinen Rebase auch auf einem anderen Branch als dem Rebase-Branch anwenden lassen. Nimm zum Beispiel den Verlauf in Abbildung 3-31. Du hattest einen Themen-Branch (server) angelegt, um ein paar serverseitige Funktionalitäten zu Deinem Projekt hinzuzufügen, und hast dann einen Commit gemacht. Dann hast Du einen weiteren Branch abgezweigt, um clientseitige Änderungen (client) vorzunehmen und dort ein paarmal committed. Zum Schluss hast Du wieder zu Deinem Server-Branch gewechselt und ein paar weitere Commits gebaut.


Abbildung 3-31. Ein Verlauf mit einem Themen-Branch basierend auf einem weiteren Themen-Branch.

Angenommen, Du entscheidest Dich, Deine clientseitigen Änderungen für einen Release in die Hauptlinie zu mergen, während Du die serverseitigen Änderungen noch zurückhalten möchtest, bis sie besser getestet wurden. Du kannst einfach die Änderungen am Client, die den Server nicht betreffen, (C8 und C9) mit der --onto-Option von git rebase erneut auf den Master-Branch anwenden:

$ git rebase --onto master server client

Das bedeutet einfach, “Checke den Client-Branch aus, finde die Patches heraus, die auf dem gemeinsamen Vorfahr der client- und server-Branches basieren und wende sie erneut auf dem master-Branch an.” Das ist ein bisschen komplex, aber das Ergebnis – wie in Abbildung 3-32 – ist richtig cool.


Abbildung 3-32. Rebasing eines Themen-Branches von einem anderen Themen-Branch.

Jetzt kannst Du Deinen Master-Branch fast-forwarden (siehe Abbildung 3-33):

$ git checkout master
$ git merge client


Abbildung 3-33. Fast-forwarding Deines Master-Branches um die Client-Branch-Änderungen zu integrieren.

Lass uns annehmen, Du entscheidest Dich, Deinen Server-Branch ebenfalls einzupflegen. Du kannst den Server-Branch auf den Master-Branch rebasen, ohne diesen vorher auschecken zu müssen, indem Du die Anweisung git rebase [Basis-Branch] [Themen-Branch] ausführst. Sie macht für Dich den Checkout des Themen-Branches (in diesem Fall server) und wiederholt ihn auf dem Basis-Branch (master):

$ git rebase master server

Das wiederholt Deine server-Arbeit auf der Basis der master-Arbeit, wie in Abbildung 3-34 ersichtlich.


Abbildung 3-34. Rebasing Deines Server-Branches auf Deinen Master-Branch.

Dann kannst Du den Basis-Branch (master) fast-forwarden:

$ git checkout master
$ git merge server

Du kannst den client- und server-Branch nun entfernen, da Du die ganze Arbeit bereits integriert wurde und sie nicht mehr benötigst. Du hinterlässt den Verlauf für den ganzen Prozess wie in Abbildung 3-35:

$ git branch -d client
$ git branch -d server


Abbildung 3-35: Endgültiger Commit-Verlauf.

Die Gefahren des Rebasings

Ahh, aber der ganze Spaß mit dem Rebasing kommt nicht ohne seine Schattenseiten, welche in einer einzigen Zeile zusammengefasst werden können:

Rebase keine Commits die Du in ein öffentliches Repository hochgeladen hast.

Wenn Du diesem Ratschlag folgst, ist alles in Ordnung. Falls nicht, werden die Leute Dich hassen und Du wirst von Deinen Freunden und Deiner Familie verachtet.

Wenn Du Zeug rebased, hebst Du bestehende Commits auf und erstellst stattdessen welche, die zwar ähnlich aber unterschiedlich sind. Wenn Du Commits irgendwohin hochlädst und andere ziehen sich diese herunter und nehmen sie als Grundlage für ihre Arbeit, dann müssen Deine Mitwirkenden ihre Arbeit jedesmal re-mergen, sobald Du Deine Commits mit einem git rebase überschreibst und verteilst. Und richtig chaotisch wird es, wenn Du versuchst, deren Arbeit in Deine Commits zu integrieren.

Lass uns mal ein Beispiel betrachten, wie das Rebasen veröffentlichter Arbeit Probleme verursachen kann. Angenommen, Du klonst von einem zentralen Server und werkelst ein bisschen daran rum. Dein Commit-Verlauf sieht wie in Abbildung 3-36 aus.


Abbildung 3-36. Klone ein Repository und baue etwas darauf auf.

Ein anderer arbeitet unterdessen weiter, macht einen Merge und lädt seine Arbeit auf den zentralen Server. Du fetchst die Änderungen und mergest den neuen Remote-Branch in Deine Arbeit, sodass Dein Verlauf wie in Abbildung 3-37 aussieht.


Abbildung 3-37. Fetche mehrere Commits und merge sie in Deine Arbeit.

Als nächstes entscheidet sich die Person, welche den Merge hochgeladen hat, diesen rückgängig zu machen und stattdessen die Commits zu rebasen. Sie macht einen git push --force, um den Verlauf auf dem Server zu überschreiben. Du lädst Dir das Ganze dann mit den neuen Commits herunter.


Abbildung 3-38. Jemand pusht rebased Commits und verwirft damit Commits, auf denen Deine Arbeit basiert.

Nun musst Du seine Arbeit erneut in Deine Arbeitslinie mergen, obwohl Du das bereits einmal gemacht hast. Rebasing ändert die SHA-1-Hashes der Commits, weshalb sie für Git wie neue Commits aussehen. In Wirklichkeit hast Du die C4-Arbeit bereits in Deinem Verlauf (siehe Abbildung 3-39).


Abbildung 3-39. Du mergst die gleiche Arbeit nochmals in einen neuen Merge-Commit.

Irgendwann musst Du seine Arbeit mergen, damit Du auch zukünftig mit dem anderen Entwickler zusammenarbeiten kannst. Danach wird Dein Commit-Verlauf sowohl den C4 als auch den C4'-Commit enthalten, welche zwar verschiedene SHA-1-Hashes besitzen, aber die gleichen Änderungen und die gleiche Commit-Beschreibung enthalten. Wenn Du so einen Verlauf mit git log betrachtest, wirst Du immer zwei Commits des gleichen Autors, zur gleichen Zeit und mit der gleichen Commit-Nachricht sehen. Was ganz schön verwirrend sein wird. Wenn Du diesen Verlauf außerdem auf den Server hochlädst, wirst Du dort alle rebasierten Commits nochmals einführen, was die Leute noch mehr verwirren kann.

Wenn Du rebasing als einen Weg getrachtest, um aufzuräumen und mit Commits zu arbeiten, bevor Du sie hochlädst und wenn Du rebase nur auf Commits anwendest, die noch nie öffentlich zugänglich waren, dann fährst Du goldrichtig. Wenn Du rebase auf Commits anwendest, die bereits veröffentlicht wurden und Leute vielleicht schon ihre Arbeit darauf aufgebaut haben, dann kannst Du Dich auf frustrierenden Ärger gefasst machen.