12k
All articles

Alte Commits mit git history korrigieren

git history in Git 2.55 schreibt alte Commits mit fixup, reword oder split neu, aktualisiert gestapelte Branches und bietet sichere Dry-run-Vorschau.

OpenReplay Team
OpenReplay Team
Alte Commits mit git history korrigieren

Der experimentelle Befehl git history schreibt einen einzelnen alten Commit um – er integriert gestagte Änderungen, ersetzt dessen Commit-Message oder teilt ihn in zwei Commits auf – und verschiebt dabei jeden lokalen Branch, der darauf aufbaut, ohne jemals ein interaktives Rebase zu starten.

Wenn Sie mehrere Branches übereinander gestapelt pflegen, kennen Sie vermutlich das Fehlermuster, das hier ersetzt wird: Sie entdecken einen Tippfehler drei Commits weiter unten, starten ein rebase -i, stoßen auf einen Konflikt in einem Commit, den Sie nie anfassen wollten – und stehen nun mitten im Rebase mit einem detached HEAD und einer Entscheidung vor sich. git history ist genau darauf ausgelegt, Sie nicht in diese Situation zu bringen. Alle Subcommands teilen eine Garantie: Die Operation wird entweder vollständig abgeschlossen oder sie bricht mit einem Fehler ab und ändert nichts.

Dieser Artikel führt durch die drei Subcommands in Git 2.55 inklusive Ausgaben, zeigt das Flag --dry-run zur Vorschau eines Rewrites, bevor sich etwas bewegt, und behandelt die Grenzen: keine Merge-Historien, keine konfliktbehafteten Operationen, keine Hooks.

Die wichtigsten Erkenntnisse

  • In Git 2.55 hat git history drei Subcommands: fixup integriert gestagte Änderungen in einen alten Commit, reword ersetzt dessen Message und split teilt ihn in zwei Commits auf.
  • Jede Operation ist atomar: Würde das Umschreiben irgendwo in der erneut abgespielten Historie einen Konflikt erzeugen, bricht der Befehl ab und ändert nichts.
  • Standardmäßig werden alle lokalen Branches aktualisiert, die vom umgeschriebenen Commit abstammen; --update-refs=head verschiebt nur den aktuellen HEAD.
  • --dry-run verschiebt nichts, sondern gibt stattdessen die Ref-Updates aus, die git update-ref später anwenden kann – der sichere Weg, ein Rewrite vorab zu prüfen.
  • Der Befehl ist experimentell, führt keine Hooks aus und verweigert Historien, die Merges enthalten; verwenden Sie dort git rebase --rebase-merges.

Warum gibt es den Befehl git history?

git history existiert, um einen einzelnen alten Commit zu korrigieren – ohne die Umstände und Risiken eines interaktiven Rebase. Das offizielle Manual stellt den Befehl git rebase gegenüber: weniger Entscheidungen und ein Befehl, nach dem man leichter greift, wenn die Änderung eng umgrenzt ist. Er trägt außerdem das Label „experimentell“, Flags und Verhalten können sich zwischen Releases also noch ändern.

Zwei Eigenschaften machen ihn lernenswert. Erstens Atomarität: Jede Operation, die in einem Merge-Konflikt enden könnte, wird gar nicht erst angeboten. Das ist eine bewusste Entscheidung, denn der Befehl behandelt ein Rewrite als einmaligen Vorgang und nicht als Sitzung, die Sie schrittweise durchlaufen. Es gibt kein --continue, kein --abort und keinen halbfertigen Zustand, aus dem Sie sich befreien müssten. Zweitens die Reichweite: Standardmäßig aktualisiert der Befehl alle lokalen Branches, die auf Nachfolger des umgeschriebenen Commits zeigen – genau das, was ein Workflow mit gestapelten Branches braucht.

Der Befehl kam in zwei Etappen. reword und split erschienen in Git 2.54; Git 2.55 ergänzte fixup. Damit ergeben sich ab 2.55 – der Version, die dieser Artikel behandelt – drei Subcommands:

Subcommand (Git 2.55)FunktionLäuft in einem Bare-Repository
git history fixupIntegriert gestagte Änderungen in einen alten CommitNein, liest den Index
git history rewordErsetzt die Message eines alten CommitsJa
git history splitTeilt einen Commit in zwei aufJa

Diese Liste wird voraussichtlich wachsen. Das Entwicklungs-Manual von Git für git history dokumentiert bereits einen vierten Subcommand, drop, der einen Commit entfernt und dessen Nachfolger auf den Parent-Commit erneut abspielt. Prüfen Sie also git history -h gegen Ihre eigene Version, bevor Sie davon ausgehen, dass drei das Ganze sind.

fixup: Gestagte Änderungen in einen alten Commit integrieren

