Undoing Changes in Git: reset, revert, checkout, and restore

Comprehensive guide to undoing changes in Git: git reset (soft/mixed/hard), git revert, git checkout, and git restore for safe version control operations.

published: reading time: 18 min read updated: March 31, 2026

Introduction

Every developer has been there: you committed to the wrong branch, staged the wrong files, or pushed a change that broke production. Git provides several commands for undoing changes, but they work in fundamentally different ways and choosing the wrong one can cause data loss or break your team’s workflow.

The key distinction is between rewriting history (changing commits that exist) and creating new history (adding commits that undo previous ones). Rewriting history is safe for local, unpushed commits but dangerous for shared branches. Creating new history is always safe but adds noise to the log. Understanding this distinction is the foundation of safe Git operations.

This guide covers git reset (with its three modes), git revert, git checkout, and the modern git restore command. You will learn when to use each, how they interact with the three-state model, and how to recover from mistakes. For the conceptual foundation, see The Three States.

When to Use / When Not to Use

Use git reset when:

  • Undoing local, unpushed commits
  • Unstaging files (--mixed or --soft)
  • Discarding local commits you no longer want
  • Squashing commits before pushing

Use git revert when:

  • Undoing commits that have been pushed to a shared branch
  • Creating an auditable undo trail for compliance
  • Reverting a specific commit in the middle of history

Use git restore when:

  • Discarding uncommitted changes in the working directory
  • Unstaging files (modern replacement for git reset HEAD)
  • Restoring files from a specific commit

Use git checkout when:

  • Switching branches (its primary purpose)
  • Restoring individual files from a specific commit (legacy syntax)

Core Concepts

The undo commands operate at different levels of the three-state model:


graph LR
    A[Working Directory] -->|git restore / git checkout| B[Discard changes]
    A -->|git add| C[Staging Area]
    C -->|git restore --staged| A
    C -->|git reset --mixed| A
    C -->|git reset --soft| D[Repository HEAD]
    D -->|git reset --mixed| C
    D -->|git reset --hard| A
    D -->|git revert| D

Three Modes of git reset


graph TD
    A[Commit C3<br/>HEAD points here] -->|git reset --soft HEAD~1| B[HEAD moves to C2<br/>Changes stay STAGED]
    A -->|git reset HEAD~1| C[HEAD moves to C2<br/>Changes stay UNSTAGED<br/>in working directory]
    A -->|git reset --hard HEAD~1| D[HEAD moves to C2<br/>Changes are DISCARDED<br/>working directory clean]

reset vs revert


graph LR
    A[C1] --> B[C2]
    B --> C[C3<br/>Bad commit]

    C -->|git reset --hard C2| D[C1 --> C2<br/>C3 is gone<br/>History rewritten]
    C -->|git revert C3| E[C1 --> C2 --> C3 --> C4<br/>C4 undoes C3<br/>History preserved]

Architecture or Flow Diagram

Decision Tree: Which Undo Command?


graph TD
    A[Need to undo something?] --> B{Is the commit pushed/shared?}
    B -->|Yes| C[Use git revert]
    B -->|No| D{What level?}

    D -->|Working directory| E[Use git restore file]
    D -->|Staging area| F[Use git restore --staged file]
    D -->|Last commit| G{Keep changes?}

    G -->|Yes, staged| H[git reset --soft HEAD~1]
    G -->|Yes, unstaged| I[git reset HEAD~1]
    G -->|No, discard| J[git reset --hard HEAD~1]

    D -->|Specific commit| K[git revert <commit>]
    D -->|Switch branches| L[git switch branch]

The Safety Spectrum


graph LR
    A[Safest] --> B[git revert]
    B --> C[git reset --soft]
    C --> D[git reset --mixed]
    D --> E[git restore --staged]
    E --> F[git restore file]
    F --> G[git reset --hard]
    G --> H[Most Dangerous]

Step-by-Step Guide / Deep Dive

git reset: Three Modes

Soft Reset: Undo Commit, Keep Changes Staged


# Scenario: You committed but want to add more files to the same commit

# Undo the last commit, keep all changes staged
git reset --soft HEAD~1

# Now you can add more files and re-commit
git add additional-file.py
git commit -m "feat: complete feature with all files"

# Result: One clean commit instead of two

The soft reset moves HEAD back one commit but leaves both the staging area and working directory untouched. Your changes are still staged, ready to be committed again.

Mixed Reset: Undo Commit, Keep Changes Unstaged


# Scenario: You committed but want to restage changes selectively

# Undo the last commit, keep changes in working directory (unstaged)
git reset HEAD~1
# Equivalent to: git reset --mixed HEAD~1

# Review what changed
git status
git diff

