Git
Chapters ▾ 2nd Edition

3.6 Git Branching - Rebasing

Rebasing

Es gibt bei Git zwei Wege, um Änderungen von einem Branch in einen anderen zu integrieren: merge und rebase. In diesem Abschnitt werden Sie erfahren, was Rebasing ist, wie Sie es anwenden, warum es ein verdammt abgefahrenes Werkzeug ist und bei welchen Gelegenheiten Sie es besser nicht einsetzen sollten.

Einfacher Rebase

Wenn Sie sich noch mal ein früheres Beispiel aus Einfaches Merging anschauen, können Sie sehen, dass Sie Ihre Arbeit verzweigt und Commits auf zwei unterschiedlichen Branches erstellt haben.

Einfacher verzweigter Verlauf
Figure 35. Einfacher verzweigter Verlauf

Der einfachste Weg, die Branches zu integrieren ist der Befehl merge, wie wir bereits besprochen haben. Er führt einen Drei-Wege-Merge zwischen den beiden letzten Zweig-Snapshots (C3 und C4) und dem jüngsten gemeinsamen Vorfahren der beiden (C2) durch und erstellt einen neuen Snapshot (und Commit).

Zusammenführen (Merging) verzweigter Arbeitsverläufe
Figure 36. Zusammenführen (Merging) verzweigter Arbeitsverläufe

Allerdings gibt es noch einen anderen Weg: Sie können den Patch der Änderungen, den wir in C4 eingeführt haben, nehmen und an der Spitze von C3 erneut anwenden. Dieses Vorgehen nennt man in Git rebasing. Mit dem Befehl rebase können Sie alle Änderungen, die in einem Branch vorgenommen wurden, übernehmen und in einem anderen Branch wiedergeben.

Für dieses Beispiel würden Sie den Branch experiment auschecken und dann wie folgt auf den master Branch neu ausrichten (engl. rebase):

$ 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 Sie arbeiten, und jener, auf den Sie rebasen möchten) geht, dann die Informationen zu den Änderungen (diffs) sammelt, welche seitdem bei jedem einzelnen 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 Sie rebasen möchten, und dann alle Änderungen erneut durchführt.

Rebase der in `C4` eingeführten Änderung auf `C3`
Figure 37. Rebase der in C4 eingeführten Änderung auf C3

An diesem Punkt können Sie zurück zum master Branch wechseln und einen fast-forward-Merge durchführen.

$ git checkout master
$ git merge experiment
Vorspulen (fast-forwarding) des `master` Branches
Figure 38. Vorspulen (fast-forwarding) des master Branches

Jetzt ist der Schnappschuss, der auf C4' zeigt, exakt derselbe wie derjenige, auf den C5 in dem Merge-Beispiel gezeigt hat. Es gibt keinen Unterschied im Endergebnis der Integration, aber das Rebase sorgt für einen klareren Verlauf. Wenn man das Protokoll eines rebasierten Branches betrachtet, sieht es aus wie eine lineare Historie: Es scheint, dass alle Arbeiten in Serie stattgefunden hätten, auch wenn sie ursprünglich parallel stattgefunden haben.

Häufig werden Sie das anwenden, damit Ihre Commits sauber auf einen Remote-Zweig angewendet werden – vielleicht in einem Projekt, zu dem Sie beitragen möchten, das Sie aber nicht pflegen. In diesem Fall würden Sie Ihre Arbeiten in einem Branch erledigen und im Anschluss Ihre Änderungen mittels Rebase zu origin/master hinzufügen, wenn Sie soweit sind, Ihre Patches dem Hauptprojekt zu übermitteln. Auf diese Weise muss der Maintainer keine Integrationsarbeiten durchführen – nur einen „fast-forward“ oder ein einfaches Einbinden Ihres Patches.

Beachten Sie, dass der Snapshot, auf welchen der letzte Commit zeigt, ob es nun der letzte des Rebase-Commits nach einem Rebase oder der finale Merge-Commit nach einem Merge ist, derselbe Schnappschuss ist, nur der Verlauf ist ein anderer. Rebasing wiederholt die Änderungsschritte von einer Entwicklungslinie auf einer anderen in der Reihenfolge, in der sie entstanden sind, wohingegen beim Mergen die beiden Endpunkte der Branches genommen und miteinander verschmolzen werden.

Weitere interessante Rebases

