Begleitender Screencast: Wie funktioniert der
Git Fixup-Workflow? (Demo)
Eine saubere Historie
Der Fixup-Workflow dient in erster Linie dazu, die Git-Historie sauber zu halten. Yannick hat bereits ausgeführt, das eine saubere Historie zum einen viel zum Verständnis dazu beiträgt, warum der Code mit dem man gerade arbeitet so aussieht, wie er es tut. Zum anderen erlaubt das aber auch kleine Fixes “mal kurz” in einen anderen Branch zu ziehen, wenn wir das gerade benötigen.
Es wäre ungeschickt, wenn wir statt einem Commit am Ende vier bis fünf verschiedene Commits, schlimmstenfalls noch nicht einmal Zusammenhängende, cherry-picken müssten, weil zwei nachträgliche Commits noch Tippfehler und zwei weitere noch Bugs fixen, obwohl alles im gleichen Branch entstanden ist.
Git erlaubt uns daher sehr einfach, beim commiten mit der --fixup
-Option solche Korrekturen nachträglich zu machen, und dann mit einem einfachen weiteren Arbeitsschritt, dem interaktiven Rebase mit der --autosquash
-Option, diese Korrekturen an die richtige Stelle in der Historie zu schieben. Damit können wir sehr elegant eine saubere Historie erreichen, die unserem Team und uns selber die zukünftige Arbeit deutlich vereinfacht.
Das Versäumnis
Stellen wir uns vor, wir arbeiten an einem neuen E-Mail-Feature einer Benutzerverwaltung:
// Vom Main-Branch ableitender Feature-Branch
A---B---C (feature)
/
X---Y---Z (main)
In Commit A haben wir eine Konfigurationsklasse für die Mail-Settings hinzugefügt, die Mail-Sender-Klasse in B und in Commit C verwenden wir nun diese beiden neuen Klassen um bei einem Passwort-Reset auch wirklich eine E-Mail zu verschicken. Während des Testens fällt uns nun auf, dass sich in unserem Sender (Commit B) ein kleiner Fehler eingeschlichen hat, den wir nun reparieren müssen.
Wir könnten nun einfach den Fehler beheben und den Fix als D commiten, was zu folgender Historie führen würde:
d0297c fix sender and receiver mixup in email sender
c028f4 add mailing functionality for password reset
b032c2 add email sender class
a0f034 add configuration options for email
Hinweis: Die Commit-Hashes in diesem Beispiel sind nicht echt und beginnen zur leichteren Identifizierung immer passend zu den im Text erwähnten Commits.
Wohlgemerkt: Wir befinden uns noch bei der ersten Implementation des Features. Für unser Zukunfts-Ich ist es nicht wirklich relevant zu wissen, das wir gerade die Sender- und Empfänger Parameter eines Funktionsaufrufes verwechselt hatten. Schöner wäre es, wenn wir den Fix gleich in B “hineinschmuggeln” könnten, so dass die Datei sofort fehlerfrei hinzugefügt wurde.
Die Reparatur
Wie Yannick in seinem Artikel erklärt hat, können wir dies mit einem interaktive Rebase selbstverständlich auch nachträglich noch machen: Wir können die Commits umsortieren und squashen. Das können wir jetzt gleich machen, oder uns für später merken, das D eigentlich ein Fix für B ist. Die erste Möglichkeit reißt uns allerdings etwas aus unserem Workflow heraus (wir denken gerade ans Mail-Senden und nicht ans Rebasen) und die zweite Option erfordert ein zuverlässiges Gedächtnis.
Genau hier kommt der Fixup-Workflow zur Hilfe: Wir brauchen uns nicht jetzt sofort um den Rebase kümmern, sondern können weiter an unserem Feature arbeiten. Und anstelle D einfach so zu commiten und dann trotz unseres Elefanten-Gedächtnisses später vielleicht doch noch zu vergessen, dass diese Änderungen eigentlich noch in B hinein sollte, können wir Git schon jetzt beim Commit sagen, dass es diesen Commit später in B “hineinschmuggeln” soll, und zwar mit folgendem Befehl:
git commit --fixup <COMMIT HASH von B>
In unserem Beispielfall also: git commit --fixup b032c2
.
Die Historie schaut dann – für den Moment – folgendermassen aus:
d01fd6 fixup! add email sender class
c028f4 add mailing functionality for password reset
b032c2 add email sender class
a0f034 add configuration options for email
Stellen wir uns vor, wir haben noch ein paar weitere Commits: Wir senden auch für die Benutzerregistrierung E-Mails (Commit E) und dann stellen wir fest, das wir eine Einstellung vergessen haben die wir auf der Konfigurationsklasse hinzufügen (Commit F) und in der Sender-Klasse verwenden müssen (Commit 0), so dass wir am Ende folgende Historie haben:
// weitere Commits in den Feature-Branch
A---B---C---D---E---F---0 (feature)
/
X---Y---Z (main)
Unser Git Log zeigt folgendes an:
001a8e fixup! add email sender class
f0c72f fixup! add configuration options for email
e08dd7 add mailing functionality for user registration
d01fd6 fixup! add email sender class
c028f4 add mailing functionality for password reset
b032c2 add email sender class
a0f034 add configuration options for email
Das Aufräumen
Bevor wir unseren Feature-Branch jetzt pushen, wollen wir die Historie aufräumen. Und da diese Commits auch mit fixup!
im Log stehen erinnern wir uns daran, so dass wir den folgenden Schritt auch nicht vergessen.
Mit einem herkömmlichen interaktiven Rebase müssten wir jetzt die Commits von Hand umsortieren und zusammenfassen. Dank unserer --fixup
Commits kann Git uns dies alles jetzt allerdings schon ganz automatisch vorschlagen. Der Befehl sieht voll ausgeschrieben so aus:
git rebase --interactive --autosquash <base> [<branch>]
Base ist in unserem Beispiel der main
-Branch. Unseren feature
-branch müssen wir nicht explizit angeben weil wir gerade darauf arbeiten und ihn bereits ausgecheckt haben. Da wir die Interactive-Option auch abkürzen können und nicht voll ausschreiben müssen reicht es aus, nur folgendes zu tippen:
git rebase -i --autosquash main
In diesem interaktiven Rebase wird uns jetzt automatisch folgendes präsentiert:
pick a0f034 add configuration options for email
fixup f0c72f fixup! add configuration options for email
pick b032c2 add email sender class
fixup d01fd6 fixup! add email sender class
fixup 001a8e fixup! add email sender class
pick c028f4 add mailing functionality for password reset
pick e08dd7 add mailing functionality for user registration
Wenn dies so ausgeführt wird, sieht unsere Historie danach folgendermaßen aus:
// Feature-Branch nach dem Aufräumen
A'---B'---C'---E' (feature)
/
X---Y---Z (main)
e18dd7 add mailing functionality for user registration
c128f4 add mailing functionality for password reset
b132c2 add email sender class
a1f034 add configuration options for email
Das Ergebnis ist eine sehr präzise und lesbare Historie. Unser Team und unser Zukunfts-ich wissen später genau was passiert ist, ohne das die uninteressanten Versäumnisse und deren Reparaturen vom eigentlich Feature ablenken würden.
Fazit
In diesem Artikel habe ich Euch den Fixup-Workflow von Git erklärt: Mit git commit --fixup <HASH>
können wir Git sagen, dass die aktuellen Änderungen in einen anderen, durch den Hash spezifizierten Commit gehören. Am Ende lassen wir uns mit git rebase --interactive --autosquash <base>
einen Vorschlag erarbeiten, wie die Commits in unserem Branch automatisch umsortiert und zusammengefasst werden sollen, und wenden diesen an.
Als Ergebnis haben wir eine saubere Historie, die nicht mehr darauf schließen lässt, dass zwischenzeitlich ein paar Fehler da waren.