Learning Objectives

By the end of this module, you will be able to:

  • Recover from a bad merge using git revert -m and git reset
  • Choose between git revert and git 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 worktree to 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 a1b2c3d

The -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 1 means "keep parent 1's state" (keep main, undo the feature) — this is almost always what you want
  • -m 2 means "keep parent 2's state" (keep the feature, undo main) — rarely used
git revert -m 1 a1b2c3d
git push origin main    # Safe — no force push needed

The 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/broken

Or 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/broken

Option 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 main

When to use which:

SituationUse revertUse reset + force push
Shared branch (main)YesOnly with team coordination
Others may have pulledYesNo
Need clean historyNo (leaves revert commit)Yes
Feature branch with 1 developerEitherYes (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 main

The --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 main

Option 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:

  1. Force push all branches
  2. Ask all collaborators to re-clone (not just git pull)
  3. Rotate the exposed credentials immediately — they're in Git's reflog and possibly in GitHub's caches
  4. 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 main

Each 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 def5678

Recovery 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 abc1234

Recovery 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 abc1234

Recovery 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=iso

4. 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~3

Git 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 main

If 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 abc1234

If you made commits and don't want them:

# Just switch to any branch — the detached commits will eventually be garbage collected
git checkout main

Preventing 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 ── H

Strategy 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 main

Strategy 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/main

Diagnosing 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 caching

6. 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 -fdX

The Flag Matrix

FlagsUntracked FilesUntracked DirsIgnored Files
-fRemovedKeptKept
-fdRemovedRemovedKept
-fdXKeptKeptRemoved
-fdxRemovedRemovedRemoved

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:

  1. git stash your work, switch branches, fix the bug, switch back, git stash pop — messy and error-prone
  2. Clone the repo again — slow, wastes disk space, different remotes/config
  3. 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-hotfix

Rules and Constraints

  • Each branch can only be checked out in one worktree. You can't have main checked out in two worktrees simultaneously.
  • All worktrees share the same .git database. 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-hotfix

Worktree 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-42

8. 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 main

ORIG_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

CommandDescription
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_HEADUndo the last dangerous operation
git reflogShow history of HEAD movements
git reflog show <branch>Show reflog for a specific branch
git fsck --lost-foundFind unreachable/dangling objects
git clean -nDry run — show untracked files that would be removed
git clean -fRemove untracked files
git clean -fdRemove untracked files and directories
git clean -fdxRemove untracked files, directories, and ignored files
git clean -fdXRemove only ignored files and directories
git clean -iInteractive 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 listList all worktrees
git worktree remove <path>Remove a linked worktree
git worktree pruneClean 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 commits

Part 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 5

Checkpoint: 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-work

Checkpoint: 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 5

Checkpoint: 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 remains

Checkpoint: 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 main

Checkpoint: 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:

  1. Create 5 commits on main
  2. Create a feature branch with 3 commits
  3. Merge feature into main
  4. Reset main back to before the merge (git reset --hard HEAD~1)
  5. Delete the feature branch (git branch -D feature)
  6. Challenge: Using only the reflog and git branch, restore both the feature branch and the merge commit, then revert the merge cleanly

Common Pitfalls

PitfallWhy It HappensHow to Avoid It
Using git reset --hard on a shared branchPanic response to a bad commitUse git revert for shared branches
Reverting a merge without -mNot understanding merge parentsAlways specify -m 1 (keep the branch you merged into)
Re-merging after revert without reverting the revertGit sees original commits as already mergedRevert the revert first, or rebase the feature branch
Making commits in detached HEAD then switching awayDidn't realize HEAD was detachedUse git switch (safer), create a branch before switching
Using git clean -fdx without dry runDestroying build caches, local configAlways git clean -n first, or use -i for interactive
Confusing git clean flags (-X vs -x)Capital X = only ignored; lowercase x = ignored + untrackedRemember: capital X is "eXclude untracked" (only clean ignored)
Forgetting worktree exists, creating conflictsWorktree has the branch checked outgit worktree list to see all active worktrees
Using git checkout instead of git switchOld habitgit switch refuses detached HEAD without --detach flag
Not rotating credentials after purging secretsTreating the purge as sufficient cleanupAlways rotate keys/passwords — caches may retain the old data
Relying on reflog that expiredDefault is 90 days for reachable refsExtend with gc.reflogExpire for critical repos

Pro Tips

  1. Always dry-run git clean. Run git clean -n (or -dn / -dnx) before the actual clean. One wrong flag can permanently delete files you need. Make git clean -n a muscle memory prerequisite.

  2. 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.

  3. Use git worktree instead of git stash for 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.

  4. After any dangerous operation, check ORIG_HEAD. Git sets it automatically before merges, rebases, and resets. A quick git reset --hard ORIG_HEAD can undo a bad operation within seconds.

  5. Learn git reflog like 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.

  6. 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:

  1. Revert the revert — this re-introduces the original changes, then merge the fixes
  2. 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:

  1. git reset --hard ORIG_HEAD — Git automatically saved the pre-reset position in ORIG_HEAD
  2. git reflog — find the entry showing where HEAD was before the reset, then git 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 main or develop

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 commit

Inspect 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.