Git — Troubleshooting & Recovery
Undo Cheat Sheet
| What happened | Fix command | Safe? |
|---|---|---|
| Unstage a file | git restore --staged file.py (modern) / git reset HEAD file.py |
Yes |
| Discard changes in file | git restore file.py (modern) / git checkout -- file.py |
Destructive |
| Undo last commit (keep changes) | git reset --soft HEAD~1 |
Yes |
| Undo last commit (unstage changes) | git reset HEAD~1 |
Yes |
| Undo last commit (discard changes) | git reset --hard HEAD~1 |
Destructive |
| Revert pushed commit | git revert abc1234 |
Yes (creates new commit) |
| Undo a merge | git revert -m 1 <merge-hash> |
Yes |
| Fix last commit message | git commit --amend |
Safe if not pushed |
| Fix last commit message inline | git commit --amend -m "new message" |
Safe if not pushed |
| Add file to last commit | git add file && git commit --amend --no-edit |
Safe if not pushed |
Reset Modes
git reset --soft HEAD~1 # Move HEAD, keep staging + working
git reset HEAD~1 # Move HEAD, keep working (default: mixed)
git reset --hard HEAD~1 # Move HEAD, discard everything
| Mode | HEAD | Staging | Working Dir |
|---|---|---|---|
--soft |
Moves back | Unchanged | Unchanged |
--mixed |
Moves back | Reset | Unchanged |
--hard |
Moves back | Reset | Reset |
Rule: Never reset --hard on commits that have been pushed to shared branches.
Reflog — Your Safety Net
Git records every HEAD movement. Default retention is typically: - reachable reflog entries: ~90 days - unreachable reflog entries: ~30 days
git reflog # Show HEAD movement history
git reflog show feature/auth # Reflog for specific branch
# Output example:
# abc1234 HEAD@{0}: reset: moving to HEAD~1
# def5678 HEAD@{1}: commit: feat: add oauth
# ghi9012 HEAD@{2}: checkout: moving from main to feature/auth
Recovery Scenarios
# Undo accidental reset --hard
git reflog
git reset --hard HEAD@{1} # Restore to previous state
# Recover deleted branch
git reflog
git branch recovered-branch abc1234 # Recreate from found hash
git checkout -b recovered-branch abc1234 # Recreate + switch immediately
# Undo bad rebase
git reflog
git reset --hard HEAD@{N} # N = entry before rebase started
Revert (Safe Undo for Pushed Commits)
git revert abc1234 # Create inverse commit
git revert abc1234 --no-commit # Stage revert without committing
git revert HEAD~3..HEAD # Revert last 3 commits
git revert -m 1 <merge-commit> # Revert a merge (keep parent 1)
revert is safe for public branches — it adds a new commit instead of rewriting history.
Conflict Resolution
git merge feature/auth # Conflict occurs
git status # See conflicted files
Conflict markers in file:
<<<<<<< HEAD
current branch code
=======
incoming branch code
>>>>>>> feature/auth
With zdiff3 conflict style (recommended — shows common ancestor):
<<<<<<< HEAD
current branch code
||||||| common ancestor
original code before both changes
=======
incoming branch code
>>>>>>> feature/auth
# After resolving conflicts manually:
git add resolved-file.py
git merge --continue # Or: git commit
# Resolve by taking one side completely (per file):
git checkout --ours path/to/file # Keep your version
git checkout --theirs path/to/file # Keep incoming version
git add path/to/file
# Abort if needed:
git merge --abort
git rebase --abort
git cherry-pick --abort
Tools for Conflict Resolution
git mergetool # Open configured merge tool
git config --global merge.tool vimdiff # Set default merge tool
Bisect — Find the Bug Commit
Binary search through history to find which commit introduced a bug.
git bisect start
git bisect bad # Current commit is broken
git bisect good v1.0.0 # This tag/commit was working
# Git checks out a middle commit → test it → mark:
git bisect good # This commit is fine
git bisect bad # This commit has the bug
# Repeat until Git finds the culprit
git bisect reset # Return to original branch
Automated Bisect
git bisect start HEAD v1.0.0
git bisect run pytest tests/test_auth.py # Auto-run command each step
Git marks commits as good/bad based on the exit code (0 = good, non-zero = bad).
Recover Staged-but-Not-Committed Changes
After git reset --hard, blobs that were staged (added with git add) survive as dangling objects in Git's object store — Git never ran garbage collection on them yet.
git fsck --lost-found # Find all dangling objects
ls .git/lost-found/other/ # Blobs (file contents without names)
cat .git/lost-found/other/<hash> # Inspect each blob
This recovers file content only (blob objects) — filenames are lost. You identify the right blob by reading its contents.
Never staged or committed: Completely unrecoverable — Git never tracked them.
Remove Committed Secrets / Sensitive Files
If a secret was committed, rotate it immediately and rewrite history with modern tooling.
pip install git-filter-repo # Or: uv tool install git-filter-repo
git filter-repo --path .env --invert-paths # Remove a file from all history
git push --force-with-lease origin main # Coordinate with team before pushing
git filter-branch (seen in older guides) is legacy and much slower; prefer git filter-repo or BFG Repo-Cleaner.
Dangerous Commands Checklist
| Command | Risk | Safer Alternative |
|---|---|---|
git reset --hard |
Destroys working dir changes | git reset --soft or git stash |
git push --force |
Overwrites remote history | git push --force-with-lease |
git clean -fd |
Deletes untracked files | git clean -n (dry run first) |
git rebase on shared branch |
Rewrites shared history | git merge |
git checkout -- . |
Discards all unstaged changes | git stash |
--force-with-lease is safer than --force: it refuses to push if someone else pushed to the branch since your last fetch.
Troubleshooting Decision Tree
Lost commits?
├── git reflog → find hash → git reset/branch
├── git fsck --lost-found → dangling objects
└── Not committed? → Unrecoverable
Merge conflict?
├── git status → see conflicted files
├── Edit files → remove markers
├── git add → git merge --continue
└── Too messy? → git merge --abort
Wrong branch?
├── Not committed? → git stash → switch → pop
├── Committed? → git cherry-pick on correct branch
└── git reset HEAD~1 on wrong branch
Bad commit pushed?
├── git revert <hash> (safe, adds inverse commit)
└── Never rewrite public history with reset/rebase