Git Reflog and Recovery: Your Safety Net for Destructive Operations
Master git reflog to recover lost commits, undo destructive operations, and understand Git's safety net. Learn recovery techniques for reset, rebase, and merge disasters.
Git Reflog and Recovery: Your Safety Net for Destructive Operations
Git has a safety net that most developers discover only after a disaster. git reflog records every movement of HEAD — every commit, checkout, reset, rebase, and merge. When you accidentally reset a branch, drop the wrong stash, or force push the wrong commit, reflog is your path back from the brink.
Unlike git log, which shows the commit history of a branch, reflog shows the history of your local repository operations. It remembers commits that are no longer reachable from any branch. It remembers the state before you ran git reset --hard. It remembers everything — for a while.
This post covers the complete reflog toolkit: recovering lost commits, undoing destructive operations, understanding reflog expiration, and the scenarios where reflog is the difference between a minor setback and a major data loss.
When to Use / When Not to Use
Use Git Reflog When
- Accidental reset — You ran
git reset --hardand lost commits - Dropped stash — You accidentally dropped a stash with important work
- Failed rebase — A rebase went wrong and you need to go back
- Force push recovery — You force pushed the wrong branch and need the old state
- Orphaned commits — Commits exist but no branch points to them
- Understanding history — You want to see what operations were performed locally
Do Not Use Git Reflog When
- Shared history recovery — Reflog is local; it can’t recover commits never pushed
- Long-term backup — Reflog entries expire (default 90 days); it’s not a backup system
- Remote recovery — Reflog doesn’t exist on the remote; it’s a local-only feature
- Deleted repository — If the
.gitdirectory is gone, reflog is gone too
Core Concepts
Git reflog tracks HEAD movements and branch updates:
| Entry Type | Trigger | Example |
|---|---|---|
commit | Creating a new commit | commit: feat: add user authentication |
checkout | Switching branches | checkout: moving from main to feature/login |
reset | Running git reset | reset: moving to HEAD~3 |
rebase | Running git rebase | rebase: picking abc1234 |
merge | Running git merge | merge main: Fast-forward |
stash | Stash operations | stash: WIP on feature: abc1234 |
cherry-pick | Running git cherry-pick | cherry-pick: def5678 |
Each reflog entry has a reference: HEAD@{n} where n is how many operations ago. HEAD@{0} is the current state, HEAD@{1} is the previous state, and so on.
graph LR
A["HEAD@{0}: Current State"] --> B["HEAD@{1}: Previous Operation"]
B --> C["HEAD@{2}: Earlier Operation"]
C --> D["HEAD@{3}: Even Earlier"]
D --> E["HEAD@{n}: Oldest Tracked"]
Architecture and Flow Diagram
The reflog structure and recovery workflow:
graph TD
A[Local Operations] --> B[HEAD Reflog]
A --> C[Branch Reflog]
B --> D["refs/heads/main@{n}"]
B --> E["refs/heads/feature@{n}"]
C --> F[".git/logs/HEAD"]
C --> G[".git/logs/refs/heads/"]
D --> H{Need Recovery?}
E --> H
H -->|Yes| I[git reflog show]
I --> J[Find Target Commit]
J --> K[git reset/checkout/branch]
K --> L[Recovered State]
Step-by-Step Guide
1. Viewing the Reflog
The basic reflog shows recent HEAD movements:
# Show HEAD reflog
git reflog
# Output example:
# abc1234 HEAD@{0}: commit: feat: add payment processing
# def5678 HEAD@{1}: commit: feat: add cart functionality
# ghi9012 HEAD@{2}: checkout: moving from main to feature/checkout
# jkl3456 HEAD@{3}: reset: moving to HEAD~2
# mno7890 HEAD@{4}: commit: feat: add product listing
# pqr1234 HEAD@{5}: clone: from https://github.com/org/project.git
# Show reflog for a specific branch
git reflog show main
# Show reflog with dates
git reflog --date=iso
# Show reflog with full commit hashes
git reflog --format='%H %gd %gs'
2. Recovering from Accidental Reset
The most common disaster scenario:
# Scenario: You accidentally reset main back 5 commits
git reset --hard HEAD~5
# Oh no! Your work is gone... but not really
# Check the reflog
git reflog
# Find the commit you were at before the reset
# abc1234 HEAD@{1}: reset: moving to HEAD~5
# def5678 HEAD@{2}: commit: feat: add user profile
# Reset back to where you were
git reset --hard def5678
# Done! Your commits are back.
3. Recovering a Dropped Stash
Stashes are commits too, and they appear in the reflog:
# Scenario: You accidentally dropped an important stash
git stash drop stash@{0}
# Find the stash commit in the reflog
git reflog | grep stash
# Or search for the stash message
git reflog | grep "WIP on feature"
# The stash commit SHA appears in the reflog
# abc1234 HEAD@{15}: stash: WIP on feature/auth
# Recreate the stash from the commit
git stash apply abc1234
# Or create a branch from it
git branch recovered-stash abc1234
4. Recovering from a Failed Rebase
When a rebase goes wrong, reflog is your escape hatch:
# Scenario: Rebase created conflicts and messed up history
git rebase main
# Conflicts everywhere... you abort but things are still broken
# Check the reflog for the state before rebase
git reflog
# Find the pre-rebase state
# def5678 HEAD@{1}: rebase: picking abc1234
# ghi9012 HEAD@{2}: checkout: moving from feature to feature
# Reset to before the rebase started
git reset --hard ghi9012
# Start fresh with a better rebase strategy
git rebase -i main
5. Recovering Orphaned Commits
Commits that no branch points to can still be found:
# Find all unreachable commits
git fsck --lost-found --no-reflogs
# This creates .git/lost-found/commit/ with orphaned commits
ls .git/lost-found/commit/
# Examine each one
git show <commit-sha>
# Create a branch from the one you want
git branch recovered-feature <commit-sha>
6. Using Reflog with Dates
Time-based recovery when you don’t remember the commit:
# What did HEAD look like yesterday?
git reflog --date=iso | grep "yesterday"
# What was main pointing to 2 hours ago?
git reflog show main --date=relative | grep "2 hours ago"
# Checkout the state from a specific time
git checkout main@{2.hours.ago}
# Create a branch from that state
git branch recovery-point main@{2.hours.ago}
Production Failure Scenarios + Mitigations
| Scenario | What Happens | Mitigation |
|---|---|---|
| Reflog expired — Entry is older than 90 days | The commit may be garbage collected | Run recovery within 90 days; use git fsck for older commits |
Garbage collection ran — git gc pruned unreachable commits | Reflog entries exist but commits are gone | Disable aggressive gc; set gc.reflogExpireUnreachable to longer period |
| Remote force push — You force pushed and lost remote commits | Local reflog can’t help if you never had the commits | Require force push protection; use --force-with-lease instead of --force |
| Deleted .git directory — The entire repository is gone | Reflog lives in .git/logs/ — it’s gone too | Regular backups; never delete .git without archiving first |
| Reflog corruption — Reflog file is damaged | Entries may be unrecoverable | Reflog is append-only; corruption is rare but possible |
| Wrong commit recovery — You recover the wrong commit | You’re back to a different wrong state | Verify the commit content with git show before resetting |
Trade-offs
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Safety net | Catches almost all local mistakes | Local only; doesn’t help with remote issues |
| Time-based | Can recover by date/time | Entries expire (default 90/30 days) |
| Comprehensive | Tracks all HEAD movements | Can be overwhelming with hundreds of entries |
| No setup required | Enabled by default | Consumes disk space for log files |
| Branch-specific | Each branch has its own reflog | Only tracks branches you’ve checked out |
| Recovery flexibility | Can reset, checkout, or branch from any entry | Requires understanding of reflog references |
Implementation Snippets
Reflog Configuration
# ~/.gitconfig
[gc]
# Don't aggressively garbage collect
auto = 6000
autopacklimit = 50
[core]
# Keep reflog entries longer
logAllRefUpdates = true
# Per-repository settings
git config gc.reflogExpire 180.days.ago
git config gc.reflogExpireUnreachable 90.days.ago
Recovery Script
#!/bin/bash
# scripts/recovery-assistant.sh
# Interactive reflog recovery assistant
echo "=== Git Reflog Recovery Assistant ==="
echo ""
# Show recent reflog
echo "Recent operations:"
git reflog --date=iso --format='%C(auto)%h %gd %C(reset)%gs %C(blue)%cr' | head -20
echo ""
echo "Options:"
echo "1) Reset HEAD to a specific reflog entry"
echo "2) Create a branch from a reflog entry"
echo "3) Show details of a specific entry"
echo "4) Search reflog by message"
echo "5) Find lost stashes"
read -r choice
case $choice in
1)
echo "Enter reflog reference (e.g., HEAD@{3} or commit SHA):"
read -r ref
echo "This will reset HEAD to: $ref"
echo "Proceed? (y/n)"
read -r confirm
if [ "$confirm" = "y" ]; then
git reset --hard "$ref"
echo "Reset complete."
fi
;;
2)
echo "Enter reflog reference:"
read -r ref
echo "Branch name:"
read -r branch
git branch "$branch" "$ref"
echo "Branch '$branch' created from $ref"
;;
3)
echo "Enter reflog reference:"
read -r ref
git show "$ref"
;;
4)
echo "Search term:"
read -r term
git reflog --grep="$term"
;;
5)
echo "Lost stashes:"
git reflog | grep "stash"
;;
*)
echo "Invalid option"
;;
esac
Pre-Destructive Hook
#!/bin/bash
# scripts/safe-reset.sh
# Wrapper for git reset that creates a safety branch first
TARGET=${1:?Usage: safe-reset.sh <target>}
# Create a safety branch pointing to current HEAD
SAFETY_BRANCH="safety/$(date +%Y%m%d-%H%M%S)"
git branch "$SAFETY_BRANCH" HEAD
echo "Safety branch created: $SAFETY_BRANCH"
echo "Current HEAD: $(git rev-parse HEAD)"
echo ""
# Perform the reset
echo "Resetting to: $TARGET"
git reset --hard "$TARGET"
echo ""
echo "Reset complete."
echo "If something went wrong, recover with:"
echo " git reset --hard $SAFETY_BRANCH"
Reflog Diff Viewer
#!/bin/bash
# scripts/reflog-diff.sh
# Compare two reflog entries
ENTRY1=${1:?Usage: reflog-diff.sh <entry1> <entry2>}
ENTRY2=${2:?Second entry required}
echo "=== Comparing $ENTRY1 vs $ENTRY2 ==="
echo ""
git diff "${ENTRY1}...${ENTRY2}" --stat
echo ""
echo "Full diff:"
git diff "${ENTRY1}...${ENTRY2}"
Observability Checklist
- Logs: Not typically applicable — reflog is local
- Metrics: Track reflog size and entry count per repository
- Alerts: Alert when reflog entries approach expiration or when disk usage from logs exceeds threshold
- Dashboards: Display reflog health: entry count, oldest entry age, and disk space used
- Recovery tracking: Log recovery operations for post-incident analysis
Security and Compliance Notes
- Local only: Reflog never leaves your machine — it’s stored in
.git/logs/ - Sensitive data: Reflog may contain commit messages with sensitive information — audit reflog entries
- No remote sync: Reflog is not pushed to remotes; each developer has their own reflog
- Audit trail: Reflog provides a local audit trail of all operations, useful for forensic analysis
- Compliance: For regulated environments, reflog can help reconstruct developer actions during an incident
Common Pitfalls and Anti-Patterns
- Assuming Reflog is Forever — Reflog entries expire. Default is 90 days for reachable commits, 30 days for unreachable ones. Don’t rely on it as a backup.
- Ignoring Unreachable Commits — After
git gc --prune, unreachable commits are gone forever. Run recovery before garbage collection. - Confusing Reflog with Log —
git logshows commit history;git reflogshows local operations. They serve different purposes. - Not Verifying Before Reset — Resetting to the wrong reflog entry compounds the problem. Always
git showthe target first. - Force Push Without Lease — Using
--forceinstead of--force-with-leasecan overwrite others’ work without warning. - Deleting .git/logs — Manually cleaning
.git/logs/destroys your safety net. Let Git manage reflog expiration. - No Backup Strategy — Reflog is not a backup. Use remote branches, tags, or external backups for critical work.
Quick Recap Checklist
- Reflog tracks all HEAD movements locally
- Use
git reflogto view recent operations - Use
git reflog show <branch>for branch-specific history - Recover lost commits with
git reset --hard <reflog-entry> - Recover dropped stashes by finding them in reflog
- Use
git fsck --lost-foundfor orphaned commits - Reflog entries expire (default 90/30 days)
- Reflog is local-only; it doesn’t sync to remotes
- Create safety branches before destructive operations
- Never use reflog as a substitute for proper backups
Recovery Decision Tree
When you’ve lost work, follow this path:
- Reflog available? →
git reflog— Find the commit SHA and reset/branch from it - Reflog expired? →
git fsck --lost-found— Check.git/lost-found/commit/for orphaned objects - Remote has it? →
git fetch origin— Check if the remote still has the old branch state - Backup exists? → Check your backup system, IDE local history, or time machine
- All else failed? → The work is gone. Accept the loss and recreate it.
Recovery Checklist
- Act quickly — Reflog entries expire; garbage collection makes recovery impossible
- Don’t run
git gc— Garbage collection permanently removes unreachable objects - Check reflog first —
git reflogis the fastest recovery path - Verify before resetting —
git show <sha>to confirm it’s the right commit - Create safety branch —
git branch recovery-attempt HEADbefore any destructive operation - Check fsck — If reflog fails,
git fsck --lost-foundmay find orphaned commits - Check remote —
git fetch originand check remote branches for the lost state - Extend reflog — For critical repos, set
gc.reflogExpireto 180 days or more
Interview Q&A
git log shows the commit history of the current branch — the ancestry chain of commits reachable from HEAD. It's a view of the project's evolution.
git reflog shows the history of local operations — every time HEAD moved, regardless of whether the commit is still reachable. It includes resets, checkouts, rebases, and stashes that don't appear in git log.
Think of it this way: git log is the project's history; git reflog is your personal history with the repository.
By default, reflog keeps entries for 90 days for commits reachable from any branch or tag, and 30 days for unreachable commits (orphaned commits not referenced by any branch).
These values are configurable via gc.reflogExpire and gc.reflogExpireUnreachable. You can extend them for critical repositories or shorten them to save disk space.
After expiration, entries are removed during garbage collection. Once garbage collected, the commits are truly gone unless you have a backup.
Only if you had the commits locally. Reflog is a local feature — it tracks operations on your machine. If you force pushed and lost commits that existed on the remote but you never had locally, reflog can't help.
If you did have the commits locally, check your reflog for the pre-force-push state and reset or create a branch from that entry. Then force push the recovered state.
To prevent this scenario, use --force-with-lease instead of --force. It checks that the remote hasn't changed since your last fetch, preventing accidental overwrites.
Interactive rebase rewrites history, but the original commits remain in the reflog:
- Run
git reflogto find the state before the rebase started - Look for the entry just before the rebase:
HEAD@{n}: rebase: picking ... - The entry before that is your pre-rebase state
- Create a branch from it:
git branch recovered HEAD@{n+1} - Compare the recovered branch with your current state to identify dropped commits
Alternatively, use git fsck --lost-found to find all unreachable commits and examine each one.
Resources
- Git reflog documentation — Official Git documentation
- Git recovery guide — Reset and recovery techniques
- Git fsck documentation — Finding orphaned objects
- Force push safety — Using —force-with-lease
- Git garbage collection — Understanding gc and reflog expiration
Category
Related Posts
Git Bisect for Bug Hunting: Binary Search Through Commit History
Master git bisect to find the exact commit that introduced a bug using binary search. Automate bug hunting with scripts and handle complex scenarios.
Git Blame and Annotate: Line-by-Line Code Attribution
Master git blame for line-by-line code attribution, understanding code history, finding when code changed, and using blame effectively for code comprehension.
git log: Master Commit History Navigation and Filtering
Master git log formatting, filtering, searching history, and navigating commit history effectively for version control debugging and auditing.