Learn Git

Rebasing

Es gibt in Git zwei Wege um Änderungen von einem Branch in einen anderen zu überführen: das merge und das rebase-Kommando. In diesem Abschnitt wirst Du kennenlernen was Rebasing ist, wie Du es anwendest, warum es ein verdammt abgefahrenes Werkzeug ist und wann Du es lieber nicht einsetzen möchtest.

Der einfache Rebase

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

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

Der einfachste Weg um Zweige zusammenzuführen ist, wie bereits behandelt, das merge-Kommando. Es produziert einen Drei-Wege-Merge zwischen den beiden letzten Branch-Zuständen (C3 und C4) und ihrem wahrscheinlichsten Vorgänger (C2). Es produziert seinerseits einen Schnappschuss des Projektes (und einen Commit), wie in Abbildung 3-28 dargestellt.

Abbildung 3-28. Das Zusammenführen eines Branches um die verschiedenen Arbeitsfortschritte zu integrieren.

Wie auch immer, es gibt noch einen anderen Weg: Du kannst den Patch der Änderungen – den wir in C3 eingeführt haben – über C4 anwenden. Dieses Vorgehen nennt man in Git rebasing. Mit dem rebase-Kommando kannst Du alle Änderungen die auf einem Branch angewendet 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 zu dem gemeinsamen/allgemeinen Vorfahren [gemeinsamer Vorfahr oder der Ursprung der beiden Branches?] der beiden Branches (des Zweiges auf dem Du arbeitest und des Zweiges auf den Du rebasen möchtest) geht, die Differenzen jedes Commits des aktuellen Branches ermittelt und temporär in einer Datei ablegt. Danach wird der aktuelle Branch auf den Schnittpunkt der beiden Zweige zurückgesetzt und alle zwischengespeicherte Commits nacheinander auf den Zielbranch angewendet. Die Abbildung 3-29 bildet diesen Prozess ab.

Abbildung 3-29. Rebasen der Änderungen durch C3 auf den Zweig 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ämliche keinen Aufwand mit der Integration – nur einen Fast-Forward oder eine saubere Integration (= Rebase?).

Beachte, dass der Schnappschuss nach dem letzten Commit, ob es der letzte der Rebase-Commits nach einem Rebase oder der finale Merge-Commit nach einem Merge ist, exakt gleich ist. Sie unterscheiden sich nur in ihrem Verlauf. 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) eröffnet um ein paar serverseitige Funktionalitäten zu Deinem Projekt hinzuzufügen und 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.

Stell Dir vor, Du entscheidest Dich Deine clientseitigen Änderungen für einen Release in die Hauptlinie zu mergen, die serverseitigen Änderungen möchtest Du aber noch zurückhalten 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 das Kommando git rebase [Basis-Branch] [Themen-Branch] ausführst. Es 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 server-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's 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. Klon 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 Commitd 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 einmergen, 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, weche 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 ist. Wenn Du diesen Verlauf außerdem auf den Server hochlädst, wirst Du dort alle rebasierten Commits einführen, was auch noch andere verwirren kann.

Wenn Du rebasing als Weg behandelst um aufzuräumen und mit Commits zu arbeiten, bevor Du sie hochlädst und wenn Du nur Commits rebased, die noch nie publiziert wurden, dann fährst Du goldrichtig. Wenn Du Commits rebased die bereits veröffentlicht wurden und Leute vielleicht schon ihre Arbeit darauf aufgebaut haben, dann bist Du vielleicht für frustrierenden Ärger verantwortlich.