Git revert: undo a commit that is already pushed

The commit is on the remote. Someone has already fetched it. Rewriting the branch would move the problem onto them, so the only clean answer is a new commit that undoes the change and leaves the record of both. That is what git revert is for, and the reason it feels awkward at first is that it is the one undo command in Git that adds to history instead of editing it.

What revert does, and what it refuses to do

Given one or more commits, git revert works out the changes those patches introduced, applies the inverse, and records new commits describing what was reversed. The original commits stay exactly where they are. History grows forward.

Two preconditions apply. The working tree has to be clean, meaning no modifications relative to HEAD, because the command needs to apply a patch to a known state. And the commits named have to be reachable, so a revert works on anything in the branch's history, not only on the tip.

The default message is generated for you, and the manual is unusually direct about it being insufficient: it is strongly recommended to explain why the original commit is being reverted. A message of Revert "Add rate limiting" tells a reader what happened and nothing about whether the feature is coming back next week or was abandoned. One extra sentence in the body is the difference between a usable log and an archaeology problem.

Running the command from a terminal opens the editor by default, which is the --edit behaviour. --no-edit skips it, and is for scripts rather than for people.

Two smaller details are worth knowing before the first revert. --reference changes the body from naming the full object hash to the shorter --pretty=reference form, and revert.reference makes that the default for a repository. And reverting a revert works, but the subjects stack into Reapply "Reapply "..." which the manual suggests rewording into something shorter and more unique. The full option list is in the git revert documentation.

Revert, reset and restore are three different jobs

The three commands have similar names and almost no overlap in purpose. The Git manual separates them in one line each.

Command What it changes Safe on a pushed branch
git revert Adds a new commit that reverses earlier commits Yes. It only moves the branch forward
git reset Moves the branch tip to add or remove commits, changing history No. It rewrites what others have fetched
git restore Restores files in the working tree, or the index, without touching the branch Not applicable. It does not change history

The practical test is one question: has anyone else had a chance to fetch this commit? If yes, the answer is revert. If the commit exists only in a local branch that has never been pushed, git reset is cheaper and leaves a tidier history, and git restore is the right tool when the goal is a file rather than a commit.

There is a trap in the middle. git revert also discards nothing from the working directory, while git reset --hard and git restore both will. Reaching for the wrong one when the tree is dirty is how uncommitted work disappears.

Finding the commit that actually caused it

A revert is only as good as the commit it names, and the commit that broke something is frequently not the one at the top of the log. Three searches narrow it down without guessing.

Search the log messages when the change had a name. --grep=<pattern> limits output to commits whose message matches the pattern, and several --grep options match any of them unless --all-match is added.

git log --oneline --grep='rate limit'

Search the diffs when the symptom points at a line of code rather than a feature. -S<string> finds commits that change the number of occurrences of that string, which is the search for where a block of code appeared or vanished. -G<regex> is the looser cousin: it matches commits whose patch text contains added or removed lines matching the expression.

git log --oneline -S'X-RateLimit-Remaining'
git log --oneline -G'retry.*backoff' -- src/

When the symptom is a behaviour that cannot be grepped, git bisect does a binary search. Name a commit that has the bug and one that does not, answer good or bad for each commit it checks out, and it narrows to the one that introduced the change. git bisect run <cmd> automates the answering when a script or test can decide, and git bisect reset returns to where you started.

Doing this before reverting also produces the material for the revert message, since by then you know which commit and which line.

Reverting a single commit, start to finish

The reference format prints exactly what a revert message will quote, which makes it a useful last look before committing to the change.

git log --oneline -20
git log --format=reference -5

Then revert it. The argument accepts any of the usual ways to name a commit, so the fourth most recent commit is HEAD~3.

git status --short
git revert 9f2c1ab
git push

That is the whole operation when the patch applies cleanly. The push is an ordinary fast-forward, because the branch only gained a commit, so nobody else has to repair anything.

Check the result before pushing rather than after. git show prints the revert commit with its diff, and the diff should be the mirror image of the original and nothing else. A revert that quietly picked up an unrelated formatting change, or that reversed only part of a commit because the rest had already been superseded, is visible here and invisible once it is on the remote.

git show --stat HEAD
git show HEAD

For several commits, a range works, and the order matters: reverting a newer commit before an older one usually applies cleanly, while the reverse often conflicts. git revert -n master~5..master~2 reverts the changes from the fifth most recent commit through the third most recent without creating any commit, leaving the combined inverse in the working tree and the index.

-n is the option that makes a batch of reverts readable. It applies the changes without committing, so several reverts can be collapsed into one commit with a message that explains the whole rollback rather than five machine-written subjects. With -n the index does not have to match HEAD, since the revert is applied against the index as it stands.

git revert -n 9f2c1ab
git revert -n 3ab77de
git status --short
git commit -m "Roll back the rate limiting change"

When the revert stops with a conflict

A revert is a merge operation, so it conflicts the same way a merge does, for the same reason: the lines the original patch touched have changed since. The state it leaves behind is the familiar one, with conflict markers in the files and the paths left unmerged in the index.

