Git: Reflog, Maintenance, and Recovery

Git's reflog is a safety net — a local log of everywhere HEAD has pointed. Almost nothing in Git is truly lost; if you committed it, the reflog can usually find it. This article also covers the maintenance commands that keep a repository healthy and performing well: garbage collection, integrity checking, conflict resolution memory, and history rewriting.

Commands Covered

git reflog · git gc · git maintenance · git fsck · git rerere · git prune · git pack-refs · git repack · git filter-branch · git replace · git notes

git reflog

The reflog records every time HEAD moves — branch switches, commits, resets, rebases. It's kept locally for 90 days by default and is your primary recovery tool after destructive operations.

# Show the reflog for HEAD
$ git reflog
# abc1234 HEAD@{0}: commit: Fix auth bug
# def5678 HEAD@{1}: reset: moving to HEAD~2
# ghi9012 HEAD@{2}: commit: Add search feature
# jkl3456 HEAD@{3}: checkout: moving from feature to main

# Show reflog for a specific branch
$ git reflog show main
$ git reflog show feature-login

# Show with timestamps
$ git reflog --date=iso
$ git reflog --date=relative

# Recover a commit after a bad reset:
$ git reset --hard HEAD~3    # oops, lost 3 commits
$ git reflog                 # find the commit before the reset
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: The commit I want back
$ git reset --hard def5678   # restore to before the reset

# Recover a deleted branch:
$ git branch -D feature-old   # branch deleted
$ git reflog                  # find the tip commit of the deleted branch
# abc1234 HEAD@{5}: commit: Last commit on feature-old
$ git switch -c feature-old abc1234   # recreate the branch

# Expire old reflog entries (normally done by gc)
$ git reflog expire --expire=90.days refs/heads/main
$ git reflog expire --expire-unreachable=30.days --all

git gc

Garbage collection — cleans up loose objects, packs them into pack files, prunes unreachable objects older than the grace period, and compresses the repo. Git runs a lightweight version automatically; explicit gc is for large repos or after bulk operations.

# Run garbage collection
$ git gc

# Aggressive optimization (slower, more thorough compression)
$ git gc --aggressive
# Roughly equivalent to repacking everything from scratch; takes time

# Quiet mode
$ git gc --quiet

# Prune objects older than the default grace period (2 weeks)
$ git gc --prune=now    # prune immediately (don't wait for grace period)
$ git gc --no-prune     # collect but don't prune

# Check what gc would do (approximate)
$ git count-objects -v
# count: 0       (loose objects)
# size: 0        (KB of loose objects)
# in-pack: 1234  (objects in pack files)
# packs: 2
# size-pack: 5678 (KB in pack files)
# prune-packable: 0
# garbage: 0

# Auto-gc threshold (how many loose objects trigger auto-gc)
$ git config gc.auto 6700      # default; set to 0 to disable auto-gc

git maintenance

A modern replacement for ad hoc git gc calls. Schedules background maintenance tasks to keep the repo fast without blocking your work. Available from Git 2.29.

# Start background maintenance for the current repo
$ git maintenance start
# Registers the repo for scheduled maintenance (cron/launchd/systemd)

# Stop scheduled maintenance
$ git maintenance stop

# Run all maintenance tasks now
$ git maintenance run

# Run specific tasks
$ git maintenance run --task=gc
$ git maintenance run --task=commit-graph
$ git maintenance run --task=fetch           # background fetch from remotes
$ git maintenance run --task=loose-objects   # pack loose objects
$ git maintenance run --task=incremental-repack
$ git maintenance run --task=prefetch

# Register a repo for maintenance without starting the schedule
$ git maintenance register

# Unregister
$ git maintenance unregister

git fsck

Verifies the connectivity and validity of objects in the database. Finds dangling objects (commits with no branch pointing to them — useful for recovery) and corruption.

# Check integrity of the object database
$ git fsck

# Show dangling objects (commits/blobs with no reference — potential recovery targets)
$ git fsck --lost-found
# Creates .git/lost-found/commit/ and .git/lost-found/other/ with dangling objects

$ git fsck --dangling

# Verbose output
$ git fsck --full --strict

# Recover a dangling commit:
$ git fsck --dangling | grep "dangling commit"
# dangling commit abc1234def
$ git show abc1234def    # inspect it
$ git switch -c recovered-work abc1234def   # create a branch from it

