Undoing Changes in Git Safely
Fix mistakes in Git: discard edits with restore, move HEAD with reset (soft/mixed/hard), undo a commit with revert, and amend. Which to use, when.

You broke something. A file you didn't mean to touch, a commit with a typo in the message, a git add of the wrong thing, or a commit you wish you'd never made. The good news: Git almost never actually loses your work, and there's a specific command for each of these. The trap is grabbing the wrong one, reaching for git reset --hard when you only wanted to unstage a file, and torching an afternoon of edits in the process.
So let's not memorize commands. Let's build a decision map: what exactly am I trying to undo? Answer that, and the command picks itself.
The one question that picks the command
Before you type anything, figure out where your change lives. Git has three places work can sit: your working directory (files you've edited but not staged), the staging area (changes you've git add-ed but not committed), and history (changes already committed). Each one has its own undo.
Walk that tree top to bottom and you'll never reach for reset --hard when restore was the answer. Now the details for each leaf.
Discard edits you haven't staged
You edited app.py, made it worse, and want the last committed version back. Nothing is staged, nothing is committed. The mess is purely in your working directory.
# throw away changes to one file
git restore app.py
# throw away changes to everything (be sure)
git restore .git restore <file> overwrites your working copy with whatever's in the last commit. This is the modern, purpose-built command. Older guides say git checkout -- app.py, which still works but does five other things too, so restore is clearer about intent.
restore deletes uncommitted work for real
git restore on a file throws away your uncommitted edits to it with no commit to recover from. Git's safety net only covers committed work. If you're unsure, git stash first. That tucks your changes away safely and you can bring them back with git stash pop.
Unstage something you added by mistake
You ran git add . and swept up a file you didn't mean to stage, like a .env, a debug script, whatever. You don't want to lose the edits, you just want them out of the staging area.
# move a file out of staging, keep your edits
git restore --staged secrets.env
# unstage everything, keep all edits
git restore --staged .The --staged flag is the key difference. Without it, restore touches your working files. With it, it only touches the staging area. Your edits stay exactly as they were, just no longer queued for the next commit. (You may have seen git reset HEAD <file> for this. restore --staged is the newer, more readable way to say the same thing.)
Quick check
You ran git add notes.txt but want it unstaged without losing your edits. Which command?
Undo the last commit
This is where people get hurt, so slow down. You committed too early, or with the wrong files, or split work you wanted in one commit across two. You want to undo the commit itself. The question that decides everything: do you want to keep the changes from that commit?
git reset HEAD~1 moves your branch back one commit (HEAD~1 means "one before HEAD"). The flag controls what happens to the work in between.
# undo the commit, keep changes staged (ready to re-commit)
git reset --soft HEAD~1
# undo the commit, keep changes but unstage them (this is the default)
git reset --mixed HEAD~1
# undo the commit AND delete the changes entirely
git reset --hard HEAD~1--soft is the gentlest: the commit is gone but everything it contained is back in your staging area, as if you'd just git add-ed it. Perfect for "oops, wrong message, let me redo the commit." --mixed (the default if you write just git reset HEAD~1) does the same but leaves the changes unstaged, so you re-pick what goes in. --hard is the nuclear option: the commit and its changes vanish from your working directory.
--hard and rewriting shared history
Two ways reset bites. First, git reset --hard discards uncommitted work permanently, and there's no undo for changes that were never committed. Second, reset rewrites history: it changes what commits your branch points to. That's fine on commits only you have, but if you've already pushed that commit, resetting it locally and force-pushing will clobber your teammates' history and break their clones. The rule: never rewrite history that other people already have. For anything shared, use revert (next section).
If you do nuke something with --hard and panic, check git reflog. It logs where HEAD has been, and you can often git reset --hard <old-hash> your way back to a commit you thought was gone. It's a lifesaver, but don't treat it as permission to be reckless.
Safely undo a commit others already have
Say the bad commit is already pushed and a teammate has pulled it. Rewriting history is off the table. Instead of erasing the commit, you add a new commit that reverses it.
# create a new commit that undoes the named one
git revert a1b2c3dgit revert figures out the inverse of that commit's changes and commits them. The original commit stays in history. The log just gains a "Revert ..." commit on top that cancels it out. Because you're only ever adding commits, nobody's history gets rewritten and nobody's clone breaks. This is the safe, collaborative undo.
Think of it this way: reset is editing the past as if the commit never happened, while revert is acknowledging it happened and publicly correcting course. On a shared branch, you almost always want the second one.
Fix the most recent commit
Two real cases that don't need any of the above. You committed and immediately spotted a typo in the message:
# rewrite the last commit's message
git commit --amend -m "fix: handle empty cart at checkout"Or you committed and realized you forgot to include a file:
git add forgotten-file.js
git commit --amend --no-edit--amend replaces the last commit with a new one that folds in whatever's currently staged. --no-edit keeps the existing message. Drop it and your editor opens so you can edit the message too. Clean, and far nicer than a follow-up "fix typo" commit cluttering your history.
Don't amend pushed commits
--amend doesn't edit a commit. It builds a brand-new one with a different hash and throws the old away. That's history rewriting again. Amend freely on commits you haven't pushed. Once a commit is shared, amending it creates the same divergence headache as reset, and you'd have to force-push to publish it. If it's already out there, fix it with a new commit or a revert.
The cheat sheet
When something's wrong, match it to one of these:
- Edited a file, want the committed version back:
git restore <file> - Staged something by accident:
git restore --staged <file>(your edits survive) - Last commit, wrong message or missing a file, not pushed:
git commit --amend - Undo the last commit, keep the work:
git reset --soft HEAD~1(staged) or--mixed(unstaged) - Undo the last commit, trash the work:
git reset --hard HEAD~1, only when you're certain, and only on local commits - Undo a commit others already have:
git revert <hash>
The single rule under all of it: reset and --amend rewrite history, so they're safe locally but dangerous once shared. revert adds history, so it's safe everywhere. When in doubt on a shared branch, revert. Git's own docs go deeper on each of these in Undoing Things.
You came from Merging and conflicts, where the risk was tangled branches. Next we leave your machine entirely and push to GitHub: Remotes and GitHub, where "rewriting shared history" stops being theoretical and these undo choices start mattering for real.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