git history fixup <commit> nimmt alles, was im Index gestagt ist, und arbeitet es in den Ziel-Commit ein. Darunter liegt ein Drei-Wege-Merge mit HEAD, dem Ziel-Commit und einem aus Ihren gestagten Änderungen gebauten Tree als den drei Eingaben. Der Ziel-Commit behält seine ursprüngliche Message und Autorenangabe, sofern Sie mit --reedit-message nicht ausdrücklich einen Editor anfordern. Das ist das funktionale Äquivalent zu git commit --fixup gefolgt von git rebase --autosquash, komprimiert in einen Schritt, der Sie nicht stranden lassen kann.

Angenommen, ein Config-Loader ist vor zwei Commits ohne einen Default gelandet und ein Feature-Branch ist darauf gestapelt:

$ git log --oneline --branches
c41f9e2 (feature/retries) add retry logic
7b2d8a0 (HEAD -> main) add http client
3e59c11 add config loader

$ git add src/config.js
$ git history fixup 3e59c11

$ git log --oneline --branches
f8a01d3 (feature/retries) add retry logic
92c6b7e (HEAD -> main) add http client
5d40e19 add config loader

Alle drei Hashes haben sich geändert, und sowohl main als auch feature/retries zeigen nun in die umgeschriebene Historie. Das ist der Standardwert --update-refs=branches in Aktion: Jeder lokale Branch, der vom Ziel-Commit abstammt, wird verschoben – nicht nur derjenige, den Sie ausgecheckt haben. Mit --update-refs=head wird nur HEAD verschoben. Das reicht weiter als git rebase --update-refs, das lediglich Refs aktualisiert, die auf Commits innerhalb des rebasten Bereichs zeigen.

Bevor Sie das auf einem Stack ausführen, der Ihnen wichtig ist, ergänzen Sie --dry-run. Keine Referenz wird verschoben. Stattdessen erhalten Sie eine ausgegebene Liste der Verschiebungen, die vorgenommen worden wären – so formatiert, dass Sie sie später an git update-ref übergeben können. Git schreibt die benötigten neuen Objekte trotzdem, weshalb ein nachträgliches Abspielen dieser Liste normalerweise problemlos funktioniert. Das ist der ehrliche Weg, genau zu sehen, welche Branches ein Rewrite berührt, bevor Sie sich darauf festlegen.

Beachten Sie den Unterschied im Wirkungsbereich gegenüber dem Amenden: git commit --amend erreicht nur HEAD, während fixup jeden Commit in Ihrer linearen Historie erreicht. Es ist außerdem der einzige Subcommand, der Ihren Index benötigt – weshalb er, anders als die beiden anderen, nicht in einem Bare-Repository laufen kann.

reword: Die Message eines alten Commits ersetzen

git history reword <commit> ändert genau eine Sache am Ziel-Commit: dessen Message. Alles Übrige am Commit wird übernommen, jeder Nachfolger wird darauf erneut abgespielt, und die Branch-Refs folgen.

$ git history reword 7b2d8a0

Ihr Editor öffnet sich, vorbelegt mit der aktuellen Message add http client. Speichern Sie eine bessere, und der Stack wird neu aufgebaut:

$ git log --oneline --branches
3ba90cf (feature/retries) add retry logic
a1d27e8 (HEAD -> main) add http client with timeout handling
5d40e19 add config loader

Da reword sowohl den Index als auch das Working Tree unangetastet lässt, läuft es problemlos in einem Bare-Repository. Sie können damit auch die Message auf einem Branch korrigieren, den Sie nicht ausgecheckt haben, ohne das zu stören, womit Sie gerade beschäftigt sind.

split: Einen Commit in zwei aufteilen

git history split <commit> führt Sie Hunk für Hunk durch das Diff, das dieser Commit eingebracht hat. Was Sie auswählen, landet in einem neuen Commit, der unterhalb des Originals als dessen neuer Parent eingefügt wird. Das Original behält die Hunks, die Sie zurückgelassen haben. Alle Hunks auszuwählen – oder keinen – wird abgelehnt: Beide Varianten würden einen der zwei Commits leer zurücklassen.

Gegeben ein Commit, der einen Rate Limiter mit unabhängigem Metrics-Code vermischt hat:

$ git history split 91b04c7

Antworten Sie mit y bei den Metrics-Hunks und mit n bei den Limiter-Hunks. Der Editor fragt beide Commit-Messages ab, die Autorenangabe bleibt die des Originals, und das Ergebnis ist ein sauberes Paar:

$ git log --oneline
e7d3f21 (HEAD -> main) add rate limiter
b19c8a4 add request metrics

Ein nachgestellter Pathspec (git history split 91b04c7 -- src/metrics.js) beschränkt den Split auf die von Ihnen genannten Dateien. Alles außerhalb dieser Liste bleibt dort, wo es ist – im ursprünglichen Commit. Wie reword arbeitet split ausschließlich auf dem Commit-Graph und läuft in einem Bare-Repository.