Sie können Ihr Rebase auch auf einen anderen Branch als den Rebase-Ziel-Branch anwenden. Nehmen Sie zum Beispiel einen Verlauf wie im Bild: Ein Verlauf mit einem Themen-Branch neben einem anderen Themen-Branch. Sie haben einen Themen-Branch (server) angelegt, um ein paar serverseitige Funktionalitäten zu Ihrem Projekt hinzuzufügen, und haben dann einen Commit gemacht. Anschließend haben Sie von diesem einen weiteren Branch abgezweigt, um clientseitige Änderungen (client) vorzunehmen, und haben ein paar Commits durchgeführt. Zum Schluss wechselten Sie wieder zurück zu Ihrem server Branch und machten ein paar weitere Commits.

Ein Verlauf mit einem Themen-Branch neben einem anderen Themen-Branch
Figure 39. Ein Verlauf mit einem Themen-Branch neben einem anderen Themen-Branch

Angenommen, Sie entscheiden sich, dass Sie für einen Release Ihre clientseitigen Änderungen mit Ihrer Hauptentwicklungslinie zusammenführen, während Sie die serverseitigen Änderungen noch zurückhalten wollen, bis diese weiter getestet wurden. Sie können einfach die Änderungen am client Branch (C8 und C9), die nicht auf server Branch sind, nehmen und mit der Anweisung git rebase zusammen mit der Option --onto erneut auf den master Branch anwenden:

$ git rebase --onto master server client

Das bedeutet im Wesentlichen, „Checke den client Branch aus, finde die Patches des gemeinsamen Vorgängers der Branches client und server heraus und wende sie erneut auf den master Branch an.“ Das ist ein wenig komplex, aber das Resultat ist ziemlich toll.

Rebasing eines Themen-Branches aus einem anderen Themen-Branch
Figure 40. Rebasing eines Themen-Branches aus einem anderen Themen-Branch

Jetzt können Sie Ihren Master-Branch vorspulen (engl. fast-forward) (siehe Vorspulen Ihres master Branches zum Einfügen der Änderungen des client Branches):

$ git checkout master
$ git merge client
Vorspulen Ihres `master` Branches zum Einfügen der Änderungen des `client` Branches
Figure 41. Vorspulen Ihres master Branches zum Einfügen der Änderungen des client Branches

Lassen Sie uns annehmen, Sie entscheiden sich dazu, Ihren server Branch ebenfalls einzupflegen. Sie können das Rebase des server Branches auf den master Branch anwenden, ohne diesen vorher auschecken zu müssen, indem Sie die Anweisung git rebase <Basis-Branch> <Themen-Branch> ausführen, welche für Sie den Themen-Branch auscheckt (in diesem Fall server) und ihn auf dem Basis-Branch (master) wiederholt:

$ git rebase master server

Das wiederholt Ihre Änderungen aus dem server Branch an der Spitze des master Branches, wie in Rebase Ihres server Branches an der Spitze Ihres master Branches gezeigt wird.

Rebase Ihres `server` Branches an der Spitze Ihres `master` Branches
Figure 42. Rebase Ihres server Branches an der Spitze Ihres master Branches

Dann können Sie den Basis-Branch (master) vorspulen (engl. fast-forward):

$ git checkout master
$ git merge server

Sie können die Branches client und server löschen, da die ganze Arbeit bereits in master integriert wurde und Sie diese nicht mehr benötigen. Ihr Verlauf für diesen gesamten Prozess sieht jetzt wie in Endgültiger Commit-Verlauf aus:

$ git branch -d client
$ git branch -d server
Endgültiger Commit-Verlauf
Figure 43. Endgültiger Commit-Verlauf

Die Gefahren des Rebasing

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

Führen Sie keinen Rebase mit Commits durch, die außerhalb Ihres Repositorys existieren und auf welche die Arbeit anderer Personen basiert.

Wenn Sie sich an diese Leitlinie halten, werden Sie gut zurechtkommen. Wenn Sie es nicht tun, werden die Leute Sie hassen, und Sie werden von Freunden und Familie verachtet werden.

Wenn Sie ein Rebase durchführen, heben Sie bestehende Commits auf und erstellen stattdessen neue, die zwar ähnlich aber dennoch unterschiedlich sind. Wenn Sie Commits irgendwohin hochladen und andere ziehen sich diese herunter und nehmen sie als Grundlage für ihre Arbeit, dann müssen Ihre Partner ihre Arbeit jedesmal erneut zusammenführen, sobald Sie Ihre Commits mit einem git rebase überschreiben und wieder hochladen. Und richtig chaotisch wird es, wenn Sie versuchen, deren Arbeit in Ihre Eigene zu integrieren.

