Kapitel ▾ 2. Auflage

3.6 Git Branching - Rebasing

Rebasing

In Git gibt es zwei Hauptmethoden, um Änderungen von einem Branch in einen anderen zu integrieren: merge und rebase. In diesem Abschnitt lernen Sie, was Rebasing ist, wie es funktioniert, warum es ein ziemlich erstaunliches Werkzeug ist und in welchen Fällen Sie es nicht verwenden sollten.

Das grundlegende Rebase

Wenn Sie zu einem früheren Beispiel aus Basic Merging zurückkehren, können Sie sehen, dass Sie Ihre Arbeit verzweigt und Commits auf zwei verschiedenen Branches erstellt haben.

Simple divergent history
Abbildung 35. Einfache verzweigte Historie

Die einfachste Methode, die Branches zu integrieren, ist, wie wir bereits behandelt haben, der Befehl merge. Er führt einen Drei-Wege-Merge zwischen den beiden neuesten Branch-Snapshots (C3 und C4) und dem letzten gemeinsamen Vorfahren der beiden (C2) durch und erstellt einen neuen Snapshot (und Commit).

Merging to integrate diverged work history
Abbildung 36. Merging zur Integration verzweigter Arbeitsverläufe

Es gibt jedoch noch eine andere Möglichkeit: Sie können den Patch der Änderung, die in C4 eingeführt wurde, nehmen und ihn auf C3 neu anwenden. In Git wird dies als Rebasing bezeichnet. Mit dem Befehl rebase können Sie alle Änderungen, die in einem Branch committet wurden, auf einen anderen Branch übertragen.

Für dieses Beispiel würden Sie zum Branch experiment wechseln und ihn dann wie folgt auf den Branch master rebassen:

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

Diese Operation funktioniert, indem sie zum gemeinsamen Vorfahren der beiden Branches (dem, auf dem Sie sich befinden, und dem, auf den Sie rebassen) geht, die Unterschiede ermittelt, die von jedem Commit des aktuellen Branches eingeführt wurden, diese Unterschiede in temporären Dateien speichert, den aktuellen Branch auf denselben Commit zurücksetzt wie den Branch, auf den Sie rebassen, und schließlich jede Änderung nacheinander anwendet.

Rebasing the change introduced in `C4` onto `C3`
Abbildung 37. Rebasing der in C4 eingeführten Änderung auf C3

An diesem Punkt können Sie zum Branch master zurückkehren und einen Fast-Forward-Merge durchführen.

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch
Abbildung 38. Fast-Forwarding des master-Branches

Jetzt ist der von C4' referenzierte Snapshot genau derselbe wie der, der in dem Merge-Beispiel von C5 referenziert wurde. Es gibt keinen Unterschied im Endergebnis der Integration, aber Rebasing sorgt für eine sauberere Historie. Wenn Sie das Log eines gerebassa-ten Branches untersuchen, sieht es wie eine lineare Historie aus: Es scheint, dass die gesamte Arbeit nacheinander stattgefunden hat, auch wenn sie ursprünglich parallel stattgefunden hat.

Oft werden Sie dies tun, um sicherzustellen, dass Ihre Commits sauber auf einem Remote-Branch angewendet werden – vielleicht in einem Projekt, zu dem Sie beitragen möchten, das Sie aber nicht pflegen. In diesem Fall würden Sie Ihre Arbeit in einem Branch erledigen und dann Ihre Arbeit auf origin/master rebassen, wenn Sie bereit sind, Ihre Patches an das Hauptprojekt zu übermitteln. Auf diese Weise muss der Maintainer keine Integrationsarbeit leisten – nur einen Fast-Forward oder eine saubere Anwendung.

Beachten Sie, dass der von dem endgültigen Commit referenzierte Snapshot, auf den Sie am Ende gelangen, sei es der letzte der gerebassa-ten Commits für ein Rebase oder der endgültige Merge-Commit nach einem Merge, derselbe Snapshot ist – nur die Historie ist unterschiedlich. Rebasing wendet Änderungen von einer Arbeitslinie auf eine andere in der Reihenfolge an, in der sie eingeführt wurden, während Merging die Endpunkte nimmt und sie zusammenführt.

Interessantere Rebases