Was macht git history nicht?

Der Abschnitt LIMITATIONS des Manuals ist kurz und wörtlich zu nehmen. Merge-Commits liegen außerhalb des Anwendungsbereichs: Enthält die Historie, die Sie umschreiben wollen, welche, verweist das Manual stattdessen auf git rebase --rebase-merges. Alles, was in einem Konflikt enden könnte, wird verweigert – eine Einschränkung, die gelockert werden könnte, sollte Git eines Tages First-Class-Konflikte erhalten. Hooks laufen derzeit ebenfalls nicht, und das Manual hält sich die Möglichkeit offen, dass sich das ändert.

fixup hat noch einen Sonderfall, der über --empty=(drop|keep|abort) behandelt wird. Es gibt hier zwei Wege zu einem leeren Commit: Ihre gestagte Korrektur kann den Ziel-Commit vollständig aufheben, oder ein späterer Commit trägt bereits genau die Änderung, die Sie eben in einen Vorfahren hinuntergeschoben haben. drop ist der Standard und verwirft solche Commits beim Neuaufbau der Historie; keep belässt sie an ihrer Stelle; abort beendet den Befehl mit einem Fehler, anstatt für Sie zu entscheiden. Das Verwerfen des Root-Commits wird noch nicht unterstützt.

Wann sollten Sie stattdessen zu rebase greifen?

git history bearbeitet einen einzelnen Commit; git rebase bleibt das Werkzeug für alles Umfassendere. Rebase ist die Antwort, wenn ein ganzer Bereich von Commits eine neue Basis braucht, und interaktives Rebase, wenn Sie mehrere Commits in einer Sitzung durcharbeiten wollen. Und wenn Sie eigentlich ein Werkzeug suchen, das konfliktbehaftete Zustände durch ein Rebase hindurchträgt, um sie später aufzulösen, dann ist das jjs Modell der First-Class-Konflikte – etwas, das git history bewusst nicht versucht.

Fazit

Für die häufigste Änderung an der Historie – die Korrektur eines Commits, auf dem Branches gestapelt sind – ersetzt git history ein riskantes interaktives Rebase durch eine Operation, die entweder vollständig gelingt oder die Ausführung verweigert. Nehmen Sie einen echten Stack, führen Sie git history fixup <commit> --dry-run darauf aus und lesen Sie die Ref-Updates, die vorgenommen worden wären. Dieses fünfminütige Experiment zeigt Ihnen, ob dieser Befehl in Ihren täglichen Workflow gehört. Behalten Sie dabei nur das Label „experimentell“ im Hinterkopf, denn Flags und Verhalten können sich von Release zu Release noch ändern.

FAQs

Was ist der Unterschied zwischen git history fixup und git commit --fixup mit autosquash?

git history fixup integriert gestagte Änderungen in einem einzigen atomaren Schritt in den Ziel-Commit. git commit --fixup erzeugt lediglich einen fixup!-Commit, den ein späteres git rebase --autosquash zusammenführen muss – und dieses interaktive Rebase kann bei Konflikten mitten im Vorgang anhalten. git history fixup verschiebt außerdem standardmäßig jeden abstammenden lokalen Branch und bricht ohne jede Änderung ab, wenn das Rewrite zu einem Konflikt führen würde.

Aktualisiert git history Remote-Branches oder Commits, die ich bereits gepusht habe?

Nein. git history aktualisiert ausschließlich lokale Branches; Remote-Tracking-Refs bleiben unberührt. Da umgeschriebene Commits neue Hashes erhalten, erfordern bereits gepushte Commits einen Force-Push (git push --force-with-lease) und Abstimmung mit allen, die die alte Historie gepullt haben – wie bei jedem Rewrite. Am sichersten ist der Befehl bei Commits, die noch nicht gepusht wurden, oder auf Branches, an denen nur Sie arbeiten.

Wie mache ich ein Rewrite mit git history rückgängig?

Über das Reflog. git history verschiebt Branch-Refs auf neue Commits, aber die ursprünglichen Commits bleiben im Repository, und in einem normalen (nicht-bare) Repository protokolliert jeder verschobene Branch das Update in seinem Reflog. Führen Sie git reflog show mit dem Branch-Namen aus, um den Hash vor dem Rewrite zu finden, und stellen Sie ihn dann mit git reset --hard auf einem ausgecheckten Branch oder andernfalls mit git update-ref wieder her.

Kann git history einen Commit vollständig löschen?

Nicht in Git 2.55. Die drei Subcommands (fixup, reword, split) verändern oder teilen einen Commit, können aber keinen entfernen. Um heute einen Commit zu löschen, verwenden Sie git rebase -i und markieren den Commit mit 'drop', oder git rebase --onto, um ihn zu überspringen. Ein Subcommand drop erscheint in der Entwicklungsdokumentation von Git für git history, eine native Option könnte also in einem künftigen Release kommen.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.