Schauen wir uns ein Beispiel an, wie ein Rebase von Arbeiten, die Sie öffentlich gemacht haben, Probleme verursachen kann. Angenommen, Sie klonen von einem zentralen Server und arbeiten dann daran. Ihr Commit-Verlauf sieht aus wie dieser:

Klonen eines Repositorys und darauf Arbeit aufbauen
Figure 44. Klonen eines Repositorys und darauf Arbeit aufbauen

Jetzt erledigt jemand anderes eine weitere Arbeit, die einen Merge einschließt, und pusht diese Arbeit auf den zentralen Server. Sie holen die Änderungen ab und mergen den neuen Remote-Branch mit Ihrer Arbeit, sodass Ihr Verlauf wie folgt aussieht.

Weitere Commits abholen und mergen mit Ihrer Arbeit
Figure 45. Weitere Commits abholen und mergen mit Ihrer Arbeit

Als nächstes entscheidet sich die Person, welche die zusammengeführte Arbeit hochgeladen hat, diese rückgängig zu machen und stattdessen Ihre Arbeit mittels Rebase hinzuzufügen. Sie führt dazu die Anweisung git push --force aus, um den Verlauf auf dem Server zu überschreiben. Sie holen das Ganze dann von diesem Server ab und laden die neuen Commits herunter.

Jemand lädt Commits nach einem Rebase hoch und verwirft damit Commits, auf denen Ihre Arbeit basiert
Figure 46. Jemand lädt Commits nach einem Rebase hoch und verwirft damit Commits, auf denen Ihre Arbeit basiert

Jetzt sitzen Sie beide in der Klemme. Wenn Sie ein git pull durchführen, würden Sie einen Merge-Commit erzeugen, welcher beide Entwicklungslinien einschließt, und Ihr Repository würde so aussehen:

Sie lassen die Änderungen nochmals in dieselbe Arbeit einfließen in einen neuen Merge-Commit
Figure 47. Sie lassen die Änderungen nochmals in dieselbe Arbeit einfließen in einen neuen Merge-Commit

Falls Sie ein git log ausführen, wenn Ihr Verlauf so aussieht, würden Sie zwei Commits sehen, bei denen Autor, Datum und Nachricht übereinstimmen, was verwirrend sein würde. Weiter würden Sie, wenn Sie diesen Verlauf zurück auf den Server pushen, alle diese vom Rebase stammenden Commits auf dem zentralen Server neu einführen, was die Leute noch weiter durcheinander bringen kann. Man kann ziemlich sicher davon ausgehen, dass der andere Entwickler C4 und C6 nicht im Verlauf haben möchte; das ist der Grund, warum derjenige das Rebase überhaupt gemacht hat.

Rebasen, wenn Sie Rebase durchführen

Wenn Sie sich in einer solchen Situation befinden, hat Git eine weitere magische Funktion, die Ihnen helfen könnte. Falls jemand in Ihrem Team gewaltsam Änderungen vorantreibt, die Arbeiten überschreiben, auf denen Sie basiert haben, besteht Ihre Herausforderung darin, herauszufinden, was Ihnen gehört und was andere überschrieben haben.

Es stellt sich heraus, dass Git neben der SHA-1-Prüfsumme auch eine Prüfsumme berechnet, die nur auf dem mit dem Commit eingeführten Patch basiert. Das nennt man eine „patch-id“.

Wenn Sie die neu geschriebene Arbeit pullen und sie mit einem Rebase auf die neuen Commits Ihres Partners umstellen, kann Git oft erfolgreich herausfinden, was allein von Ihnen ist und kann sie wieder auf den neuen Branch anwenden.

