Mert Özen Further With Every Line
DevOps Git & GitHub — #17

Undoing Changes in Git, Part 2: git revert

Mert Özen Nov 6, 2026 7 min 2 views
Undoing Changes in Git, Part 2: git revert

Safe undo in shared history: the inverse commit of git revert, choosing between reset and revert, reverting a merge commit with -m 1, reverting the revert before merging a branch again, several commits with --no-commit and conflicts during a revert.

Version note: The commands in this post were tested with Git 2.54.

Guess First

The discount commit Ada pushed yesterday has a bug: the discount is subtracted twice. The commit is already on origin/main, and another commit has been made on top of it. To undo the faulty commit, Ada uses git revert:

$ git log --oneline
39db42c Add Ulysses
da98f2c Add discount to cart total
7f7eeec Add cart total
61419c1 Add book list
5102cb8 Add README and .gitignore
$ git revert --no-edit HEAD~1

How many lines does git log --oneline show after this command? Does da98f2c disappear from the list?

[main 94e1693] Revert "Add discount to cart total"
 Date: Wed Nov 11 10:04:00 2026 +0300
 1 file changed, 2 insertions(+), 3 deletions(-)
$ git log --oneline
94e1693 Revert "Add discount to cart total"
39db42c Add Ulysses
da98f2c Add discount to cart total
7f7eeec Add cart total
61419c1 Add book list
5102cb8 Add README and .gitignore

Six lines. No commit disappeared; one was added at the top. The reset from the previous post rewound history; revert instead writes a new commit that does the opposite of the faulty one, without touching history. It's like a reversing entry in accounting: you don't tear the wrong receipt out of the ledger, you issue a new one that cancels it.

What Does Revert Do?

Revert takes the target commit's diff, applies its inverse to the working tree and the index, and commits the result. It writes the message itself too:

$ git log -1 --format=%B
Revert "Add discount to cart total"

This reverts commit da98f2c3aef8bedd08bfa250631455852238d5a1.
$ git show --stat --format=%s HEAD
Revert "Add discount to cart total"

 src/cart.js | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)
$ git push
To ../bookstore.git
   39db42c..94e1693  main -> main

The message body carries the full hash of the reverted commit; someone looking at git log months later can trace what was undone and why from here. A good habit is to add the reason below that line. Without --no-edit, Git would have opened the message in your editor; you can use that to add the reason. The push was an ordinary push: since a new commit was added at the end of history it was a fast-forward, no force was needed and nobody's history was broken.

Revert recomputes the change; it doesn't delete the commit. 39db42c, which came after the faulty commit, touched another file and wasn't affected at all. If the lines changed by the reverted commit were changed again in later commits, revert may stop with a conflict; I'll get to that below.

Reset or Revert?

What if we had tried to undo the same mistake with reset? I tried it in another clone that still had the state from before the revert:

$ git reset --hard HEAD~1
HEAD is now at 39db42c Add Ulysses
$ git push
To ../bookstore.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to '../bookstore.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 says it can't rewind the history on the remote. The only way past that is a force push, and that clashes with the history of everyone who has the old commits locally. The choice is actually simple: two cases and a short tradeoff. If the commit is still only on your machine, use reset: history stays clean and nobody is affected. If the commit has been shared, meaning it was pushed and especially if others built on it, use revert: history gets one more commit, but nobody's repo breaks. On a protected main on GitHub you can't force push anyway; there revert is the only way to undo, and GitHub even offers a Revert button on a merged PR's page for it; the button doesn't touch main directly, it opens a new PR containing the revert.

Reverting a Merge Commit

This time a whole branch has to be undone. feature/search was merged into main with two commits, and then it turned out the search was causing problems in production:

$ git switch -q main && git merge --no-ff --no-edit feature/search
Merge made by the 'ort' strategy.
 README.md     | 2 ++
 src/search.js | 5 +++++
 2 files changed, 7 insertions(+)
 create mode 100644 src/search.js
$ git log --oneline --graph -4
*   9782f41 Merge branch 'feature/search'
|\
| * 77fe4b0 Document book search
| * e3b6858 Add book search
|/
* 94e1693 Revert "Add discount to cart total"
$ git revert HEAD
error: commit 9782f41a85249b203613489cf36556707676e383 is a merge but no -m option was given.
fatal: revert failed
$ git revert -m 1 --no-edit HEAD
[main 7672c03] Revert "Merge branch 'feature/search'"
 Date: Wed Nov 11 10:17:00 2026 +0300
 2 files changed, 7 deletions(-)
 delete mode 100644 src/search.js

A merge commit has two parents, and Git can't know which one to compute the inverse diff against. -m 1 means "relative to the first parent"; the first parent is the branch you merged into, here main's tip before the merge, 94e1693. In other words: "undo everything this merge brought into main". The value you want is almost always 1. In the graph, the first parent is the line going straight down from the merge row.

Reverting the Revert

This approach has a trap, and almost everyone falls into it once. The search has been fixed and you want to merge the branch again:

$ git status -s && ls src
books.js
cart.js
$ git merge feature/search
Already up to date.

As far as Git is concerned, feature/search's commits are already in main's history, because the merge commit is still there. The revert undid their changes but didn't remove the commits from history. Even if you add a new fix commit to the branch and merge it, only that new commit comes in, not the changes of the first two. The way out is to revert the revert itself:

$ git revert --no-edit HEAD
[main a6939ee] Reapply "Merge branch 'feature/search'"
 Date: Wed Nov 11 10:20:00 2026 +0300
 2 files changed, 7 insertions(+)
 create mode 100644 src/search.js
$ ls src
books.js
cart.js
search.js
$ git log --oneline -4
a6939ee Reapply "Merge branch 'feature/search'"
7672c03 Revert "Merge branch 'feature/search'"
9782f41 Merge branch 'feature/search'
77fe4b0 Document book search

Git calls the revert of a revert "Reapply". The search is back; now you can merge the fix commits on the branch as usual. The order matters: first undo the revert, then merge the new commits.

Several Commits with --no-commit

The last two commits raised prices, and both are to be undone. If you don't want a separate revert commit for each, you collect the changes with --no-commit and gather them into a single commit:

$ git log --oneline -3
95595a3 Raise Emma price
c4d1075 Raise Dune price
a6939ee Reapply "Merge branch 'feature/search'"
$ git revert --no-commit HEAD~2..HEAD
$ git status -s
M  src/books.js
$ git commit -m "Revert price increases"
[main 5de9101] Revert price increases
 1 file changed, 2 insertions(+), 2 deletions(-)
$ git log --oneline -4
5de9101 Revert price increases
95595a3 Raise Emma price
c4d1075 Raise Dune price
a6939ee Reapply "Merge branch 'feature/search'"

The range HEAD~2..HEAD doesn't include HEAD~2; so it's the last two commits. Git undoes them from newest to oldest, so commits touching the same lines don't trip over each other. The changes piled up in the index and were saved with a single commit. Since you wrote the message yourself this time, it's a good idea to list the reverted commits in the body.

Conflicts During a Revert

If the lines changed by the commit you're reverting were changed again in a later commit, Git can't apply the inverse cleanly and stops with conflict markers, just like a merge. The flow is the same as conflict resolution in the tenth post: fix the files, mark them with git add, then continue with git revert --continue. If you change your mind, git revert --abort takes everything back to the state before the revert. When reverting a range, git revert --skip lets you skip just the current commit.

Common Pitfalls

The first pitfall is thinking revert means "delete". The reverted commit stays in history. If you committed a secret, say a token, revert won't save you; revoke the token right away.

The second is reverting a merge commit and later trying to merge the branch again. As you saw above, Git says "Already up to date" or brings in only the new commits. Revert the revert first.

The third is the wrong -m value. -m 2 undoes the merge from the feature branch's point of view: it undoes the changes that entered main after the branch split off and leaves the feature in place. If you're not sure, check the merge commit's parent order with git show HEAD.

The fourth is trying reset first on a shared commit and then "fixing" it with a force push. As you saw in the previous post and in this one, the way to undo in shared history is revert.

A Small Experiment

★☆☆ Make three commits and undo the middle one with git revert. Compare the output of git log -p -1 with the git show output of the middle commit: are the diffs the inverse of each other?

★★☆ Make a commit that changes a line, then a second commit that changes the same line again. Try to revert the first commit, resolve the conflict and finish with git revert --continue.

★★★ Merge a branch with --no-ff, revert the merge with -m 1, add a fix commit to the branch and merge it again. Look at the final state of the files: are the changes from the first commits there? Then replay the same scenario from scratch, this time reverting the revert.

We've seen how to undo with revert without breaking shared history. In the next post we learn to set unfinished work aside: saving changes with git stash, the difference between apply and pop, untracked files, stashing only the stage and creating a branch from a stash.