Git Stash and Undoing Changes Safely
Learn to stash work in progress, undo commits with restore, reset, and revert, and recover lost work from the reflog — with clear warnings about which commands rewrite history.
What you'll learn
- ✓How to stash uncommitted work, list stashes, and pop them back
- ✓How git restore discards changes in the working tree and index
- ✓The three modes of git reset (soft, mixed, hard) and when each is safe
- ✓Why git revert is the right tool for undoing shared commits
- ✓How to recover seemingly lost commits with git reflog
Prerequisites
- •Comfortable with everyday Git — see Install Git and Make Your First Commit
- •Understand branches — see Git Branching Basics
Most Git fear comes from a small handful of commands that look similar but do very different things. reset, restore, revert, stash, checkout — they all “undo” something. This post sorts out which is which, when each is safe, and how to rescue work that looks lost.
A useful rule up front: any commit Git has ever seen is recoverable for at least 30 days, even if it looks gone. The reflog (covered at the end) is your safety net.
Stashing: parking work in progress
You are halfway through a feature when an urgent bug comes in. You need to switch branches, but git switch refuses because of uncommitted changes. You do not want to commit half-broken code.
git stash is the answer. It saves your uncommitted changes (both staged and unstaged) and reverts the working tree to a clean state:
git stash push -m "wip: refactoring auth flow"
# Saved working directory and index state On feature-auth: wip: refactoring auth flow
Now git status is clean and you can switch branches freely.
Some teams write git stash (no push) — same thing. push -m "..." is the modern, explicit form.
Listing and inspecting stashes
git stash list
# stash@{0}: On feature-auth: wip: refactoring auth flow
# stash@{1}: On main: experimental layout
Stashes form a stack — stash@{0} is the most recent. To see what is inside one:
git stash show stash@{0} # summary
git stash show -p stash@{0} # full diff
Bringing changes back: pop vs apply
pop re-applies the latest stash and removes it from the list:
git stash pop
apply re-applies the stash but leaves it in the list:
git stash apply stash@{0}
Use apply when you might want the stash on multiple branches, or as a safety net while you confirm everything still works. Use pop when you are done with it.
Dropping a stash
git stash drop stash@{0} # remove just one
git stash clear # remove ALL stashes (irreversible!)
git stash clear is genuinely destructive — there is no undo. Use it only when you are certain.
Try it yourself. In a practice repo, edit a tracked file but do not commit. Run git stash push -m "test", then git status (clean), then git stash list. Bring the change back with git stash pop and confirm with git status that your edit is restored.
Discarding changes with git restore
git restore is the modern, focused command for throwing away changes in the working tree or index. It replaced confusing forms of git checkout in Git 2.23+.
Discard unstaged changes to a file (revert to what’s committed):
git restore file.txt
Discard unstaged changes to everything:
git restore .
Unstage a file (move it from staged back to the working tree, keeping the edit):
git restore --staged file.txt
The mental model:
- No flag — work on the working tree.
--staged— work on the index (staging area).- Both — discard both.
git restore --staged --worktree file.txt
Warning.
git restore file.txt(without--staged) discards your edits permanently if they were never committed. There is no stash, no undo, no reflog. If in doubt,git stashfirst, then restore.
git reset — the three modes
git reset moves the current branch pointer to a different commit. The three modes differ in what they do with your working tree and index along the way.
Imagine your branch looks like this:
A---B---C (HEAD -> main)
You decide you want main to point at B instead. Three options:
--soft — move only the branch pointer
git reset --soft B
Result:
mainpoints atB.- The index and working tree still contain
C’s changes — already staged.
Useful when you want to combine commits: reset back, then make a single new commit.
--mixed (default) — move pointer and reset index
git reset B # same as: git reset --mixed B
Result:
mainpoints atB.- The index matches
B. - The working tree still has
C’s changes — unstaged.
Useful when you want to redo the commit completely, perhaps choosing different hunks.
--hard — move pointer, reset index, reset working tree
git reset --hard B
Result:
mainpoints atB.- Index matches
B. - Working tree matches
B. C’s changes are gone from disk.
Warning.
git reset --harddiscards uncommitted changes silently and immediately. It is the single most common way developers accidentally lose work. Always rungit statusfirst; considergit stashbefore--hard.
The commit C itself is still in the repository — the reflog can recover it — but if C was never committed, the data is genuinely gone.
Resetting relative to HEAD
You usually do not type the full hash. Use HEAD~1, HEAD~2, etc:
git reset --soft HEAD~1 # undo last commit, keep changes staged
git reset HEAD~1 # undo last commit, keep changes unstaged
git reset --hard HEAD~1 # undo last commit and throw the changes away
revert vs reset: the shared-history rule
git reset rewrites history. If you reset a branch that you have already pushed and others have pulled, you create a mess: their history disagrees with yours, and the next push will be rejected (or force-push will overwrite their work).
The rule:
- Local, unpushed commits —
resetis fine. - Pushed, shared commits — use
revertinstead.
git revert does not delete the bad commit. It creates a new commit that undoes the changes. History stays linear and shared:
git revert abc123
# (opens an editor for the revert commit message)
The result:
A---B---C---D (where D is "Revert C")
Everyone else gets D as a normal new commit when they pull. No history surgery, no force-pushes. Use revert for any commit that has left your machine.
You can revert multiple commits:
git revert --no-commit abc123 def456
git commit -m "Revert two bad changes"
Common undo recipes
A cheat sheet for the situations that come up most often.
”I just committed but the message is wrong”
git commit --amend -m "Correct message"
--amend rewrites the last commit. Safe before pushing, dangerous after — same shared-history rule applies.
”I committed but forgot to add a file”
git add forgotten.txt
git commit --amend --no-edit
--no-edit keeps the existing message.
”I want to undo the last commit but keep the changes”
git reset HEAD~1
The commit is gone, your edits return to the working tree as if you had never committed.
”I want to throw away everything since the last commit”
git restore .
git clean -fd # also remove untracked files and directories
Warning.
git clean -fdis destructive — it deletes untracked files outright. Usegit clean -ndfirst (thenis “dry run”) to see what would be removed.
”I want to undo a published commit”
git revert <commit>
git push
The reflog — your safety net
The reflog records every time HEAD moved on your local machine: commits, resets, rebases, checkouts. Entries are kept for at least 30 days (configurable, but the default is generous).
git reflog
# 7f3e2a1 HEAD@{0}: reset: moving to HEAD~1
# c4d5e6f HEAD@{1}: commit: Add greeting
# b2c3d4e HEAD@{2}: commit: Initial commit
Read top-to-bottom as “most recent action first.” If you accidentally git reset --hard and panic, your “lost” commit is right there at HEAD@{1}. To get it back:
git reset --hard HEAD@{1}
Or, more conservatively, create a new branch pointing at the lost commit:
git branch recovered HEAD@{1}
git switch recovered
The reflog is local-only and per-repository — it does not survive rm -rf .git. Treat it as a strong safety net, not an absolute guarantee.
Try it yourself. In a practice repo: make a commit, run git reset --hard HEAD~1 to “lose” it, then run git reflog to find the hash and git reset --hard <hash> to bring it back. Confirm the commit returned with git log --oneline.
A practical decision flow
When you need to “undo something,” ask in order:
- Have I pushed it? If yes, use
git revert. Stop here. - Are the changes still in the working tree, uncommitted?
- Want to save for later?
git stash push. - Want to discard?
git restore .(with care).
- Want to save for later?
- Is the change in a commit I haven’t pushed?
- Just the message?
git commit --amend. - Roll back the commit, keep the work?
git reset HEAD~1. - Roll back and throw the work away?
git reset --hard HEAD~1— only if you are sure.
- Just the message?
- Did I just lose something?
git reflog, thengit reset --hard HEAD@{N}or create a recovery branch.
That flow covers nearly every undo question you will ever ask Git.
Recap
You now know:
git stashparks uncommitted work;popandapplybring it back.git restorediscards changes in the working tree or unstages files.git resetmoves a branch pointer;--softkeeps staged changes,--mixedkeeps unstaged changes,--hardthrows everything away.git revertis the safe way to undo a commit that has already been pushed — it adds a new commit instead of rewriting history.- The reflog records every move of
HEADfor ~30 days and can recover commits you thought were lost. - The golden rule: reset for local, revert for shared.
Next steps
The next post zooms out to working with other developers — remotes, the difference between fetch and pull, and the pull-request workflow that powers nearly every team on GitHub.
→ Next: Git Remotes and Pull Requests Explained
Questions or feedback? Email codeloomdevv@gmail.com.