TechEarl

How to Undo the Last Git Commit

Undo your last Git commit without losing work. When to use amend, reset --soft, reset --mixed, reset --hard, and revert, plus the rule for commits you already pushed.

Ishan Karunaratne⏱️ 7 min readUpdated
Share thisCopied
How to undo the last Git commit using amend, reset, and revert

To undo your last Git commit but keep all the changes staged, run git reset --soft HEAD~1. The commit disappears, your files are untouched, and everything is staged ready to recommit. That single command covers the most common case. Which command you actually want depends on two questions: do you want to keep the changes or throw them away, and have you already pushed the commit.

Here is the short version before the detail:

bash
# Undo the commit, keep changes staged (most common)
git reset --soft HEAD~1

# Undo the commit, keep changes but unstage them
git reset --mixed HEAD~1   # or just: git reset HEAD~1

# Undo the commit AND throw the changes away (destructive)
git reset --hard HEAD~1

# Just fix the last commit's message or add a forgotten file
git commit --amend

# Undo a commit you already pushed (safe for shared branches)
git revert HEAD
Undoing the last commit with git reset --soft in the terminal
git reset --soft HEAD~1 removes the commit but keeps your changes staged.

If you are still finding your feet with Git, the Git for beginners guide walks through the whole mental model first. This page assumes you have made a commit you want to take back.

The one rule that decides everything: have you pushed?

There are two families of "undo" in Git, and the only thing that decides which family you can safely use is whether the commit has left your machine.

  • Not pushed yet (local only): you can rewrite history freely. reset and amend are fine. They edit the commit graph as if the bad commit never existed.
  • Already pushed to a shared branch: do not rewrite history. Other people may have pulled that commit. Use revert, which creates a new commit that undoes the changes, leaving the original in place.

Rewriting a commit that someone else has already pulled is how you end up with the divergent-branches mess and force-push arguments. If the commit is on main (or any branch a teammate tracks) and it has been pushed, reach for revert. Everything else in this article is for commits that are still local.

Keep the changes: git reset --soft HEAD~1

This is the command most people actually want. It removes the last commit from history but leaves your working tree and the staging area exactly as they were. The changes from that commit are now staged, waiting for you to commit again.

bash
git reset --soft HEAD~1

HEAD~1 means "one commit before the current HEAD", so this moves the branch pointer back by one commit. Use it when you committed too early, picked a bad commit message, or want to fold a couple of commits together by recommitting. After running it, git status shows your changes staged in green:

text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/app.js

Recommit when you are ready:

bash
git commit -m "A better message this time"

Keep the changes but unstage them: git reset --mixed HEAD~1

--mixed is the default mode of git reset, so git reset HEAD~1 does the same thing. It removes the last commit and keeps your file changes, but it unstages them. The edits are still in your working tree; they just are not in the staging area anymore.

bash
git reset --mixed HEAD~1
# identical to:
git reset HEAD~1

Reach for this when you want to re-stage selectively, splitting one fat commit into several smaller ones, for example. After the reset, git status shows the changes as unstaged ("Changes not staged for commit"), and you re-add the pieces you want with git add. If the difference between staged and unstaged is fuzzy, the Git staging area explained breaks down exactly what git add does and why the index sits between your files and a commit.

Throw the changes away: git reset --hard HEAD~1

--hard is the destructive one. It removes the last commit and discards the changes from your working tree and staging area. After this, the work in that commit is gone from your normal view of the repo.

bash
git reset --hard HEAD~1

Only use this when you are certain you want to lose the work, for example a commit full of debugging junk you never want to see again. There is one safety net: a reset moves a branch pointer, and Git keeps a log of every place HEAD has been. If you run --hard and panic, you can usually get the commit back from the reflog for a while (the dangling commit lives until garbage collection). I wrote a full walkthrough of that recovery in recover lost commits with git reflog. For discarding uncommitted edits rather than a whole commit, see how to discard local changes in Git.

If you want to keep the changes but stash them out of the way instead of destroying them, git stash is the gentler option.

Just fix the last commit: git commit --amend

Sometimes you do not want to undo the commit at all. You typed a typo in the message, or you forgot to stage one file. git commit --amend rewrites the most recent commit in place instead of removing it.

Fix the message:

bash
git commit --amend -m "The corrected commit message"

Add a file you forgot, keeping the same message:

bash
git add forgotten-file.js
git commit --amend --no-edit

--amend replaces the last commit with a new one (it gets a new commit hash, because the content or message changed). That is still a history rewrite, so the pushed-commit rule applies: amending a commit you already pushed means a force-push to a non-fast-forward remote, which you should avoid on shared branches. For getting commit messages right the first time so you amend less often, see my notes on Git commit message best practices.

Already pushed it? Use git revert

If the commit is already on a shared remote, do not rewrite it. git revert creates a brand-new commit that is the inverse of the one you name, so history stays intact and everyone's clones reconcile cleanly on the next pull.

bash
git revert HEAD

Git opens an editor with a prefilled message like Revert "the original subject"; save and close it (or pass --no-edit to accept the default). You then push the new revert commit like any other commit. The bad change is undone for everyone, and the original commit is still in the log as a record of what happened. For the full comparison of these two approaches and the decision tree, I have a dedicated piece: git reset vs git revert: when to use each.

Which command should I use?

SituationCommandKeeps changes?Rewrites history?
Committed too early, want to recommitgit reset --soft HEAD~1Yes, stagedYes (local only)
Want to re-stage selectivelygit reset HEAD~1Yes, unstagedYes (local only)
Commit was pure junk, drop itgit reset --hard HEAD~1No, discardedYes (local only)
Just a bad message or a missing filegit commit --amendYesYes (local only)
Commit is already pushed and sharedgit revert HEADNew inverse commitNo, adds a commit

The pattern is consistent: reset and amend edit local history and are off-limits once a commit is shared; revert adds a new commit and is the only safe choice for anything that has been pushed. When you are working alone on a feature branch, branching gives you room to rewrite local history freely without affecting anyone.

Undoing more than one commit

HEAD~1 walks back one commit; HEAD~2 walks back two, and so on. To undo the last three local commits but keep all their combined changes staged:

bash
git reset --soft HEAD~3

For surgical edits across several recent commits (reword, drop, or combine specific ones rather than blanket-resetting them all), interactive rebase is the right tool. The same pushed-commit rule still applies: rewriting commits that are already shared causes pain for everyone tracking the branch.

FAQ

Sources

Authoritative references this article was fact-checked against.

Tagsundo git commitGitVersion Controlgit resetgit revertgit amend

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Find the Largest Files on Disk (find, sort, du)

find / -xdev -type f -printf '%s %p\n' | sort -rn | head -20 gives you a ranked list of the biggest files on a full disk. The GNU one-liner, the BSD/macOS stat variant, why -xdev matters, human-readable output with numfmt, when to switch to du or ncdu for per-directory totals, and the mistakes that send a scan into /proc.

How to Use git stash

How to use git stash to set work aside without committing. Save, list, pop, apply, and drop stashes, stash untracked files, do a partial stash, and switch branches cleanly.

How to rsync Only the Files find Selected

rsync has no native time filter, so the standard trick is to let find pick the files and feed the list to rsync. The one-liner is find ... -print0 | rsync --files-from=- --from0, and the failure mode is always the same: the paths in the list have to be relative to the rsync source argument. The breakdown, the dry run habit, and when rsync's own filters make find unnecessary.