Wie ihr gelöschte Commits nach reset und Rebase zurückholt
Ein unbedachter git reset --hard oder ein missglückter Rebase fühlt sich nach einem endgültigen Verlust an, ist es aber selten. Das Reflog protokolliert lokal jede Bewegung von HEAD und macht verschwundene Commits meist innerhalb weniger Minuten wieder auffindbar. Dieser Artikel zeigt, wie das Reflog funktioniert, wie lange Einträge erhalten bleiben und wie eine saubere Recovery in der Praxis abläuft.
Inhaltsverzeichnis
- 1. Was das Reflog wirklich ist: Jede HEAD-Bewegung im Blick
- 2. Reflog vs. Commit-Historie: Lokal, flüchtig und unverzichtbar
- 3. Das Reflog-Format lesen: HEAD@{n}, ORIG_HEAD und Zeitstempel
- 4. Der Klassiker: Commit nach git reset --hard wiederherstellen
- 5. Rebase geht schief: verlorene Commits nach interaktivem Rebase retten
- 6. Gelöschte Branches und verwaiste Commits wiederfinden
- 7. Praktischer Recovery-Workflow: reflog, Diff-Prüfung, Wiederherstellung
- 8. Grenzen und Ablauf: reflog-Expiry und gc.reflogExpire konfigurieren
- 9. Reflog im Vergleich: riskante vs. sichere Recovery-Strategien
- 10. Zusammenfassung
- 11. FAQ
1. Was das Reflog wirklich ist: Jede HEAD-Bewegung im Blick
Das Reflog ist Gits eingebautes Bewegungsprotokoll für Referenzen wie HEAD und Branch-Zeiger. Anders als die Commit-Historie, die nur die Abstammungslinie der aktuell erreichbaren Commits zeigt, protokolliert das Reflog jede einzelne Bewegung von HEAD chronologisch, unabhängig davon, ob der Zielcommit danach noch über einen Branch erreichbar ist. Jeder git commit, git checkout, git merge, git rebase, git cherry-pick und sogar jeder git reset hinterlässt einen eigenen Eintrag mit Zeitstempel und einer kurzen Beschreibung der Aktion.
Genau diese lückenlose Protokollierung macht das Reflog zum wichtigsten Sicherheitsnetz bei Git. Während ein git reset --hard den Branch-Zeiger sofort auf einen anderen Commit verschiebt und die vorherige Position aus der sichtbaren Historie verschwindet, bleibt der Eintrag im Reflog erhalten. Das Reflog kennt also nicht nur den aktuellen Zustand eines Branches, sondern jeden Zustand, den er in der jüngeren Vergangenheit hatte, und genau darauf basiert die gesamte Recovery-Strategie in diesem Artikel.
2. Reflog vs. Commit-Historie: Lokal, flüchtig und unverzichtbar
Der entscheidende Unterschied zwischen git log und git reflog liegt in der Datenquelle. git log läuft entlang der Eltern-Zeiger der Commit-Objekte und zeigt damit den Abstammungsgraphen, der beim Klonen, Pushen und Fetchen zwischen Repositories geteilt wird. Das Reflog dagegen ist reines lokales Metadaten: Es existiert ausschließlich als Datei im Verzeichnis .git/logs/, etwa .git/logs/HEAD für die HEAD-Referenz und .git/logs/refs/heads/<branch> für einzelne Branches.
Diese lokale Natur hat zwei wichtige Konsequenzen. Erstens wird das Reflog niemals mit git push übertragen und niemals mit git clone kopiert. Ein Kollege, der euer Repository klont, sieht euer Reflog nicht und kann es folglich auch nicht für eine Recovery nutzen. Zweitens ist das Reflog nicht dauerhaft: Es unterliegt einer Ablaufzeit, die in Abschnitt 8 im Detail erklärt wird. Wer verlorene Commits retten will, muss also lokal und innerhalb eines begrenzten Zeitfensters handeln, nicht Wochen später auf einer neuen Maschine.
3. Das Reflog-Format lesen: HEAD@{n}, ORIG_HEAD und Zeitstempel
Jede Zeile im Reflog folgt demselben Muster: eine gekürzte SHA, gefolgt von der Referenz und einem Index in geschweiften Klammern, etwa HEAD@{0} für den aktuellsten Eintrag, HEAD@{1} für den vorherigen Zustand von HEAD und so weiter. Diese Indizes sind relativ und ändern sich mit jeder neuen Aktion, weshalb ein Eintrag, den man retten möchte, am besten sofort mit seiner SHA notiert oder direkt weiterverarbeitet wird, statt sich auf eine später möglicherweise verschobene Nummer zu verlassen.
Neben dem numerischen Index versteht Git auch relative Zeitangaben wie HEAD@{2.hours.ago} oder HEAD@{yesterday}, was besonders hilfreich ist, wenn man den ungefähren Zeitpunkt eines Fehlers kennt, aber nicht die genaue Anzahl der Schritte. Zusätzlich setzt Git vor jedem reset, merge, rebase und am automatisch die spezielle Referenz ORIG_HEAD auf die vorherige Position von HEAD. ORIG_HEAD ist damit ein einstufiges Sicherheitsnetz, das ganz ohne Reflog-Lesen funktioniert, aber eben nur den letzten Schritt zurückgeht.
$ git reflog
a1b2c3d (HEAD -> feature/checkout) HEAD@{0}: commit: Add express checkout button
e4f5a6b HEAD@{1}: commit: Refactor payment step validation
9c8d7e6 HEAD@{2}: checkout: moving from main to feature/checkout
3f2e1d0 (main) HEAD@{3}: commit: Fix currency rounding in cart totals
7b6a5c4 HEAD@{4}: reset: moving to HEAD~2
d0e9f8a HEAD@{5}: commit: WIP tax calculation
b3c2d1e HEAD@{6}: commit: Initial tax calculation draft
# Time-based reference instead of an index
$ git show HEAD@{2.hours.ago}
# ORIG_HEAD points to HEAD before the last reset/rebase/merge
$ git log -1 ORIG_HEAD
4. Der Klassiker: Commit nach git reset --hard wiederherstellen
git reset --hard ist der häufigste Auslöser für scheinbar verlorene Commits. Der Befehl verschiebt den Branch-Zeiger auf den angegebenen Commit und überschreibt sowohl den Index als auch das Arbeitsverzeichnis, ohne Rückfrage. Wichtig zu verstehen: reset --hard löscht keine Commit-Objekte aus der Objektdatenbank. Die vorherigen Commits werden lediglich unerreichbar, weil kein Branch, Tag oder anderer Zeiger mehr direkt auf sie verweist, sie bleiben aber als sogenannte "dangling commits" im Objektspeicher liegen, bis sie irgendwann von der Garbage Collection entfernt werden.
Genau hier setzt die Recovery an: Der Reflog-Eintrag unmittelbar vor dem Reset zeigt exakt auf die alte Position des Branches. Ein einfaches git reset --hard HEAD@{1} stellt den vorherigen Zustand vollständig wieder her, inklusive aller Commits, des Index und des Arbeitsverzeichnisses zum damaligen Zeitpunkt. Wer unsicher ist, ob der gefundene Eintrag wirklich der richtige ist, sollte vorher mit git show oder git log HEAD@{1} gegenprüfen, bevor der nächste harte Reset erneut Daten überschreibt.
# Working tree before the mistake: 3 commits on top of main
$ git log --oneline -4
c4d5e6f (HEAD -> feature/pricing) Add tiered discount logic
b3c4d5e Fix rounding edge case
a2b3c4d Add unit tests for discount calculator
9f8e7d6 (main) Merge pull request #142
# Accidental hard reset, discarding all three commits
$ git reset --hard HEAD~3
HEAD is now at 9f8e7d6 Merge pull request #142
# The commits are gone from the branch, but not from the object database
$ git reflog
9f8e7d6 (HEAD -> feature/pricing) HEAD@{0}: reset: moving to HEAD~3
c4d5e6f HEAD@{1}: commit: Add tiered discount logic
b3c4d5e HEAD@{2}: commit: Fix rounding edge case
a2b3c4d HEAD@{3}: commit: Add unit tests for discount calculator
# Restore the branch to the state right before the reset
$ git reset --hard HEAD@{1}
HEAD is now at c4d5e6f Add tiered discount logic
5. Rebase geht schief: verlorene Commits nach interaktivem Rebase retten
Ein interaktiver Rebase mit git rebase -i schreibt Commits um und erzeugt dabei neue SHAs, selbst wenn sich der Inhalt kaum ändert. Wird beim Squashen, Reordern oder Droppen ein Commit versehentlich verworfen oder eine falsche Reihenfolge gewählt, wirken die ursprünglichen Commits aus Sicht von git log komplett verschwunden. Tatsächlich erzeugt jeder einzelne Schritt eines Rebase einen eigenen Reflog-Eintrag: rebase (start) markiert den Beginn, rebase (pick) und rebase (squash) jeden verarbeiteten Commit, und rebase (finish) den Abschluss.
Für die Recovery bedeutet das: Im Reflog lässt sich der komplette Rebase-Ablauf Schritt für Schritt nachvollziehen, einschließlich der Zwischenzustände, die im finalen Ergebnis gar nicht mehr sichtbar sind. Ein verworfener Commit lässt sich gezielt mit git cherry-pick <sha> auf den aktuellen Branch übertragen, ohne den gesamten Rebase rückgängig zu machen. Soll dagegen der komplette Zustand vor dem Rebase wiederhergestellt werden, reicht git reset --hard ORIG_HEAD, solange seitdem kein weiterer Reset oder Rebase stattgefunden hat, der ORIG_HEAD überschrieben hätte.
# Interactive rebase squashes and reorders 4 commits into 2
$ git rebase -i HEAD~4
# After the rebase, git log only shows the rewritten history
$ git log --oneline -2
7a8b9c0 (HEAD -> feature/api-client) Add retry logic and tests
5d6e7f8 (main) Implement base API client
# The reflog still holds every intermediate step of the rebase
$ git reflog
7a8b9c0 HEAD@{0}: rebase (finish): returning to refs/heads/feature/api-client
7a8b9c0 HEAD@{1}: rebase (pick): Add retry logic and tests
1e2f3a4 HEAD@{2}: rebase (squash): Add retry logic and tests
9b8c7d6 HEAD@{3}: rebase (start): checkout HEAD~4
c3d4e5f HEAD@{4}: commit: Add retry configuration constants
# Recover the commit that was dropped during the squash
$ git cherry-pick c3d4e5f
6. Gelöschte Branches und verwaiste Commits wiederfinden
git branch -D löscht ausschließlich den Branch-Zeiger, niemals die zugehörigen Commit-Objekte. Wurde der gelöschte Branch zuvor mindestens einmal ausgecheckt, finden sich seine Commits im Reflog von HEAD, konkret in den checkout-Einträgen, die den Wechsel auf und von diesem Branch dokumentieren. Wichtig ist dabei eine Einschränkung: Das Löschen eines Branches entfernt auch dessen eigene Reflog-Datei unter .git/logs/refs/heads/<branch>, weshalb git reflog show <branchname> nach dem Löschen nicht mehr funktioniert. Das globale HEAD-Reflog bleibt davon jedoch unberührt.
Die Wiederherstellung läuft in zwei Schritten ab: Zuerst wird über git reflog, kombinierbar mit grep nach dem Branch-Namen, die letzte bekannte SHA des gelöschten Branches ermittelt. Danach erzeugt git branch <neuer-name> <sha> einen neuen Branch-Zeiger genau an dieser Stelle, ohne dass ein einziger Commit neu geschrieben werden muss. Wurde der Branch dagegen nie lokal ausgecheckt, etwa weil er nur über git fetch bekannt war, hilft nur die Objektdatenbank direkt über git fsck --lost-found, was deutlich unzuverlässiger ist als das Reflog.
# Accidental branch deletion before merging
$ git branch -D hotfix/payment-timeout
Deleted branch hotfix/payment-timeout (was 4f5e6d7).
# The branch ref is gone, but HEAD's reflog still knows the tip commit
$ git reflog | grep hotfix
4f5e6d7 HEAD@{2}: checkout: moving from hotfix/payment-timeout to main
# Recreate the branch pointing at the last known commit
$ git branch recovery/hotfix-payment-timeout 4f5e6d7
$ git checkout recovery/hotfix-payment-timeout
7. Praktischer Recovery-Workflow: reflog, Diff-Prüfung, Wiederherstellung
Ein zuverlässiger Recovery-Workflow folgt immer demselben Muster, unabhängig von der konkreten Fehlerursache. Erster Schritt ist git reflog beziehungsweise das ausführlichere git reflog show HEAD, um die chronologische Liste aller HEAD-Bewegungen zu sehen. Zweiter Schritt ist die Identifikation des richtigen Eintrags anhand von Zeitstempel und Aktionsbeschreibung, etwa reset: moving to HEAD~3 oder rebase (start), die schon im Klartext verraten, was passiert ist.
Vor jeder destruktiven Aktion sollte der gefundene Commit mit git show <sha> oder git log -p -1 <sha> geprüft werden, um sicherzustellen, dass es wirklich der gesuchte Zustand ist. Erst danach folgt die eigentliche Wiederherstellung, und hier gibt es drei gängige Wege: git reset --hard HEAD@{n} für eine vollständige Rückkehr zu diesem Zustand, git cherry-pick <sha> für die Übernahme eines einzelnen Commits in die aktuelle Historie, oder als sicherste Variante git branch recovery-branch <sha>, das den gefundenen Zustand als neuen Branch sichert, ohne den aktuellen Branch überhaupt anzufassen.
8. Grenzen und Ablauf: reflog-Expiry und gc.reflogExpire konfigurieren
Das Reflog ist kein dauerhaftes Archiv. Git unterscheidet zwischen zwei Ablaufzeiten: Reflog-Einträge, die noch über einen Branch oder Tag erreichbar sind, verfallen standardmäßig nach 90 Tagen, gesteuert über gc.reflogExpire. Einträge, die zu keinem erreichbaren Commit mehr gehören, sogenannte unreachable Entries, etwa nach einem überschriebenen Reset oder Rebase, verfallen bereits nach 30 Tagen über gc.reflogExpireUnreachable. Nach Ablauf dieser Fristen entfernt git gc, das regelmäßig automatisch im Hintergrund läuft, sowohl den Reflog-Eintrag als auch das zugehörige Objekt endgültig aus dem Repository.
Für die Praxis bedeutet das ein reales, aber großzügiges Zeitfenster: Die meisten verlorenen Commits lassen sich problemlos wiederherstellen, solange seit dem Fehler nicht mehr als 30 Tage vergangen sind. Wer dieses Fenster bewusst verlängern möchte, etwa in einem Repository mit besonders wertvoller Historie, kann beide Werte in der lokalen oder globalen .git/config anpassen. Ein Wert von never deaktiviert den Ablauf komplett, was allerdings die Repository-Größe über Zeit spürbar wachsen lässt.
[gc]
; Reflog entries still reachable from a ref expire after 90 days by default
reflogExpire = 90.days.ago
; Entries no longer reachable from any ref expire after 30 days by default
reflogExpireUnreachable = 30.days.ago
auto = 6700
[core]
; Keep an explicit safety margin before any automatic pruning
logAllRefUpdates = true
9. Reflog im Vergleich: riskante vs. sichere Recovery-Strategien
Die folgende Übersicht stellt für fünf typische Fehlerszenarien den riskanten, oft aus Panik gewählten Reflex einer sicheren, reflog-basierten Vorgehensweise gegenüber.
| Szenario | Riskanter Reflex | Sichere Vorgehensweise |
|---|---|---|
| Commit nach reset --hard verloren | git log durchsuchen, zeigt nur die aktuelle Historie | git reflog show HEAD, dann reset --hard HEAD@{n} |
| Branch versehentlich gelöscht | Branch als endgültig verloren abschreiben | git reflog + git branch <name> <sha> |
| Falscher amend überschreibt Commit | Force-Push versuchen, um es zu "reparieren" | git reflog, alten Commit per cherry-pick zurückholen |
| Rebase mit falschem Ergebnis | Commits manuell von Hand neu schreiben | git reset --hard ORIG_HEAD oder HEAD@{n} |
| Force-Push überschreibt Remote-Branch | Hoffen, dass ein Kollege eine lokale Kopie hat | Eigenes Reflog prüfen, künftig push --force-with-lease |
In allen fünf Fällen gilt dieselbe Grundregel: Vor jeder weiteren destruktiven Aktion zuerst das Reflog prüfen, den passenden Zustand identifizieren und erst danach handeln. Wer stattdessen sofort mit einem weiteren reset --hard oder Force-Push reagiert, riskiert, dass der eigentlich rettbare Zustand überschrieben wird, bevor er wiederhergestellt werden konnte.
Mironsoft
Git-Workflows, Recovery-Playbooks und CI/CD-Absicherung für Magento- und Hyvä-Teams
Git-Workflows, die eurem Team Sicherheit geben?
Wir richten robuste Git-Branching-Strategien, Pre-Commit-Hooks und CI/CD-Absicherungen für eure Magento- und Hyvä-Projekte ein, damit ein einzelner Fehlgriff nie zum Datenverlust wird.
Git-Workflow-Audit
Branching-Strategie, Hook-Konfiguration und Recovery-Playbooks für euer Team
Onboarding & Schulung
Praxisnahe Git-Trainings zu Reflog, Rebase und sicherem Force-Push
CI/CD-Absicherung
Automatisierte Backups, geschützte Branches und Force-Push-Regeln in der Pipeline
10. Zusammenfassung
Das Reflog löst ein Kernproblem im Git-Alltag: Der Unterschied zwischen einem echten Datenverlust und einem schnell behebbaren Fehler liegt fast immer im Wissen um git reflog. Jede Bewegung von HEAD wird lokal protokolliert, unabhängig davon, ob der Zielcommit danach noch über einen Branch erreichbar ist. Ein git reset --hard, ein misslungener interaktiver Rebase oder ein versehentlich gelöschter Branch sind damit selten endgültig, solange man innerhalb des Ablauffensters von 90 beziehungsweise 30 Tagen reagiert.
Der entscheidende Unterschied zwischen einer stressfreien und einer chaotischen Recovery liegt in der Reihenfolge: erst git reflog lesen, den richtigen Eintrag mit git show verifizieren, und erst danach mit reset --hard, cherry-pick oder einem neuen Recovery-Branch handeln. Wer diese Reihenfolge einhält und die Reflog-Ablaufzeiten kennt, verliert in der Praxis so gut wie nie einen Commit unwiederbringlich.
Das Reflog als Rettungsanker - Das Wichtigste auf einen Blick
Reflog verstehen
Protokolliert jede HEAD-Bewegung lokal, unabhängig von der sichtbaren Commit-Historie in git log.
Nach reset --hard
git reflog zeigt die vorherige Position, git reset --hard HEAD@{1} stellt sie vollständig wieder her.
Nach Rebase
Jeder Rebase-Schritt hinterlässt einen Eintrag. cherry-pick oder reset --hard ORIG_HEAD retten den alten Zustand.
Ablaufzeit beachten
90 Tage für erreichbare, 30 Tage für unreachable Einträge, konfigurierbar über gc.reflogExpire.