Sobald wir im vorhergehenden Szenario, beispielsweise bei Jemand lädt Commits nach einem Rebase hoch und verwirft damit Commits, auf denen Ihre Arbeit basiert, die Anweisung git rebase teamone/master ausführen, anstatt ein Merge durchzuführen, dann wird Git

  • bestimmen, welche Änderungen an unserem Branch einmalig sind (C2, C3, C4, C6, C7),

  • bestimmen, welche der Commits keine Merge-Commits sind (C2, C3, C4),

  • bestimmen, welche Commits nicht neu in den Zielbranch geschrieben wurden (bloß C2 und C3, da C4 der selbe Patch wie C4' ist), und

  • diese Commits an der Spitze des teamone/master Branches anwenden.

Rebase an der Spitze von Änderungen eines „force-pushed“-Rebase
Figure 48. Rebase an der Spitze von Änderungen eines „force-pushed“-Rebase.

Das funktioniert nur, wenn es sich bei C4 und C4', welchen Ihr Teamkollege erstellt hat, um fast genau denselben Patch handelt. Andernfalls kann die Datenbank nicht erkennen, dass es sich um ein Duplikat handelt und fügt einen weiteren, dem Patch C4 ähnlichen, hinzu (der wahrscheinlich nicht sauber angewendet wird, da die Änderungen bereits (vollständig) oder zumindest teilweise vorhanden wären).

Sie können das auch vereinfachen, indem Sie ein git pull --rebase anstelle eines normalen git pull verwenden. Oder Sie könnten es manuell mit einem git fetch machen, in diesem Fall gefolgt von einem git rebase teamone/master.

Wenn Sie git pull benutzen und ` -rebase` zur Standardeinstellung machen wollen, können Sie den pull.rebase Konfigurationswert mit etwas wie git config --global pull.rebase true einstellen.

Wenn Sie nur Commits rebasen, die noch nie Ihren eigenen Computer verlassen haben, wird es Ihnen gut gehen. Wenn Sie Commits, die gepusht wurden, aber niemand sonst hat, basierend auf den Commits, rebast, werden Sie auch in Ordnung sein. Wenn Sie Commits, die bereits veröffentlicht wurden, rebasen und Leute die Arbeit auf diesen Commits basieren, dann werden Sie vielleicht frustrierende Probleme und die Verachtung Ihrer Teamkollegen ernten.

Wenn Sie oder ein Partner es irgendwann für unbedingt notwendig halten, stellen Sie sicher, dass jeder weiß, dass er anschließend git pull --rebase laufen lassen muss. So kann er versuchen, den Schaden einzugrenzen, nachdem er passiert ist, um alles etwas einfacher zu machen.

Rebase vs. Merge

Nachdem Sie jetzt Rebasen und Merging in Aktion erlebt haben, fragen Sie sich vielleicht, welches davon besser ist. Bevor wir das beantworten können, lassen Sie uns ein klein wenig zurückblicken und darüber reden, was der Verlauf bedeutet.

Ein Standpunkt ist, dass der Commit-Verlauf Ihres Repositorys eine Aufzeichnung davon ist, was wirklich passiert ist. Es ist ein wertvolles Dokument, das nicht manipuliert werden sollte. Aus diesem Blickwinkel ist das Ändern der Commit-Historie fast blasphemisch. Man belügt sich über das, was tatsächlich passiert ist. Was wäre, wenn es eine verwirrende Reihe von Merge-Commits gäbe? So ist es nun mal passiert, und das Repository sollte das beibehalten.

Der entgegengesetzte Standpunkt ist, dass die Commit-Historie die Story ist, in der beschrieben wird, wie das Projekt realisiert wurde. Sie würden die erste Fassung eines Buches nicht veröffentlichen, und das Handbuch für die Wartung Ihrer Software verdient eine sorgfältige Bearbeitung. Das ist das Lager, das Tools wie rebase und filter-branch verwendet, um die Story so zu erzählen, wie es für zukünftige Nutzer am sinnvollsten ist.

Nun zur Frage, ob das Mergen oder Rebasen besser ist: hoffentlich werden Sie bemerken, dass es nicht so einfach ist. Git ist ein mächtiges Werkzeug und ermöglicht es Ihnen, viele Dinge mit und an Ihrem eigenen Verlauf vorzunehmen, aber jedes Team und jedes Projekt ist anders. Jetzt, da Sie wissen, wie diese beiden Möglichkeiten funktionieren, liegt es an Ihnen, zu entscheiden, welche für Ihre spezielle Situation die Beste ist.

Für gewöhnlich lassen sich die Vorteile von beiden Techniken nutzen: Nehmen Sie lokale Änderungen zurück, bevor Sie Ihren Verlauf bereinigen, aber nehmen Sie niemals etwas zurück, das Sie irgendwohin gepusht haben.