Learning Objectives

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

  1. Explain the three modes of git reset (--soft, --mixed, --hard) and how each affects HEAD, the index, and the working directory
  2. Use git reset to unstage files, undo local commits, and collapse commit history
  3. Distinguish between git reset (rewrites history) and git revert (creates new undo commits)
  4. Revert regular commits and merge commits on shared branches
  5. Recover "lost" commits after a reset using git reflog

1. Understanding the Three Areas

Before diving into reset, you need a crystal-clear picture of Git's three areas. Every reset mode affects a different combination of them.

┌──────────────────┐    git add    ┌──────────────────┐   git commit   ┌──────────────────┐
│ Working Directory │ ──────────→  │  Staging Area    │  ──────────→   │   Repository     │
│  (your files)     │              │  (index)         │                │   (HEAD)         │
└──────────────────┘              └──────────────────┘                └──────────────────┘
  • Working Directory — The actual files on disk that you edit
  • Staging Area (Index) — A snapshot of what will go into the next commit; updated by git add
  • Repository (HEAD) — The commit history; updated by git commit

When all three match, git status says "nothing to commit, working tree clean."


2. git reset — The Three Modes

git reset moves the current branch pointer (and HEAD) to a different commit. The three modes control what else gets modified along the way.

The Mental Model

Think of git reset <target> as moving backwards through three checkpoints. Each mode stops at a different point:

git reset --soft <target>
  ✓ Moves HEAD/branch pointer    ← stops here
  ✗ Index unchanged
  ✗ Working directory unchanged

git reset --mixed <target>     (this is the default)
  ✓ Moves HEAD/branch pointer
  ✓ Resets index to match target  ← stops here
  ✗ Working directory unchanged

git reset --hard <target>
  ✓ Moves HEAD/branch pointer
  ✓ Resets index to match target
  ✓ Resets working directory to match target  ← DESTRUCTIVE

--soft: Undo the Commit, Keep Everything Staged

git reset --soft HEAD~1

This moves HEAD back one commit. The changes from the undone commit remain staged (in the index). Your working directory is untouched.

Before:
  A ← B ← C    ← HEAD (main)
  Index: matches C
  Working dir: matches C

After git reset --soft HEAD~1:
  A ← B    ← HEAD (main)
       C   (orphaned, but changes still in index)
  Index: still matches C (changes are staged)
  Working dir: still matches C

Use cases:

  • "I want to redo my last commit with a different message"
  • "I want to combine the last few commits into one" (squash)
  • "I committed too early — I want to add more changes to this commit"
# Redo the last commit with a different message:
git reset --soft HEAD~1
git commit -m "Better message"
 
# Squash the last 3 commits into one:
git reset --soft HEAD~3
git commit -m "Combined feature"

--mixed (Default): Undo the Commit, Unstage the Changes

git reset HEAD~1           # --mixed is the default
git reset --mixed HEAD~1   # Same thing, explicit

This moves HEAD back one commit and resets the index. The changes become unstaged but remain in your working directory.

Before:
  A ← B ← C    ← HEAD (main)
  Index: matches C
  Working dir: matches C

After git reset HEAD~1:
  A ← B    ← HEAD (main)
  Index: matches B (reset)
  Working dir: still matches C (changes are unstaged)

Use cases:

  • "I want to re-stage my changes differently" (split into multiple commits)
  • "I want to unstage files I accidentally added"
  • Collapsing WIP commits: reset to main, then selectively stage and commit
# Unstage a file you accidentally added:
git reset HEAD path/to/file
 
# Collapse all commits on a branch into unstaged changes:
git reset main
# Now selectively stage and commit in logical groups

--hard: Undo Everything (DESTRUCTIVE)

git reset --hard HEAD~1

This moves HEAD back, resets the index, and resets the working directory. All changes from the undone commit are gone. Any uncommitted modifications are also gone.

Before:
  A ← B ← C    ← HEAD (main)
  Index: matches C
  Working dir: matches C (+ uncommitted edits)

After git reset --hard HEAD~1:
  A ← B    ← HEAD (main)
  Index: matches B
  Working dir: matches B (everything from C and your edits: GONE)

Use cases:

  • "I want to completely discard my recent work"
  • "I want to match the remote exactly": git reset --hard origin/main
  • Restoring to a known state during experimentation

Warning: --hard is the only reset mode that can cause data loss. Committed changes can be recovered via git reflog, but uncommitted changes (unstaged edits, untracked files) are gone forever.

