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 areagit revert → add a new commit that reverses an old commitgit reset → move the current branch and optionally reset staged/filesystem stateChoosing the command starts with one question: Where is the mistake now?
The three places involved
Section titled “The three places involved”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.
Discard unstaged edits
Section titled “Discard unstaged edits”Suppose PaymentService.cs was modified but not staged:
git statusgit diff -- PaymentService.csTo replace its working-tree content with the version currently in the staging area:
git restore PaymentService.csWith 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:
git restore .This does not normally remove untracked files. Inspect git status instead of
assuming every visible file will change.
Unstage without discarding file content
Section titled “Unstage without discarding file content”If a file was staged accidentally:
git restore --staged PaymentService.csThis 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.”
Restore from a chosen commit
Section titled “Restore from a chosen commit”We can explicitly choose another source:
git restore --source=HEAD~1 -- PaymentService.csThis 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.
Restore both staged and working copies
Section titled “Restore both staged and working copies”This command is intentionally destructive for the selected path:
git restore --source=HEAD --staged --worktree -- PaymentService.csIt 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.
Part 2: git revert for shared history
Section titled “Part 2: git revert for shared history”Suppose a bad commit has already been pushed to a shared main branch:
A──B──C main ↑ bad changeRemoving C by rewriting the branch would change published history. Instead,
create a new commit that applies the inverse change:
git revert CThe result is:
A──B──C──R main ↑ reverses CBoth facts remain visible:
- Commit
Cintroduced the change. - Commit
Rdeliberately reversed it.
That makes revert the normal safe choice for a bad commit that teammates may already have.
Revert the latest commit
Section titled “Revert the latest commit”git revert HEADGit creates a new commit by default and may open an editor for its message.
Revert without committing immediately
Section titled “Revert without committing immediately”To prepare the inverse change for inspection:
git revert --no-commit <commit-id>git diff --stageddotnet testgit 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.
Revert conflicts
Section titled “Revert conflicts”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:
git add PaymentService.csgit revert --continueTo abandon the operation:
git revert --abortReverting a merge commit
Section titled “Reverting a merge commit”A merge commit has more than one parent, so Git needs to know which parent is the mainline that should remain:
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, HEADRunning a reset to B moves the branch:
A──B feature-payment, HEAD \ C no longer named by this branchThe change introduced by C may be kept staged, kept only in the working tree,
or discarded from tracked files depending on the mode.
git reset --soft
Section titled “git reset --soft”git reset --soft HEAD~1This 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.
git reset --mixed
Section titled “git reset --mixed”git reset --mixed HEAD~1--mixed is the default mode, so this is equivalent:
git reset HEAD~1It 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.
git reset --hard
Section titled “git reset --hard”git reset --hard HEAD~1This 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.
Comparing reset modes
Section titled “Comparing reset modes”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 filesHEAD~1 and exact targets
Section titled “HEAD~1 and exact targets”HEAD~1 means the first parent of the current commit—usually one commit back
on a simple history:
git reset --soft HEAD~1Before a destructive reset, inspect the exact destination:
git log --oneline --decorate -5git show --stat HEAD~1git statusA commit ID can be clearer than a relative expression when communicating with a team:
git reset --mixed a12bc34Can Reflog recover a reset commit?
Section titled “Can Reflog recover a reset commit?”Because reset moves HEAD and the branch reference, the previous position is
usually recorded in the local Reflog:
git reflogIf the lost commit is still available, recreate a safety branch before making more changes:
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.
Realistic decision examples
Section titled “Realistic decision examples”“I staged the wrong file”
Section titled ““I staged the wrong file””git restore --staged appsettings.Development.jsonThe file’s edits stay in the working tree.
“I want to discard my unstaged experiment”
Section titled ““I want to discard my unstaged experiment””git diff -- PaymentService.csgit restore PaymentService.csThe 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””git switch maingit pull --ff-onlygit revert <bad-commit-id>dotnet testHistory 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””git reset --mixed HEAD~1git statusgit add PaymentService.csgit commit -m "fix: validate payment currency"The original local commit is removed from the branch, while its changes remain available to stage selectively.
Decision guide
Section titled “Decision guide”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)Common mistakes
Section titled “Common mistakes”- 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--harddiffer only in severity; they reset different Git areas. - Treating
git reset --hard HEAD~1as a universal undo button. - Forgetting that revert can conflict because later code depends on the old change.
- Reverting a merge commit with
-mwithout checking which parent is the intended mainline. - Undoing code without rebuilding and testing the resulting behavior.
Summary
Section titled “Summary”- Use
git restorefor working-tree and staging-area corrections. git restore --stagedunstages without deleting the file’s edits.- Use
git revertfor shared commits because it adds history instead of rewriting it. - Use
git resetmainly to rebuild unpublished local history. --softkeeps changes staged,--mixedkeeps them unstaged, and--harddiscards tracked state to match the target.- Inspect status, diffs, and the exact target before every destructive action.
Knowledge check
Section titled “Knowledge check”- Which command unstages a file while preserving its working-tree edits?
- Why is Reflog normally unable to restore edits discarded before any commit or stash?
- Why is revert safer than reset for a commit already pushed to
main? - What remains after
git reset --soft HEAD~1? - How does
--mixeddiffer from--hard? - What does
-m 1select when reverting a merge commit?