Skip to main content
DevOps Insightsguides & answers

Git · commonly confused

Git reset vs revert

Short answer

Reset moves your branch back to an earlier commit, so the commits after it stop being part of the branch. Revert keeps every commit and adds a new one that undoes the changes. On commits only you have, reset is fine; on anything already pushed, revert is the one that does not rewrite history — and reset --hard also throws away uncommitted work: changes you never staged are gone for good.

Reproduced on Git 2.50, . Where the text says “reproduced”, the behaviour was run on that version, not taken from documentation.

First published

On this page · 10 sections

In simple terms

Both are ways to undo a commit, which is why they get swapped. They undo different things. Reset changes where your branch points. Revert changes the files, with a new commit that says so.

Reset is moving a bookmark back. The commits after it still exist for a while, but the branch no longer includes them, and anyone who already has them now has a different history from yours.

Revert is writing a correction. The original commit stays in the history, followed by a commit that applies its exact opposite. Everyone's history keeps moving forward, so it pushes and pulls like any other commit.

Reset also comes in three strengths, and the strongest is the one that loses work: --hard rewrites your files to match the commit you reset to.

Words used on this page

commit
A saved snapshot of the project with a message and a link to the commit before it, its parent.
branch
A name that points at one commit. Committing moves it forward; git reset can move it anywhere, including backwards.
HEAD
The commit you are on now, usually through the current branch. HEAD~1 is its parent.
working tree
The files you see and edit on disk.
index
Also called the staging area: what git add has prepared for the next commit.
ORIG_HEAD
Where the branch pointed before the last reset, kept by Git so git reset --hard ORIG_HEAD can undo it. Other commands, such as merge, move it too.
reflog
Your local record of where HEAD and each branch have pointed. It is not pushed, and its entries expire: by default after 90 days, or 30 for commits no longer on the branch.
fast-forward
A push that only adds commits on top of what the remote already has, so the remote accepts it without force.
force push
git push --force: replace the remote branch with yours even when that removes commits from it.
mainline parent
For a merge commit, the parent that counts as the branch's own history. git revert -m 1 names the first parent.

What actually happens

git reset moves the current branch to another commit, then optionally updates the index and the working tree. Reproduced on Git 2.50, undoing one commit three ways from the same state: --soft kept the undone changes staged; --mixed, the default, kept them as unstaged edits; --hard made the files match the earlier commit. A new file that had been staged but never committed was left untracked by --mixed and deleted by --hard. Git's manual: with --hard, any changes to tracked files in the working tree since the commit are discarded.

What reset removes from the branch is recoverable for a while. Git sets ORIG_HEAD to the old tip and the reflog records every move, so git reset --hard ORIG_HEAD put the branch back exactly. What it removes from the working tree is another matter: uncommitted work was never in a commit, so neither ORIG_HEAD nor the reflog has it. Staged changes were written to Git's object store, though, and git fsck --lost-found can recover them until garbage collection runs. Reproduced on Git 2.50: after reset --hard, a staged file came back from .git/lost-found/other/, and an unstaged edit did not.

git revert makes a new commit whose parent is your current tip and whose changes are the inverse of the commit you name. Nothing is rewritten. Reproduced: the history read Revert "bad", bad, two, one; the file was back to its earlier content; and the push was an ordinary fast-forward. Resetting the same pushed commit instead made the next push fail with ! [rejected] main -> main (non-fast-forward).

Merge commits need one more decision. A merge has two parents, so revert has to be told which one is the mainline, and without it Git refuses: "is a merge but no -m option was given". With -m 1 it undoes what the merge brought in. Git's manual states the consequence: reverting a merge declares that you will never want those changes, so later merges only bring in commits that are not ancestors of it. Reproduced: merging the same branch again said "Already up to date" and brought nothing back, until the revert was itself reverted.

Undoing a commit, each way

# Undo the last commit, keeping its changes staged / as edits / not at all.
git reset --soft HEAD~1
git reset HEAD~1            # --mixed, the default
git reset --hard HEAD~1     # also discards uncommitted work in tracked files