Summary Table

ModeHEADIndexWorking DirData Loss?
--softMovesUnchangedUnchangedNo
--mixed (default)MovesResetUnchangedNo
--hardMovesResetResetYes (uncommitted changes lost)

3. Common git reset Patterns

Unstaging Files

# Unstage a specific file (keep changes in working directory):
git reset HEAD file.txt
 
# Modern alternative (Git 2.23+):
git restore --staged file.txt

This is --mixed (the default) applied to a single file. HEAD doesn't move — only the index is updated for that file.

Undoing the Last Commit

# Keep changes staged (ready to re-commit):
git reset --soft HEAD~1
 
# Keep changes unstaged (ready to re-stage differently):
git reset HEAD~1
 
# Discard changes completely:
git reset --hard HEAD~1

Squashing Commits with Reset

Instead of interactive rebase, you can use reset to squash:

# You have: A ← B ← C ← D ← E  (5 commits on feature branch)
# You want: A ← F               (1 clean commit)
 
git reset --soft HEAD~4       # Undo last 4 commits, keep changes staged
git commit -m "Add complete feature"
 
# Or, for more control over staging:
git reset HEAD~4              # Undo last 4 commits, unstage changes
git add model.py
git commit -m "Add data model"
git add controller.py
git commit -m "Add controller"

Resetting to Match the Remote

# Discard all local changes and match origin/main exactly:
git fetch origin
git reset --hard origin/main

This is the "nuclear option." It's what your colleague does when you force-push a rebased branch they had checked out.

Using git reset Without a Target

git reset           # Unstage everything (same as git reset HEAD)
git reset --hard    # Discard all uncommitted changes (same as git reset --hard HEAD)

When no target is specified, reset defaults to HEAD — the current commit. This doesn't move the branch pointer but still resets the index (and working directory for --hard).


4. git commit --amend

Amending is the most common "undo" operation. It replaces the last commit with a new one that includes your additional changes.

# Fix a typo in the last commit message:
git commit --amend -m "Corrected message"
 
# Add a forgotten file to the last commit:
git add forgotten-file.txt
git commit --amend --no-edit    # Keep the same message

What Amend Does Behind the Scenes

git commit --amend is equivalent to:

git reset --soft HEAD~1         # Undo last commit, keep changes staged
git commit -c ORIG_HEAD         # Recommit with the original message

The original commit becomes orphaned (new SHA, because the content changed). If you had already pushed, you'll need git push --force-with-lease.

Before amend:
  A ← B ← C    ← HEAD

After amend (added a file):
  A ← B ← C'   ← HEAD  (new SHA, includes the forgotten file)
       ↖
        C       (orphaned, recoverable via reflog)

5. git revert — Safe Undo for Shared History

The Problem with Reset on Shared Branches

git reset rewrites history — it removes commits. If you've already pushed those commits and others have pulled them, resetting causes the same chaos as rebasing shared branches (duplicate commits, broken trust).

The Solution: Revert

git revert creates a new commit that undoes the changes from a previous commit. History is preserved — nothing is rewritten.

git revert <commit-sha>
Before:
  A ← B ← C ← D    ← main

git revert C:
  A ← B ← C ← D ← R    ← main
                    ↑
              "Revert C"
              (R undoes the changes introduced by C)

Commit R has the exact opposite diff of commit C. If C added a line, R removes it. If C deleted a file, R recreates it. The important thing: commits B, C, and D remain in history. Nothing is rewritten.

Basic Usage

# Revert a specific commit:
git revert abc1234
 
# Revert the most recent commit:
git revert HEAD
 
# Revert without immediately committing (stage the undo):
git revert --no-commit abc1234
# Useful for reverting multiple commits into a single revert commit

Reverting a Range of Commits

# Revert commits C, D, and E (newest to oldest order):
git revert HEAD~2..HEAD
 
# Or revert multiple into one commit:
git revert --no-commit HEAD~2..HEAD
git commit -m "Revert features X, Y, and Z"

When reverting a range, Git processes them in reverse chronological order (newest first), which minimizes conflicts.


6. Reverting Merge Commits

Merge commits are special — they have two parents. When you revert a merge, Git needs to know which parent's changes to keep. You specify this with the -m flag.

          D ← E
         /      \
A ← B ← C ←──── M    ← main (M is the merge commit)
                  ↑
            parent 1 = C (the mainline)
            parent 2 = E (the merged branch)