# Stage only the changes you want
git add -p src/app.py

# Commit selectively
git commit -m "feat: partial feature implementation"

The mixed reset (default mode) moves HEAD back and unstages all changes. Your working directory is untouched — you still have all your changes, just not staged.

Hard Reset: Undo Commit, Discard Everything


# Scenario: You want to completely discard the last commit and all changes

# WARNING: This permanently deletes uncommitted changes
git reset --hard HEAD~1

# Reset to a specific commit
git reset --hard abc1234

# Reset to match the remote branch (discard all local changes)
git reset --hard origin/main

The hard reset moves HEAD back and makes both the staging area and working directory match the target commit. All uncommitted changes are permanently lost.

git revert: Safe Undo for Shared History


# Revert the last commit
git revert HEAD

# Revert a specific commit
git revert abc1234

# Revert a range of commits
git revert abc1234..def5678

# Revert without opening the editor (auto-generate message)
git revert --no-edit abc1234

# Revert a merge commit (requires specifying the parent)
git revert -m 1 abc1234  # -m 1 means revert relative to first parent

git revert creates a new commit that is the inverse of the specified commit. If the original commit added lines, the revert removes them. If it deleted lines, the revert adds them back. The original commit remains in history.

git restore: Modern File-Level Undo

Introduced in Git 2.23, git restore separates the file-restoration functionality from git checkout:


# Discard changes in working directory (uncommitted changes)
git restore src/app.py

# Discard all changes in working directory
git restore .

# Unstage a file (move from staging area back to working directory)
git restore --staged src/app.py

# Restore a file from a specific commit
git restore --source=abc1234 src/app.py

# Restore and stage in one command
git restore --staged --worktree src/app.py

git checkout: Branch Switching and File Restoration


# Switch branches
git checkout feature-branch
# Modern alternative:
git switch feature-branch

# Create and switch to a new branch
git checkout -b new-feature
# Modern alternative:
git switch -c new-feature

# Restore a file from HEAD (legacy syntax)
git checkout -- src/app.py
# Modern alternative:
git restore src/app.py

# Restore a file from a specific commit
git checkout abc1234 -- src/app.py
# Modern alternative:
git restore --source=abc1234 src/app.py

Common Undo Scenarios

Scenario 1: Committed to Wrong Branch


# You committed to main but should have been on feature

# Undo the commit, keep changes staged
git reset --soft HEAD~1

# Switch to the correct branch
git switch feature

# Commit on the correct branch
git commit -m "feat: add feature"

Scenario 2: Forgot to Stage a File


# You committed but forgot a file

# Add the forgotten file
git add forgotten-file.py

# Amend the last commit
git commit --amend --no-edit

Scenario 3: Committed with Wrong Message


# Fix the commit message
git commit --amend -m "Correct commit message"

Scenario 4: Want to Squash Last Two Commits


# Soft reset both commits, keep changes staged
git reset --soft HEAD~2

# Re-commit as one
git commit -m "feat: combined feature implementation"

Scenario 5: Discard All Local Changes


# Reset working directory and staging area to match remote
git fetch origin
git reset --hard origin/main

# Clean untracked files too
git clean -fd

Production Failure Scenarios + Mitigations

ScenarioImpactMitigation
git reset --hard on shared branchBreaks teammates’ clones, forces everyone to re-syncNever reset pushed commits; use git revert instead
git reset --hard loses uncommitted workHours of development lost permanentlyCommit frequently; use git stash for temporary saves; check git reflog for recovery
git revert on a merge commit without -mError or incorrect revertAlways specify -m 1 or -m 2 when reverting merge commits
git checkout on uncommitted changesChanges may be overwritten or lostUse git stash before switching branches with uncommitted work
Reverting a commit that was already revertedDouble-negative restores original changesCheck git log to see if a revert commit already exists
git reset on a subdirectoryUnexpected behavior, partial resetAlways reset from the repository root

Trade-offs

ApproachAdvantagesDisadvantagesWhen to Use
git reset --softPreserves all changes staged, clean undoOnly works for unpushed commitsSquashing, amending, restaging
git reset --mixedPreserves changes, allows selective restagingRequires restagingRestaging selectively, undoing commits
git reset --hardComplete clean slatePermanently loses uncommitted changesDiscarding experimental work, syncing with remote
git revertSafe for shared history, auditableCreates extra commits, may cause conflictsUndoing pushed commits, compliance
git restoreClear intent, modern syntaxNot available in Git < 2.23Discarding file changes, unstaging
git checkout --Works everywhereAmbiguous intent (switching vs restoring)Legacy scripts, older Git versions

Implementation Snippets

The Safe Undo Workflow


