← Zurück zum Blog

Interactive Rebase ohne Angst: ein praktischer Leitfaden

tutorial git

Das meistgemiedene Werkzeug in Git

Fragen Sie einen Raum voller Entwickler, welcher Git-Befehl sie nervös macht, und git rebase -i landet ganz oben auf der Liste. Der interaktive Rebase hat einen Ruf: Er schreibt die Historie um, er öffnet eine seltsame Todo-Liste in Ihrem Editor, und es fühlt sich gefährlich leicht an, etwas kaputt zu machen. Also meiden ihn die meisten einfach -- und ihre Pull Requests kommen beladen mit Commits wie "wip", "fix typo" und "actually fix it" an.

Das ist schade, denn der interaktive Rebase ist das mit Abstand beste Werkzeug, um eine unordentliche Branch in eine saubere, gut reviewbare Historie zu verwandeln. In diesem Leitfaden entmystifizieren wir ihn anhand eines konkreten durchgehenden Beispiels: einer Feature-Branch mit 9 unordentlichen Commits, die wir vor dem Öffnen einer Pull Request in 3 saubere umformen werden. Wenn Sie sich noch fragen, ob Rebase überhaupt die richtige Wahl für Ihren Workflow ist, beginnen Sie mit unserem Vergleich Rebase vs. Merge -- dieser Artikel geht davon aus, dass Sie sich für den Rebase entschieden haben und ihn gut machen wollen.

Warum eine saubere Historie wichtig ist

Commits vor einer Pull Request aufzuräumen ist keine Frage der Ästhetik. Eine ordentliche Historie zahlt sich auf drei sehr praktische Weisen aus:

  • Reviewbarkeit. Ein Reviewer, der drei logische Commits liest -- "Add login form with validation", "Add remember me option", "Add error messages" -- kann jede Änderung isoliert prüfen. Derselbe Code, verteilt auf neun halbfertige Commits, zwingt ihn, den gesamten Diff auf einmal zu reviewen, und die Review-Qualität sinkt.
  • git bisect. Wenn Wochen später ein Bug auftaucht, durchläuft git bisect die Historie, um den Commit zu finden, der ihn eingeführt hat. Das funktioniert nur, wenn jeder Commit für sich allein baut und Sinn ergibt. Eine Historie voller "wip"-Commits, die nicht einmal kompilieren, macht bisect nahezu nutzlos.
  • Reverts. Wenn ein Feature zurückgenommen werden muss, ist das Reverten eines in sich geschlossenen Commits trivial. Ein Feature zu reverten, das über neun verschachtelte Commits verschmiert ist, ist ein Nachmittag Archäologie.

Unser durchgehendes Beispiel: 9 unordentliche Commits

Hier ist die Branch, die wir aufräumen werden. Es ist eine völlig normale Arbeitshistorie -- so codet in Wirklichkeit jeder:

$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton

Es ist nichts falsch daran, während der Arbeit so zu committen. Das Problem entsteht nur, wenn es so ausgeliefert wird. Unser Ziel sind am Ende drei Commits:

  1. Add login form with validation
  2. Add remember me option
  3. Add error messages with styling

Den Rebase starten: git rebase -i HEAD~9

Um die letzten 9 Commits umzuschreiben, führen Sie aus:

git rebase -i HEAD~9

HEAD~9 bedeutet "der Commit 9 Schritte vor dem aktuellen" -- alles nach diesem Punkt steht zur Bearbeitung frei. Git öffnet Ihren Editor mit der Todo-Liste:

pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file

Zwei Dinge sollten Sie verstehen, bevor Sie irgendetwas anfassen. Erstens: Das ist nur eine Textdatei. Nichts passiert, bis Sie sie speichern und schließen, und wenn Sie jede Zeile löschen oder ohne nennenswerte Änderungen schließen, wird der Rebase folgenlos abgebrochen. Zweitens: Die Reihenfolge ist älteste zuerst -- das Gegenteil von git log. Git spielt die Commits von oben nach unten erneut ab und wendet dabei das Verb am Anfang jeder Zeile an.

Die sechs Verben

pick

Den Commit exakt so behalten, wie er ist. Das ist der Standard für jede Zeile.

reword

Die Änderungen des Commits behalten, aber anhalten, damit Sie die Commit-Nachricht bearbeiten können. Perfekt, um "wip 2" zu korrigieren, ohne den Code anzufassen.