git rerere

"Reuse Recorded Resolution" — records how you resolved merge conflicts and automatically applies the same resolution next time the same conflict appears. Invaluable when rebasing long-lived branches repeatedly.

# Enable rerere globally
$ git config --global rerere.enabled true

# After enabling, when you resolve a conflict:
# 1. Git records the pre-resolution conflict and your resolution
# 2. Next time the same conflict appears, it applies automatically

# Show recorded resolutions
$ git rerere status
$ git rerere diff     # show what rerere would apply

# Apply recorded resolutions to current conflicts
$ git rerere

# Clear a specific recorded resolution (if you got it wrong)
$ git rerere forget path/to/file

# List all saved resolutions
$ ls .git/rr-cache/

git prune

Removes objects that are no longer reachable from any reference. Normally called by git gc; rarely needed directly.

# Prune unreachable objects (dry run — see what would be removed)
$ git prune -n
$ git prune --dry-run

# Prune unreachable objects older than a date
$ git prune --expire=2.weeks.ago
$ git prune --expire=now     # prune everything immediately

# Prune packed objects already in pack files
$ git prune-packed

git pack-refs

Packs loose ref files (.git/refs/heads/*, .git/refs/tags/*) into a single .git/packed-refs file. Speeds up operations in repos with many branches or tags.

# Pack all refs
$ git pack-refs --all

# Pack all refs and prune stale packed refs
$ git pack-refs --all --prune

git repack

Repacks object database into pack files. More control than git gc over how objects are packed.

# Repack all objects into a single pack file
$ git repack -a -d
# -a = all objects into one pack
# -d = delete redundant pack files after repacking

# Aggressive optimization
$ git repack -a -d -f --depth=250 --window=250

# Create incremental pack (only new loose objects)
$ git repack

# Show pack statistics
$ git verify-pack -v .git/objects/pack/*.idx | sort -k3 -n | tail -20
# Shows largest objects in the pack

git filter-branch

Rewrites history by applying filters to every commit — removing files, changing author info, removing secrets. Powerful but slow and complex. For new projects, git filter-repo (a separate tool) is recommended over filter-branch.

# WARNING: filter-branch rewrites every affected commit's SHA.
# Anyone who has cloned the repo will need to re-clone or rebase.

# Remove a file from all commits (e.g. accidentally committed secret)
$ git filter-branch --force --index-filter \
    'git rm --cached --ignore-unmatch secrets.txt' \
    --prune-empty --tag-name-filter cat -- --all

# Rewrite author email across all commits
$ git filter-branch --env-filter '
OLD_EMAIL="old@example.com"
NEW_EMAIL="new@example.com"
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then
    export GIT_AUTHOR_EMAIL="$NEW_EMAIL"
fi
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then
    export GIT_COMMITTER_EMAIL="$NEW_EMAIL"
fi
' --tag-name-filter cat -- --all

# After filter-branch, force-push and have collaborators re-clone
$ git push --force --all
$ git push --force --tags

# Modern alternative: git-filter-repo (pip install git-filter-repo)
$ git filter-repo --path secrets.txt --invert-paths

git replace

Creates replacement references — makes Git use one object in place of another without rewriting history. Useful for grafting history or local-only replacements.

# Replace one commit with another (locally)
$ git replace old-commit-sha new-commit-sha

# List replacements
$ git replace -l

# Delete a replacement
$ git replace -d old-commit-sha

# Edit a commit (replace it with an edited version)
$ git replace --edit abc1234

# Graft old history (attach a repo's history to another)
$ git replace --graft child-commit parent-commit

git notes

Attaches additional text to commits without changing the commit SHA. Notes are stored separately and don't affect commit hashes — useful for adding review comments, build results, or metadata without rewriting history.

# Add a note to the current commit
$ git notes add -m "Reviewed by Alice on 2024-11-15"

# Add a note to a specific commit
$ git notes add -m "Build: PASSED" abc1234

# Show notes in git log
$ git log --show-notes

# Show a commit with its notes
$ git show abc1234

# Edit existing note
$ git notes edit abc1234

# Remove a note
$ git notes remove abc1234

# List all notes
$ git notes list

# Push notes to remote (notes are not pushed by default)
$ git push origin refs/notes/commits
# Fetch notes from remote
$ git fetch origin refs/notes/commits:refs/notes/commits

References