Coding tips

git reset vs git revert vs git restore: Differences, --soft vs --mixed vs --hard and Which One to Use (Tested Examples)

git restore undoes changes to files, git reset moves your branch back and can rewrite history, and git revert adds a new commit that undoes an old one. Here is what each command really changes, with tested output from Git 2.47 and a simple rule for choosing.

By 12 min read 0 views
Cover image for git reset vs git revert vs git restore: Differences, --soft vs --mixed vs --hard and Which One to Use (Tested Examples)

Use git restore to throw away or unstage changes to files that you have not committed. Use git reset to move your branch back to an earlier commit on a branch that only you use: --soft keeps the changes staged, --mixed (the default) keeps them as unstaged edits, and --hard deletes them. Use git revert to undo a commit that is already pushed, because it adds a new commit instead of rewriting history.

All three commands "undo" something, which is why they get mixed up. The difference is what they touch: your working files, the staging area (the index), or the commit history. Pick the wrong one and you either lose work or end up with a push that the remote rejects.

Every command and output block below comes from a real run with Git 2.47.3 in a throwaway repository with a local bare remote. I fixed the author dates, so the commit hashes are the same each time I run the script.

In this guide:

  • The short version: files, branch or new commit
  • git restore: discard changes to a file
  • git restore --staged: unstage without losing work
  • git restore --source: get a file from an older commit
  • git reset --soft: undo the commit, keep everything staged
  • git reset --mixed: the default, changes become unstaged
  • git reset --hard: everything goes back, and how to rescue it
  • git revert: undo a commit by adding a new one
  • Why git reset breaks a branch you already pushed
  • Reverting a merge commit needs -m
  • Reverting several commits at once
  • git reset vs revert vs restore compared
  • Which one should you use?
  • What about git checkout?
  • Frequently asked questions

The short version: files, branch or new commit

Think of each command by the one thing it is built to change:

  • git restore changes files. It never moves a branch or creates a commit.
  • git reset moves the branch pointer (HEAD) to another commit, and then optionally updates the staging area and working files to match.
  • git revert creates a brand-new commit whose changes are the exact opposite of an older commit. History only grows.

Here is the starting history used for every test. Commit C holds a bug that we want to get rid of:

$ git log --oneline
19abcd9 C: third version (bug)
8abf669 B: second version
c0773ad A: first version

git restore: discard changes to a file

You edited app.txt, did not commit, and want the last saved version back:

$ git status --short
 M app.txt
$ git restore app.txt
$ git status --short
$ cat app.txt
v3 (buggy)

git restore app.txt replaced the file with the version in the staging area, which here matches the last commit. The edit is gone for good, since Git never saved it. The git restore documentation describes it as restoring working tree files, and it was added in Git 2.23 to take over this job from git checkout -- file.

git restore --staged: unstage without losing work

If you ran git add too early, --staged takes the file out of the staging area and leaves your edits alone:

$ git status --short
M  notes.md
$ git restore --staged notes.md
$ git status --short
 M notes.md
$ git restore notes.md

The status changed from M (staged) to M (modified, not staged). The last line then discards the edit, which is only there to clean up the demo. Older tutorials use git reset HEAD notes.md for the same unstaging. It still works, but restore --staged says what it does. If you want a file out of the repository entirely while keeping it on disk, that is a different job, covered in my guide to removing a file from Git but keeping it locally.

git restore --source: get a file from an older commit

You can also pull one file back from any commit without moving the branch:

$ git restore --source=HEAD~2 app.txt
$ cat app.txt
v1
$ git status --short
 M app.txt
$ git restore app.txt

app.txt now holds the content from commit A, and Git shows it as a normal modification that you can commit or throw away. This is the safest way to undo one file in an old commit, because nothing else in the repository changes.

git reset --soft: undo the commit, keep everything staged

--soft moves the branch back and does nothing else. The changes from the undone commit stay staged, ready to commit again:

$ git reset --soft HEAD~1
$ git log --oneline
8abf669 B: second version
c0773ad A: first version
$ git status --short
M  app.txt
$ git commit -qm "C: third version (fixed message)"
$ git log --oneline
3dec275 C: third version (fixed message)
8abf669 B: second version
c0773ad A: first version

Commit C disappeared from the log, app.txt showed as staged, and one git commit put it back with a better message. Notice the new hash, 3dec275 instead of 19abcd9: this is a new commit, which is why reset counts as rewriting history. For the step-by-step version of this use case, see my post on undoing the last commit but keeping your changes.

