← Blog
Product Engineering

Remove a Git commit without destroying the recovery path

Removing a Git commit safely starts with a recovery ref and a decision about whether the commit is private or shared. Reset, rebase, revert, and force-with-lease solve different problems.

By Emmanuel Corels

A bad commit can mean four different things: uncommit the latest local snapshot but keep its files, discard its files too, remove an older private commit, or neutralize a commit already shared with a team. Each needs a different operation. The dangerous mistake is reaching for reset --hard before deciding which state must survive.

Freeze the scene before moving a ref

First stop automated pushes and inspect the repository. These commands are observational or create a recovery reference; none rewrites the branch.

git status --short --branch
git log --oneline --decorate --graph -12
git branch backup/before-rewrite
git rev-parse HEAD

Save the displayed object ID in the change record. A backup branch is cheap and explicit. A stash is useful for selected uncommitted work, including untracked files when requested, but inspect it before assuming it contains everything:

git stash push --include-untracked -m 'before history repair'
git stash list
git stash show --stat 'stash@{0}'

Ignored files are not included by that option. Submodules, linked worktrees, Git LFS content, and generated artifacts have their own state. If any matter, audit them separately. Do not run a cleanup command as part of “making the tree safe.”

Choose from the publication state

Latest commit, local only, keep the changes staged

git reset --soft HEAD~1

The branch moves to its parent while the index and working tree retain the commit’s content. This is suitable for correcting the message, splitting the change, or adding a forgotten file. Git’s current reset documentation confirms that reset moves HEAD and that mode controls what happens to the index and working tree.

Latest commit, local only, keep changes but unstage them

git reset --mixed HEAD~1
# --mixed is the default, but spelling it out documents intent

The branch and index move back; working-tree files remain. Review git diff before recommitting.

Latest local commit and its tracked file changes are truly disposable

git reset --hard HEAD~1

This is the narrow case described in the accepted answer. --hard resets the branch, index, and working tree; tracked modifications can be lost. The similarly spelled git reset --hard HEAD does not remove the latest commit—it keeps the branch at the same commit and discards tracked index and working-tree changes.

Older unpublished commit

Use an interactive rebase from the parent of the earliest commit being edited, change pick to drop, then resolve and test:

git rebase -i <bad-commit>^
# In the editor: change only the intended line from pick to drop.

Every descendant is recreated with a new object ID. Merge commits and complex topology need a deliberate plan; do not assume a linear rebase preserves them. A secret committed at any point also requires credential revocation and a full approved history-cleaning procedure—removing one visible commit is not sufficient containment.

Commit already published or consumed by others

git revert <bad-commit>
# resolve if necessary, test, then push normally

Revert records a new commit that reverses the selected patch and leaves shared ancestry intact. Current Git documentation explicitly distinguishes it from reset for this reason. Reverting a merge requires selecting a mainline parent and affects future merge behavior, so review git show --no-patch --pretty=raw <merge> and coordinate before using -m.

If shared history must be rewritten

Sometimes policy requires removing large data or sensitive material from all reachable history. Coordinate a freeze, rotate any exposed credential first, record the remote tip, rewrite with the approved tool, validate a fresh clone, and update only the intended branch.

git fetch origin
git rev-parse origin/main
# rewrite and validate locally
git push --force-with-lease=refs/heads/main:<expected-old-oid> \
  origin HEAD:refs/heads/main

An explicit lease refuses the update if the server’s branch no longer equals the object ID you reviewed. That is safer than blind --force, which can overwrite collaborators’ new commits. Branch protection may correctly reject both; obtain review instead of bypassing it. Tags, release refs, forks, cached build inputs, and existing clones can retain the old objects, so a remote rewrite is a coordinated incident, not deletion from every copy.

Verify content and topology

git status --short --branch
git log --oneline --decorate --graph --all -20
git diff backup/before-rewrite..HEAD
git fsck --no-dangling

Then run the project’s tests, build, linters, dependency checks, and secret scan. For a rewrite, fetch into a clean disposable clone and confirm the remote branch points at the intended ID. Compare the tree as well as the log: deleting the wrong commit can silently remove changes that later commits depended on.

Failure modes and recovery

  • Wrong reset target: move the branch back with git reset --hard backup/before-rewrite only after preserving any new work.
  • Backup branch missing: inspect git reflog. Reflogs record local ref movements and can identify the prior tip, but entries expire and are local—not a backup guarantee.
  • Conflicted revert: resolve, stage, and continue with git revert --continue, or return to the pre-operation state with git revert --abort.
  • Conflicted rebase: resolve and continue, or use git rebase --abort. Compare against the recovery branch afterward.
  • Bad force push: stop further pushes, locate the recorded old object ID in a trusted clone, coordinate, and restore it with an explicit lease.

Do not immediately expire reflogs or run aggressive garbage collection after a mistake; that can remove the objects needed for recovery. Keep the recovery branch until CI, deployment, and collaborators confirm the repaired history.

Production trade-offs

Revert adds history but preserves auditability, signatures on earlier commits, and safe collaboration. Reset or rebase produces a tidier private history but changes object IDs and can invalidate reviews, build provenance, signatures, and downstream work. A force-with-lease reduces concurrency risk but does not make a rewrite harmless. Choose according to who has observed the commit, not according to which log looks cleaner.

Before deleting a commit, make its old tip easy to name. Recovery should be part of the command sequence, not a hope.

Attribution and verification

This guide was prompted by hap497’s Stack Overflow question and gahooa’s accepted answer, used under CC BY-SA. The safer decision tree was verified against current official Git documentation for reset, revert, force-with-lease, and reflog recovery.

Primary source: Review the official reference ↗