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
dispelled