You ran git commit too early — wrong message, wrong files, or just the wrong moment. Now you need to git undo last commit without losing your work, or without blowing up a shared branch. The problem is git gives you five different ways to do this, each with different consequences. git reset --soft keeps your changes staged. git reset --hard deletes them permanently. git revert creates a new commit that reverses the change — the only safe option after you've pushed. This guide maps every scenario to the right command, explains what each one actually does to your repository's state, and covers the safety check that most tutorials skip entirely: verifying exactly what you're about to lose before running any destructive command.

Quick Answer: Which Command for Which Situation

Pick your scenario. The table below maps every common "undo commit" situation to the right command. Details follow in the sections linked from each row.

Situation Command Changes preserved? Safe after push?
Undo last commit, re-stage everything git reset --soft HEAD~1 Yes — in staging area Local only
Undo last commit, edit files before recommitting git reset HEAD~1 (mixed) Yes — in working tree Local only
Delete the last commit and all its changes git reset --hard HEAD~1 No — changes gone Local only
Undo a pushed commit safely (shared branch) git revert HEAD Yes — via new commit Yes
Fix commit message or add a forgotten file git commit --amend Yes — rewrites last commit Local only
Recover a commit after a bad reset git reflog + git reset --hard <SHA> Yes — if within ~90 days Local only
Undo a pushed commit on a non-shared branch git reset HEAD~1 + git push --force-with-lease Yes (with --soft/--mixed) Risky — coordinate first

The most common mistake: using git reset --hard when you meant git reset --soft. The difference is whether your file edits survive. Read the next section to understand exactly which layer of git state each command touches.

Git's State Layers: Working Tree → Staged → Committed → Pushed

Git tracks your work across four distinct layers. Every undo command operates at a specific layer boundary — once you have this model in your head, every reset and revert command becomes predictable.

Working Tree Staging Index Local Commits Remote (pushed) git add git commit git push Git's four state layers local — safe to rewrite shared
The four layers of git state. Commands on shared branches (right of the dashed line) cannot be rewritten without force-pushing.
  • Working tree: Files on disk as you're editing them. No git knows about these changes until you git add them.
  • Staging area (index): The snapshot that will become your next commit. Files land here after git add. You can stage partial changes, or stage and then unstage before committing.
  • Local commits: Permanent-ish snapshots stored in .git/objects, referenced by SHA hashes. "Permanent-ish" because local commits can be rewritten until they leave your machine.
  • Remote (pushed) commits: Once commits reach a shared remote — GitHub, GitLab, Bitbucket — other developers may have pulled them. Rewriting history at this point requires force-pushing, which breaks everyone else's local copy of the branch.

The layer where your changes currently live determines which undo command to reach for. git reset moves the HEAD pointer backward in your local commit history, optionally adjusting what happens to the staging area and working tree. git revert never touches existing commits — it creates a new commit that reverses the changes, making it safe for any branch that others are watching.

When you need to inspect what a commit actually changed before undoing it, diff it first. The git diff between two files guide covers how to compare a specific commit against its parent before you reset anything.

git reset --soft HEAD~1 — Undo Commit, Keep Changes Staged

This is the safest of the three reset modes. It moves HEAD back by one commit but leaves the staging area and working tree completely untouched. Your files stay exactly as they were — the only thing that disappears is the commit object itself.

# Undo the last commit; all changes remain staged (git add already done)
git reset --soft HEAD~1

# Verify: your changes are in the staging area, ready to re-commit
git status
git diff --staged

After running this, git status will show your files under "Changes to be committed" — the same state they were in immediately before you ran git commit. You can now:

  • Edit the commit message and recommit: git commit -m "corrected message"
  • Add more files with git add and then commit
  • Split the staged changes into multiple commits using git add -p
  • Unstage specific files with git restore --staged <file> before recommitting

When to use it: You committed too early (forgot a file, or need to adjust the message before the commit leaves your machine). The --soft flag preserves changes in the staging index — nothing is unstaged or deleted.

To undo the last two commits while keeping everything staged:

# Go back two commits, staging area untouched
git reset --soft HEAD~2

git reset --mixed HEAD~1 — Undo Commit, Keep Changes Unstaged

--mixed is the default when you run git reset HEAD~1 without a flag. It moves HEAD back and resets the staging area to match the new HEAD, but the working tree is untouched. Your edits survive as unstaged changes.

# These two commands are identical — --mixed is the default
git reset HEAD~1
git reset --mixed HEAD~1

# After: files are in working tree as unstaged edits
git status
# On branch main
# Changes not staged for commit:
#   modified:   src/app.ts
#   modified:   src/config.ts

The --mixed (default) mode moves changes to the working tree. This is different from --soft, which keeps them in the staging area. The practical difference: after a mixed reset you need to run git add before you can recommit. After a soft reset you can recommit immediately.

When to use it: You want to re-examine your changes, possibly edit them further, before deciding what to stage and commit. Also the right choice when you need to git undo local commit work and want maximum flexibility about what goes into the next commit, or when you plan to reset last commit git style but keep every edit visible in the working tree first.

# Undo last three commits, keep all changes as unstaged edits
git reset HEAD~3

# Now selectively re-stage only what belongs in each commit
git add -p src/app.ts
git commit -m "feat: add user auth middleware"

git add -p src/config.ts
git commit -m "chore: move config to env vars"

git reset --hard HEAD~1 — Delete the Commit AND Its Changes

--hard is destructive. It moves HEAD back, resets the staging area, and resets the working tree to match the new HEAD. Any changes in that commit — and any uncommitted changes in your working tree — are discarded.

# DESTRUCTIVE: deletes the last commit and all its file changes
git reset --hard HEAD~1

Before you run this, diff what you are about to lose:

# See exactly what is in the last commit before deleting it
git diff HEAD~1 HEAD

# Or show the commit with its full diff
git show HEAD

If you have uncommitted changes in your working tree alongside the commit you want to drop, those disappear too. Check first:

# See all uncommitted changes (staged + unstaged)
git status
git diff HEAD

When to use it: You want to completely remove the last commit and start that work over from scratch — the changes are wrong, experimental, or you accidentally committed a secret. This is also how you git delete latest commit and git discard last commit with no recovery path (short of git reflog within ~90 days). Git's own Git Basics: Undoing Things chapter walks through the same reset modes with more upstream context.

Critical rule: Never run git reset --hard on commits that have already been pushed to a shared branch. If you need to git undo pushed commit work, use git revert instead (see Section 7). For the full flag reference, see the official git-reset documentation.

Comparison Table: --soft vs --mixed vs --hard

What survives after each reset mode --soft --mixed (default) --hard HEAD pointer moved back moved back moved back Staging index preserved (staged) reset to HEAD reset to HEAD Working tree preserved preserved (unstaged) wiped preserved reset / wiped
--soft preserves all your work; --mixed (default) moves changes to the working tree; --hard permanently deletes changes.
Flag HEAD pointer Staging area (index) Working tree (files on disk) Best for
--soft Moved back Unchanged (staged changes preserved) Unchanged Recommitting with a corrected message or added files
--mixed (default) Moved back Reset to match new HEAD Unchanged (edits preserved as unstaged) Re-examining and re-staging changes before recommitting
--hard Moved back Reset to match new HEAD Reset to match new HEAD (changes discarded) Completely discarding the last commit's work

A mnemonic: soft = only the commit label moves; mixed = commit label + staging move; hard = everything moves including your files. The --soft flag preserves changes in the staging index, while --mixed (default) moves them to the working tree. --hard discards both.

All three operate only on your local history. None of them is safe to use after a commit has been pushed to a remote branch that other people are tracking.

Undoing Pushed Commits with git revert (the Safe Path)

Once a commit has been pushed to a shared remote — especially main or any branch other team members have checked out — git reset is the wrong tool. Rewriting history forces everyone else to reconcile their local branches, and on protected branches, the remote will reject the push outright.

git revert takes a different approach: instead of moving HEAD backward, it creates a new commit that applies the inverse of the target commit's changes. History grows forward — no rewriting, no force-push required.

