You committed something you did not mean to: the wrong files, a secret, a half-finished change, or simply a bad message. Here is the situation in a real repository (Git 2.54.0), and what the most common undo does to it:
$ git log --oneline
d16d9b6 Add pricing and env
fe08f32 Add app skeleton
$ git reset --soft HEAD~1
$ git status --short
A .env
A pricing.js
$ git log --oneline
fe08f32 Add app skeletonThe commit is gone from the branch, and its changes are back in the staging area, ready to be recommitted differently. “Undo the last commit” actually covers four different operations, and the right one depends on two questions: has the commit been pushed, and do you want to keep its changes.
Quick fix checklist
- Not pushed, keep changes staged:
git reset --soft HEAD~1 - Not pushed, keep changes but unstage them:
git reset HEAD~1(same as--mixed) - Not pushed, discard the changes entirely:
git reset --hard HEAD~1(checkgit statusfirst) - Just fix the message or add a forgotten file:
git commit --amend - Already pushed to a shared branch:
git revert HEAD, then push - Undid the wrong thing:
git reflog, thengit reset --hard HEAD@{1}
Before you start
Run git status and git status -sb. The first shows uncommitted work that --hard would destroy. The second’s first line shows whether you are ahead of the remote: ## main...origin/main [ahead 1] means the last commit exists only locally, so rewriting it is safe. If there is no [ahead N], the commit is already on the server.
You should also know Git’s three areas: the working tree (files on disk), the index or staging area (what the next commit will contain), and HEAD (the commit your branch points to). The reset modes differ only in how many of those they move.
Why it happens
A branch is just a pointer to a commit, and each commit points to its parent. “Undoing” a commit can mean either of two very different things:
- Move the branch pointer back (
git reset). The commit is no longer on the branch, as if it never happened. This rewrites history, which is fine locally but breaks other people’s clones if the commit was already pushed. - Add a new commit that cancels it (
git revert). History keeps both the mistake and the fix. Nobody’s clone breaks, which is why it is the only safe option on shared branches.
git reset then decides what happens to the content of the removed commit:
| Command | Branch pointer | Index (staging) | Working tree |
|---|---|---|---|
reset --soft HEAD~1 |
moves back | keeps the commit’s content (staged) | untouched |
reset --mixed HEAD~1 (default) |
moves back | reset to parent (changes unstaged) | untouched |
reset --hard HEAD~1 |
moves back | reset to parent | reset to parent (changes lost) |
HEAD~1 means “the first parent of HEAD”. On the very first commit of a repository it does not exist, so git reset --soft HEAD~1 fails with fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree. Use git update-ref -d HEAD there instead: it deletes the branch pointer and leaves the files staged under “No commits yet”.
Step-by-step walkthrough
Step 1: Decide whether you are rewriting or reverting
Check git status -sb. [ahead 1] means local only: use reset or amend. No ahead marker, or the branch is shared, means pushed: use revert. If you reset a commit that is already on the server and then push, the push is rejected as non-fast-forward, and forcing it removes the commit from everyone else’s history.
Step 2: Reset with the mode that matches your intent
Recommitting only part of a commit is a typical case. Here .env should never have been committed:
$ git reset HEAD~1
$ git status --short
?? .env
?? pricing.js
$ git add pricing.js && git commit -qm "Add pricing"
$ echo ".env" >> .gitignoreWith --mixed, the files come back as untracked (they were new) or as modified. For an edited file Git prints a summary:
$ git reset HEAD~1
Unstaged changes after reset:
M pricing.jsUse --soft when the content is right but you want a different commit shape, for example to squash the last three commits into one: git reset --soft HEAD~3 && git commit.
Step 3: Amend when the commit is almost right
If you forgot a file or misspelled the message, there is no need to undo anything:
$ git add pricing.js
$ git commit --amend --no-edit
[main e406c04] Raise price to 12
Date: Sun Oct 4 18:25:35 2026 +0530
1 file changed, 2 insertions(+), 1 deletion(-)
$ git commit --amend -m "Raise price to 12 and round"
[main 33d3da3] Raise price to 12 and round
Date: Sun Oct 4 18:25:35 2026 +0530
1 file changed, 2 insertions(+), 1 deletion(-)Amend replaces the last commit with a new one (new ID; the original author date is kept), so the same pushed-or-not rule applies as for reset.
Step 4: Revert a commit that is already pushed
$ git revert --no-edit HEAD
[main e86d8c9] Revert "Raise price to 12 and round"
Date: Sun Oct 4 18:25:39 2026 +0530
1 file changed, 1 insertion(+), 2 deletions(-)
$ git push origin main
33d3da3..e86d8c9 main -> mainThe push is a fast-forward because history only grew. To revert a merge commit you must say which parent is the mainline: git revert -m 1 <merge-sha>.
Step 5: Recover from the wrong undo with reflog
Every move of HEAD is recorded in the reflog, so a commit removed by reset --hard is still reachable for a while (90 days by default for reachable entries, 30 for unreachable ones):
$ git reset --hard HEAD~1
HEAD is now at 35c481d Add pricing
$ git reflog -3
35c481d HEAD@{0}: reset: moving to HEAD~1
9d56f40 HEAD@{1}: commit: Raise price to 12
35c481d HEAD@{2}: commit: Add pricing
$ git reset --hard HEAD@{1}
HEAD is now at 9d56f40 Raise price to 12What the reflog cannot bring back is work that was never committed. --hard overwrites modified tracked files in the working tree without a trace, which is why it deserves a git status check (or a git stash) first.
Worked scenario
Priya commits a config change with an API key in .env, pushes to her feature branch, and only then notices.
Broken instinct:
git reset --hard HEAD~1 # the key file is gone locally...
git push --force # ...and from the branch tipDiagnosis: two problems. --hard also deleted her other uncommitted edits. And the key is still on the server: the old commit remains reachable by its ID, and anyone who fetched has it. Rewriting history does not un-leak a secret.
Fix:
- Rotate the key at the provider first. Once pushed, treat it as public.
- Remove the file going forward, keeping history intact on the shared branch:
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "Stop tracking .env"
git push- If policy requires scrubbing history, use a dedicated tool such as
git filter-repoand coordinate a force push, accepting that every clone must be re-synced.
Had the commit not been pushed, git reset HEAD~1 followed by adding .env to .gitignore and recommitting would have been the whole fix.
Common mistake
Using --hard as the default undo. It is the only mode that destroys data reflog cannot recover. Start with --soft or --mixed; you can always discard the changes afterwards with git restore.
Resetting a pushed commit and force-pushing a shared branch. Teammates who pulled the old commit get have diverged messages, and a careless git pull brings the commit straight back. On shared branches, revert.
Confusing git revert with “go back to that version”. git revert X undoes the changes introduced by X only, not everything after it. To return files to an older state, use git restore --source=<sha> -- <path> and commit the result.
Verify the behavior
After a reset, confirm three things:
git log --oneline -3 # the unwanted commit is no longer at the top
git status --short # changes are where you expected: staged (soft), unstaged (mixed), gone (hard)
git status -sb | head -1 # ## main...origin/main with no [behind] if you never pushed itAfter a revert, git show --stat HEAD should show the inverse of the original diff, and git diff HEAD~2 HEAD should be empty if nothing else changed in between.
Interview exercise
“You pushed a bad commit to main ten minutes ago and two teammates have already pulled. Walk me through undoing it, and explain why git reset is the wrong tool here even though it is how you would undo a local commit.”
Answer and reasoning
I would run git revert <sha> (or git revert HEAD if it is still the tip), check the result builds and tests pass, and push. That adds a commit applying the inverse diff. History only grows, so the push is a fast-forward and teammates simply pull the revert.
git reset moves the branch pointer back, which is a history rewrite. Pushing it requires a force push, which branch protection on main usually forbids. Even if allowed, teammates’ clones still contain the bad commit; their next pull reports divergence, and a merge-style pull reintroduces it. Reset is perfect before a push because nobody else has the commit yet.
I would also mention the edge cases: reverting a merge needs -m 1 to pick the mainline parent, and if the bad commit leaked a secret, the revert does not remove it from history, so the credential must be rotated regardless.