Sie können Ihr Rebase auch auf etwas anderes als den Rebase-Ziel-Branch anwenden lassen. Nehmen Sie zum Beispiel eine Historie wie Eine Historie mit einem Topic-Branch von einem anderen Topic-Branch. Sie haben einen Topic-Branch (server) erstellt, um dem Projekt serverseitige Funktionalität hinzuzufügen, und einen Commit erstellt. Dann haben Sie davon abgeleitet, um clientseitige Änderungen vorzunehmen (client) und einige Commits erstellt. Schließlich sind Sie zu Ihrem server-Branch zurückgekehrt und haben einige weitere Commits erstellt.

A history with a topic branch off another topic branch
Abbildung 39. Eine Historie mit einem Topic-Branch von einem anderen Topic-Branch

Nehmen wir an, Sie entscheiden, dass Sie Ihre clientseitigen Änderungen in Ihre Hauptlinie für eine Veröffentlichung integrieren möchten, aber die serverseitigen Änderungen zurückhalten möchten, bis sie weiter getestet sind. Sie können die Änderungen an client, die nicht auf server sind (C8 und C9), nehmen und sie auf Ihren master-Branch anwenden, indem Sie die Option --onto von git rebase verwenden.

$ git rebase --onto master server client

Dies bedeutet im Grunde: „Nimm den client-Branch, ermittle die Patches, seit er sich vom server-Branch getrennt hat, und wende diese Patches im client-Branch an, als ob er direkt auf dem master-Branch basieren würde.“ Es ist etwas komplex, aber das Ergebnis ist ziemlich cool.

Rebasing a topic branch off another topic branch
Abbildung 40. Rebasing eines Topic-Branches von einem anderen Topic-Branch

Jetzt können Sie Ihren master-Branch per Fast-Forward aktualisieren (siehe Fast-Forwarding Ihres master-Branches zur Aufnahme der Änderungen des client-Branches)

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the `client` branch changes
Abbildung 41. Fast-Forwarding Ihres master-Branches zur Aufnahme der Änderungen des client-Branches

Nehmen wir an, Sie beschließen, auch Ihren server-Branch zu integrieren. Sie können den server-Branch auf den master-Branch rebassen, ohne ihn vorher auschecken zu müssen, indem Sie git rebase <basebranch> <topicbranch> ausführen – dies checkt den Topic-Branch (in diesem Fall server) für Sie aus und wendet ihn auf den Basis-Branch (master) an.

$ git rebase master server

Dies wendet Ihre server-Arbeit auf Ihre master-Arbeit an, wie in Rebasing Ihres server-Branches auf Ihren master-Branch gezeigt.

Rebasing your `server` branch on top of your `master` branch
Abbildung 42. Rebasing Ihres server-Branches auf Ihren master-Branch

Dann können Sie den Basis-Branch (master) per Fast-Forward aktualisieren.

$ git checkout master
$ git merge server

Sie können die Branches client und server löschen, da alle Arbeiten integriert sind und Sie sie nicht mehr benötigen. Die Historie dieses gesamten Prozesses sieht dann wie in Endgültige Commit-Historie aus.

$ git branch -d client
$ git branch -d server
Final commit history
Abbildung 43. Endgültige Commit-Historie

Die Gefahren des Rebasing

Aber die Glückseligkeit des Rebasing ist nicht ohne Nachteile, die sich in einer einzigen Zeile zusammenfassen lassen:

Rebasen Sie keine Commits, die außerhalb Ihres Repositories existieren und auf denen andere möglicherweise Arbeit aufgebaut haben.

Wenn Sie diese Richtlinie befolgen, werden Sie keine Probleme haben. Wenn nicht, werden die Leute Sie hassen und Sie werden von Freunden und Familie verachtet werden.

Wenn Sie Dinge rebassen, verwerfen Sie bestehende Commits und erstellen neue, die ähnlich, aber unterschiedlich sind. Wenn Sie Commits irgendwo pushen und andere sie herunterladen und darauf aufbauend arbeiten, und Sie dann diese Commits mit git rebase umschreiben und wieder hochpushen, müssen Ihre Mitarbeiter ihre Arbeit erneut zusammenführen, und es wird chaotisch, wenn Sie versuchen, ihre Arbeit wieder in Ihre zu integrieren.

