Learning Objectives
By the end of this module, you will be able to:
- Recover from a bad merge using
git revert -mandgit reset - Choose between
git revertandgit reset+ force push for undoing pushed commits - Use the reflog to find and restore lost commits, branches, and stashes
- Resolve "detached HEAD" situations without losing work
- Fix "diverged branches" when your local and remote have split
- Clean untracked files and directories safely with
git clean - Use
git worktreeto work on multiple branches simultaneously without stashing or cloning
1. Recovering from a Bad Merge
You merged a feature branch into main, pushed it, and now production is broken. This is one of the most stressful Git situations, but there are clean solutions.
Option 1: Revert the Merge Commit
git revert creates a new commit that undoes the changes introduced by the merge. This is the safest option for shared branches because it doesn't rewrite history:
# Find the merge commit
git log --oneline --merges -5
# a1b2c3d Merge branch 'feature/broken' into main
# ...
# Revert the merge commit
git revert -m 1 a1b2c3dThe -m 1 flag is critical. Merge commits have two parents, and you must tell Git which parent to revert to:
Parent 1 (main) Parent 2 (feature/broken)
╲ ╱
╲ ╱
╲ ╱
M ← merge commit a1b2c3d
│
R ← revert commit (undoes parent 2's changes)
-m 1means "keep parent 1's state" (keepmain, undo the feature) — this is almost always what you want-m 2means "keep parent 2's state" (keep the feature, undomain) — rarely used
git revert -m 1 a1b2c3d
git push origin main # Safe — no force push neededThe Re-merge Trap
There's a subtle problem: after reverting a merge, you can't simply re-merge the same branch later. Git thinks those commits are already in main's history (they are — they were merged and then reverted). To re-merge the feature after fixing it:
# 1. The merge was reverted
git revert -m 1 a1b2c3d # Commit R
# 2. Fix the feature branch
git checkout feature/broken
# ... make fixes ...
git commit -m "fix: resolve the bugs"
# 3. Revert the revert (re-introduces the original changes)
git checkout main
git revert <hash-of-R>
# 4. Now merge the fixes
git merge feature/brokenOr more simply, rebase the feature branch to create new commits:
git checkout feature/broken
git rebase main
# Now the commits have new hashes — Git treats them as new work
git checkout main
git merge feature/brokenOption 2: Reset and Force Push (Private Branches Only)
If the bad merge hasn't been fetched by others (or you coordinate with your team), you can erase it entirely:
# Move main back to before the merge
git checkout main
git reset --hard HEAD~1 # or: git reset --hard <commit-before-merge>
# Force push to rewrite the remote
git push --force-with-lease origin mainWhen to use which:
| Situation | Use revert | Use reset + force push |
|---|---|---|
Shared branch (main) | Yes | Only with team coordination |
| Others may have pulled | Yes | No |
| Need clean history | No (leaves revert commit) | Yes |
| Feature branch with 1 developer | Either | Yes (simpler) |
2. Undoing a Pushed Commit
Scenario: You Pushed a Bad Commit
You committed sensitive data, a broken change, or an accidental file to a shared branch. Here are your options in order of safety.
Option 1: git revert (Safe, History-Preserving)
# Undo the most recent commit
git revert HEAD
git push origin main
# Undo a specific older commit
git revert abc1234
git push origin main
# Undo multiple consecutive commits (oldest to newest order)
git revert --no-commit abc1234..def5678
git commit -m "revert: undo commits abc1234 through def5678"
git push origin mainThe --no-commit flag stages the reversions without committing each one, letting you create a single revert commit.
Option 2: git reset + Force Push (History-Rewriting)
# Remove the last commit from history
git reset --hard HEAD~1
git push --force-with-lease origin main
# Remove the last 3 commits
git reset --hard HEAD~3
git push --force-with-lease origin mainOption 3: For Sensitive Data — Complete Purge
If you pushed passwords, API keys, or secrets, git revert isn't enough — the sensitive data still exists in the Git history. You need to purge it:
# Using git-filter-repo (recommended over filter-branch)
pip install git-filter-repo
# Remove a specific file from all history
git filter-repo --invert-paths --path secrets.env
# Remove a specific string from all files in history
git filter-repo --replace-text <(echo 'my-api-key==>REDACTED')After purging:
- Force push all branches
- Ask all collaborators to re-clone (not just
git pull) - Rotate the exposed credentials immediately — they're in Git's reflog and possibly in GitHub's caches
- Contact your hosting provider to clear server-side caches
3. Recovering Lost Commits with Reflog
The reflog (reference log) is your safety net. It records every change to HEAD and branch tips for the last 90 days (by default). Even after a hard reset or deleted branch, the commits still exist in the object database — the reflog helps you find them.
Understanding the Reflog
# View the reflog
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~3 ← you reset
# def5678 HEAD@{1}: commit: feat: add dashboard ← lost commit 3
# 789abcd HEAD@{2}: commit: feat: add sidebar ← lost commit 2
# 012efgh HEAD@{3}: commit: feat: add header ← lost commit 1
# 345ijkl HEAD@{4}: checkout: moving from feature to mainEach entry shows:
- The commit hash at that point
- The reflog reference (
HEAD@{N}) - What operation caused the change
- A description
Recovery Scenario 1: Undo a Hard Reset
# Oops! You reset too far
git reset --hard HEAD~3 # Lost 3 commits!
# Find them in the reflog
git reflog
# Current HEAD is 345ijkl
# HEAD@{1} was def5678 (the commit before the reset)
# Recover by resetting back
git reset --hard def5678
# Or create a new branch from the lost commit
git branch recovered-work def5678Recovery Scenario 2: Restore a Deleted Branch
# Delete a branch by accident
git branch -D feature/important-work
# Deleted branch feature/important-work (was abc1234).
# Git helpfully tells you the commit hash!
# But if you missed it, use reflog:
git reflog
# abc1234 HEAD@{5}: commit: last commit on feature/important-work
# Recreate the branch
git branch feature/important-work abc1234Recovery Scenario 3: Find a Lost Stash
# You accidentally dropped a stash
git stash drop stash@{0}
# Stash entries create commits too — find them
git fsck --unreachable | grep commit
# unreachable commit abc1234def5678...
# Inspect each unreachable commit to find your stash
git show abc1234
# If that's the one, apply it
git stash apply abc1234Recovery Scenario 4: Recover After a Bad Rebase
# Rebase went wrong
git rebase main # Conflicts everywhere, wrong result
# ORIG_HEAD saves you — Git sets it before dangerous operations
git reset --hard ORIG_HEAD
# Or use the reflog to find the pre-rebase state
git reflog
# Find the entry just before "rebase: ..."
git reset --hard HEAD@{N}Reflog Expiration
The reflog isn't eternal:
# Default expiration: 90 days for reachable, 30 days for unreachable
git config gc.reflogExpire # default: 90 days
git config gc.reflogExpireUnreachable # default: 30 days
# Extend for extra safety
git config --global gc.reflogExpire 180.days
git config --global gc.reflogExpireUnreachable 90.days
# View reflog with timestamps
git reflog --date=iso4. Fixing the "Detached HEAD" Panic
What Is Detached HEAD?
Normally, HEAD points to a branch name, which points to a commit. In detached HEAD state, HEAD points directly to a commit — you're not on any branch:
Normal: Detached HEAD:
HEAD → main → abc1234 HEAD → abc1234
(no branch!)
How You Get Into Detached HEAD
# Checking out a specific commit
git checkout abc1234
# Checking out a tag
git checkout v1.0.0
# Checking out a remote branch directly
git checkout origin/main
# During an interactive rebase (temporary — normal)
git rebase -i HEAD~3Git warns you:
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
The Danger: Making Commits in Detached HEAD
Commits made in detached HEAD are not on any branch. When you switch to another branch, those commits become "orphaned" — reachable only through the reflog until garbage collection removes them:
Before switching: After switching to main:
HEAD → xyz789 HEAD → main → abc1234
│
C (detached commit) C (orphaned! No branch points here)
│
abc1234 abc1234
How to Escape Detached HEAD
If you made no commits (just looking around):
# Simply go back to a branch
git checkout main
# or
git switch mainIf you made commits and want to keep them:
# Option 1: Create a branch right where you are
git checkout -b my-new-branch
# You're now on a proper branch with your commits
# Option 2: If you already switched away and need to recover
git reflog
# Find the detached commit
git branch recovered-work abc1234If you made commits and don't want them:
# Just switch to any branch — the detached commits will eventually be garbage collected
git checkout mainPreventing Detached HEAD Confusion
Use git switch instead of git checkout — it refuses to put you in detached HEAD without an explicit flag:
git switch main # safe — switches branches
git switch --detach v1.0.0 # explicit — you know what you're doing
git switch --detach abc1234 # explicit
git checkout abc1234 # silently detaches HEAD (confusing)5. Resolving "Diverged Branches"
The Problem
You see this message:
Your branch and 'origin/main' have diverged,
and have 2 and 3 different commits each, respectively.
This means both your local branch and the remote branch have moved forward independently:
D ── E (your local: 2 ahead)
╱
A ── B ── C (common ancestor)
╲
F ── G ── H (origin/main: 3 ahead)
How This Happens
- You committed locally, and someone else pushed to the same branch
- You rebased or amended a commit that was already pushed
- You reset your branch to a different point than the remote
Resolution Strategies
Strategy 1: Merge (Preserves Both Histories)
git pull origin main
# Git creates a merge commit combining both sides
# D ── E ─╲
# ╱ M (merge commit)
# A ── B ── C ╱
# ╲ ╱
# F ── G ── HStrategy 2: Rebase (Linear History)
git pull --rebase origin main
# Your commits are replayed on top of the remote's commits
# A ── B ── C ── F ── G ── H ── D'── E'
# (your commits reapplied)Strategy 3: Force Your Version (Destructive — Coordinate First)
# If YOUR version is correct and the remote is wrong
git push --force-with-lease origin mainStrategy 4: Force the Remote's Version (Destructive — Loses Local Work)
# If the REMOTE version is correct and your local is wrong
git reset --hard origin/mainDiagnosing Before Deciding
Before choosing a strategy, understand what diverged:
# See what you have that the remote doesn't
git log --oneline origin/main..HEAD
# D: feat: add search
# E: feat: add filters
# See what the remote has that you don't
git log --oneline HEAD..origin/main
# F: fix: login bug
# G: refactor: database layer
# H: feat: add caching
# Visual comparison
git log --oneline --left-right --graph HEAD...origin/main
# < D feat: add search
# < E feat: add filters
# > F fix: login bug
# > G refactor: database layer
# > H feat: add caching6. git clean: Removing Untracked Files
git clean removes files that aren't tracked by Git — build artifacts, generated files, editor temp files, etc. It's the complement to git checkout / git restore (which revert tracked files).
The Danger
git clean permanently deletes files. There's no recycle bin, no undo. Always dry-run first.
Usage
# Dry run — show what WOULD be deleted (ALWAYS do this first)
git clean -n
# Would remove build/
# Would remove temp.log
# Would remove .DS_Store
# Actually delete untracked files
git clean -f
# Delete untracked files AND directories
git clean -fd
# Delete untracked AND ignored files (nuclear option)
git clean -fdx
# Delete only ignored files (clean build artifacts, keep new files)
git clean -fdXThe Flag Matrix
| Flags | Untracked Files | Untracked Dirs | Ignored Files |
|---|---|---|---|
-f | Removed | Kept | Kept |
-fd | Removed | Removed | Kept |
-fdX | Kept | Kept | Removed |
-fdx | Removed | Removed | Removed |
Interactive Mode
git clean -i
# Would remove the following items:
# build/ temp.log .DS_Store
#
# *** Commands ***
# 1: clean 2: filter by pattern 3: select by numbers
# 4: ask each 5: quit 6: help
# What now>Option 4 ("ask each") is the safest interactive approach — it prompts for each file individually.
Safe Patterns
# Clean everything except specific patterns
git clean -fd --exclude="*.log"
# Clean only within a specific directory
git clean -fd -- build/
# The safest workflow:
git clean -n # 1. Preview
git clean -i # 2. Interactive (or -f if preview looks right)7. git worktree: Multiple Branches, No Stashing
The Problem
You're deep in feature work when an urgent bug comes in. Your options used to be:
git stashyour work, switch branches, fix the bug, switch back,git stash pop— messy and error-prone- Clone the repo again — slow, wastes disk space, different remotes/config
- Commit half-done work — pollutes history
The Solution: git worktree
git worktree creates additional working directories linked to the same repository. Each worktree can have a different branch checked out, and they share the same .git database:
~/.../project/ ← main worktree (feature branch)
├── .git/
├── src/
└── ...
~/.../project-hotfix/ ← linked worktree (main branch)
├── .git (file, points to main worktree's .git)
├── src/
└── ...
Both share the same objects, refs, and configuration!
Basic Usage
# Create a worktree for a hotfix (new directory, existing branch)
git worktree add ../project-hotfix main
# Create a worktree with a new branch
git worktree add ../project-experiment -b experiment/new-idea
# Create a worktree for a specific commit or tag
git worktree add ../project-v1 v1.0.0
# List all worktrees
git worktree list
# /Users/you/code/project abc1234 [feature/search]
# /Users/you/code/project-hotfix def5678 [main]
# /Users/you/code/project-experiment 789abcd [experiment/new-idea]The Hotfix Workflow
# You're working on a feature
cd ~/code/project
# ... lots of uncommitted work in progress ...
# Urgent bug! Create a worktree for the fix (no stashing needed)
git worktree add ../project-hotfix main
cd ../project-hotfix
# Fix the bug on main
echo "fix applied" >> bugfix.js
git add . && git commit -m "fix: critical login bug"
git push origin main
# Go back to your feature work (exactly as you left it)
cd ../project
# ... continue where you left off ...
# Clean up the worktree when done
git worktree remove ../project-hotfixRules and Constraints
- Each branch can only be checked out in one worktree. You can't have
mainchecked out in two worktrees simultaneously. - All worktrees share the same
.gitdatabase. Commits made in any worktree are visible to all others. - Worktrees are cheap. They don't duplicate the object database — only the working directory and index.
- Detached HEAD worktrees are allowed. Useful for inspecting old versions.
Worktree Management
# List worktrees
git worktree list
# Remove a worktree (deletes the directory)
git worktree remove ../project-hotfix
# If the directory was already deleted manually
git worktree prune
# Move a worktree to a different directory
git worktree move ../project-hotfix ../hotfix-workspace
# Lock a worktree (prevent accidental removal)
git worktree lock ../project-hotfix
git worktree unlock ../project-hotfixWorktree Patterns
Parallel testing: Run tests on main in one worktree while developing in another.
Code review: Create a worktree to check out a PR branch for hands-on review without disturbing your work.
Comparison: Open two worktrees side by side in your editor to visually compare branches.
# Review a PR
git fetch origin pull/42/head:pr-42
git worktree add ../review-pr-42 pr-42
cd ../review-pr-42
# ... run, test, review ...
cd ../project
git worktree remove ../review-pr-428. Other Recovery Tools
git restore: Undoing Working Directory and Staging Changes
# Discard changes in a tracked file (working directory)
git restore app.js
# Unstage a file (keep changes in working directory)
git restore --staged app.js
# Restore a file to a specific commit's version
git restore --source=HEAD~3 app.js
# Restore everything to the last commit
git restore .git switch: Safer Branch Switching
# Switch to an existing branch
git switch main
# Create and switch to a new branch
git switch -c feature/new
# Switch with uncommitted changes (carries them along if no conflicts)
git switch feature/other
# Switch to a branch, discarding local changes (dangerous)
git switch -f mainORIG_HEAD: The Automatic Bookmark
Git sets ORIG_HEAD to the previous HEAD position before operations that move HEAD significantly:
# After a merge
git merge feature
# ORIG_HEAD = where HEAD was before the merge
git reset --hard ORIG_HEAD # undo the merge
# After a rebase
git rebase main
git reset --hard ORIG_HEAD # undo the rebase
# After a reset
git reset --hard HEAD~5
git reset --hard ORIG_HEAD # undo the reset (go back)git fsck: Finding Truly Lost Objects
When even the reflog doesn't help (expired entries, corrupted repo):
# Find all unreachable objects
git fsck --unreachable
# Find dangling commits (commits not referenced by any branch or tag)
git fsck --lost-found
# Lost objects are saved to .git/lost-found/
ls .git/lost-found/commit/
# Shows hashes of recoverable commits
# Inspect them
git show <hash>
# Recover one
git branch recovered <hash>Command Reference
| Command | Description |
|---|---|
git revert -m 1 <merge-commit> | Revert a merge commit, keeping parent 1 |
git revert --no-commit <range> | Revert multiple commits into one staged change |
git reset --hard <commit> | Move branch tip and reset working directory |
git reset --hard ORIG_HEAD | Undo the last dangerous operation |
git reflog | Show history of HEAD movements |
git reflog show <branch> | Show reflog for a specific branch |
git fsck --lost-found | Find unreachable/dangling objects |
git clean -n | Dry run — show untracked files that would be removed |
git clean -f | Remove untracked files |
git clean -fd | Remove untracked files and directories |
git clean -fdx | Remove untracked files, directories, and ignored files |
git clean -fdX | Remove only ignored files and directories |
git clean -i | Interactive clean |
git worktree add <path> <branch> | Create a linked worktree |
git worktree add <path> -b <new-branch> | Create a worktree with a new branch |
git worktree list | List all worktrees |
git worktree remove <path> | Remove a linked worktree |
git worktree prune | Clean up stale worktree references |
git restore <file> | Discard working directory changes |
git restore --staged <file> | Unstage a file |
git restore --source=<commit> <file> | Restore file from a specific commit |
git switch <branch> | Switch to a branch (safe alternative to checkout) |
git switch -c <new-branch> | Create and switch to a new branch |
Hands-On Lab: Disaster Recovery Scenarios
Setup
mkdir recovery-lab && cd recovery-lab
git init
# Create some history to work with
echo "line 1" > file.txt
git add . && git commit -m "commit 1"
echo "line 2" >> file.txt
git add . && git commit -m "commit 2"
echo "line 3" >> file.txt
git add . && git commit -m "commit 3"
echo "line 4" >> file.txt
git add . && git commit -m "commit 4"
echo "line 5" >> file.txt
git add . && git commit -m "commit 5"
git log --oneline
# Should show 5 commitsPart 1: Recover from a Hard Reset
Goal: Accidentally lose commits, then recover them with the reflog.
# 1. Record the current state
git log --oneline
# Save the hash of "commit 5" mentally
# 2. "Accidentally" reset to commit 2
git reset --hard HEAD~3
git log --oneline
# Only commits 1 and 2 remain!
# 3. Panic! Then remember the reflog
git reflog
# You should see HEAD@{1} pointing to "commit 5"
# 4. Recover
git reset --hard HEAD@{1}
git log --oneline
# All 5 commits are back!Checkpoint: All 5 commits should be visible in git log --oneline.
Part 2: Recover a Deleted Branch
Goal: Delete a branch, then recover it.
# 1. Create a branch with unique work
git checkout -b feature/important
echo "important work" > important.txt
git add . && git commit -m "feat: critical feature"
echo "more important work" >> important.txt
git add . && git commit -m "feat: extend critical feature"
# 2. Go back to main and delete the branch
git checkout main
git branch -D feature/important
# Note the hash Git reports!
# 3. Try to check it out (fails)
git checkout feature/important
# error: pathspec 'feature/important' did not match any file(s)
# 4. Find it in the reflog
git reflog | grep "critical feature"
# You'll see the commit hash
# 5. Recreate the branch
git branch feature/important HEAD@{2} # adjust the reflog index
git checkout feature/important
git log --oneline
# Both commits should be there plus the original 5Checkpoint: The feature/important branch exists with both "critical feature" commits.
Part 3: Escape Detached HEAD with Commits
Goal: Make commits in detached HEAD state, then save them.
# 1. Go to detached HEAD
git checkout main
git log --oneline -3 # note the hash of "commit 3"
git checkout HEAD~2 # detach HEAD at commit 3
# 2. Git warns you about detached HEAD — make commits anyway
echo "detached work 1" > detached.txt
git add . && git commit -m "work in detached HEAD 1"
echo "detached work 2" >> detached.txt
git add . && git commit -m "work in detached HEAD 2"
# 3. Now save this work to a branch BEFORE switching
git checkout -b saved-detached-work
git log --oneline
# Shows commit 1-3 + your two detached commits
# 4. Go back to main
git checkout main
git log --oneline
# main still has commits 1-5 (unaffected)
# 5. Merge or cherry-pick the detached work if needed
git merge saved-detached-workCheckpoint: The saved-detached-work branch contains the two commits made in detached HEAD.
Part 4: Revert a Merge Commit
Goal: Merge a bad branch, then undo it with revert.
# 1. Create a "broken" feature branch
git checkout main
git checkout -b feature/broken
echo "BROKEN CODE" > broken.txt
git add . && git commit -m "feat: add broken feature"
# 2. Merge it into main
git checkout main
git merge --no-ff feature/broken -m "Merge feature/broken into main"
# 3. Realize it's broken — revert the merge
git log --oneline -3 # Note the merge commit hash
git revert -m 1 HEAD # HEAD is the merge commit
# 4. Verify
cat broken.txt 2>/dev/null || echo "broken.txt is gone (reverted successfully)"
git log --oneline -3
# Shows: revert commit, merge commit, commit 5Checkpoint: broken.txt no longer exists in the working directory. The merge commit is still in history, but its effects are undone.
Part 5: git clean — Safe Untracked File Removal
Goal: Create untracked files and remove them safely.
# 1. Create various untracked files and directories
mkdir -p build/output
echo "compiled" > build/output/app.min.js
echo "temporary" > temp.log
echo "notes" > scratch.txt
# Also create an ignored file
echo "build/" > .gitignore
echo "*.log" >> .gitignore
git add .gitignore && git commit -m "chore: add gitignore"
# 2. See the current state
git status
# Untracked: scratch.txt (temp.log and build/ are ignored)
# 3. Dry run — see what clean would delete
git clean -n
# Would remove scratch.txt
git clean -dn
# Would remove scratch.txt (directories shown with -d)
git clean -dnx
# Would remove build/ scratch.txt temp.log (ignored files too with -x)
git clean -dnX
# Would remove build/ temp.log (ONLY ignored files with -X)
# 4. Remove only untracked (non-ignored) files
git clean -f
ls scratch.txt 2>/dev/null || echo "scratch.txt removed"
ls temp.log # still exists (ignored)
ls build/output/ # still exists (ignored)
# 5. Remove ignored files
git clean -fdX
ls temp.log 2>/dev/null || echo "temp.log removed"
ls build/ 2>/dev/null || echo "build/ removed"Checkpoint: After step 4, only scratch.txt is removed. After step 5, ignored files (temp.log, build/) are also removed.
Part 6: git worktree — Parallel Branch Work
Goal: Work on two branches simultaneously without stashing.
# 1. Make some uncommitted changes on main (simulating in-progress work)
git checkout main
echo "work in progress" >> file.txt
git status
# modified: file.txt (not committed)
# 2. Urgent bug! Create a worktree for the fix
git worktree add ../recovery-lab-hotfix main~1 -b hotfix/urgent
# 3. List worktrees
git worktree list
# Shows both the main worktree and the hotfix worktree
# 4. Fix the bug in the hotfix worktree
cd ../recovery-lab-hotfix
echo "bug fixed" > bugfix.txt
git add . && git commit -m "fix: urgent production bug"
# 5. Go back to main worktree — your changes are still there!
cd ../recovery-lab
git status
# modified: file.txt (still has your in-progress work!)
# 6. Clean up the worktree
git worktree remove ../recovery-lab-hotfix
git worktree list
# Only the main worktree remainsCheckpoint: Your uncommitted changes in file.txt survived the entire hotfix workflow — no stashing, no committing half-done work.
Part 7: Resolve Diverged Branches
Goal: Simulate and resolve branch divergence.
# 1. Set up a "remote" bare repo
git init --bare /tmp/diverge-test.git
git remote add test-remote /tmp/diverge-test.git
git push test-remote main
# 2. Create divergence — push from a parallel clone
git clone /tmp/diverge-test.git /tmp/diverge-other
cd /tmp/diverge-other
echo "remote change 1" >> file.txt
git add . && git commit -m "remote: change 1"
echo "remote change 2" >> file.txt
git add . && git commit -m "remote: change 2"
git push origin main
# 3. Make local commits in our original repo (without fetching)
cd ~/recovery-lab
echo "local change 1" > local.txt
git add . && git commit -m "local: change 1"
echo "local change 2" > local2.txt
git add . && git commit -m "local: change 2"
# 4. Try to push (will fail — diverged!)
git push test-remote main
# ! [rejected] main -> main (fetch first)
# 5. Fetch and see the divergence
git fetch test-remote
git status
# Your branch and 'test-remote/main' have diverged
# 6. Diagnose
git log --oneline --left-right HEAD...test-remote/main
# < local: change 1
# < local: change 2
# > remote: change 1
# > remote: change 2
# 7. Resolve with rebase (linear history)
git rebase test-remote/main
# Resolve any conflicts...
git push test-remote mainCheckpoint: After the rebase, git log --oneline shows both local and remote changes in a linear history.
Challenge: The Catastrophic Recovery
Perform all of these operations in sequence on a single repository, then recover to a clean state:
- Create 5 commits on
main - Create a
featurebranch with 3 commits - Merge
featureintomain - Reset
mainback to before the merge (git reset --hard HEAD~1) - Delete the
featurebranch (git branch -D feature) - Challenge: Using only the reflog and
git branch, restore both thefeaturebranch and the merge commit, then revert the merge cleanly
Common Pitfalls
| Pitfall | Why It Happens | How to Avoid It |
|---|---|---|
Using git reset --hard on a shared branch | Panic response to a bad commit | Use git revert for shared branches |
Reverting a merge without -m | Not understanding merge parents | Always specify -m 1 (keep the branch you merged into) |
| Re-merging after revert without reverting the revert | Git sees original commits as already merged | Revert the revert first, or rebase the feature branch |
| Making commits in detached HEAD then switching away | Didn't realize HEAD was detached | Use git switch (safer), create a branch before switching |
Using git clean -fdx without dry run | Destroying build caches, local config | Always git clean -n first, or use -i for interactive |
Confusing git clean flags (-X vs -x) | Capital X = only ignored; lowercase x = ignored + untracked | Remember: capital X is "eXclude untracked" (only clean ignored) |
| Forgetting worktree exists, creating conflicts | Worktree has the branch checked out | git worktree list to see all active worktrees |
Using git checkout instead of git switch | Old habit | git switch refuses detached HEAD without --detach flag |
| Not rotating credentials after purging secrets | Treating the purge as sufficient cleanup | Always rotate keys/passwords — caches may retain the old data |
| Relying on reflog that expired | Default is 90 days for reachable refs | Extend with gc.reflogExpire for critical repos |
Pro Tips
-
Always dry-run
git clean. Rungit clean -n(or-dn/-dnx) before the actual clean. One wrong flag can permanently delete files you need. Makegit clean -na muscle memory prerequisite. -
Set up a reflog safety margin. Extend reflog expiration to 6 months on repositories where history matters:
git config --global gc.reflogExpire 180.days. The storage cost is negligible compared to the recovery value. -
Use
git worktreeinstead ofgit stashfor branch switching. Worktrees are more explicit — your in-progress work stays in its directory, visible and editable. Stashes are invisible, stackable, and easy to forget or apply to the wrong branch. -
After any dangerous operation, check
ORIG_HEAD. Git sets it automatically before merges, rebases, and resets. A quickgit reset --hard ORIG_HEADcan undo a bad operation within seconds. -
Learn
git refloglike a firefighter learns their equipment. You won't need it often, but when you do, speed matters. Practice recovery scenarios regularly so you're calm under pressure when real data is at stake. -
Bookmark the Git disaster recovery cheat sheet: When you're in a real emergency, calmly identify the situation, consult the reflog, and remember: if a commit existed, it's almost certainly still in the object database.
Quiz / Self-Assessment
1. How do you revert a merge commit, and what does -m 1 mean?
Show Answer
Use git revert -m 1 <merge-commit-hash>. The -m 1 flag specifies which parent to revert to. Merge commits have two parents:
- Parent 1: the branch you merged into (usually
main) - Parent 2: the branch you merged from (the feature branch)
-m 1 keeps parent 1's state and undoes the changes from parent 2. This is almost always the correct choice — it undoes the feature while preserving main's existing state.
2. After reverting a merge, why can't you simply re-merge the same feature branch?
Show Answer
Git considers the original commits already part of main's history (they were merged, then the merge was reverted). Re-merging the same branch results in no changes because Git thinks those commits were already integrated.
To re-merge, you must either:
- Revert the revert — this re-introduces the original changes, then merge the fixes
- Rebase the feature branch — creating new commit hashes that Git treats as new work
3. You ran git reset --hard HEAD~5 on main and immediately realize you went too far. How do you recover?
Show Answer
Two immediate options:
git reset --hard ORIG_HEAD— Git automatically saved the pre-reset position inORIG_HEADgit reflog— find the entry showing where HEAD was before the reset, thengit reset --hard HEAD@{1}(or the appropriate reflog reference)
Both approaches recover all 5 lost commits instantly. The commits were never actually deleted — only the branch pointer moved.
4. What's the difference between git clean -fdX and git clean -fdx?
Show Answer
git clean -fdX(capital X): Removes only ignored files and directories (build artifacts, caches, etc.). Keeps untracked files that aren't in.gitignore.git clean -fdx(lowercase x): Removes both untracked and ignored files and directories — the most aggressive clean option.
A helpful mnemonic: capital X "eXcludes untracked" (only cleans ignored files).
5. You made 3 commits in detached HEAD state and then ran git checkout main without saving them. How do you recover?
Show Answer
Use the reflog to find the last detached commit:
git reflog
# Look for "commit: ..." entries from your detached HEAD session
# Find the hash of the last commit you made
git branch recovered-work <hash>The commits still exist in Git's object database — they're just not referenced by any branch. The reflog retains references for 30 days (by default for unreachable commits), giving you time to recover.
6. When should you use git revert vs. git reset + force push to undo a pushed commit?
Show Answer
Use git revert when:
- The branch is shared (others may have pulled)
- You want to preserve a complete audit trail
- You're working on
mainordevelop
Use git reset + force push when:
- You're the only person working on the branch (personal feature branch)
- You've coordinated with everyone who might be affected
- You want to completely remove the commit from history (e.g., accidentally committed large file or secret — though secrets require additional cleanup)
7. What advantage does git worktree have over git stash for switching between tasks?
Show Answer
git worktree creates separate working directories, so:
- Your in-progress work stays exactly where it is — no need to stash/unstash
- You can have both branches open simultaneously in your editor
- No risk of applying a stash to the wrong branch
- No risk of forgetting stashed changes or dealing with stash conflicts
- You can run tests, builds, or servers from both branches simultaneously
The trade-off is disk space (separate working directory), but worktrees share the .git object database, so the overhead is minimal.
8. You accidentally dropped a stash with git stash drop. Can you recover it?
Show Answer
Yes. Stash entries are stored as commits in Git's object database. To find them:
git fsck --unreachable | grep commitInspect each unreachable commit with git show <hash> to find your stash. When you find it, apply it with:
git stash apply <hash>This works as long as garbage collection hasn't run (git gc) and the stash hasn't been purged. Act quickly.
9. What is ORIG_HEAD and which Git operations set it?
Show Answer
ORIG_HEAD is a special reference that Git sets to the previous value of HEAD before operations that significantly move it. Operations that set ORIG_HEAD include:
git merge(before the merge commit is created)git rebase(before the rebase starts)git reset(before HEAD moves)git pull(since it internally merges or rebases)
You can always run git reset --hard ORIG_HEAD immediately after any of these operations to undo them. It's like a one-level undo for HEAD-moving operations.
10. You ran git clean -fdx without doing a dry run first and accidentally deleted important untracked configuration files. Can Git help you recover them?
Show Answer
No. git clean permanently deletes files from the filesystem. Since these files were untracked (never committed to Git), they are not in Git's object database, reflog, or anywhere else Git manages. There is no Git-based recovery.
Your only options are:
- Operating system backups (Time Machine, etc.)
- File recovery tools (unlikely to help with SSDs)
- Recreating the files from memory or documentation
This is why git clean -n (dry run) should always precede git clean -f. Consider using git clean -i (interactive mode) for an extra safety layer.