Skip to content

Undoing Changes: restore, revert, and reset

“Undo” is not one operation in Git. A change can exist in the working tree, the staging area, a local commit, or published shared history. Each location needs a different tool.

The three commands in this lesson solve three different jobs:

git restore → change files or the staging area
git revert → add a new commit that reverses an old commit
git reset → move the current branch and optionally reset staged/filesystem state

Choosing the command starts with one question: Where is the mistake now?

Recall Git’s basic flow:

Working tree ──git add──> Staging area ──git commit──> Repository history
  • The working tree contains the files we currently edit.
  • The staging area describes the next proposed commit.
  • The repository history contains completed commits.

restore, revert, and reset affect different parts of this model.

Part 1: git restore for file-level changes

Section titled “Part 1: git restore for file-level changes”

git restore changes file content in the working tree or staging area. It does not normally move the current branch pointer.

Suppose PaymentService.cs was modified but not staged:

Terminal window
git status
git diff -- PaymentService.cs

To replace its working-tree content with the version currently in the staging area:

Terminal window
git restore PaymentService.cs

With the usual clean index, this looks like restoring the file from HEAD. More precisely, the default source for working-tree restoration is the index.

To discard all unstaged changes to tracked files in the current directory and its descendants:

Terminal window
git restore .

This does not normally remove untracked files. Inspect git status instead of assuming every visible file will change.

If a file was staged accidentally:

Terminal window
git restore --staged PaymentService.cs

This copies the version from HEAD into the staging area while leaving the working-tree file unchanged. The edits remain on disk; they are simply removed from the next proposed commit.

This is usually the safer first step when the problem is “I staged the wrong file,” not “I want to delete my edits.”

We can explicitly choose another source:

Terminal window
git restore --source=HEAD~1 -- PaymentService.cs

This writes an older version into the working tree, but it does not create a commit automatically. Review and commit the resulting change if it is the desired fix.

Use -- before a path when it helps distinguish a revision from a file name.

This command is intentionally destructive for the selected path:

Terminal window
git restore --source=HEAD --staged --worktree -- PaymentService.cs

It makes both the index and working-tree copy match HEAD, discarding staged and unstaged changes to that file. Use it only after inspecting the exact path.

Suppose a bad commit has already been pushed to a shared main branch:

A──B──C main
↑
bad change

Removing C by rewriting the branch would change published history. Instead, create a new commit that applies the inverse change:

Terminal window
git revert C

The result is:

A──B──C──R main
↑
reverses C

Both facts remain visible:

  • Commit C introduced the change.
  • Commit R deliberately reversed it.

That makes revert the normal safe choice for a bad commit that teammates may already have.

Terminal window
git revert HEAD

Git creates a new commit by default and may open an editor for its message.

To prepare the inverse change for inspection:

Terminal window
git revert --no-commit <commit-id>
git diff --staged
dotnet test
git commit -m "revert: remove faulty payment retry"

This is useful when several reversals need testing or one carefully written result, but it leaves a revert operation in progress until it is completed or aborted.

A later commit may have changed the same code. Git can then pause because it cannot apply the inverse patch automatically.

After resolving each file:

Terminal window
git add PaymentService.cs
git revert --continue

To abandon the operation:

Terminal window
git revert --abort

A merge commit has more than one parent, so Git needs to know which parent is the mainline that should remain:

Terminal window
git revert -m 1 <merge-commit-id>

-m 1 does not mean “revert commit number one.” It selects parent 1 as the mainline. Verify the merge parents and understand the later integration impact before reverting a merge commit.

Part 3: git reset for moving local history

Section titled “Part 3: git reset for moving local history”

git reset <target> moves the current branch pointer to another commit. The mode decides what happens to the staging area and working tree.

Suppose local history is:

A──B──C feature-payment, HEAD

Running a reset to B moves the branch:

A──B feature-payment, HEAD
\
C no longer named by this branch

The change introduced by C may be kept staged, kept only in the working tree, or discarded from tracked files depending on the mode.

Terminal window
git reset --soft HEAD~1

This moves the branch back one commit while keeping the index and working tree unchanged:

  • The last commit disappears from this branch’s visible history.
  • Its changes remain staged.
  • The files remain as they were before the reset.

Use it when a local commit contains the correct content but needs to be recreated—for example, to change how commits are grouped.

Terminal window
git reset --mixed HEAD~1

--mixed is the default mode, so this is equivalent:

Terminal window
git reset HEAD~1