# Revert the merge, keeping mainline (parent 1):
git revert -m 1 <merge-sha>

-m 1 means "undo everything that came from the second parent (the merged branch), keeping the first parent's state." This is almost always what you want — it undoes the feature that was merged in.

After git revert -m 1 M:
  A ← B ← C ←──── M ← R    ← main
         ↖       ↗
          D ← E

  R undoes the changes from D and E

Warning: After reverting a merge, you cannot simply re-merge the same branch later. Git thinks those changes were already applied and reverted intentionally. To re-introduce the feature, you must revert the revert: git revert <revert-sha>.


7. ORIG_HEAD — Your Safety Net

Every time you run a destructive operation (reset, merge, rebase), Git saves the previous HEAD position in a special reference called ORIG_HEAD.

# Oops, that reset was wrong:
git reset --hard ORIG_HEAD
 
# Check where ORIG_HEAD points:
git rev-parse ORIG_HEAD

ORIG_HEAD only stores the last one — it's overwritten by every operation that sets it. For deeper recovery, use git reflog.


8. Recovery with git reflog

git reflog records every time HEAD moves. It's your safety net for recovering from mistakes.

git reflog
abc1234 HEAD@{0}: reset: moving to HEAD~3
def5678 HEAD@{1}: commit: Add tests
ghi9012 HEAD@{2}: commit: Add controller
jkl3456 HEAD@{3}: commit: Add model
mno7890 HEAD@{4}: commit: Initial commit

To recover:

# Go back to before the reset:
git reset --hard def5678
 
# Or use the reflog syntax:
git reset --hard HEAD@{1}

Reflog entries are kept for 90 days by default. After that, they're pruned by garbage collection. Don't wait too long to recover.

The Recovery Flowchart

Lost commits after reset?
├─ Was it --soft or --mixed?
│   └─ Changes are still in working dir/index — just recommit
├─ Was it --hard?
│   ├─ Was the change committed before the reset?
│   │   └─ git reflog → find SHA → git reset --hard <sha>
│   └─ Was the change uncommitted?
│       └─ GONE. No recovery possible.
└─ Not sure?
    └─ git reflog → examine the entries → decide

9. Reset vs Revert — When to Use Which

ScenarioUseWhy
Undo local commits not yet pushedgit resetSafe — no one else has the commits
Undo a pushed commit on a shared branchgit revertPreserves history, no force push needed
Unstage accidentally staged filesgit reset HEAD <file>Only affects the index
Completely discard local changesgit reset --hardNuclear option — clears everything
Undo a merge that broke productiongit revert -m 1 <merge>Safe for shared branches
Squash local WIP commitsgit reset --soft HEAD~NKeeps changes staged for clean recommit
Match remote exactly after mishapgit reset --hard origin/mainColleague's recovery after your force push

The rule of thumb: If the commits are only local (never pushed), use reset. If others have the commits, use revert.


Command Reference

