Reflog: rescue work that looks lost
The interactive lesson needs JavaScript. The text below is the complete lesson.
After this lesson you can
- explain what the reflog records, and why commits that look lost usually still exist understand
- rescue commits after
git reset --hardand aftergit branch -Dby creating a branch from a reflog entry apply - undo a rebase with
ORIG_HEADor the branch's own reflog apply - explain why the reflog is not a backup understand
Understand: A lost pointer is not a lost commit
Here is main with three commits. Then you run git reset --hard HEAD~2 by mistake. You only meant to throw away a file change, not two commits.
main jumps back to A. B and C are now dashed: no branch reaches them, and git log no longer shows them. But they are not deleted. The commits are still in the repository. Only the label moved.
So how do you find them? Git keeps a diary called the reflog. Every time HEAD or a branch moves, Git writes a line about where it went. HEAD@{0} is where HEAD is now, and HEAD@{1} where it was one move before. Here that is C.
Before you do anything else, give the commit a name. git branch rescue HEAD@{1} makes a new branch there. B and C are reachable again, and you can look around calmly. Afterwards you can bring main back.
Deleting a branch is the same story. git branch -D rescue removes only the label, and B and C are dashed again. Git prints the ID the branch had. The branch's own reflog disappears, but HEAD's reflog still remembers the commits.
A rebase also leaves commits nobody points to: the originals. Here the plan dropped the test commit by mistake, so you want the branch back where it started. Which name points there?
A rebase moves HEAD many times, so HEAD@{1} is just the last step inside the rebase. Use ORIG_HEAD instead, the old tip Git saved before the rebase. ORIG_HEAD only remembers the latest reset, merge or rebase, so after that the branch's own reflog, feature@{1}, is the way back.
The reflog is a safety net, not a backup. It lives only in your own .git folder and is never pushed. Entries for commits no branch reaches expire by default 30 days after they were written, and then Git may delete the commits. And it only knows commits, never uncommitted changes.
Watch: Rescue after reset --hard
Here is the accident. We reset main two commits back, with hard. Both commits vanish from the branch, and their files vanish from the folder.
$ git reset --hard HEAD~2
The log shows only the first snapshot. It looks as if the work is gone. In the graph the two commits are dashed.
$ git log --oneline
Now we open the diary. Look for the two commits that vanished.
$ git reflog
The line just below the top, HEAD@{1}, is the commit Add budget. We give it a name right away, with a rescue branch. The dashed commits become solid again.
$ git branch rescue HEAD@{1}
main lies directly behind rescue on the same line, so a merge is a fast-forward. Both files are back.
$ git merge rescue
Everything is back. Once you have checked that the content is right, you can delete rescue.
$ git log --oneline
Tasks
- Find out where HEAD was before the reset.
- Create a branch
rescueat the commit HEAD pointed to just before the reset. - Bring
mainback to the rescued commits.
Watch: Undo a rebase that went wrong
We want to tidy feature while we rebase it onto main. In the plan we delete a line by mistake, the one with the test. A missing line means drop, and Git does not warn us.
$ git rebase -i main
The test commit is gone from the branch. We want feature back exactly as it was before the rebase.
$ git log --oneline
Let's read the last five lines of the reflog. Notice how many lines the rebase wrote.
$ git reflog -5
ORIG_HEAD points at the tip from before the rebase. A hard reset moves feature back there. That is safe here, because we have nothing uncommitted.
$ git reset --hard ORIG_HEAD
The fork is back, with both login commits. Now we can rebase again, and this time keep every line in the plan.
$ git log --oneline --graph --all
Tasks
- Look at the last five moves of HEAD.
- Put
featureback exactly as it was before the rebase.
Practise: Transfer: a café with three accidents
A café repository where three things went wrong earlier today. You are on main. Read the reflog before you act.
Tasks
- Someone ran a hard reset one step back, twice in a row. Bring
mainback to the commitAdd price list. - Earlier today the branch
experimentwas deleted withgit branch -D. Bring it back, with its commitTry dark theme. - Earlier today
menuwas rebased ontomainwith the wrong range, and the commitAdd menuvanished from the branch. Putmenuback as it was before the rebase.
Check yourself: Recall it
git branch -D experiment printed Deleted branch experiment (was f4edb90). You regret it at once. What do you run?
git reflog show experimentgit branch experiment f4edb90- Nothing,
-Ddeleted the commits
Your laptop dies. Can a classmate rescue your unpushed commits with git reflog in their clone?
- Yes, the reflog is shared through
origin - No, the reflog exists only in your own
.gitfolder - Yes, if the classmate runs
git fetchfirst
You ran git reset --hard while you had changes that were never staged or committed. What can the reflog bring back?
- The changes, from the line before the reset
- None of them, because Git never stored them
- Everything, if you act within 30 days
Review from “Branches and HEAD”: you made a commit on a detached HEAD and then switched to main. What is the best way to keep that commit?
- Nothing, Git keeps every commit forever
- Give it a branch, for example with
git branch keep <id> - Switch back to it and make a new commit on top
Common misconceptions
- “After
git reset --hardthe commits are deleted.” They still exist. No branch points to them, but the reflog does. - “The reflog is a backup.” It is local, never pushed, expires, and has never seen changes you did not commit.
- “
HEAD@{1}always means before my last command.” It is HEAD's previous position. A rebase moves HEAD many times.
Read more
What the reflog records
Every time a ref moves, Git appends a line to that ref's log in .git/logs/: the old position, the new one, and why. There is one reflog for HEAD and one for each local branch. git reflog shows HEAD's, git reflog show main shows the branch's, and git log -g shows the same entries in log format.
$ git reset --hard HEAD~2
HEAD is now at a5ea1be Initial snapshot
$ git reflog
a5ea1be HEAD@{0}: reset: moving to HEAD~2
f4edb90 HEAD@{1}: commit: Add budget
ae89efa HEAD@{2}: commit: Write project plan
a5ea1be HEAD@{3}: commit (initial): Initial snapshot
$ git reflog show main
a5ea1be main@{0}: reset: moving to HEAD~2
f4edb90 main@{1}: commit: Add budget
ae89efa main@{2}: commit: Write project plan
a5ea1be main@{3}: commit (initial): Initial snapshot
You can use the names anywhere Git expects a commit:
| Name | Means |
|---|---|
HEAD@{2} |
where HEAD was two moves ago |
main@{1} |
where main was before its last move |
main@{yesterday}, HEAD@{2.hours.ago} |
where the ref was at that time |
@{-1} |
the branch you were on before the last switch |
HEAD moves far more often than a branch: every switch, every step of a rebase. A branch's own reflog only records changes to that branch, so feature@{1} is often the easier way back after a rebase.
A calm rescue routine
- Stop. Do not try more commands to "fix" it.
- Read
git status,git log --oneline --graph --allandgit reflog. - Name what you find before anything else:
git branch rescue <id>. A branch keeps the commits reachable whatever happens next. - Inspect
rescue, then bring your branch back with a merge, a reset or a cherry-pick. - Delete
rescueonly when you have checked the result.
Deleted branches
git branch -D removes the label and the branch's own reflog, but prints the old ID, and HEAD's reflog still lists every commit HEAD has visited:
$ git branch -D rescue
Deleted branch rescue (was f4edb90).
$ git branch rescue f4edb90
ORIG_HEAD
Commands that move HEAD drastically save the old position in ORIG_HEAD first: git reset, git merge, git rebase and git am, and therefore also git pull. Ordinary git commit, git commit --amend, git switch, git cherry-pick and git revert do not touch it. There is only one ORIG_HEAD, and the next such command overwrites it, so use it right away. Note that git reset --hard ORIG_HEAD is itself a reset, so afterwards ORIG_HEAD points to where you just were.
When the reflog does not have it: git fsck
Some commits never appear in a reflog. A dropped stash is the classic case: git stash drop removes the reflog entry of refs/stash, but the stash commit stays in the object database. git fsck lists such dangling objects, which nothing points to:
$ git stash
Saved working directory and index state WIP on main: a5ea1be Initial snapshot
$ git stash drop
Dropped refs/stash@{0} (11acfee0f6acd5aeedfa14a2f0f123603c56be6f)
$ git fsck --lost-found
dangling commit 11acfee0f6acd5aeedfa14a2f0f123603c56be6f
$ git log --oneline -1 11acfee
11acfee WIP on main: a5ea1be Initial snapshot
$ git stash apply 11acfee
A dangling commit is the tip of a lost chain: nothing points to it, not even another commit. Plain git fsck counts the reflog as a reference, but --lost-found ignores it, just like --no-reflogs. That is why it also lists commits that only the reflog still remembers. Look at each one with git log -1 before you act.
Files you staged but never committed are also in the object database, as blobs. After a careless git reset --hard you can still find them, although without file names:
$ echo "Draft menu: soup, bread" > menu.txt && git add menu.txt
$ git reset --hard
HEAD is now at a5ea1be Initial snapshot
$ git fsck --lost-found
dangling blob e064b3ad67676a5cbae5998a99c9b253d261c4b2
dangling commit 11acfee0f6acd5aeedfa14a2f0f123603c56be6f
$ git show e064b3ad
Draft menu: soup, bread
Changes you never ran git add on were never stored by Git. No command can bring them back.
Expiry and garbage collection
Old reflog entries are removed by git gc, which Git also runs automatically now and then. The defaults:
| Setting | Default | Applies to |
|---|---|---|
gc.reflogExpire |
90 days | reflog entries for commits still on the branch |
gc.reflogExpireUnreachable |
30 days | entries for commits the branch no longer reaches |
gc.pruneExpire |
2 weeks | loose objects nothing refers to any more |
The age counts from when the entry was written, not from when the commit became unreachable. You can extend them, for example git config gc.reflogExpireUnreachable 90.days. Once an entry has expired and no ref reaches the commit, the next garbage collection may delete it for good. git reflog expire --expire=now --all followed by git gc --prune=now deletes everything unreachable at once. It is the last step once a secret has already been rewritten out of history, and exactly why you should never run it in panic: afterwards there is no way back.
Why the reflog is not a backup
The reflog lives only in your own .git folder. It is not pushed, not fetched and not copied by git clone, and it expires. It protects you against your own mistakes on your own machine. Against a lost laptop you need a copy on another machine, for example by pushing your commits to a remote.
Per-Arne's Git Good!