Der Befehl git rebase hat den Ruf, Git-Hokuspokus zu sein, von dem Anfänger besser ihre Finger lassen sollten. Er kann dem Entwicklerteam jedoch das Leben wesentlich einfacher machen, wenn er mit Vorsicht eingesetzt wird. In diesem Artikel werden wir git rebase mit dem verwandten git merge-Befehl vergleichen und alle potenziellen Chancen identifizieren, um Rebasing in den typischen Git-Workflow einzubinden.

Konzept-Überblick

Du solltest wissen, dass der Befehl git rebase dasselbe Problem löst wie git merge. Beide Befehle sind dazu gedacht, Änderungen von einem Branch in einen anderen zu integrieren, sie tun es nur auf unterschiedliche Weise.

Überlege, was passiert, wenn du in einem dedizierten Branch an einem neuen Feature arbeitest und ein anderes Teammitglied den master-Branch mit neuen Commits aktualisiert. Dies führt zu einem abgespaltenen Verlauf, was jedem bekannt sein sollte, der Git schon mal als Tool für die Zusammenarbeit verwendet hat.

A forked commit history

Nehmen wir einmal an, dass die neuen Commits im master für das Feature, an dem du arbeitest, relevant sind. Um die neuen Commits in deinen feature-Branch zu integrieren, hast du zwei Optionen: Merging oder Rebasing.

Die Merging-Option

Der einfachste Weg ist, den master-Branch in den Feature-Branch zu mergen, etwa mit dem folgenden Befehl:

git checkout feature
git merge master

Du kannst es auch in einem Einzeiler zusammenfassen:

git merge feature master

Dabei entsteht ein neuer Merge-Commit im Feature Branch, der den Verlauf beider Branches vereint und eine Branch-Struktur erstellt, die wie folgt aussieht:

Merging master into the feature branch

Merging ist praktisch, weil es sich um einen nicht destruktiven Vorgang handelt. Die vorhanden Branches werden in keiner Weise geändert. Dadurch werden auch die potenziellen Tücken des Rebasing (siehe unten) umgangen.

Andererseits bedeutet dies, dass der feature-Branch jedes Mal, wenn du Upstream-Änderungen integrieren musst, einen irrelevanten Merge-Commit erhält. Wenn der master sehr aktiv ist, kann dies den Verlauf deines feature-Branch ziemlich durcheinanderbringen. Dieses Problem kann zwar mit erweiterten git log-Optionen gemindert werden, andere Entwickler können dann aber den Projektverlauf nicht so leicht nachvollziehen.

Die Rebasing-Option

Als Alternative zum Mergen kannst du den Feature Branch auf den master-Branch mithilfe der folgenden Befehle rebasen:

git checkout feature
git rebase master

Der gesamte feature-Branch wird zur Spitze des master-Branches verschoben und alle neuen Commits werden effektiv in master integriert. Anstatt einen Merge-Commit zu nutzen, wird der Projektverlauf beim Rebasing neu geschrieben, indem für jeden Commit im originalen Branch völlig neue Commits erstellt werden.

Rebasing the feature branch onto master

Der Hauptvorteil des Rebasing liegt darin, dass du einen wesentlich übersichtlicheren Verlauf erhältst. Erstens werden dabei unnötige Merge-Commits eliminiert, die für git merge erforderlich sind. Zweitens führt Rebasing zu einem völlig linearen Projektverlauf, wie du am obigen Diagramm erkennen kannst. Du kannst von der Spitze des Features ohne Verzweigungen bis hinunter zum Beginn des Projekts gehen. Somit kannst du mit Befehlen wie git log, git bisect und gitk einfacher durch dein Projekt navigieren.

Allerdings geht dieser makellose Commit-Verlauf mit zwei Kompromissen einher, nämlich in Bezug auf Sicherheit und Nachverfolgbarkeit. Wenn du dich nicht an die goldene Regel des Rebasing hältst, kann das Neuschreiben des Projektverlaufs möglicherweise katastrophale Folgen für deinen Zusammenarbeits-Workflow haben. Daneben geht beim Rebasing Kontext verloren, den ein Merge-Commit bietet. Denn du kannst nicht erkennen, wann Upstream-Änderungen in das Feature integriert wurden.

