git rebase auf Nummer sicher…

Für viele Entwickler ist es schon in Fleisch und Blut übergegangen, der Rebase vor dem Merge mit dem master bzw. Hauptbranch. Ziel ist meist eine lineare Historie des Branches. Vorsicht ist immer geboten, wenn der Rebase mit einen bereits veröffentlichten Branch durchgeführt wird. Git kann aber mit Bordmitteln ein mögliches Unheil verhindern.

Was macht der Rebase in git? Ein Rebase schreibt vor allem die Geschichte eines Branches neu und sollte daher nicht auf die leichte Schulter genommen werden. Große Auswirkungen kann ein Rebase auf Branches haben, die bereits auf Remotes veröffentlicht wurden. Vor allem um dieses Szenario soll es in diesem Artikel gehen. Zur Erinnerung sei noch kurz erwähnt, das ein Branch in git nichts mehr als eine Referenz auf einen Commit darstellt. Da ein Commit auch seinen Vorgänger referenziert, lässt sich so die komplette Historie des Branches ermitteln.

Abb. 1

Um den Rebase zu verdeutlichen ist in Abb. 1 der Branch F1 zu sehen, der beim Commit C0 vom master Branch abgesplittet wurde. Der Branch F1 besteht damit aus den Commits C0, C2 und C3. Dieser soll nun auf den aktuellen Stand des master gebracht werden. Nun könnte F1 mit einem klassischem Merge in den master integriert werden. Doch erzeugt dieser einen klassischen Merge-Commit und so dem Wunsch nach einer linearen Historie im Wege steht.

Mit dem Rebase kann der Merge-Commit umgangen werden. Die Commits C2 und C3 des F1-Branches werden daher auf eine neue Basis (Base), dem Commit C1, angewandt. C0 ist der gemeinsame Vorgänger beider Branches und muss daher nicht betrachtet werden. Nach dem Rebase ergibt sich dann die Struktur wie in Abb. 2 abgebildet. Diese Branchstruktur erfüllt nun die Voraussetzungen eines Fast-Forward-Merge. Entsteht bei einem Merge in git kein Merge-Commit, so wird von einem Fast-Forward-Merge gesprochen.

Abb. 2

Im Nachhinein wurde nun der Punkt, an dem F1 von master abgesplittet wurde (C0) verändert und auf den aktuellen HEAD von master (C1) gesetzt. Aus den Commits C2 und C3 sind die Commits C2' und C3' entstanden. Jeder Commit wird durch einen SHA-1 Hash eindeutig adressiert. Da jeder Commit auch einen Verweis auf seinen Vorgänger-Commit hat, ändert sich dessen Hash ebenso, wenn sich sein Vorgänger ändert. In diesem Fall hat sich der Vorgänger von C2 geändert und in Folge dessen ändert sich auch der Hash von C3. Der Branch hat nach dem Rebase eine andere Historie als vorher.

Abb. 3

Bei bereits veröffentlichen Branches sind die Auswirkungen eines Rebase noch deutlicher zu erkenne. Der lokale Branch F1 wurde durch einen Rebase auf master aktualisiert. Der Remote-Branch origin/F1 entspricht der Historie des Branches F1 vor dem Rebase. Nun wird auch grafisch recht deutlich das sich die Historie des Branches durch den Rebase geändert hat.

Problematisch wird das ganze wenn der Rebase auf Branches angewandt wird, welche bereits auf einen Remote veröffentlicht wurden. Die Information über den Branch hat sich dann bereits auf die Repositories mehrerer Entwickler verteilt und möglicherweise wird der Stand des Branches irgendwo referenziert. So könnte ein Entwickler den Branch als Basis eines neuen Branches genutzt haben.

Soll ein Branch nach einem Rebase auf den Remote gepusht werden, so wird ein git push mit einem Fehler abbrechen. Der Server erkennt, das der Commit C3, auf den origin/F1 aktuell zeigt, nicht mehr in der Historie des Branches auftaucht und verweigert somit die Annahme der Änderungen. Der push muss mit Hilfe eines Parameters angewiesen werden den aktuellen Branch hart zu überschreiben. Dafür gibt es den Parameter -f oder --force.

Allerdings kann es durch den “force push” auch zum Verlust von Commits kommen. Die Commits sind danach nicht wirklich verschwunden, aber sie werden in dem Branch nicht mehr referenziert. Wie Abb. 4 zeigt, hat Lisa auf den ursprünglichen Branch F1 noch einen Commit übertragen. Aus Gründen der Übersicht ist dieser Branch hier mit F1-Lisa benannt.

Abb. 4

Nun überträgt ein Kollege von Lisa den Branch F1 mit git push -f auf den Remote und setzt damit den Branch auf den Zustand von F1. Damit ist nun der Commit C4 nicht mehr im Branch F1 enthalten. Das harte setzen von F1 auf dem Remote hat den Commit C4 nun komplett aus dem Branch F1 entfernt.

Um solche Situationen zu vermeiden gibt es zum Glück eine Alternative zum klassischen Force-Push. Neben dem -f bzw. --force gibt es ein alternatives --force-with-lease. Hier wird sichergestellt, das sich der ursprüngliche Branch auf dem Remote nicht weiterentwickelt hat. In diesem konkreten Beispiel würde git beim git push --force-with-lease darauf achten, dass der origin/F1 des Aufrufers sich nicht vom F1 Branch des Servers unterscheidet. Sollten beide Branches einen unterschiedlichen Stand aufweisen, so sind die Branches divergiert und der aktuelle push wird abgebrochen. Um den Konflikt zu lösen ist z.B. ein erneuter Rebase auf den aktuellen Stand origin/F1 Branches nötig.

Die goldene Regel beim Rebase in git ist noch immer, das ein Rebase nur auf lokale, nicht veröffentlichte, Branches durchgeführt werden sollte. Das gilt auch für das interaktive Rebase mit denen eine Reihe von Commits zusammengefasst, neu geordnet oder auch gelöscht werden können, denn auch dies ändert die Historie des Branches.

Sollte dennoch ein Rebase auf einen bereits veröffentlichten Branch nötig sein, bietet sich der hier erwähnte --force-with-lease Parameter als echte Alternative zum --force an.

Nützliche Links zum Thema: