Ch. 16 · Git & CI/CD

Git Revert Versus Reset for Undoing Changes

Reset moves a branch back; revert adds a commit that undoes one. Which is safe when, and the merge-revert trap that drops work on re-merge.

~6 min readbeginnerupdated Oct 7, 2026

Both commands “undo” something, which is why people mix them up. They undo in completely different ways. git reset moves your branch back to an earlier commit, as if the later ones never happened on this branch. git revert leaves history alone and adds a new commit whose change is the exact opposite of an earlier one. One rewrites the past; the other records a correction.

Interviewers ask “revert or reset?” to see whether you think about who else has the commits. The answer to that one question decides it almost every time. The follow-up that separates people who’ve used revert in anger is about merge commits, because reverting a merge has a consequence that surprises nearly everyone the first time.

Reset moves the pointer

git reset <commit> points the current branch at <commit>. Commits after it are no longer on the branch. The three modes differ only in what happens to your files:

  • --soft: the index and working tree keep the changes from the dropped commits, staged and ready to recommit.
  • --mixed (the default): the changes stay in your working tree but are unstaged.
  • --hard: the index and working tree are reset too, so the changes are gone from your files.

The dropped commits still exist until garbage collection, and git reflog can find them. The undo-last-commit guide walks through each mode for the common “I just committed the wrong thing” case.

The problem with reset is shared history. If you reset main back two commits and force-push, everyone who already pulled those commits has a history the server no longer agrees with. Their next pull either reintroduces the commits you removed or fails with a “diverged” message. So the rule: reset is for commits that only exist in your repository, or on a branch only you use.

Revert adds a commit

git revert <commit> computes the diff that commit introduced, inverts it and commits the result. History only grows. Everyone can pull it normally, and the log shows both the original change and the decision to undo it, which is often exactly what an incident review needs.

Revert works on any commit, not just the latest. If “Billing tweak” was three commits ago and turned out wrong, git revert <its-sha> undoes just that change and leaves the later commits in place. If later commits touched the same lines, you’ll get a conflict to resolve, the same as any merge. A range works too: git revert --no-commit A^..B stages the inverse of every commit from A to B, which you then commit once.

Reverting a merge commit

Here’s a typical case. A feature branch with two commits was merged into main, and the feature is causing errors in production:

$ git log --oneline --graph
*   179175a Merge branch 'feature/autocomplete'
|\
| * 38f3a79 Cache autocomplete results
| * fee5bc7 Add autocomplete
* | 6b55291 Billing tweak
|/
* 122fce8 Initial
Text

Plain revert refuses:

$ git revert HEAD
error: commit 179175ae0a18d78fc8b2212f438b18d3f6e28f51 is a merge but no -m option was given.
fatal: revert failed
Text

A merge commit has two parents, so “the change it introduced” depends on which parent you compare against. -m 1 means “relative to the first parent,” which is the branch you merged into (main). That’s what you want almost every time: undo everything the feature brought in.

$ git revert --no-edit -m 1 HEAD
[main 16db088] Revert "Merge branch 'feature/autocomplete'"
 Date: Thu Oct 1 10:00:00 2026 +0000
 2 files changed, 2 deletions(-)
 delete mode 100644 autocomplete-cache.js
 delete mode 100644 autocomplete.js
Text

Production is fixed, and nobody’s history was rewritten.

The trap: merging the branch again

The team fixes the bug on the feature branch with a new commit, “Debounce autocomplete requests,” and merges the branch into main again:

$ git merge --no-edit feature/autocomplete
Merge made by the 'ort' strategy.
 autocomplete-debounce.js | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 autocomplete-debounce.js
$ ls
autocomplete-debounce.js
billing.js
search.js
Text

No conflict, and only one file arrived. autocomplete.js and autocomplete-cache.js are still missing. The feature is “merged” but most of it isn’t there.

The reason: a revert doesn’t undo the merge in Git’s eyes. The original feature commits fee5bc7 and 38f3a79 are still ancestors of main, because the first merge is still in history. When you merge again, Git finds the merge base, which is now the tip from the first merge, and only brings in commits after it, the debounce fix. The revert commit, which deleted the files, stays in effect.

The fix is to revert the revert before (or right after) re-merging:

$ git revert --no-edit 16db088
[main c01b1f8] Reapply "Merge branch 'feature/autocomplete'"
 Date: Thu Oct 1 10:00:00 2026 +0000
 2 files changed, 2 insertions(+)
 create mode 100644 autocomplete-cache.js
 create mode 100644 autocomplete.js
Text

Now all three files are present. (Recent Git versions title a revert of a revert “Reapply”; older ones produce Revert "Revert ...".) Git’s own documentation discusses this case at length in its how-to on reverting a faulty merge. The interview takeaway: if you revert a merge, write down that the branch can’t simply be merged again, and make “revert the revert” part of the plan for bringing the feature back.

An alternative is to recreate the feature branch’s commits with new hashes, for example git rebase --no-ff onto its original base, which replays every commit even when it could fast-forward, and merge that branch instead. Git then sees them as new work. Reverting the revert is simpler and leaves a clearer trail.

Choosing quickly

Ask two questions:

  1. Has anyone else got these commits? If they’re pushed to a shared branch, or might have been pulled, use revert. If they’re only local, reset is fine and leaves cleaner history.
  2. Do I want the record? Even on your own branch, a revert can be useful when you want the log to show that something was tried and backed out.

A mistake interviewers listen for: “I’d reset main and force-push, it’s quicker.” On a protected branch the force-push will be refused anyway. Where it isn’t, it causes work for every developer and any CI system that had the old commits, and it can drop commits pushed in the meantime.

Interview practice

On Friday, PR #482 (a merge commit 9c1e7a2 with five commits) went to production and broke checkout for some users. It’s now Monday, and twelve other PRs have merged to main since. The team wants checkout fixed now and the feature back next week after a fix. What do you do, and what will you tell the feature’s author?

Answer

main is shared and has twelve later merges on top, so reset is out: it would remove all twelve and break everyone’s clones. I’d run git revert -m 1 9c1e7a2, which creates one new commit undoing everything #482 introduced, relative to main’s side of the merge. If any of the later PRs touched the same code, I’d resolve the conflicts carefully and run the full test suite, then deploy.

I’d tell the author two things. First, the five feature commits are still in main’s history, so merging the same branch again later will only bring in their new fix commits and silently leave out the original feature. Second, the way to bring it back is to revert my revert commit, ideally in the same PR as the fix, so main gets the original changes back plus the fix and the tests run on that combination. I’d put both notes in the revert commit’s message so they’re found by anyone reading the history.

Further reading

More in Git & CI/CD

read ✓Git & CI/CD · easy

Git Reflog and Recovering Lost References

Undo a bad reset --hard, restore a deleted branch or a dropped stash with git reflog and git fsck, and learn which lost work Git can't recover.

~6 min readread →
esc