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.

published: reading time: 14 min read updated: March 31, 2026

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 --hard and 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 .git directory is gone, reflog is gone too

Core Concepts

Git reflog tracks HEAD movements and branch updates:

Entry TypeTriggerExample
commitCreating a new commitcommit: feat: add user authentication
checkoutSwitching branchescheckout: moving from main to feature/login
resetRunning git resetreset: moving to HEAD~3
rebaseRunning git rebaserebase: picking abc1234
mergeRunning git mergemerge main: Fast-forward
stashStash operationsstash: WIP on feature: abc1234
cherry-pickRunning git cherry-pickcherry-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

ScenarioWhat HappensMitigation
Reflog expired — Entry is older than 90 daysThe commit may be garbage collectedRun recovery within 90 days; use git fsck for older commits
Garbage collection rangit gc pruned unreachable commitsReflog entries exist but commits are goneDisable aggressive gc; set gc.reflogExpireUnreachable to longer period
Remote force push — You force pushed and lost remote commitsLocal reflog can’t help if you never had the commitsRequire force push protection; use --force-with-lease instead of --force
Deleted .git directory — The entire repository is goneReflog lives in .git/logs/ — it’s gone tooRegular backups; never delete .git without archiving first
Reflog corruption — Reflog file is damagedEntries may be unrecoverableReflog is append-only; corruption is rare but possible
Wrong commit recovery — You recover the wrong commitYou’re back to a different wrong stateVerify the commit content with git show before resetting

Trade-offs

AspectAdvantageDisadvantage
Safety netCatches almost all local mistakesLocal only; doesn’t help with remote issues
Time-basedCan recover by date/timeEntries expire (default 90/30 days)
ComprehensiveTracks all HEAD movementsCan be overwhelming with hundreds of entries
No setup requiredEnabled by defaultConsumes disk space for log files
Branch-specificEach branch has its own reflogOnly tracks branches you’ve checked out
Recovery flexibilityCan reset, checkout, or branch from any entryRequires 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

  1. 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.
  2. Ignoring Unreachable Commits — After git gc --prune, unreachable commits are gone forever. Run recovery before garbage collection.
  3. Confusing Reflog with Loggit log shows commit history; git reflog shows local operations. They serve different purposes.
  4. Not Verifying Before Reset — Resetting to the wrong reflog entry compounds the problem. Always git show the target first.
  5. Force Push Without Lease — Using --force instead of --force-with-lease can overwrite others’ work without warning.
  6. Deleting .git/logs — Manually cleaning .git/logs/ destroys your safety net. Let Git manage reflog expiration.
  7. 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 reflog to 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-found for 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:

  1. Reflog available?git reflog — Find the commit SHA and reset/branch from it
  2. Reflog expired?git fsck --lost-found — Check .git/lost-found/commit/ for orphaned objects
  3. Remote has it?git fetch origin — Check if the remote still has the old branch state
  4. Backup exists? → Check your backup system, IDE local history, or time machine
  5. 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 firstgit reflog is the fastest recovery path
  • Verify before resettinggit show <sha> to confirm it’s the right commit
  • Create safety branchgit branch recovery-attempt HEAD before any destructive operation
  • Check fsck — If reflog fails, git fsck --lost-found may find orphaned commits
  • Check remotegit fetch origin and check remote branches for the lost state
  • Extend reflog — For critical repos, set gc.reflogExpire to 180 days or more

Interview Q&A

What is the difference between git log and git reflog?

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.

How long does git reflog keep entries?

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.

Can git reflog recover a force-pushed branch?

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.

How do you find a commit that was lost during an interactive rebase?

Interactive rebase rewrites history, but the original commits remain in the reflog:

  1. Run git reflog to find the state before the rebase started
  2. Look for the entry just before the rebase: HEAD@{n}: rebase: picking ...
  3. The entry before that is your pre-rebase state
  4. Create a branch from it: git branch recovered HEAD@{n+1}
  5. 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

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 #version-control #debugging

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 #version-control #git-blame

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.

#git #git-log #history