Interaktives Rebasing

Interaktives Rebasing gibt dir die Möglichkeit, Commits zu modifizieren, wenn sie in den neuen Branch verschoben werden. Das ist sogar noch leistungsstärker als ein automatisiertes Rebasing, da du damit die vollständige Kontrolle über den Commit-Verlauf des Branches erhältst. Diese Option wird normalerweise genutzt, um einen unübersichtlichen Verlauf zu bereinigen, bevor ein feature-Branch in den master gemergt wird.

Um eine interaktive Rebasing-Sitzung zu beginnen, gibst du die Option i für den Befehl git rebase an:

git checkout feature
git rebase -i master

Damit wird ein Texteditor geöffnet, in dem alle zu verschiebenden Commits aufgeführt sind:

pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

Diese Liste legt genau fest, wie der Branch nach dem Rebasing aussehen wird. Durch die Änderung des pick-Befehls und/oder die Umsortierung der Einträge kannst du den Branch-Verlauf so gestalten, wie du es gerne möchtest. Wenn der zweite Commit beispielsweise ein kleines Problem im ersten Commit behebt, kannst du mit dem Befehl fixup beide in einem einzelnen Commit zusammenfassen:

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

Wenn du die Datei speicherst und schließt, führt Git das Rebasing nach deinen Anweisungen durch. Das Ergebnis ist ein Projektverlauf, der ähnlich wie dieser aussieht:

Squashing a commit with an interactive rebase

Das Eliminieren nicht signifikanter Commits wie hier macht den Verlauf des Features viel verständlicher – git merge vermag das nicht.

Die goldene Rebasing-Regel

Sobald wir einmal verstanden haben, was Rebasing ist, müssen wir auch direkt lernen, wann wir es nicht anwenden dürfen. Die goldene Regel lautet, dass git rebase nie auf öffentlichen Branches genutzt werden darf.

Überlege einmal, was passiert, wenn du den master auf deinen feature-Branch rebasen würdest:

Rebasing the master branch

Das Rebasing verschiebt alle Commits in master zur Spitze von feature. Das Problem ist, dass dies nur in deinem Repository erfolgte. Alle anderen Entwickler arbeiten noch mit dem ursprünglichen master. Da das Rebasing zu völlig neuen Commits führt, wird Git annehmen, dass der Verlauf deines master-Branches von dem aller anderen abweicht.

Der einzige Weg, die beiden master-Branches zu synchronisieren, besteht darin, sie wieder zusammen zu mergen. Daraus resultieren ein zusätzlicher Merge-Commit und zwei Sets von Commits, die dieselben Änderungen enthalten (die ursprünglichen und die aus dem rebasten Branch). Wir sehen: Die Situation wird unübersichtlich.

Bevor du also git rebase ausführst, solltest du dich immer fragen, ob noch jemand diesen Branch ansieht. Kannst du dies bejahen, dann nimm die Hände von der Tastatur und überlege, ob es eine nicht destruktive Methode für deine Änderungen gibt (z. B. den Befehl git revert). Andernfalls kannst du den Verlauf neu schreiben, ganz so wie du magst.

Verschieben erzwingen

Wenn du versuchst, den rebasten master-Branch zurück in ein Remote-Repository zu pushen, wird Git dich davon abhalten, denn es würden Konflikte mit dem Remote-master-Branch entstehen. Allerdings kannst du das Pushen mit der Option --force erzwingen:

# Be very careful with this command!
git push --force

Damit wird der Remote-master-Branch überschrieben und in Übereinstimmung mit dem rebasten Master in deinem Repository gebracht, was die Dinge für den Rest des Teams sehr verwirrend macht. Sei mit diesem Befehl also vorsichtig und verwende ihn nur, wenn du genau weißt, was du tust.