Betrachten wir ein Beispiel dafür, wie das Rebasing von öffentlich gemachter Arbeit zu Problemen führen kann. Angenommen, Sie klonen von einem zentralen Server und erledigen dann einige Arbeiten daran. Ihre Commit-Historie sieht so aus:

Clone a repository, and base some work on it
Abbildung 44. Klonen eines Repositories und darauf aufbauende Arbeit

Nun leistet jemand anderes weitere Arbeit, die einen Merge beinhaltet, und pusht diese Arbeit zum zentralen Server. Sie holen sie und mergen den neuen Remote-Branch in Ihre Arbeit, sodass Ihre Historie ungefähr so aussieht:

Fetch more commits, and merge them into your work
Abbildung 45. Holen Sie sich weitere Commits und mergen Sie sie in Ihre Arbeit

Als Nächstes beschließt die Person, die den gemergten Code gepusht hat, zurückzugehen und stattdessen ihre Arbeit zu rebassen; sie führt einen git push --force aus, um die Historie auf dem Server zu überschreiben. Sie holen dann von diesem Server und erhalten die neuen Commits.

Someone pushes rebased commits, abandoning commits you’ve based your work on
Abbildung 46. Jemand pusht gerebassa-te Commits und verwirft Commits, auf denen Sie Ihre Arbeit aufgebaut haben

Jetzt sind Sie beide in der Klemme. Wenn Sie git pull ausführen, erstellen Sie einen Merge-Commit, der beide Historienzweige enthält, und Ihr Repository sieht so aus:

You merge in the same work again into a new merge commit
Abbildung 47. Sie mergen die gleiche Arbeit erneut in einem neuen Merge-Commit

Wenn Sie git log ausführen, wenn Ihre Historie so aussieht, sehen Sie zwei Commits mit demselben Autor, Datum und derselben Nachricht, was verwirrend ist. Wenn Sie diese Historie dann wieder auf den Server pushen, werden Sie all diese gerebassa-ten Commits erneut auf dem zentralen Server einführen, was die Leute weiter verwirren kann. Es ist ziemlich sicher anzunehmen, dass der andere Entwickler C4 und C6 nicht in der Historie haben möchte; deshalb haben sie in erster Linie gerebasst.

Rebase bei Rebase

Wenn Sie sich in einer solchen Situation befinden, gibt es in Git weitere Magie, die Ihnen helfen kann. Wenn jemand in Ihrem Team Änderungen mit Gewalt pusht, die Arbeit überschreiben, auf der Sie Ihre Arbeit aufgebaut haben, besteht Ihre Herausforderung darin, herauszufinden, was Ihnen gehört und was sie umgeschrieben haben.

Es stellt sich heraus, dass Git neben der Commit-SHA-1-Prüfsumme auch eine Prüfsumme berechnet, die nur auf dem mit dem Commit eingeführten Patch basiert. Dies wird als "patch-id" bezeichnet.

Wenn Sie umgeschriebene Arbeit herunterladen und sie auf die neuen Commits Ihres Partners anwenden, kann Git oft erfolgreich feststellen, was einzigartig von Ihnen ist, und es wieder auf den neuen Branch anwenden.