git reset --mixed: the default, changes become unstaged

Plain git reset HEAD~1 uses --mixed. It moves the branch and clears the staging area, but your files keep the changes:

$ git reset HEAD~1
Unstaged changes after reset:
M	app.txt
$ git status --short
 M app.txt
$ git diff --stat
 app.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git commit -qam "C: third version (bug)"

Git even prints Unstaged changes after reset to tell you nothing was lost. The git reset documentation lists --mixed as the default mode. Use this when you want to split one commit into several smaller ones: reset, then git add pieces and commit them one at a time.

git reset --hard: everything goes back, and how to rescue it

--hard moves the branch and overwrites both the staging area and your files:

$ git reset --hard HEAD~1
HEAD is now at 8abf669 B: second version
$ git log --oneline
8abf669 B: second version
c0773ad A: first version
$ cat app.txt
v2
$ git reflog -3
8abf669 HEAD@{0}: reset: moving to HEAD~1
19abcd9 HEAD@{1}: commit: C: third version (bug)
8abf669 HEAD@{2}: reset: moving to HEAD~1
$ git reset --hard HEAD@{1}
HEAD is now at 19abcd9 C: third version (bug)
$ git log --oneline
19abcd9 C: third version (bug)
8abf669 B: second version
c0773ad A: first version

The commit looked gone, but the git reflog still listed it as HEAD@{1}, and git reset --hard HEAD@{1} brought it straight back. The git reflog documentation explains that Git records every move of HEAD, and these entries expire after 90 days by default.

The reflog only saves commits, though. Watch what happens to work that was never committed:

$ git reset --hard HEAD
HEAD is now at 19abcd9 C: third version (bug)
$ git status --short
$ cat app.txt
v3 (buggy)

Before that reset I appended "two hours of work" to app.txt. After it, the status is clean and the line is nowhere to be found, and no Git command can bring it back. If you might need the changes, run git stash first; my comparison of git stash pop vs apply explains how to get them back afterwards.

git revert: undo a commit by adding a new one

git revert leaves commit C in place and adds commit D that reverses it:

$ git revert --no-edit HEAD
[main eeea100] Revert "C: third version (bug)"
 Date: Mon Nov 9 12:00:00 2026 +0000
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline
eeea100 Revert "C: third version (bug)"
19abcd9 C: third version (bug)
8abf669 B: second version
c0773ad A: first version
$ cat app.txt
v2
$ git show --stat --format=%s HEAD
Revert "C: third version (bug)"

 app.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push origin main
To /tmp/gitdemo-blitz/origin.git
   19abcd9..eeea100  main -> main

app.txt is back to v2, the log keeps the full story, and the push went through as a normal fast-forward. That is why the git revert documentation recommends it for commits that other people may already have.

Why git reset breaks a branch you already pushed

Now try the reset approach on the same pushed branch:

$ git reset --hard HEAD~2
HEAD is now at 8abf669 B: second version
$ git push origin main
To /tmp/gitdemo-blitz/origin.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to '/tmp/gitdemo-blitz/origin.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git reset --hard origin/main
HEAD is now at eeea100 Revert "C: third version (bug)"

The remote refused the push as non-fast-forward, because my local branch no longer contained commits that the remote had. You could force it with git push --force-with-lease, but anyone who pulled those commits would then have a history that conflicts with yours. On a shared branch, revert instead. The git push documentation covers the fast-forward rule and the force options in detail.

Reverting a merge commit needs -m

Merge commits have two parents, so Git cannot guess which side you want to keep:

$ git log --oneline --graph -3
*   491b7ee Merge feature
|\  
| * fa32f1d D: add feature
|/  
* eeea100 Revert "C: third version (bug)"
$ git revert --no-edit HEAD
error: commit 491b7ee1a6c92b4b354f7c0aaa95fb5f871279ce is a merge but no -m option was given.
fatal: revert failed
$ git revert --no-edit -m 1 HEAD
[main 78d8579] Revert "Merge feature"
 Date: Mon Nov 9 12:00:00 2026 +0000
 1 file changed, 1 deletion(-)
 delete mode 100644 feature.txt
$ ls
app.txt
notes.md
$ git log --oneline -2
78d8579 Revert "Merge feature"
491b7ee Merge feature

-m 1 means "keep the first parent", the branch you merged into, so the revert removed everything the feature branch added. Be careful later: if you merge the same feature branch again, Git sees its commits as already merged. To bring the feature back, revert the revert commit. My guide to merge vs rebase vs squash explains why merge commits behave this way.