CommandDescription
git reset --soft <target>Move HEAD; keep index and working dir unchanged
git reset <target>Move HEAD; reset index; keep working dir (--mixed default)
git reset --hard <target>Move HEAD; reset index and working dir (destructive)
git reset HEAD <file>Unstage a file (index only, HEAD doesn't move)
git resetUnstage everything (same as git reset HEAD)
git commit --amendReplace last commit (opens editor for message)
git commit --amend --no-editReplace last commit, keep same message
git revert <sha>Create a new commit that undoes <sha>
git revert --no-commit <sha>Apply the undo without auto-committing
git revert -m 1 <merge-sha>Revert a merge commit (keep parent 1)
git reflogShow where HEAD has been
git reset --hard ORIG_HEADUndo the last reset/merge/rebase

Hands-On Lab: Reset, Revert, and Recover

Setup

mkdir reset-revert-lab && cd reset-revert-lab
git init
 
echo "line 1: foundation" > app.txt
git add app.txt
git commit -m "Initial commit"
 
echo "line 2: feature A" >> app.txt
git add app.txt
git commit -m "Add feature A"
 
echo "line 3: feature B" >> app.txt
git add app.txt
git commit -m "Add feature B"
 
echo "line 4: feature C" >> app.txt
git add app.txt
git commit -m "Add feature C"
git log --oneline

Checkpoint: You should see 4 commits. Note the SHAs.

Part A: --soft Reset

Step 1 — Undo the last commit, keeping changes staged

git reset --soft HEAD~1
git status

Checkpoint: git status shows "line 4: feature C" as a staged change. The commit is gone from git log, but the change is ready to recommit.

Step 2 — Recommit with a different message

git commit -m "Add feature C (improved message)"
git log --oneline

Part B: --mixed Reset (Default)

Step 3 — Undo the last 2 commits, unstaging changes

git reset HEAD~2
git status

Checkpoint: git status shows app.txt as modified (unstaged). The file contains lines 1-4, but only the first 2 commits remain in git log.

Step 4 — Restage and recommit as a single commit

git add app.txt
git commit -m "Add features B and C together"
git log --oneline

Checkpoint: 3 commits. The last one contains both feature B and C changes.

Part C: --hard Reset

Step 5 — Add an uncommitted change, then hard reset

echo "line 5: experimental" >> app.txt
git status

Verify the file has 5 lines.

git reset --hard HEAD~1
cat app.txt

Checkpoint: The file has only 2 lines (back to "Add feature A"). The uncommitted "experimental" line AND the committed B+C changes are gone.

Step 6 — Recover the lost commit using reflog

git reflog

Find the SHA for "Add features B and C together."

git reset --hard <sha>
cat app.txt

Checkpoint: The file is back to 4 lines. Reflog saved you.

Part D: git revert

Step 7 — Revert a specific commit

git log --oneline

Find the SHA for "Add feature A" (the second commit). Revert it:

git revert <sha-of-feature-A>

Git opens your editor for the revert message. Accept the default.

cat app.txt
git log --oneline

Checkpoint: The file no longer contains "line 2: feature A" but still has lines 3 and 4. A new "Revert" commit appears at the top of the log. The original "Add feature A" commit is still in history.

Part E: Reverting a Merge Commit

Step 8 — Create a merge to revert

# Create a branch with changes
git switch -c feature-x
echo "line 5: feature X" >> app.txt
git add app.txt
git commit -m "Add feature X"
 
# Merge it into main
git switch main
git merge --no-ff feature-x -m "Merge feature-x"
git log --oneline --graph

Checkpoint: You see the merge commit with two parents in the graph.

Step 9 — Revert the merge

git revert -m 1 HEAD
cat app.txt
git log --oneline --graph

Checkpoint: "line 5: feature X" is gone. A "Revert" commit sits on top. The merge commit is still in history. The graph shows the full story.

Part F: Unstaging Files

Step 10 — Practice unstaging

echo "secret=abc123" > .env
echo "config=true" > config.txt
git add .env config.txt
git status

Both files are staged. Unstage the sensitive one:

git reset HEAD .env
git status

Checkpoint: config.txt is still staged, .env is unstaged (shown in red).

# Clean up - don't commit .env
rm .env
git commit -m "Add config"

Cleanup

cd ..
rm -rf reset-revert-lab

Challenge

Create a repository with 5 commits. Push it to a remote. Then:

  1. Use git reset --soft HEAD~2 locally, recommit, and force push
  2. Practice recovering the original state from reflog
  3. Use git revert to undo a commit in the middle of the history without rewriting any history
  4. Create and merge a feature branch, then revert the merge with -m 1

Common Pitfalls & Troubleshooting

PitfallSymptomFix
Using --hard when you meant --mixedUncommitted changes lostgit reflog for committed changes; uncommitted changes are unrecoverable
Resetting a shared branchColleagues get duplicate commits after pullUse git revert for shared branches instead
Forgetting --soft preserves stagingExpected unstaged changes, got stagedUse git reset HEAD (mixed) to unstage
Reverting a merge without -mGit error: "commit is a merge but no -m option"Add -m 1 to specify the mainline parent
Re-merging a reverted branchGit says "Already up to date"Revert the revert first: git revert <revert-sha>
Using ORIG_HEAD after multiple operationsPoints to wrong commitUse git reflog for precise navigation
Confusion between git reset HEAD <file> and git reset HEADOne unstages a file, the other unstages everythingWith a file path: only that file. Without: all staged changes
Reset to wrong commitBranch history looks wronggit reflog → find correct SHA → git reset --hard <sha>

Pro Tips

  1. Set up a git undo alias for the most common reset:

    git config --global alias.undo "reset --soft HEAD~1"

    Now git undo removes the last commit but keeps all changes staged.

  2. Use ORIG_HEAD immediately after a mistake:

    git reset --hard ORIG_HEAD    # Undo the last reset/merge/rebase
  3. Prefer git restore --staged over git reset HEAD for unstaging (Git 2.23+). It's clearer in intent and doesn't have the footgun of accidentally using --hard.

  4. git revert --no-commit for batch reverts — Revert multiple commits into a single revert commit:

    git revert --no-commit HEAD~3..HEAD
    git commit -m "Revert last 3 commits"
  5. The git reset and git checkout distinction:

    • git reset moves the branch pointer (and HEAD follows)
    • git checkout moves HEAD only (branch stays, you get detached HEAD) Both can move backwards, but they move different things.
  6. Amend often, push oncegit commit --amend --no-edit is safe to use freely on local commits. Only push when you're satisfied with the commit.


Quiz / Self-Assessment

Q1: Explain the difference between git reset --soft HEAD~1, git reset HEAD~1, and git reset --hard HEAD~1.

Answer

All three move HEAD and the branch pointer back one commit. The difference is what else they reset:

  • --soft: Only moves HEAD. Index and working directory are unchanged. Changes from the undone commit remain staged.
  • --mixed (default, no flag): Moves HEAD and resets the index. Changes from the undone commit are unstaged but remain in the working directory.
  • --hard: Moves HEAD, resets index, and resets working directory. Changes from the undone commit are gone. Any uncommitted changes are also lost.

Q2: You accidentally committed a file with secrets (.env). It's only local (not pushed). What's the fastest way to undo this?

Answer
git reset HEAD~1              # Undo the commit, keep changes unstaged
# Remove .env from staging and add to .gitignore
echo ".env" >> .gitignore
git add .gitignore
git add <other-files>         # Re-add everything except .env
git commit -m "Add feature (without secrets)"

Or if .env was the only change: git reset --hard HEAD~1 and add .env to .gitignore.

Q3: When should you use git revert instead of git reset?

Answer

Use git revert when the commits have been pushed to a shared branch that others have pulled from. git revert creates a new commit that undoes the changes, preserving history. git reset rewrites history, which causes problems for anyone who already has the old commits. Rule: local-only → reset; shared/pushed → revert.

Q4: Why does reverting a merge commit require the -m flag?

Answer

A merge commit has two parents. When you revert a merge, Git needs to know which parent represents the "mainline" — the branch you want to keep. -m 1 means "undo everything from the second parent (the merged branch)." -m 2 would undo the mainline changes instead. Without -m, Git doesn't know which side to undo and refuses with an error.

Q5: After git reset --hard HEAD~3, can you recover the lost commits?

Answer

Yes, if the changes were committed before the reset. Use git reflog to find the SHA of the commit you want to recover, then git reset --hard <sha>. Reflog entries are kept for 90 days by default. However, any uncommitted changes that were in the working directory at the time of the hard reset are lost permanently.

Q6: What does git reset HEAD file.txt do?

Answer

It unstages file.txt — removes it from the index (staging area) without modifying the working directory or moving HEAD. The file's changes remain in your working directory as unstaged modifications. This is --mixed applied to a single file path.

Q7: What is ORIG_HEAD and when is it useful?

Answer

ORIG_HEAD is a special reference that Git sets before operations that move HEAD drastically (reset, merge, rebase). It stores where HEAD was before the operation. It's useful for quick undo: git reset --hard ORIG_HEAD reverses the last reset/merge/rebase. It only stores one value — it's overwritten by each new operation.

Q8: You reverted a merge commit. Later, you want to re-introduce the same feature. What happens if you try to merge the same branch again?

Answer

Git says "Already up to date" and does nothing. From Git's perspective, those commits were already merged (and then reverted). The solution is to revert the revert: git revert <revert-commit-sha>. This re-introduces the original changes, and then you can merge new commits from the feature branch.

Q9: What is the equivalent of git commit --amend using git reset?

Answer
git reset --soft HEAD~1      # Undo commit, keep changes staged
git add <any-new-files>      # Stage additional changes
git commit -m "new message"  # Create a new commit

This is exactly what --amend does behind the scenes, except --amend also supports -c ORIG_HEAD to pre-fill the original message.

Q10: True or false: git reset --hard can lose uncommitted changes that are not recoverable by any Git command.

Answer

True. git reset --hard resets the working directory to match the target commit. Any changes that were not committed (and not stashed) are lost permanently. git reflog only tracks committed states. If you modified a file but never ran git add or git commit, those modifications cannot be recovered after a hard reset.