GitHub’s web interface has exactly one undo button: Revert on a merged pull request, which opens a new pull request undoing those changes. There is no way to undo an individual pushed commit from the browser. Everything else requires the terminal, and which command you need depends on one question — has anyone else pulled it?
That question is the whole decision. Get it wrong in the safe direction and you have an extra commit in history. Get it wrong in the other direction and you have rewritten history other people are working on, which is a coordination problem rather than a git problem.
Table of contents
- What the web interface can do
- Undoing a commit you have not pushed
- Undoing a commit you have pushed
- When rewriting history is justified
- What happens to everyone else
- Choosing in one line
- How this fits the rest of the stack
- FAQ
What the web interface can do
The Revert button on a merged pull request is the one genuinely useful browser affordance. It creates a branch with a commit undoing everything the pull request introduced, and opens a new pull request for it.
This is the right tool when a merged feature needs to come out. It goes through review, it is auditable, and it does not touch anyone’s local clone.
It fails in one situation worth knowing: if there have been conflicting changes since the merge, GitHub cannot generate the revert cleanly and tells you to do it locally. That is not a limitation so much as a signal that the revert needs a human to resolve.
What the browser cannot do: undo a commit pushed directly to a branch, remove a commit from history, or amend a commit message on anything already pushed. All of those are terminal operations.
Undoing a commit you have not pushed
The most common case and the easiest. Three variants, differing only in what happens to your changes:
# Undo the commit, keep changes staged
git reset --soft HEAD~1
# Undo the commit, keep changes in the working tree unstaged
git reset HEAD~1
# Undo the commit and discard the changes entirely
git reset --hard HEAD~1
—soft is what you want when the commit was premature — you want to add another file or write a better message. The changes are still staged, ready to commit again.
—hard is the only destructive one. Uncommitted work in the working tree is gone and the reflog cannot recover it, because it was never committed. Stash first if there is any doubt.
To fix just the message on the most recent unpushed commit, amend rather than reset:
git commit --amend -m "A better message"
Undoing a commit you have pushed
Once it is on the remote, the safe operation is revert. It creates a new commit that undoes the changes and leaves history intact:
git revert <commit-hash>
git push
Nobody else has to do anything. Their next pull brings in the revert commit like any other change, and no clone is left in a broken state.
To revert several commits at once without one revert commit per original:
git revert --no-commit <oldest>^..<newest>
git commit -m "Revert the broken feature"
Note that reverting a merge commit needs the -m flag to say which parent is the mainline, because a merge has two parents and git cannot guess. That is a distinct case with its own trap around re-merging.
When rewriting history is justified
Reset plus force-push rewrites the remote branch. It is the wrong default and there are two cases where it is correct.
A secret was committed. An API key or password in a commit must be removed from history, not just deleted in a later commit — anyone who clones the repository gets the full history including the secret. This requires history rewriting and, critically, rotating the credential regardless. Assume it is compromised the moment it was pushed, because it was.
Your own branch that nobody else uses. Cleaning up a feature branch before review is entirely normal.
When you do force-push, use the safe variant:
git reset --hard HEAD~1
git push --force-with-lease
—force-with-lease refuses if the remote has commits you have not seen, which prevents overwriting someone else’s work that landed while you were preparing the push. Plain —force does not check and will happily destroy it. There is no good reason to use plain —force interactively.
One thing worth knowing about the secrets case: rewriting history does not remove the commit from forks, existing clones, or a hosting provider’s cached views. Rotation is the actual fix; the rewrite is cleanup.
What happens to everyone else
The reason revert is the default recommendation is entirely about other people’s clones.
After a revert, a colleague pulls and gets a new commit. Nothing else changes. There is no action for them to take.
After a force-push, a colleague’s next pull conflicts with a history that no longer matches. Git tells them their branch has diverged. They now have to work out whether to reset to the remote — discarding local work — or rebase on top of it. If they get it wrong, the commits you removed come back on their next push.
That last part is the one that turns a five-minute fix into an afternoon: someone reintroduces the removed commits without noticing, and the branch is back where it started.
If you must force-push a shared branch, tell people first and tell them what to run:
git fetch origin
git reset --hard origin/main
That discards their local branch state in favour of the remote, which is what you want after a deliberate rewrite — and it is destructive, so it needs to be said explicitly rather than assumed.
Choosing in one line
The decision procedure, compressed:
- Not pushed?
git reset— soft to keep changes, hard to discard. - Pushed to a shared branch?
git revert. Always. - Pushed to your own branch, nobody else on it?
git resetplusgit push --force-with-lease. - A merged pull request? The Revert button in the browser.
- A secret got committed? Rotate the credential first, then rewrite history.
The bias throughout is toward adding commits rather than removing them. An extra revert commit in history is a small cost and it is reversible. A rewritten shared branch is neither.
How this fits the rest of the stack
The pattern in every one of these is that a previous good state being addressable makes the fix cheap, and reconstructing it by hand makes the fix expensive. That extends past the repository — a bad commit that reached production is much less alarming when redeploying the previous build is a selection rather than a rebuild. Deploying from GitHub on RunxBuild ties each deploy to the commit that produced it and keeps the previous one available, and the RunxBuild hosting calculator itemises what those deploys run on.
Useful related references:
- Does PostgreSQL COMMIT Release Memory? What Actually Gets Freed
- Docker Commit: Useful for Debugging, Wrong for Reproducible Builds
- actions/setup-node: The GitHub Actions Node Setup That Does Not Fight Your Deploy
- Deploying from GitHub on RunxBuild
FAQ
Can I undo a commit from the GitHub website?
Only for a merged pull request, using the Revert button, which opens a new pull request undoing the changes. There is no browser option to undo an individual pushed commit or remove one from history.
What is the difference between git reset and git revert?
Reset moves the branch pointer backwards and rewrites history, which is only safe for commits nobody else has. Revert adds a new commit undoing the changes and leaves history intact, so it is safe on shared branches.
What does —force-with-lease do?
It refuses the push if the remote has commits you have not fetched, preventing you from overwriting work that landed while you were preparing. Use it instead of plain —force, which performs no such check.
I committed an API key. Is deleting it enough?
No. Rotate the credential immediately — anyone who cloned the repository has it in their history. Rewriting history is worthwhile cleanup, but it does not reach existing clones, forks, or cached views.
How do I change a commit message after pushing?
For the most recent commit, amend it and force-push with —force-with-lease, which is only appropriate on a branch nobody else uses. On a shared branch, leave the message alone — it is not worth rewriting history for.