Reverting several commits at once

You can revert a range. Git creates one revert commit per original commit, newest first:

$ git log --oneline -3
0ed19f1 F: add f.txt
c65ad80 E: add e.txt
78d8579 Revert "Merge feature"
$ git revert --no-edit HEAD~2..HEAD
[main 4b06335] Revert "F: add f.txt"
 Date: Mon Nov 9 12:00:00 2026 +0000
 1 file changed, 1 deletion(-)
 delete mode 100644 f.txt
[main 4be00eb] Revert "E: add e.txt"
 Date: Mon Nov 9 12:00:00 2026 +0000
 1 file changed, 1 deletion(-)
 delete mode 100644 e.txt
$ git log --oneline -5
4be00eb Revert "E: add e.txt"
4b06335 Revert "F: add f.txt"
0ed19f1 F: add f.txt
c65ad80 E: add e.txt
78d8579 Revert "Merge feature"
$ ls
app.txt
notes.md

Add --no-commit if you prefer a single combined revert commit: Git applies all the reversals to your files and staging area, and you commit once at the end. If a revert hits a conflict, fix the files, git add them, and run git revert --continue, or abandon it with git revert --abort.

git reset vs revert vs restore compared

Here is what each form changes, based on the runs above:

CommandMoves the branch?Staging areaWorking filesRewrites history?Safe after push?
git restore <file>NoUntouchedFile reset to the staged versionNoYes
git restore --staged <file>NoFile unstagedUntouchedNoYes
git reset --soft HEAD~1YesKeeps the undone changes stagedUntouchedYesNo
git reset HEAD~1 (--mixed)YesCleared to match the targetChanges kept as unstaged editsYesNo
git reset --hard HEAD~1YesClearedOverwritten, uncommitted work lostYesNo
git revert <commit>Adds a new commitUpdated by the new commitUpdated by the new commitNoYes

The Reset Demystified chapter of Pro Git describes the same three layers as HEAD, the index and the working directory, and it is the best diagram-based explanation if you want to go deeper.

Which one should you use?

Ask two questions: is the change committed, and is the commit pushed?

  1. Not committed, want to discard it: git restore <file>.
  2. Staged by mistake: git restore --staged <file>.
  3. Committed but not pushed, want to redo the commit: git reset --soft HEAD~1.
  4. Committed but not pushed, want to rework the changes: git reset HEAD~1.
  5. Committed but not pushed, want it gone completely: git reset --hard HEAD~1, after a git status check.
  6. Already pushed: git revert <commit>, with -m 1 for a merge.

If you are not sure whether your commit is pushed, run git fetch and compare with git status. My post on git fetch vs git pull shows how to read the ahead and behind counts.

What about git checkout?

Before Git 2.23, git checkout did two unrelated jobs: switching branches and restoring files. That is why git checkout -- app.txt still discards changes today. Git split the jobs into git switch for branches and git restore for files, and the git checkout documentation now points to both. Old commands still work, so you will see them in older answers, but the new ones are harder to misuse.

Frequently asked questions

What is the difference between git reset and git revert?

git reset moves your branch back to an older commit and removes later commits from the branch history. git revert keeps history and adds a new commit that undoes an older one, so it is safe on branches you have pushed.

What is the difference between git restore and git reset?

git restore only changes files in your working directory or staging area and never moves a branch. git reset moves the branch pointer to another commit and can also update the staging area and files.

Does git reset --hard delete commits?

It removes them from the branch, but the commits stay in the reflog for about 90 days by default, so git reset --hard HEAD@{1} can bring them back. Uncommitted changes are lost for good.

Is git reset --soft safe?

Yes, on a branch you have not pushed. It moves the branch back and keeps all the changes staged, so nothing in your files is lost. After a push, it rewrites history that others may have.

How do I undo a git revert?

Revert the revert commit with git revert <hash of the revert commit>. That adds another commit that reapplies the original change, and Git names it Reapply in recent versions.

Why does git revert say commit is a merge but no -m option was given?

A merge commit has two parents, so Git needs to know which side to keep. Use git revert -m 1 <merge hash> to keep the branch you merged into and undo the merged branch.

Should I use git checkout or git restore to discard changes?

Both work. git restore <file> is the modern command, added in Git 2.23, and it only handles files, so it cannot switch branches by accident like git checkout can.

Last checked: 9 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.

Share X LinkedIn WhatsApp Facebook
Was this helpful?

0%