# Revert the most recent commit (creates a new "revert" commit)
git revert HEAD

# Revert a specific commit by SHA
git revert a1b2c3d

# Revert a range of commits (newest first, do not reverse the range)
git revert HEAD~3..HEAD

# Revert without auto-committing (lets you edit the revert commit message)
git revert --no-commit HEAD

After git revert HEAD, your editor opens a commit message pre-filled with "Revert "original commit message"". Save and close it; the revert commit is created. Then push normally — no force required:

git push origin main

When revert is the right choice:

  • The commit is on a shared branch (main, staging, etc.)
  • Others may have already pulled the commit
  • The branch has protections that reject non-fast-forward pushes
  • You need a clean audit trail showing what was reversed and when

To review the diff of what git revert will introduce before you commit it, read the unified diff format guide — the revert output is a standard unified diff with additions and deletions flipped from the original.

Revert merge commits: reverting a merge commit requires the -m flag specifying which parent to treat as the mainline:

# Revert a merge commit; -m 1 keeps parent 1 (usually the branch you merged into)
git revert -m 1 <merge-commit-sha>

Undoing Pushed Commits with git reset + --force-with-lease (the Risky Path)

There are legitimate cases for rewriting pushed history: a feature branch you own alone, a commit with a hardcoded secret that must disappear from the log, or a personal fork. In these cases, you can reset locally and force-push — but use --force-with-lease instead of --force.

Force-push blast radius git push --force Teammate A diverged history Teammate B lost commits Teammate C merge chaos CI / PR reviews broken SHA refs Use --force-with-lease on branches you own alone
Every teammate's local branch diverges when you rewrite shared history. Coordinate before force-pushing and never force-push to main or master.
# Step 1: undo the commit locally (keep changes if you want them)
git reset --soft HEAD~1

# Step 2: verify your local state is what you expect
git log --oneline -5
git diff --staged

# Step 3: force-push — but only with --force-with-lease
git push --force-with-lease origin feature/my-branch

--force-with-lease is safer than --force because it checks that your local tracking ref matches what the remote reports. If someone else pushed to the branch since your last fetch, the push is rejected — preventing you from accidentally overwriting their work. Plain --force has no such check and will overwrite whatever is on the remote, no questions asked.

What happens to other developers when you force-push:

  • Their local branch still points to the old commit SHA. If they pull, git will see a diverged history and may refuse or create a merge commit.
  • They need to run git fetch and then git reset --hard origin/feature/my-branch to realign with the remote — discarding any local commits they had on top.
  • Open pull requests targeting the branch may lose their review comments if the commit SHAs change.

Rule: Only force-push to branches where you are the sole developer, or after coordinating with all team members. Never force-push to main, master, or any release branch.

For Windows users handling these operations in PowerShell, the PowerShell diff guide covers how to inspect file states before and after a reset outside a Unix shell.

git commit --amend — Fix the Last Commit Instead of Undoing It

