Git undo last commit: keep the changes, stay safe
reset --soft keeps them staged, reset HEAD~1 unstaged, revert for shared branches — three lanes, one wrong commit, and reflog the gutter beneath all three.
■ space · prose lane
Undo the last commit, keep the changes: git reset --soft HEAD~1 (staged) or git reset HEAD~1 (unstaged).
Already pushed? git revert HEAD — a new commit that undoes it, safe on shared branches.
reset --hard is the only line that deletes work — and git reflog still knows, for ~90 days.
THE THREE LANES
One wrong commit, three exits. The flag decides where your changes land — follow the lanes:
fig — the only lane that deletes work is --hard; the reflog is the gutter running beneath all three
KEEP THE CHANGES: THE TWO COMMANDS
The most common situation: you committed, then spotted a typo, a missing file, or realised the change belongs in a different commit. Nothing is pushed yet. Undo the commit and put everything back:
# commit gone — changes go back to the staging area
git reset --soft HEAD~1
# changes go back to the working tree (unstaged) instead
git reset HEAD~1
HEAD~1 means "one commit before where HEAD points now". After either command your files are untouched on disk — only the branch pointer moved. Check with git status: with --soft the changes are staged, ready to re-commit with fixes folded in; without a flag they are unstaged, so you can edit freely first.
verify: git status → staged (--soft) or unstaged (default) · git log --oneline -1 → the commit before
A safer habit when you just want to add files to the last commit is not to undo it at all:
git add forgotten-file.txt
git commit --amend --no-edit
--amend replaces the last commit in place (no message change here). Note that amending a pushed commit rewrites history — more below.
THE THREE FLAGS, SIDE BY SIDE
This is the part worth memorising, because the flag decides where your changes end up — and whether they can be lost:
| Flag | Commit undone? | Changes on disk | Staged? | Typical use |
|---|---|---|---|---|
--soft | Yes | Kept | Yes | Re-commit with small fixes |
--mixed (default) | Yes | Kept | No | Re-group changes, re-stage selectively |
--hard | Yes | Deleted | — | Throw the work away entirely |
git revert | No (new commit) | Kept | — | Undo a commit that was already pushed |
--hard is the only dangerous one: it discards the commit and the changes. Before any hard reset, stash or branch what you have:
git branch backup-before-reset # cheap insurance
git reset --hard HEAD~1 # commit + changes gone
Already ran the dangerous version? All is not lost — git reflog remembers where HEAD has been. The reflog keeps dangling commits for around 90 days by default, so "I hard-reset by mistake" is almost always recoverable if you act before garbage collection:
git reflog # find the commit hash you lost
git reset --hard HEAD@{1} # or: git reset --hard <hash>
verify: git reflog → the dropped commit sits at HEAD@{1}
ALREADY PUSHED?
Shared history gets a new commit, never a rewrite. If the commit is on a shared branch — anything other than your own feature branch — do not rewrite history. git revert computes the opposite change and commits it. Everyone who pulls simply gets a new commit that removes the old one's changes. No force-push, no broken colleagues:
git revert HEAD
git push
Undoing a run of commits is the same move over a range: git revert --no-commit HEAD~3..HEAD && git commit. The alternative — git reset --hard HEAD~1 && git push --force-with-lease — is only acceptable on a branch nobody else builds on: --force-with-lease refuses the push if the remote moved since your last fetch; plain --force overwrites whatever appeared. Force-pushing shared branches is how teams lose commits and CI logs mysteriously stop matching anyone's checkout.
verify: git log --oneline -2 → Revert "…" sits on top of the undone commit
GOOD WORK, WRONG ADDRESS
Sometimes the commit is fine — wrong branch, or too early. Instead of undoing it, move it. Park the whole commit on a new branch, then clean your current branch — or take just that commit to another branch without touching the current one:
git branch stash-commit # park the commit on a new branch
git reset --hard HEAD~1 # then clean your current branch
git cherry-pick <hash> # while on the target branch
Between reset --soft, cherry-pick and revert, there is no commit you cannot relocate or nullify — the trick is choosing "move" versus "undo" before reaching for a flag.
THE WORD IS THE COMMAND
Half the confusion is vocabulary: six verbs people type into the search box, three commands that actually exist. Match the word in your head to the flag that does it:
| The word you typed | What you want | The command | Changes kept? |
|---|---|---|---|
| undo / uncommit | commit gone, work back | git reset --soft HEAD~1 | yes — staged |
| undo, unstaged | commit gone, work back loose | git reset HEAD~1 | yes — mixed |
| remove / drop | commit and edits gone | git reset --hard HEAD~1 | no — reflog ~90 days |
| revert / cancel, pushed | public undo, history intact | git revert HEAD | yes — new commit |
The verbs are not synonyms in Git: revert never touches the past, reset rewrites it. Say the wrong word to a teammate and you get the wrong command. The whole decision, as a checklist:
- Not pushed, want to fix and re-commit →
git reset --soft HEAD~1 - Not pushed, want to re-stage selectively →
git reset HEAD~1(mixed) - Want the changes gone completely →
git reset --hard HEAD~1(reflog knows, if you regret it) - Already pushed to a shared branch →
git revert HEAD - Commit belongs on another branch →
cherry-pick, don't undo
Want the decision made for you? The git-undo generator asks these questions and prints the command. And one last operational tip: if a bad commit did make it to a server — a botched deploy hook, a CI runner misbehaving after a force-push — the next place to look is the machine's logs, not Git: on any systemd box, journalctl -u <service> -n 100 shows you exactly what ran and when. And when Git refuses to run at all — fatal: not a git repository — that is a different problem from undoing anything.
end of the undo page · nothing fetched · everything local