Intro – Why you need a safety net and who this is for
You’ve just typed git checkout <hash> or git commit --amend and the terminal looks like a crime scene. The HEAD is detached, a commit landed on the wrong branch, or you accidentally rewrote history. Panic is natural, but Git has a built‑in black box called reflog that records every move of your HEAD and branch pointers.
This tutorial is for developers who are comfortable with basic Git commands (clone, add, commit, push) but have never needed to dig into reflog. By the end you’ll have three repeatable rescue recipes that turn a “Git mess” into a clean state without losing work.
Part 1 – Understanding the reflog safety net
| What you see | Meaning | Typical scenario |
|---|---|---|
HEAD@{0} |
Current position of HEAD | After any command |
HEAD@{n} |
n‑th previous position of HEAD | “Where was I 3 moves ago?” |
branch@{0} |
Current tip of a branch | Normal branch tip |
branch@{n} |
n‑th previous tip of that branch | After a forced push or reset |
Key point: Every time you move HEAD (checkout, merge, rebase, reset, commit, amend) Git writes a line to the reflog. Even if you delete a branch, its reflog lives on until garbage collection runs (default 90 days).
How to read it
git reflog --date=iso
The output shows a short SHA, the action, and a human‑readable message, e.g.:
c3f9a2b HEAD@{0}: commit: Fix typo in README
a7b1d4e HEAD@{1}: checkout: moving from master to feature/login
...
You can jump back to any entry with git checkout <ref> or git reset --hard <ref>.
Part 2 – Rescue recipe #1: Detached HEAD → back to a branch
When it happens
You ran git checkout <commit‑hash> to inspect an old version, or a merge conflict left you on a detached HEAD. Any new commits you make now sit on an orphaned pointer.
Step‑by‑step
| Step | Command | What it does |
|---|---|---|
| 1️⃣ | git status |
Confirms you’re in “detached HEAD” state. |
| 2️⃣ | git reflog -1 |
Shows the commit you were on before the detach. |
| 3️⃣ | git branch temp-fix <commit‑hash> |
Creates a temporary branch at the current (detached) commit. |
| 4️⃣ | git checkout master (or your target branch) |
Returns you to a proper branch. |
| 5️⃣ | git merge temp-fix |
Moves the work you made while detached onto the real branch. |
| 6️⃣ | git branch -d temp-fix |
Cleans up the helper branch. |
One‑liner rescue
git checkout -b rescue-branch && git checkout master && git merge rescue-branch && git branch -d rescue-branch
If you don’t need the detached work, simply git checkout master and discard the dangling commits; they’ll disappear after reflog expiration.
Part 3 – Rescue recipe #2: Bad commit on the wrong branch
Typical mistake
You thought you were on feature/login but you were still on master. You committed, pushed, and now the history looks wrong.
Step‑by‑step
| Step | Command | Explanation |
|---|---|---|
| 1️⃣ | git branch |
Verify you’re on the wrong branch. |
| 2️⃣ | git reflog |
Locate the SHA of the bad commit (call it BAD). |
| 3️⃣ | git checkout feature/login |
Switch to the intended branch. |
| 4️⃣ | git cherry-pick BAD |
Apply the bad commit on the right branch. |
| 5️⃣ | git checkout master |
|
| 6️⃣ | git revert BAD (if already pushed) |
Creates a new commit that undoes the bad change. |
| 7️⃣ | git push |
Propagate both the cherry‑pick and the revert. |
When the bad commit is the latest on master and you haven’t pushed yet
git reset --hard HEAD~1 # move master back one commit
git checkout feature/login
git cherry-pick <BAD_SHA>
Quick cheat sheet
# Move a commit from the current branch to another
git checkout target-branch
git cherry-pick <bad-sha>
# Undo it on the source branch (if already pushed)
git checkout source-branch
git revert <bad-sha>
Part 4 – Rescue recipe #3: Accidentally rewound history (forced push)
Scenario
You ran git push -f after a rebase and now the remote is missing several commits. Your local reflog still knows about them.
Step‑by‑step
| Step | Command | Why |
|---|---|---|
| 1️⃣ | git fetch --all |
Ensure you have the latest remote refs. |
| 2️⃣ | git reflog show origin/main |
Look for the SHA of the remote tip before the force‑push. |
| 3️⃣ | git checkout -b restore-branch <old‑remote‑sha> |
Recover the lost commits in a new branch. |
| 4️⃣ | git push origin restore-branch:main |
Re‑publish the original history (needs force if you want to replace again). |
| 5️⃣ | git branch -d restore-branch |
Clean up after verification. |
Safety tip: Before you push a forced update, create a backup branch:
git branch backup-before-force
git push origin backup-before-force
Hands‑on example – From panic to clean repo in 8 commands
Below is a complete, copy‑pasteable session that demonstrates all three recipes. Open a fresh terminal and run:
# 1️⃣ Set up a demo repo
git init demo-repo && cd demo-repo
echo "Hello" > file.txt
git add . && git commit -m "Initial commit"
# 2️⃣ Create a feature branch and make a good commit
git checkout -b feature/login
echo "Login page" >> file.txt
git commit -am "Add login page"
# 3️⃣ Oops – detach HEAD and add a bad commit
git checkout HEAD~1 # detached at Initial commit
echo "Buggy change" >> file.txt
git commit -am "Buggy commit on detached HEAD"
# 4️⃣ Rescue the detached work (Recipe #1)
git branch temp-fix HEAD # create temp branch at buggy commit
git checkout feature/login # go back to proper branch
git merge temp-fix # bring buggy work onto feature
git branch -d temp-fix # delete helper
# 5️⃣ Realize the buggy commit belongs on a different branch
git checkout -b bugfix
git cherry-pick HEAD~1 # move it to bugfix
git checkout feature/login
git revert HEAD~1 # undo it on feature
# 6️⃣ Simulate a forced push disaster
git checkout main
git reset --hard HEAD~1 # lose the initial commit locally
git push origin main --force # (pretend remote had the old commit)
# 7️⃣ Recover the lost commit (Recipe #3)
git reflog show origin/main # find old SHA, assume abc1234
git checkout -b restore abc1234
git push origin restore:main # restore remote history
git branch -d restore
All commands are runnable on any system with Git installed. After step 7 you’ll see the repository back in a sane state, with the buggy change safely on its own bugfix branch.
Common mistakes / troubleshooting
-
Forgot to create a temporary branch before leaving detached HEAD
Fix: Rungit branch tmp <detached‑sha>immediately after the detach; otherwise the commit may become unreachable. -
git revertfails because the commit is already reverted
Fix: Usegit revert -m 1 <merge‑sha>for merge commits, or simply delete the stray commit withgit reset --hard HEAD~1if it hasn’t been pushed. -
Reflog entry not found after
git gc
Fix: Rungit reflog expire --expire-unreachable=now --all && git gc --prune=nowonly after you have recovered what you need. Until then, avoid manual garbage collection. -
Force‑pushing the restored branch gets rejected
Fix: Add--force-with-leaseinstead of plain--forceto protect against overwriting newer remote work. -
Cherry‑pick reports conflicts you don’t understand
Fix: Open the conflicted files, resolve manually, then rungit cherry-pick --continue. If the conflict is too messy, abort withgit cherry-pick --abortand trygit format-patch+git amas an alternative.
Try it yourself
- Clone any small public repo (e.g.,
git clone https://github.com/git/git.git). - Create a new branch, make a commit, then intentionally detach HEAD and add another commit.
- Use the detached‑HEAD rescue recipe to bring the stray commit back onto your branch without losing any work.
What’s next
- Branch‑level backups – automate creation of reflog‑based snapshots before risky operations.
- Interactive reflog explorer – using
gitk --reflogor third‑party GUIs to visualize history. - Recovering deleted branches – a deep dive into
git fsck --lost-found. - Safety‑first workflows – how to set up protected branches and pre‑push hooks that prevent accidental force pushes.
Bookmark this page for the next time Git throws you a curveball.