If the commit itself is fine but you need to fix the message, add a forgotten file, or make a small correction to the content, git commit --amend is cleaner than resetting and recommitting. It rewrites the last commit in place — same parent, same tree changes (plus anything you've staged), new SHA.

# Fix only the commit message (opens your editor)
git commit --amend

# Fix the message without opening an editor
git commit --amend -m "feat: add user authentication middleware"

# Add a forgotten file to the last commit without changing the message
git add src/forgotten-file.ts
git commit --amend --no-edit

# Add a change AND update the message
git add src/app.ts
git commit --amend -m "feat: add auth middleware and fix session timeout"

--amend produces a new commit SHA. This means it rewrites history, just like git reset. The same rule applies: only amend commits that have not yet been pushed to a shared branch. If you have already pushed and want to amend, you need to force-push afterward — follow the same cautions as Section 8.

When amend beats reset:

  • You only need to change the commit message — no file changes involved
  • You forgot one file but everything else about the commit is correct
  • You want to update the author or timestamp: git commit --amend --reset-author

When reset beats amend:

  • You want to split the commit into multiple smaller commits
  • You want to change which files are in the commit (not just add one)
  • You want to completely scrap the commit's work (--hard)

Recovering a "Lost" Commit with git reflog

You ran git reset --hard HEAD~1 and realized immediately it was a mistake. The commit seems gone — it's no longer in git log. But git keeps a reference log of every position HEAD has ever pointed to, for ~90 days by default. That record is the reflog, and it's your recovery path.

Recovering a lost commit with git reflog A a3f9e12 good commit B b7c4d81 bad commit git reset --hard HEAD~1 (HEAD now at A, B seems gone) $ git reflog a3f9e12 HEAD@{0}: reset: moving to HEAD~1 b7c4d81 HEAD@{1}: commit: bad commit a3f9e12 HEAD@{2}: commit: good commit git reset --hard b7c4d81 commit B recovered!
Even after git reset --hard, the reflog records every HEAD position. Locate the lost SHA and reset back to it within ~30–90 days.
# Show the reflog — every recent HEAD position with timestamps
git reflog

# Example output:
# a3f9e12 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
# b7c4d81 HEAD@{1}: commit: feat: add payment webhook handler
# a3f9e12 HEAD@{2}: commit: chore: update dependencies
# 91d2c55 HEAD@{3}: commit: fix: correct tax calculation rounding

In this output, b7c4d81 is the commit you just reset away. To recover it:

# Option A: restore HEAD to the lost commit (discards any work done since)
git reset --hard b7c4d81

# Option B: create a new branch at the lost commit (safe — doesn't move main)
git checkout -b recovery/payment-webhook b7c4d81

# Option C: cherry-pick the lost commit onto your current branch
git cherry-pick b7c4d81

Before restoring, verify the commit is what you think it is by diffing it against the current state. This is exactly where a visual diff pays off — paste the two versions into Diff Checker to see the exact changes side by side before deciding how to reintegrate them. For Unix-side diff workflows, the diff command in Unix guide covers how to produce and compare file snapshots directly from the filesystem if you've exported them before the reset.

# Inspect the lost commit before restoring it
git show b7c4d81

# Diff the lost commit against your current HEAD
git diff HEAD b7c4d81

# Diff a specific file between lost commit and current state
git diff HEAD b7c4d81 -- src/payments/webhook.ts

Reflog expiry: By default, unreachable commits (those no longer referenced by any branch or tag) are kept for 30 days; commits reachable from a ref are kept 90 days. git gc prunes objects outside this window. If you've let too much time pass, the SHA may no longer resolve — another reason to act quickly after an accidental reset.

The Diff Checker validation trick: After recovering a commit with git reset --hard <SHA>, run git diff HEAD~1 HEAD to produce the recovered commit's full diff. Copy it into a visual side-by-side tool to confirm all expected changes are present before continuing your work. This is especially useful when recovering a large commit with many files touched.

Safety Framework and the 5 Mistakes Beginners Make

Pre-reset safety checklist Has this commit been pushed? git log --oneline origin/main..HEAD — zero results = local only, safe to reset Do you need the changes in the commit? git show HEAD — use --soft or --mixed to keep them; --hard destroys them ! Do you have uncommitted work in your tree? git status — stash it first: git stash, then reset, then git stash pop ! Are teammates working on this branch? Coordinate first — use git revert instead of reset for shared branches go — proceed with reset caution — address before continuing
Run through these four checks before any destructive reset. Red items require action — not just acknowledgment — before you proceed.

Every destructive git operation follows the same safety pattern: inspect, confirm, execute, verify. Skipping inspection is where most data loss happens.

The pre-reset checklist

# 1. What is the last commit? What does it contain?
git log --oneline -5
git show HEAD

# 2. Do I have uncommitted changes that --hard will destroy?
git status
git diff HEAD

# 3. Has this commit been pushed?
git log --oneline origin/main..HEAD   # shows local-only commits

# 4. Is anyone else working on this branch?
git log --oneline HEAD..origin/main   # shows remote-only commits (they pushed)

If step 3 shows zero commits, the commit is local-only and any reset mode is safe. If step 3 shows commits, decide between git revert (safe) or a coordinated force-push (risky).

Mistake 1: Using --hard when you meant --soft

The most common mistake when trying to git undo last commit keep changes. The fix is reflog recovery (Section 10). Prevention: default to git reset --soft HEAD~1 — you can always unstage changes afterward, but you cannot unstage changes that were deleted.

Mistake 2: Resetting a pushed commit on a shared branch

Running git reset HEAD~1 on main after pushing. The local branch now diverges from the remote. Pushing without --force fails; pushing with --force overwrites teammates' work. Use git revert for pushed commits on shared branches — always.

Mistake 3: Confusing git revert with git reset

git revert HEAD creates a new commit; it does not move HEAD back. git reset HEAD~1 moves HEAD back; it does not create a new commit. They produce opposite results in terms of history length. When someone asks "how do I revert recent commit git?", the answer depends entirely on whether they want history to grow (revert) or shrink (reset).

Mistake 4: Not checking for uncommitted work before --hard

git reset --hard HEAD~1 resets both the commit and any uncommitted work in your working tree. If you had unsaved debug changes, half-written code, or unstaged edits, they disappear along with the commit. Always run git status and git stash any work you want to keep before a hard reset:

# Stash current uncommitted work before a hard reset
git stash
git reset --hard HEAD~1
git stash pop   # restore your in-progress work

Mistake 5: Using --force instead of --force-with-lease

git push --force overwrites the remote regardless of what's there. If a colleague pushed between your last fetch and your force-push, their commits are gone. --force-with-lease prevents this by failing if the remote has moved ahead of your tracking ref. Make --force-with-lease your default anytime a force-push is genuinely necessary:

# Safer than --force: fails if someone else pushed since your last fetch
git push --force-with-lease origin feature/my-branch

The visual validation habit

The single biggest safety improvement for any undo workflow: diff before you destroy. Run git show HEAD or git diff HEAD~1 HEAD before any reset. If the output is long or complex, paste it into a visual side-by-side tool to see exactly what you are about to remove. The unified diff format and Unix diff flags referenced earlier in this guide pair naturally with the recovery commands here — a quick git show before a reset --hard is the cheapest insurance you can buy.

Frequently Asked Questions

How do I undo the last commit but keep my changes?

Run git reset --soft HEAD~1 to undo the last commit and keep all changes staged, ready to re-commit. Run git reset HEAD~1 (mixed default) to undo the last commit and move the changes back to the working tree as unstaged edits. Both commands preserve your work — only the commit object is removed. Use --soft when you want to git undo commit keep changes and recommit quickly; use --mixed when you want to re-examine and selectively re-stage first.

What is the difference between git reset and git revert?

git reset moves HEAD to a previous commit and shortens history — it rewrites the local commit log and is not safe after pushing to a shared branch. git revert creates a new commit that applies the inverse of the target commit's changes, leaving all existing history intact. Rule of thumb: if the commit is local-only, use git reset; if it has been pushed to a shared branch, use git revert.

How do I undo a git commit that has already been pushed?

Use git revert HEAD to create a new commit that reverses the pushed commit. This is the safe path for shared branches — it does not rewrite history and requires no force-push. If the branch is yours alone, you can reset locally with git reset HEAD~1 and then push with git push --force-with-lease origin <branch>, but coordinate with anyone who may have pulled the branch first. Never force-push to main, master, or a release branch.

Can I recover a commit after git reset --hard?

Yes, usually. Run git reflog to see every recent HEAD position with SHAs and timestamps. Find the SHA of the commit you want to restore, then run git reset --hard <SHA> to move HEAD back to it, or git checkout -b recovery <SHA> to create a new branch at that commit without disturbing your current branch. Unreachable commits are kept for about 30–90 days before git garbage collection prunes them permanently.

What is the difference between git reset --soft, --mixed, and --hard?

All three move HEAD back, but they differ in what they do to your files. --soft moves only HEAD and leaves the staging area and working tree untouched — your changes stay staged. --mixed (the default) also resets the staging area to match the new HEAD, so your changes become unstaged edits in the working tree. --hard resets HEAD, staging, and working tree — every file change from the removed commit is discarded.