Ein Pushing solltest du eigentlich nur dann erzwingen, wenn du eine lokale Bereinigung durchgeführt hast, nachdem ein privater Feature-Branch in ein Remote-Repository gepusht wurde (z. B. zu Backup-Zwecken). Das ist wie: "Hoppla, ich wollte die Originalversion eigentlich nicht zum Feature-Branch pushen. Hier ist die aktuelle Version." Ich wiederhole noch einmal: Niemand arbeitet mit den Commits der Originalversion des Feature-Branch.

Schritt für Schritt durch den Workflow

Das Rebasing kann in deinen bestehenden Git-Workflow integriert werden, und zwar in dem Maße, wie es zu deinem Team passt. In diesem Abschnitt gehen wir auf die Vorteile ein, die das Rebasing in verschiedenen Phasen der Feature-Entwicklung bietet.

Der erste Schritt bei jedem Workflow, der den Befehl git rebase nutzt, ist die Erstellung eines dedizierten Branch für jedes Feature. Dadurch erhältst du die für eine sichere Nutzung des Rebasing notwendige Branch-Struktur:

Developing a feature in a dedicated branch

Lokale Bereinigung

Die Bereinigung von lokalen Features, die gerade bearbeitet werden, ist eine der besten Möglichkeiten, um Rebasing in deinen Workflow zu implementieren. Wenn du regelmäßig ein interaktives Rebasing durchführst, kannst du sicherstellen, dass jeder Commit deines Features fokussiert und sinnvoll ist. So kannst du Code schreiben, ohne dir darüber Gedanken zu machen, ob du ihn in einzelne Commits zerteilen musst – du kannst dich einfach anschließend darum kümmern.

Wenn du git rebase aufrufst, hast du für die neue Base zwei Optionen: den übergeordneten Branch des Features (z. B. master) oder einen früheren Commit in deinem Feature. Ein Beispiel der ersten Option haben wir im Abschnitt Interaktives Rebasing angeschaut. Die letzte Option ist gut, wenn du nur die letzten Commits korrigieren musst. Der nächste Befehl startet beispielsweise ein interaktives Rebasing von den letzten 3 Commits.

git checkout feature
git rebase -i HEAD~3

Wenn du HEAD~3 als neue Base spezifizierst, verschiebst du den Branch nicht tatsächlich – du schreibst nur die 3 folgenden Commits interaktiv neu. Beachte, dass dabei keine Upstream-Änderungen in den feature-Branch integriert werden.

Rebasing onto Head~3

Wenn du mithilfe dieser Methode das gesamte Feature neu schreiben willst, kann sich der Befehl git merge-base als nützlich erweisen, um die Original-Base des feature-Branch zu finden. Der folgende Befehl gibt die Commit-ID der Original-Base aus, die du dann an git rebase weitergeben kannst:

git merge-base feature master

Diese Nutzung des interaktiven Rebasing ist eine gute Möglichkeit, um git rebase in deinen Workflow einzubinden, da es nur lokale Branches betrifft. Andere Entwickler sehen später nur das Endprodukt in Form eines sauberen, einfach nachvollziehbaren Feature-Branch-Verlaufs.

Ich wiederhole aber, dass die nur bei privaten Feature-Branches funktioniert. Wenn du mit anderen Entwicklern am selben Feature-Branch arbeitest, ist dieser öffentlich und du darfst seinen Verlauf nicht neu schreiben.

Es gibt keine Alternative für git merge, um lokale Commits mit einem interaktiven Rebasing zu bereinigen.

Upstream-Änderungen in ein Feature implementieren

Im Abschnitt Übersicht zum Konzept haben wir gesehen, wie ein Feature-Branch Upstream-Änderungen vom master-Branch über die Befehle git merge oder git rebase integrieren kann. Merging ist eine sichere Option, die den gesamten Verlauf deines Repositorys bewahrt, während beim Rebasing durch das Verschieben deines Feature-Branches an die Spitze des master-Branches ein linearer Verlauf erstellt wird.

Die Verwendung von git rebase ähnelt einer lokalen Bereinigung (und kann gleichzeitig durchgeführt werden), bei dem Vorgang werden aber die Upstream-Commits vom master-Branch integriert.

Denke daran, dass es völlig in Ordnung ist, auf einen Remote-Branch zu rebasen, anstatt auf den master. Dies kann passieren, wenn du mit anderen Entwicklern am selben Feature arbeitest und du deren Änderungen in dein Repository integrieren musst.