It moves the branch and resets the staging area to the target commit, while leaving working-tree content in place:

  • The removed commit’s changes remain as unstaged edits.
  • The next proposed commit is cleared back to the target state.

Use it when a local commit needs to be dismantled so its files can be reviewed and staged again selectively.

Terminal window
git reset --hard HEAD~1

This moves the branch and makes both the index and tracked working-tree files match the target commit. Changes after that target are discarded from those places.

--hard does not mean “delete every untracked file.” Cleaning untracked and ignored files is a separate concern. However, an untracked file or directory that blocks writing a tracked path may be removed, so a clean status and backup still matter.

Assume HEAD points to C and we reset to B:

Command Branch moves to B Staging area after reset Working tree after reset
git reset --soft B Yes Keeps C’s result staged Keeps current files
git reset --mixed B Yes Matches B; changes become unstaged Keeps current files
git reset --hard B Yes Matches B Tracked files match B

A useful memory aid:

--soft → move the branch only
--mixed → move the branch + reset the index
--hard → move the branch + reset the index + tracked working files

HEAD~1 means the first parent of the current commit—usually one commit back on a simple history:

Terminal window
git reset --soft HEAD~1

Before a destructive reset, inspect the exact destination:

Terminal window
git log --oneline --decorate -5
git show --stat HEAD~1
git status

A commit ID can be clearer than a relative expression when communicating with a team:

Terminal window
git reset --mixed a12bc34

Because reset moves HEAD and the branch reference, the previous position is usually recorded in the local Reflog:

Terminal window
git reflog

If the lost commit is still available, recreate a safety branch before making more changes:

Terminal window
git branch recovery/payment-work <commit-id>

Reflog is local and expires; it is a recovery window, not a backup. It also does not make unstaged edits erased by restore or reset --hard reliably recoverable.

Terminal window
git restore --staged appsettings.Development.json

The file’s edits stay in the working tree.

“I want to discard my unstaged experiment”

Section titled ““I want to discard my unstaged experiment””
Terminal window
git diff -- PaymentService.cs
git restore PaymentService.cs

The selected unstaged edits are discarded.

“A bad commit was already pushed to shared main”

Section titled ““A bad commit was already pushed to shared main””
Terminal window
git switch main
git pull --ff-only
git revert <bad-commit-id>
dotnet test

History remains additive and honest.

“My latest local commit should be rebuilt in smaller pieces”

Section titled ““My latest local commit should be rebuilt in smaller pieces””
Terminal window
git reset --mixed HEAD~1
git status
git add PaymentService.cs
git commit -m "fix: validate payment currency"

The original local commit is removed from the branch, while its changes remain available to stage selectively.

Where is the mistake?
├─ Unstaged tracked-file edits
│ └─ git restore <path> (discards those edits)
├─ Staging area only
│ └─ git restore --staged <path> (keeps file content)
├─ Commit already shared
│ └─ git revert <commit> (adds an inverse commit)
└─ Unpublished local commit
├─ Keep changes staged → git reset --soft <target>
├─ Keep changes unstaged → git reset --mixed <target>
└─ Discard tracked state → git reset --hard <target> (danger)
  • Running git restore <file> when the intention was only to unstage it.
  • Assuming Reflog stores every uncommitted version of every file.
  • Using reset on shared history when revert would preserve collaboration.
  • Thinking --soft, --mixed, and --hard differ only in severity; they reset different Git areas.
  • Treating git reset --hard HEAD~1 as a universal undo button.
  • Forgetting that revert can conflict because later code depends on the old change.
  • Reverting a merge commit with -m without checking which parent is the intended mainline.
  • Undoing code without rebuilding and testing the resulting behavior.
  • Use git restore for working-tree and staging-area corrections.
  • git restore --staged unstages without deleting the file’s edits.
  • Use git revert for shared commits because it adds history instead of rewriting it.
  • Use git reset mainly to rebuild unpublished local history.
  • --soft keeps changes staged, --mixed keeps them unstaged, and --hard discards tracked state to match the target.
  • Inspect status, diffs, and the exact target before every destructive action.
  1. Which command unstages a file while preserving its working-tree edits?
  2. Why is Reflog normally unable to restore edits discarded before any commit or stash?
  3. Why is revert safer than reset for a commit already pushed to main?
  4. What remains after git reset --soft HEAD~1?
  5. How does --mixed differ from --hard?
  6. What does -m 1 select when reverting a merge commit?