The sequencer keeps the state in .git/sequencer, and four subcommands drive it.

git revert --continue
git revert --skip
git revert --abort
git revert --quit

--continue resumes after the conflicts are resolved and added. --skip drops the current commit and moves on to the rest of the sequence, which is what a range revert needs when one of its commits has already been undone by hand. --abort cancels and returns to the state before the sequence started. --quit forgets the operation in progress without restoring anything, which is for the case where the working tree is already where it should be.

Resolving is the same work as resolving a merge. Edit the files, git add each path that is settled, then continue. The marker style is worth setting once before it matters, because the default hides the original text of the conflicting area.

git config --global merge.conflictStyle zdiff3
git grep -n '^<<<<<<<\|^>>>>>>>'

The grep is the last check before continuing. Conflict markers are valid text to most parsers until one of them reaches the line, so a marker left in a fixture, a template or a translation file can pass a build and ship.

git revert accepts --strategy and -X strategy options, and --rerere-autoupdate if recorded resolutions are in use. The options behave as they do for git merge.

Reverting a merge commit

This is the case that goes wrong most often, and it has two separate difficulties.

The first is mechanical. A merge commit has more than one parent, so reversing it is ambiguous until you say which side counts as the mainline. -m 1 means the first parent, which on a branch that received a merge is the branch itself, so git revert -m 1 <merge> undoes the changes that arrived from the side branch.

git log --oneline --graph -10
git revert -m 1 4c81f0d

The second difficulty is not mechanical and catches teams out months later. Reverting a merge commit declares that the tree changes brought in by that merge are not wanted. Later merges from the same branch will then only bring in tree changes from commits that are not ancestors of the reverted merge. In other words, re-merging the same branch will not bring the reverted work back, because Git considers it already accounted for.

Worth checking first: whether the change arrived as a merge at all. A branch integrated with --squash, or with the squash option of a hosting service, lands as one ordinary commit with a single parent, so reverting it needs no -m and carries none of the re-merge consequences described above. git log --oneline --graph shows which of the two happened, and git show --no-patch <commit> lists the parents outright. Reaching for -m 1 on a commit that has one parent simply fails, which is the harmless half of getting this wrong.

That is correct behaviour when the branch was genuinely wrong. When the branch merely arrived too early and will be wanted later, the usual path is to revert the revert when the time comes, or to rebuild the work on a fresh branch. The Git project documents this case separately as a how-to on reverting a faulty merge, and the reason it needs its own document is that the surprise arrives long after the revert.

Choosing revert over a rewrite, deliberately

The alternative to reverting is rewriting the branch and force pushing, and there are narrow cases for it: a commit containing a credential, or a large file that should never have entered the repository. Even then, a rewrite does not remove anything from clones that already exist, so a leaked secret has to be rotated regardless.

When a rewrite really is the answer, git push --force-with-lease is the form to use. It refuses the push when the remote ref is not at the value your remote-tracking branch recorded, so it fails instead of silently discarding a colleague's commit that arrived while you were rebasing. Plain --force performs no such check.

For everything else, the revert commit is an asset rather than noise. It records that the change was tried, when it was withdrawn and, if the message was written properly, why. A month later that is the artefact that stops the same change being attempted twice.

Where all of this gets tedious is the switching: read the log in one window, check the file in another, confirm the folder in a third. A file manager with a built-in terminal collapses that into one view, and the comparison with other file managers lays out what each option actually combines.

What to change first

Before the next rollback, decide the rule once: pushed commits get git revert, local-only commits get git reset. Then write one sentence of reasoning into every revert message, because that sentence is the only part a future reader cannot reconstruct. If moving between the log, the diff and the folder means three windows, Atriens makes it one.

Frequently asked questions

Does git revert delete the original commit?

No. It adds a new commit whose content is the inverse of the original, and the original stays in history exactly where it was. That is precisely why it is safe on a branch other people have fetched: the branch only moves forward, so their next pull is an ordinary fast-forward.

How do you revert a commit that is already pushed to the remote?

Make sure the working tree is clean, run git revert <commit>, write a message explaining why, then git push as normal. No force push is involved because nothing was rewritten. If several commits are being rolled back together, use git revert -n for each and make a single commit at the end.

Why does reverting a merge commit need the -m option?

A merge commit has two or more parents, so there is no single previous state to reverse towards until the mainline is named. -m 1 selects the first parent, which is the receiving branch. Be aware that reverting a merge tells Git the merged changes are unwanted, so re-merging that branch later will not bring them back on its own.

What happens if a git revert hits a conflict?

It stops in the same way a merge does, with markers in the files and the paths unmerged in the index. Resolve the files, git add them, then git revert --continue. git revert --abort returns to the state before the revert started, and git revert --skip moves past one commit of a multi-commit sequence.

Can a revert itself be reverted?

Yes, and that is the normal way to reapply work that was rolled back. Reverting the revert restores the original change as a new commit. The only caution is the subject line, which accumulates into Reapply "Reapply "..." if it happens more than once, so rewriting it into something shorter is worth the extra few seconds.

Back to all posts