Zum Beispiel, im vorherigen Szenario, wenn wir statt eines Merges bei Jemand pusht gerebassa-te Commits und verwirft Commits, auf denen Sie Ihre Arbeit aufgebaut haben git rebase teamone/master ausführen, wird Git:

  • Ermitteln, welche Arbeit einzigartig für unseren Branch ist (C2, C3, C4, C6, C7)

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

  • Ermitteln, welche nicht in den Ziel-Branch umgeschrieben wurden (nur C2 und C3, da C4 derselbe Patch ist wie C4')

  • Diese Commits oben auf teamone/master anwenden.

Anstatt des Ergebnisses, das wir in Sie mergen die gleiche Arbeit erneut in einem neuen Merge-Commit sehen, würden wir etwas Ähnliches wie in Rebase auf zwangsweise gepushte Rebase-Arbeit erhalten.

Rebase on top of force-pushed rebase work
Abbildung 48. Rebase auf zwangsweise gepushte Rebase-Arbeit

Dies funktioniert nur, wenn C4 und C4', die Ihr Partner erstellt hat, fast genau derselbe Patch sind. Andernfalls kann das Rebase nicht erkennen, dass es sich um ein Duplikat handelt, und fügt einen weiteren C4-ähnlichen Patch hinzu (der wahrscheinlich nicht sauber angewendet werden kann, da die Änderungen bereits teilweise vorhanden wären).

Sie können dies auch vereinfachen, indem Sie git pull --rebase anstelle eines normalen git pull ausführen. Oder Sie könnten es manuell mit einem git fetch gefolgt von einem git rebase teamone/master in diesem Fall tun.

Wenn Sie git pull verwenden und --rebase zum Standard machen möchten, können Sie den Konfigurationswert pull.rebase mit etwas wie git config --global pull.rebase true setzen.

Wenn Sie nur Commits rebassen, die Ihr eigenes Computer nie verlassen haben, ist alles in Ordnung. Wenn Sie Commits rebassen, die gepusht wurden, aber auf denen andere keine Commits aufgebaut haben, ist ebenfalls alles in Ordnung. Wenn Sie Commits rebassen, die bereits öffentlich gepusht wurden und auf denen andere möglicherweise Arbeit aufgebaut haben, könnten Sie auf frustrierende Probleme stoßen und die Verachtung Ihrer Teammitglieder auf sich ziehen.

Wenn Sie oder ein Partner es zu einem bestimmten Zeitpunkt für notwendig erachtet, stellen Sie sicher, dass jeder weiß, dass git pull --rebase ausgeführt werden soll, um die Nachwirkungen nach dem Auftreten zu vereinfachen.

Rebase vs. Merge

Nachdem Sie nun Rebasing und Merging in Aktion gesehen haben, fragen Sie sich vielleicht, welches besser ist. Bevor wir dies beantworten können, lassen Sie uns einen Schritt zurücktreten und darüber sprechen, was Historie bedeutet.

Ein Standpunkt dazu ist, dass die Commit-Historie Ihres Repositories eine Aufzeichnung dessen ist, was tatsächlich passiert ist. Es ist ein historisches Dokument, das an sich wertvoll ist und nicht manipuliert werden sollte. Aus diesem Blickwinkel ist die Änderung der Commit-Historie fast blasphemisch; Sie lügen darüber, was tatsächlich geschehen ist. Was macht es, wenn es eine unübersichtliche Reihe von Merge-Commits gab? So ist es passiert, und das Repository sollte dies für die Nachwelt erhalten.

Der entgegengesetzte Standpunkt ist, dass die Commit-Historie die Geschichte der Entstehung Ihres Projekts ist. Sie würden nicht den ersten Entwurf eines Buches veröffentlichen, warum also Ihre unübersichtliche Arbeit zeigen? Wenn Sie an einem Projekt arbeiten, benötigen Sie möglicherweise eine Aufzeichnung all Ihrer Fehltritte und Sackgassen, aber wenn es an der Zeit ist, Ihre Arbeit der Welt zu zeigen, möchten Sie vielleicht eine kohärentere Geschichte erzählen, wie man von A nach B gelangt. Personen in diesem Lager verwenden Werkzeuge wie rebase und filter-branch, um ihre Commits zu überschreiben, bevor sie in den Haupt-Branch integriert werden. Sie verwenden Werkzeuge wie rebase und filter-branch, um die Geschichte so zu erzählen, wie es für zukünftige Leser am besten ist.

Nun zur Frage, ob Merging oder Rebasing besser ist: Hoffentlich erkennen Sie, dass es nicht so einfach ist. Git ist ein mächtiges Werkzeug und ermöglicht es Ihnen, viele Dinge mit Ihrer Historie zu tun, aber jedes Team und jedes Projekt ist anders. Jetzt, da Sie wissen, wie beides funktioniert, liegt es an Ihnen zu entscheiden, was für Ihre spezielle Situation am besten ist.

Sie können das Beste aus beiden Welten haben: Lokale Änderungen rebassen, bevor Sie pushen, um Ihre Arbeit zu bereinigen, aber niemals etwas rebassen, das Sie irgendwohin gepusht haben.