# Before any destructive operation, create a safety branch
git branch backup-before-reset

# Now you can safely reset
git reset --hard HEAD~3

# If something goes wrong, recover
git reset --hard backup-before-reset
git branch -d backup-before-reset

Recovery with reflog


# See all HEAD movements (including resets)
git reflog

# Find the commit you lost
git reflog | head -20

# Recover a lost commit
git reset --hard abc1234

# Or cherry-pick it
git cherry-pick abc1234

Bulk Operations


# Revert all commits since a tag
git log --oneline v1.0.0..HEAD | while read hash msg; do
    git revert --no-edit $hash
done

# Reset all modified files to HEAD
git checkout -- $(git diff --name-only)

# Unstage all files
git restore --staged .

Interactive Undo


# Interactive rebase to edit, squash, or drop commits
git rebase -i HEAD~5

# In the editor:
# pick = keep commit
# reword = keep but edit message
# edit = stop for manual changes
# squash = combine with previous
# fixup = combine, discard message
# drop = remove commit

Observability Checklist

  • Logs: Use git reflog as your safety net — it records every HEAD movement
  • Metrics: Track the frequency of git reset --hard — high frequency indicates workflow issues
  • Traces: Use git log --oneline before and after undo operations to verify the result
  • Alerts: Pre-push hooks should warn about force-push requirements after resets
  • Audit: Use git reflog to audit who performed undo operations and when
  • Health: Periodically check git status to ensure no unintended state changes
  • Validation: After any undo operation, run tests to verify the codebase is still functional

Security/Compliance Notes

  • git reset --hard destroys evidence: In regulated environments, discarding commits may violate audit requirements. Use git revert instead to maintain a complete audit trail
  • Reflog is local: git reflog is not shared with remotes. Each developer has their own reflog. It is not a substitute for proper audit logging
  • Reverted commits remain visible: git revert preserves the original commit in history, which is essential for compliance. Anyone can see what was done and what was undone
  • Force-push after reset: If you reset a pushed branch, you must force-push. This rewrites shared history and may violate organizational policies
  • Sensitive data removal: git reset --hard does not remove sensitive data from Git’s object database. Use git filter-repo or BFG Repo-Cleaner for permanent removal

Common Pitfalls / Anti-Patterns

  • Using git reset --hard as a reflex: This is the most dangerous Git command for data loss. Always consider git reset --mixed first, which preserves your changes
  • Resetting pushed commits: This rewrites shared history and breaks everyone who pulled. Use git revert for shared branches
  • Confusing git checkout and git restore: git checkout does two things (switch branches and restore files), which is confusing. Use git switch for branches and git restore for files
  • Not checking reflog after mistakes: git reflog can recover almost any lost commit. Before panicking about lost work, check the reflog
  • Reverting without understanding conflicts: A revert may conflict with subsequent changes. Always review the result of git revert before committing
  • Using git reset on a subdirectory: git reset operates on HEAD, not on paths. git reset -- subdir does not do what you might expect
  • Forgetting that git revert creates a new commit: The undo is itself a commit. This means reverting a revert restores the original changes

Quick Recap Checklist

  • git reset --soft moves HEAD back, keeps changes staged
  • git reset --mixed (default) moves HEAD back, keeps changes unstaged
  • git reset --hard moves HEAD back, discards all changes permanently
  • git revert creates a new commit that undoes a previous commit (safe for shared history)
  • git restore file discards uncommitted changes in a file
  • git restore --staged file unstages a file
  • git checkout is primarily for switching branches (use git switch instead)
  • git reflog can recover lost commits after reset
  • Never reset or amend commits that have been pushed to shared branches
  • Always create a backup branch before destructive operations
  • Use git revert for compliance and audit trails
  • git reset --hard origin/main syncs your branch with the remote

Interview Q&A

What is the difference between `git reset` and `git revert`?

git reset rewrites history by moving the branch pointer backward. The commits after the reset point become unreachable (though recoverable via reflog). git revert creates new history — it adds a new commit that is the inverse of the specified commit. The original commit remains in history. Reset is safe for local, unpushed commits. Revert is safe for shared, pushed commits. The mnemonic: reset erases, revert reverses.

What happens to your changes with `git reset --soft`, `--mixed`, and `--hard`?

--soft moves HEAD to the target commit but keeps all changes staged — the staging area and working directory are untouched. --mixed (default) moves HEAD and unstages all changes but keeps them in the working directory. --hard moves HEAD and discards everything — both the staging area and working directory are reset to match the target commit. The progression is: soft keeps everything, mixed keeps working files, hard keeps nothing.

How do you recover a commit that was lost after `git reset --hard`?