edit

Den Rebase bei diesem Commit pausieren. Sie können ihn amenden, in mehrere Commits aufteilen oder testen und dann mit git rebase --continue fortfahren. Das ist das fortgeschrittenste Verb und das, das Sie am seltensten verwenden werden.

squash

Diesen Commit mit dem darüberliegenden verschmelzen und den Editor öffnen, damit Sie die beiden Commit-Nachrichten zu einer kombinieren können.

fixup

Wie squash, aber die Nachricht dieses Commits wird vollständig verworfen. Der darüberliegende Commit behält seine Nachricht unverändert. Genau das wollen Sie für "fix typo"-Commits -- ihre Nachrichten tragen keine Information, die es wert wäre, behalten zu werden.

drop

Den Commit und seine Änderungen vollständig löschen. Die Zeile aus der Todo-Liste zu löschen hat denselben Effekt, aber drop zu schreiben macht Ihre Absicht explizit.

Die Branch aufräumen, Schritt für Schritt

Wenden wir das nun auf unsere 9 Commits an. Zwei Operationen sind nötig: Umsortieren und Verben ändern.

Zuerst das Umsortieren. Beim Blick auf die Diffs zeigt sich: "oops forgot the css file" ist in Wirklichkeit das Stylesheet für die Remember-me-Checkbox, nicht für die Fehlermeldungen. In der Todo-Liste bedeutet Umsortieren einfach, eine Zeile zu verschieben: Schneiden Sie die Zeile c9d0e1f aus und fügen Sie sie direkt nach der Remember-me-Gruppe ein. Git spielt die Commits in der neuen Reihenfolge erneut ab.

Dann die Verben. Alles bis "actually fix it" gehört zum Login-Formular, also kollabiert es in den ersten Commit. Wir verwenden squash für "add form validation", weil wir dessen Nachricht in die finale einfließen lassen wollen, und fixup für die Rausch-Commits, deren Nachrichten verschwinden dürfen. Hier ist die bearbeitete Todo-Liste:

pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages

Speichern und schließen. Git spielt die Commits erneut ab und hält zweimal an: einmal, damit Sie die kombinierte Nachricht für die erste Gruppe schreiben (tippen Sie "Add login form with validation"), und einmal für das reword (tippen Sie "Add error messages with styling"). Für den Remember-me-Commit hätte auch ein schnelles reword funktioniert -- hier haben wir ihn als pick belassen, und seine ursprüngliche Nachricht war bereits brauchbar. Das Ergebnis:

$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation

Aus neun Commits wurden drei, jeder in sich geschlossen und gut reviewbar. Beachten Sie, dass sich alle Hashes geändert haben -- das sind brandneue Commits. Behalten Sie dieses Detail im Hinterkopf; es ist der Kern der goldenen Regel weiter unten.

Der Profi-Trick: --autosquash

Sobald Sie sich sicher fühlen, können Sie das Aufräumen während der Arbeit vorbereiten, statt es am Ende zu entwirren. Wenn Sie etwas in einem früheren Commit korrigieren, halten Sie es so fest:

git commit --fixup a1b2c3d

Git erstellt einen Commit, dessen Nachricht fixup! add login form skeleton lautet. Später führen Sie aus:

git rebase -i --autosquash main

Git füllt die Todo-Liste vorab aus, wobei jeder fixup!-Commit bereits unter sein Ziel verschoben und als fixup markiert ist. Sie müssen nur noch speichern und schließen. Um das zum Standardverhalten zu machen, setzen Sie es einmal:

git config --global rebase.autosquash true

Die goldene Regel: Niemals geteilte Commits rebasen

Rebase verändert Commits nicht -- es ersetzt sie durch neue. Wenn Teamkollegen die alten Commits bereits gepullt und ihre Arbeit darauf aufgebaut haben, erzeugt das Umschreiben dieser Commits zwei divergierende Historien, und der nächste Merge wird ein Chaos aus Duplikaten und Konflikten.