# Changed your mind straight after a reset? The old tip is kept.
git reset --hard ORIG_HEAD
git reflog                  # every earlier position of HEAD, once ORIG_HEAD has moved on

# Undo a commit that is already pushed: a new commit, history intact.
git revert <commit>
git push

# Undo a merge: name the parent that was the mainline.
git revert -m 1 <merge-commit>

# Bring a reverted merge's changes back later.
git revert <the-revert-commit>

Run git status before any --hard. Changes in tracked files are discarded and a staged new file is deleted; none of it was ever committed, so the reflog cannot return it.

Try it yourself

Before you start

  • Git installed (git --version). Everything on this page was reproduced on Git 2.50.1.
  • A clean starting point. git status should say nothing to commit, working tree clean; if it does not, commit or git stash first. Git's manual says revert needs a clean working tree. In practice, reproduced on Git 2.50.1, it refused staged changes and edits to files the revert touches, and let an unrelated unstaged edit through, so start clean rather than rely on that.
  • The ID of the commit you want to undo, from git log --oneline, and whether anyone else already has it. git branch -r --contains <commit> lists the remote branches that include it: if any are listed, use revert.
  • Push access to the branch. If the branch is protected, push the revert to a new branch and open a pull request like any other change.
  • For a merge commit, which parent is the branch's own history. git show --no-patch --format=%P <merge> prints the parents in order; the first is usually the branch you merged into.

Undoing a pushed commit with revert

Reproduced on Git 2.50.1 (Apple Git-155) in a scratch repository whose remote was a local bare repository, with global and system config switched off and the time zone set to UTC. Three commits had been pushed; the last changed the port in app.conf from 8080 to 9090. The author is a placeholder and the repository path is replaced with <path>.

Command

git log --oneline
git revert --no-edit HEAD
git log --oneline
cat app.conf
git push origin main

Output

$ git log --oneline
5a3dd0d Change port to 9090
b2df759 Set request timeout
92d604d Add app config

$ git revert --no-edit HEAD
[main f6e7366] Revert "Change port to 9090"
 Date: Mon Oct 12 09:06:25 2026 +0000
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
f6e7366 Revert "Change port to 9090"
5a3dd0d Change port to 9090
b2df759 Set request timeout
92d604d Add app config

$ cat app.conf
PORT=8080
TIMEOUT=30

$ git push origin main
To <path>/origin.git
   5a3dd0d..f6e7366  main -> main

How to read it

  • git revert --no-edit HEAD made a new commit, f6e7366, whose message names what it undoes. --no-edit accepts that message as it is; without it Git opens your editor first so you can explain why.
  • The log still contains Change port to 9090. Nothing was removed, so anyone who already pulled it has nothing to reconcile: their next pull simply adds the revert.
  • app.conf is back to PORT=8080, and TIMEOUT=30 from the earlier commit is untouched. A revert undoes one commit's changes, not everything since.
  • The push printed 5a3dd0d..f6e7366, two dots and no + flag: a fast-forward. Git's manual uses three dots and a + for a forced update, which is what rewriting pushed history needs.
  • In the same repository, git reset --hard HEAD~1 on that pushed commit made the next push fail with ! [rejected] main -> main (non-fast-forward), and git status suggested git pull, which would have fetched the commit straight back.

The mistake this causes

Using git reset --hard to undo a commit that has already been pushed, or to undo a commit while there are uncommitted changes in the working tree.

Why people do it Reset rewrites where the branch points. A pushed commit is already in other people's history, so the branch now disagrees with the remote. And --hard rewrites the working tree as well, so uncommitted work goes with it.

What you see The next push is rejected as non-fast-forward. Forcing it removes the commit from the shared branch and leaves everyone who pulled it holding a commit the remote no longer has. Locally, edits made since the last commit are gone, and git reflog does not list them, because they were never committed.

If it goes wrong

error: your local changes would be overwritten by revert., or Your local changes to the following files would be overwritten by merge
You have uncommitted work. Both messages end with fatal: revert failed, and nothing was changed. Commit the work or run git stash, revert, then git stash pop. If your edits touch the lines the revert changed, the pop stops with a conflict to resolve, and Git keeps the stash until you do.
error: commit <id> is a merge but no -m option was given.
Check which parent is the mainline with git show --no-patch --format=%P <merge>, then run git revert -m 1 <merge> (or -m 2 if the second parent is your branch).
CONFLICT (content): Merge conflict in <file> followed by error: could not revert <id>
A later commit changed the same lines. Edit the file to the result you want, git add <file>, then git revert --continue. To give up and return to where you started, run git revert --abort.
The push is rejected with (non-fast-forward) straight after a reset
You reset a commit the remote already has. Do not force the push. Put the branch back with git reset --hard ORIG_HEAD (this also discards uncommitted edits), then undo the commit with git revert.
Work is missing after git reset --hard
Committed work is in git reflog: find the entry and run git reset --hard <entry>, or keep it on a new branch with git branch rescue <entry>. Staged changes can sometimes be found with git fsck --lost-found. Edits that were never staged were never in Git, and Git cannot bring them back.

How it shows up in production

A bad commit reaches main, and the deploy that follows breaks.

The quickest-looking fix is git reset --hard HEAD~1 and a force push. It works on the server: main no longer contains the commit, and the next deploy is clean.

Meanwhile other people have pulled main. Their next pull reports divergent branches, one of them merges, and the bad commit comes back with their next push.

git revert would have produced the same files with a commit that records what happened, pushed as a fast-forward, with nothing for anyone else to reconcile.

How to tell which one you are hitting

Has the commit been pushed anywhere others pull from?
Then revert. Reset is for commits only you have.
Do you have uncommitted changes?
Then not --hard. Use --soft or --mixed, or commit or stash first; --hard discards them. Staged changes can sometimes be dug out with git fsck --lost-found; unstaged ones cannot.
Is the commit a merge?
Revert needs -m 1 to know which parent was the mainline, and once it is reverted, merging that branch again brings nothing back until the revert is reverted.
Did you just reset the wrong thing?
git reset --hard ORIG_HEAD straight away. If other commands have moved ORIG_HEAD since, find the old position in git reflog.

Security considerations

  • Neither command deletes anything from history. A revert leaves the original commit in place, and a reset leaves it in your repository until its reflog entry expires (30 days by default for commits no longer on the branch) and garbage collection prunes it (loose objects older than two weeks by default), as well as in every clone and fork that already fetched it. If the commit contained a password or key, rotate it: removing it from history is a separate job, covered on the page about a push declined for a detected secret.
  • Force-pushing after a reset removes commits from the shared branch for everyone, without telling them. If you really must rewrite a shared branch, use --force-with-lease --force-if-includes, which refuse when the remote has commits you have not integrated, and keep branch protection that blocks force pushes on main.
  • Read what you are undoing with git show <commit> before you revert it. Reverting a fix brings back the problem it fixed, including a security fix, and the revert message is the place to say why.
  • On a branch that requires signed commits, the revert is a new commit and must be signed as well: git revert -S <commit>.

In an interview

How this gets asked in an interview

Usually phrased as What is the difference between git reset and git revert?

What a strong answer contains Reset moves the branch to another commit and, depending on the mode, rewrites the index and working tree; it changes history, so it belongs on commits nobody else has. Revert adds a new commit that applies the inverse of an earlier one; history only moves forward, so it is the safe way to undo something already pushed. A strong answer adds the sharp edges: reset --hard discards uncommitted work the reflog cannot recover, and reverting a merge needs -m, after which re-merging that branch brings nothing back until the revert is reverted.

The follow-up "You ran reset --hard and lost a commit. Can you get it back?" Yes, if it was committed: ORIG_HEAD or git reflog has the old position. Not for uncommitted changes, which were never in a commit.