There are three ways to remove a commit and the right one is decided by a single question: has it been pushed? If no, use git reset for the most recent commit or git rebase -i for one further back. If yes, use git revert and leave history alone — rewriting a shared branch turns your cleanup into everybody else’s recovery.
“Remove” also hides an ambiguity worth settling first. Do you want the commit gone but the code kept, or the code gone too? Git treats those as different operations and the flags are easy to mix up.
Table of contents
- First decide: keep the changes or discard them
- Removing the most recent commit
- Removing a commit from the middle with interactive rebase
- When the commit is already pushed: revert
- If you must force-push, use —force-with-lease
- Recovering when it goes wrong
- How this fits the rest of the stack
- FAQ
First decide: keep the changes or discard them
git reset takes a flag that decides what happens to the work in that commit. This is the distinction people get wrong, usually in the direction that loses an afternoon.
--soft— commit removed, changes stay staged. You are about to recommit them differently.--mixed(the default) — commit removed, changes stay in the working tree but unstaged.--hard— commit removed, changes destroyed. No prompt, no undo button in the UI.
# Undo the last commit, keep the work staged
git reset --soft HEAD~1
# Undo the last commit, keep the work as unstaged edits
git reset HEAD~1
# Undo the last commit and throw the work away
git reset --hard HEAD~1
If you are unsure, use --soft. You can always throw work away afterwards; you cannot easily get it back the other way round.
Removing the most recent commit
git reset --soft HEAD~1 is the answer to the common case, and it is the one to reach for when you committed too early, committed to the wrong branch, or want to split one commit into two.
# Committed to main by mistake and want it on a branch instead
git reset --soft HEAD~1 # commit gone, changes staged
git stash # park them
git checkout -b feat/thing # correct branch
git stash pop # changes back
git commit -m "The change, on the right branch this time"
HEAD~1 means “one commit before HEAD”. HEAD~3 removes the last three. The branch pointer moves back; the commits become unreachable but are not immediately deleted, which matters for the recovery section below.
Removing a commit from the middle with interactive rebase
When the commit is buried a few back, interactive rebase is the tool. It replays the commits after your chosen starting point and lets you edit the list on the way.
# Open the last 5 commits for editing
git rebase -i HEAD~5
An editor opens with one line per commit, oldest at the top:
pick 9f8e7d6 Add rate limiting
pick 4c5b6a7 Debug logging, remove me later
pick 1a2b3c4 Fix the header
pick 7e8f9a0 Update tests
pick c3d4e5f Bump version
Change pick to drop on the commit you want gone, or just delete its line entirely — both work. Save and close, and Git replays the rest without it.
If the removed commit’s changes are touched by later commits, you get a conflict. Resolve it, git add the files, and git rebase --continue. If it turns into a mess, git rebase --abort puts everything back exactly as it was.
When the commit is already pushed: revert
Both reset and rebase -i rewrite history. On a branch nobody else has, that is free. On a shared branch, it means every other clone now disagrees with the remote, and the next git pull produces a mess of duplicated commits or a hard rejection.
The polite tool is revert. It does not remove the commit — it adds a new one containing the inverse changes.
# Undo one pushed commit
git revert 4c5b6a7
# Undo several, newest first, as one commit
git revert --no-commit 4c5b6a7 1a2b3c4
git commit -m "Revert the debug logging and the broken header fix"
The commit stays in history and an extra commit undoes it. That is not untidiness — it is an accurate record that the change existed and was withdrawn, which is what you want when someone asks in three months why the feature vanished.
If you must force-push, use —force-with-lease
Sometimes you genuinely need to rewrite a pushed branch — a secret in a commit, or your own feature branch that only you use.
git push --force-with-lease origin feat/billing
--force-with-lease refuses if the remote has commits you have not seen, so it cannot silently delete a colleague’s work the way plain --force can. Make it the default in your muscle memory; there is no case where --force is better and several where it is worse.
A note on secrets: removing a commit that contained a credential does not make the credential safe. It may be in a fork, in CI logs, in someone’s local clone, or in the provider’s cache. Rotate the credential. The history rewrite is cleanup, not remediation.
Recovering when it goes wrong
git reflog is the safety net and it is the most underused command in Git. It records where HEAD has been, including states that are no longer reachable from any branch.
git reflog
# c3d4e5f HEAD@{0}: reset: moving to HEAD~1
# 7e8f9a0 HEAD@{1}: commit: The commit I just destroyed
# 1a2b3c4 HEAD@{2}: commit: Fix the header
# Put it back
git reset --hard HEAD@{1}
This works even after git reset --hard. The commits survive for around 90 days by default before garbage collection. So the honest version of “be careful with --hard” is: be careful, but do not panic, because reflog has almost certainly kept it.
How this fits the rest of the stack
The decision tree is short. Not pushed and it is the last commit: git reset --soft HEAD~1. Not pushed and it is buried: git rebase -i and drop the line. Already pushed: git revert. Went wrong: git reflog.
Where this touches deployment is that history changes trigger builds. A revert or a force-push on a connected branch kicks off a new deploy, and that deploy needs to be as easy to undo as the commit was. On RunxBuild every deploy is kept and rollback is one action, with the build log and runtime log for each attempt in the same view. If you are working out what a project costs to run before you commit to it, the RunxBuild hosting calculator shows the service, database, storage, and bandwidth as separate line items.
Useful related references:
- Undo Commit on GitHub: What You Can Do in the Browser and What Needs the Terminal
- Does PostgreSQL COMMIT Release Memory? What Actually Gets Freed
- Docker Commit: Useful for Debugging, Wrong for Reproducible Builds
- Services on RunxBuild
FAQ
How do I remove the last commit but keep my changes?
git reset --soft HEAD~1. The commit disappears and the changes stay staged, ready to be recommitted. Use plain git reset HEAD~1 if you would rather have them unstaged in the working tree. Avoid --hard unless you genuinely want the work destroyed.
How do I delete a commit that is not the most recent one?
Run git rebase -i HEAD~N where N covers far enough back to include it, then change that commit’s pick to drop or delete its line. Git replays the remaining commits without it. Expect conflicts if later commits touched the same lines, and use git rebase --abort if it becomes unmanageable.
Is it safe to remove a commit that has been pushed?
Not with reset or rebase. Both rewrite history and every other clone of the branch will conflict with the remote. Use git revert instead, which adds a new commit undoing the change. Only rewrite a pushed branch when it is yours alone, and then use --force-with-lease rather than --force.
Can I recover a commit I deleted with git reset —hard?
Almost always. git reflog lists recent HEAD positions including unreachable ones, so find the commit’s entry and run git reset --hard HEAD@{n}. Unreferenced commits survive roughly 90 days before garbage collection, so recovery is realistic as long as you have not waited months.
Does removing a commit that contained a password make it secure?
No. Rewriting history removes it from your branch, but the credential may already exist in forks, CI logs, other clones, or provider caches. Rotate the secret first and treat the history rewrite as tidying up afterwards, never as the fix itself.