Wenn beispielsweise du und ein anderer Entwickler namens John dem Feature-Branch Commits hinzugefügt habt, könnte dein Repository wie das folgende aussehen, nachdem du den Remote-Feature-Branch von Johns Repository abgerufen hast:

Collaborating on the same feature branch

Diese Verzweigung kannst du auf die exakt gleiche Weise auflösen wie bei der Integration von Upstream-Änderungen vom master: Entweder mergst du deinen lokalen Feature-Branch mit john/feature oder du bringst dein lokales Feature per Rebasing an die Spitze von john/feature.

Merging vs. rebasing onto a remote branch

Beachte, dass dieses Rebasing nicht gegen die goldene Regel verstößt, denn nur deine lokalen feature-Commits werden verschoben, alles andere davor bleibt unberührt. Das käme der Aufforderung "füge meine Änderungen zu dem hinzu, was John bereits gemacht hat" gleich. In den meisten Fällen ist der Vorgang intuitiver als die Synchronisation mit dem Remote-Branch über ein Merge-Commit.

Normalerweise führt der Befehl git pull einen Merge aus. Du kannst damit aber erzwingen, dass der Remote-Branch in einen Rebasing-Vorgang integriert wird, indem du die Option --rebase angibst.

Feature mit einem Pull Request reviewen

Wenn du im Code-Review-Prozess Pull-Requests nutzt, musst du den Befehl git rebase nach Erstellung des Pull-Request vermeiden. Sobald ein Pull-Request erstellt wurde, sehen sich andere Entwickler deine Commits an. Es handelt sich dann also um einen öffentlichen Branch. Das Neuschreiben des Verlaufs würde es Git und deinen Teamkollegen unmöglich machen, jedwede nachfolgenden Commits nachzuverfolgen, die dem Feature hinzugefügt wurden.

Änderungen von anderen Entwicklern müssen mit git merge integriert werden, nicht mit git rebase.

Aus diesem Grund ist es immer eine gute Idee, deinen Code mit einem interaktiven Rebasing zu bereinigen, bevor du deinen Pull Request erstellst.

Genehmigtes Feature integrieren

Nachdem ein Feature von deinem Team freigegeben wurde, hast du die Option, das Feature an die Spitze des master-Branches zu rebasen, bevor du git merge nutzt, um das Feature in die Haupt-Codebasis zu integrieren.

Das ist eine ähnliche Situation wie beim Integrieren von Upstream-Änderungen in den Feature-Branch. Da du aber im master-Branch keine Commits neu schreiben darfst, musst du git merge nutzen, um das Feature zu integrieren. Bei Durchführung eines Merging vor dem Rebasing stellst du sicher, dass ein Fast-Forward-Merge ausgeführt und damit ein perfekter linearer Verlauf entsteht. Dies gibt dir auch die Möglichkeit, jedwede nachfolgenden Commits zu vermeiden, die während einem Pull Request hinzugefügt werden.

Integrating a feature into master with and without a rebase

Wenn du im Umgang mit git rebase noch nicht ganz sicher bist, kannst du immer noch ein Rebasing in einem temporären Branch durchführen. Auf diese Weise kannst du den Original-Branch auschecken und einen neuen Versuch starten, wenn du den Verlauf deines Features versehentlich durcheinander bringst. Ein Beispiel:

git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Verlauf bereinigen]
git checkout master
git merge temporary-branch

Zusammenfassung

Das ist im Grunde alles, was du zum Rebasing deiner Branches wissen musst. Wenn dir ein sauberer, linearer Verlauf ohne unnötige Merge-Commits lieber ist, solltest du git rebase anstelle von git merge nutzen, um Änderungen aus einem anderen Branch zu integrieren.

Wenn du andererseits den kompletten Projektverlauf erhalten und das Risiko vermeiden willst, öffentliche Commits neu zu schreiben, kannst du bei git merge bleiben. Jede Option ist zulässig, aber du hast jetzt zumindest die Möglichkeit, die Vorteile von git rebase zu nutzen.