Use git reflog to find the lost commit. The reflog records every HEAD movement, including resets. Run git reflog to see the history of HEAD positions, find the commit hash from before the reset, and then run git reset --hard <hash> to restore it. The reflog typically retains entries for 90 days. This is why you should never panic after a reset — the reflog is your safety net.

Why is `git restore` preferred over `git checkout --` for file restoration?

git checkout has two unrelated responsibilities: switching branches and restoring files. This dual purpose is confusing — git checkout file.txt restores a file, but git checkout branch switches branches. git restore (introduced in Git 2.23) separates these concerns: use git switch for branches and git restore for files. This makes the intent clear and reduces mistakes. The old syntax still works but is deprecated.

How Reset Moves HEAD, Index, and Working Tree

git reset operates by moving the HEAD reference and optionally updating the index (staging area) and working directory. Understanding exactly what each mode touches is critical to using reset safely:


graph TD
    A[HEAD points to C3] --> B{reset mode?}
    B -->|--soft| C[HEAD moves to C2<br/>Index = C3 snapshot<br/>Working tree = C3 snapshot]
    B -->|--mixed| D[HEAD moves to C2<br/>Index = C2 snapshot<br/>Working tree = C3 snapshot]
    B -->|--hard| E[HEAD moves to C2<br/>Index = C2 snapshot<br/>Working tree = C2 snapshot]

    C --> F[Changes preserved as STAGED]
    D --> G[Changes preserved as UNSTAGED]
    E --> H[Changes DISCARDED permanently]
  • --soft: Only HEAD moves. Index and working tree keep the content of the original commit. Your changes appear as staged.
  • --mixed (default): HEAD and index move. Working tree keeps the original content. Your changes appear as unstaged modifications.
  • --hard: HEAD, index, and working tree all move. Everything matches the target commit. Uncommitted changes are destroyed.

Production Failure: git reset --hard on Shared Branch

A senior developer notices a bad commit on the shared develop branch. Instead of reverting, they run git reset --hard HEAD~2 and force-push. Consequences:

  • Five teammates lose work — each had pulled the now-deleted commits and built on top of them
  • Divergent histories — every teammate’s local branch now has different commit hashes for the same logical changes
  • Hours of manual recovery — each developer must stash, re-fetch, rebase, and resolve conflicts individually
  • Lost commits — one teammate’s unpushed WIP commit becomes orphaned and is only recovered via reflog
  • Team trust broken — developers become afraid to pull, slowing the entire team

What should have happened:


# Safe undo that preserves history
git revert abc1234
git revert def5678
git push origin develop

Rule: Never reset a branch that others have pulled. If it is shared, revert it.

Trade-offs: reset vs revert vs restore

CommandWhat it doesSafety levelHistory impactWhen to use
git reset --softMoves HEAD back, keeps changes stagedSafe (local only)Rewrites local historySquashing commits, fixing last commit
git reset --mixedMoves HEAD back, unstages changesSafe (local only)Rewrites local historyRestaging selectively, undoing commit
git reset --hardMoves HEAD back, discards everythingDangerousRewrites local history, destroys workDiscarding experimental work, syncing with remote
git revertCreates new commit that undoes old oneSafe (shared OK)Preserves all historyUndoing pushed commits, compliance
git restoreDiscards file changes or unstagesSafeNo history impactThrowing away uncommitted changes
git checkoutSwitches branches or restores filesSafeNo history impactBranch switching (use git switch instead)

Security and Compliance: Why Revert Over Reset for Shared Branches

In regulated environments (finance, healthcare, government), every change must have an auditable trail. git reset erases commits from the visible history, while git revert preserves them:

  • Audit trail: git revert leaves both the original commit and the revert commit in history. Auditors can see what was done and what was undone. git reset removes evidence entirely.
  • Non-repudiation: With signed commits, a revert chain proves who made changes and who authorized their removal. Reset breaks this chain.
  • Compliance requirements: SOC 2, HIPAA, and PCI-DSS all require change management records. Revert commits serve as those records; reset commits destroy them.
  • Team accountability: When revert is used, everyone can see the undo operation in git log. When reset is used, the change simply disappears, raising questions about what was removed and why.

Policy recommendation: Configure branch protection rules to reject force-pushes on shared branches. Enforce git revert as the only acceptable undo mechanism for any commit that has been pushed.

Resources

Category

Related Posts

Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each

Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.

#git #version-control #svn

Automated Changelog Generation: From Commit History to Release Notes

Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.

#git #version-control #changelog

Choosing a Git Team Workflow: Decision Framework for Branching Strategies

Decision framework for selecting the right Git branching strategy based on team size, release cadence, project type, and organizational maturity. Compare Git Flow, GitHub Flow, and more.

#git #version-control #branching-strategy