How to Undo the Last Commit Without Losing Changes in Git
Quick answer
You committed too early β maybe you forgot a file, wrote the wrong message, or want to split the commit into smaller pieces. You need to undo the commit itself...
You committed too early β maybe you forgot a file, wrote the wrong message, or want to split the commit into smaller pieces. You need to undo the commit itself while keeping every line you changed intact in your working directory.
The Problem
Running git log shows the unwanted commit sitting at the tip of your branch:
$ git log --oneline -3
a1b2c3d (HEAD -> main) fix: typo in auth logic
9e8f7g6 feat: add login endpoint
1234abc initial commit
The instinct to run git reset --hard HEAD~1 is dangerous here β it deletes the commit and wipes out the working directory changes that came with it, which is not what you want if the goal is to keep the edits.
Why It Happens
This isn't an error β it's a normal situation caused by:
- Committing before running tests or a linter and catching a mistake right after.
- Realizing the commit message is wrong or too vague.
- Wanting to break one large commit into several smaller, more reviewable commits.
- Accidentally including a file that shouldn't be tracked (an
.envfile, a build artifact, a debug log).
Git commits are just pointers, so undoing one is safe and cheap as long as you understand what each reset mode does to your working directory and staging area.
The Fix
To undo the commit but keep your changes staged (ready to commit again immediately), use a soft reset:
git reset --soft HEAD~1
Check git status and you'll see your edits sitting in the staging area exactly as they were before the commit, ready to be re-committed with a new message:
git commit -m "feat: add login endpoint with input validation"
If you want the changes unstaged instead β so you can review, split, or partially stage them again β use a mixed reset (this is also the default if you omit the flag):
git reset HEAD~1
Your files remain modified in the working directory, but nothing is staged. From there you can stage selectively:
git add src/login.js
git commit -m "feat: add login endpoint"
git add tests/login.test.js
git commit -m "test: cover login validation"
If you only want to change the commit message and keep everything else identical, skip the reset entirely and amend instead:
git commit --amend -m "fix: correct typo in auth logic"
Still Not Working?
If you already pushed the commit to a remote before realizing the mistake, resetting locally isn't enough β the remote still has the old commit. After resetting and re-committing locally, you'll need to update the remote branch too:
git push --force-with-lease origin main
Only do this on branches you're sure nobody else has pulled from yet, since rewriting pushed history can cause conflicts for collaborators who already have the old commit.
It's also worth knowing the difference between undoing a commit and reverting one, since they solve different problems. Everything above assumes the commit hasn't been shared widely yet, or you're comfortable rewriting history on that branch. If the commit is already public and other people have built work on top of it, rewriting history with reset is risky β instead, create a new commit that undoes the changes with git revert, which preserves history instead of erasing it:
git revert HEAD
This opens an editor for a revert commit message, then adds a brand-new commit that applies the inverse of the unwanted changes, leaving the original commit intact in history. It's the safer choice for anything already pushed to a shared branch like main or develop.
One more edge case worth mentioning: if you've already staged additional new changes on top of the commit you want to undo, a soft reset will bundle those new changes together with the ones from the undone commit in the staging area. If you need to keep them separate, stash the newer changes first, do the reset, then reapply:
git stash
git reset --soft HEAD~1
git commit -m "correct commit message"
git stash pop
This keeps your history clean and avoids accidentally merging two unrelated sets of changes into a single commit.