Die Regel ist einfach: Rebasen Sie nur Commits, die noch nicht gepusht wurden oder die auf einer Branch liegen, an der nur Sie arbeiten. Die eigene Feature-Branch vor dem Öffnen einer Pull Request aufzuräumen ist der sichere Lehrbuchfall. Wenn Sie die Branch bereits gepusht haben und immer noch ihr einziger Autor sind, pushen Sie die umgeschriebene Historie mit git push --force-with-lease, das sich weigert, Arbeit zu überschreiben, die jemand anderes in der Zwischenzeit gepusht hat. Schreiben Sie niemals main um oder irgendeine Branch, von der Ihr Team pullt.

Wenn etwas schiefgeht

Mitten im Rebase können zwei Dinge passieren. Wenn ein erneut abgespielter Commit mit einer früheren Änderung in Konflikt gerät, pausiert Git und bittet Sie, den Konflikt zu lösen, dann die Dateien mit git add hinzuzufügen und git rebase --continue auszuführen. Und wenn Sie sich an irgendeinem Punkt verloren fühlen, gibt es einen kompletten Schleudersitz:

git rebase --abort

Das versetzt die Branch ohne Nachfragen exakt in den Zustand zurück, in dem sie vor dem Start war. Solange Sie sich daran erinnern, dass --abort existiert, kann Sie ein laufender Rebase niemals in die Falle locken.

Was, wenn der Rebase abgeschlossen ist und Sie feststellen, dass das Ergebnis falsch ist? Selbst dann ist nichts verloren. Die alten Commits existieren noch -- Git bewahrt sie wochenlang auf, und das Reflog zeichnet jede Position auf, auf die Ihre Branch je gezeigt hat, sodass Sie mit einem einzigen Reset zum Zustand vor dem Rebase zurückkehren können. Dieses Sicherheitsnetz behandeln wir im Detail in verlorene Commits mit git reflog wiederherstellen. Und wenn Ihr Fehler kleiner ist -- nur der letzte Commit muss geändert werden -- brauchen Sie überhaupt keinen Rebase: siehe wie man einen Git-Commit rückgängig macht.

Interaktiver Rebase in GitSquid

Die Todo-Liste ist mächtig, aber Verben in einer Textdatei zu bearbeiten ist genau der Teil, der die Leute nervös macht. GitSquid ersetzt sie durch einen visuellen interaktiven Rebase: Sie sortieren Commits per Drag & Drop um und wählen die Aktion für jeden Commit -- pick, squash, fixup, reword oder drop -- aus einem Dropdown-Auswahlmenü. Der gesamte Plan ist auf einen Blick sichtbar, bevor irgendetwas ausgeführt wird, was den Angstfaktor größtenteils beseitigt.

GitSquid hilft auch schon, bevor Sie überhaupt anfangen: Der Conflict Predictor, in der kostenlosen Version enthalten, zeigt, welche Dateien in Konflikt geraten werden, bevor Sie einen Rebase starten, sodass Sie wissen, worauf Sie sich einlassen, statt Konflikte erst auf halbem Weg zu entdecken. Der Rebase ist direkt über die Kontextmenüs von Branches und Commits im Commit-Graphen verfügbar.

Kurzreferenz

Befehl / Verb Was er macht
git rebase -i HEAD~n Die letzten n Commits interaktiv umschreiben
pick Den Commit unverändert behalten
reword Die Änderungen behalten, die Nachricht bearbeiten
edit Bei diesem Commit anhalten, um ihn zu amenden oder aufzuteilen
squash Mit dem vorherigen Commit verschmelzen, Nachrichten kombinieren
fixup Mit dem vorherigen Commit verschmelzen, diese Nachricht verwerfen
drop Den Commit vollständig löschen
git commit --fixup <hash> Eine Korrektur festhalten, die für einen früheren Commit bestimmt ist
git rebase -i --autosquash fixup!-Commits automatisch in der Todo-Liste platzieren
git rebase --continue Nach dem Lösen eines Konflikts fortsetzen
git rebase --abort Abbrechen und den Zustand vor dem Rebase wiederherstellen

Der interaktive Rebase belohnt ein wenig Übung mit viel Macht. Beginnen Sie auf einer Wegwerf-Branch, behalten Sie --abort im Hinterkopf, respektieren Sie die goldene Regel, und "wip, fix typo, actually fix it" in eine saubere Historie zu verwandeln wird zur Routine. Und wenn Sie den Plan lieber sehen möchten, statt eine Textdatei zu bearbeiten, laden Sie GitSquid kostenlos herunter und probieren Sie den visuellen Rebase auf Ihrer nächsten Feature-Branch aus.