Stash: A Shelf for Work in Progress
Here is the situation stash was built for. You are halfway through the checkout page, nothing works yet, and it is nowhere near commit-worthy. Your teammate messages that the login page is broken and the demo is in an hour. You need to switch to main, and Git will refuse because switching would overwrite your unfinished edits.
git stash takes your uncommitted changes, saves them somewhere retrievable, and gives you a clean working directory — as though you had never made them. You fix the login bug, commit, push, come back, and git stash pop puts your half-finished checkout page back exactly as it was.
There is one detail that catches everybody: by default git stash only saves changes to files Git is already tracking. Brand-new files that have never been added are left sitting in your working directory. You stash, switch branches, and find your new file still there, now on the wrong branch, and possibly getting committed by accident. Pass -u to include untracked files.
Stash is a shelf, not a filing cabinet. Stashes have no branch, no message unless you write one, and they accumulate silently. A stash you made three weeks ago will be completely unidentifiable when you find it. Use it for hours, not days — if the work matters, make a commit on a branch instead, because a commit has a message, a place and a history.
# Shelve tracked changes
git stash
# Saved working directory and index state WIP on main: a81c9f2 Fix header
# INCLUDE untracked (new) files — usually what you actually want
git stash -u
# Always give it a message when you have more than one
git stash push -m "WIP: checkout page layout"
# What is on the shelf?
git stash list
# stash@{0}: On main: WIP: checkout page layout
# stash@{1}: WIP on feature/cart: 4b8e0c3 Add quantity selector
# Look inside one without applying it
git stash show -p stash@{0}
# Bring it back and remove it from the list
git stash pop
# Bring it back but KEEP it on the shelf (safer if unsure)
git stash apply stash@{1}
# Turn a stash into a branch — ideal if it has grown into real work
git stash branch feature/checkout stash@{0}
# Remove entries
git stash drop stash@{0}
git stash clear # deletes ALL stashes, no confirmation git stash popcan conflict, just like a merge. If it does, Git leaves conflict markers for you to resolve — and importantly, it does not drop the stash entry when there is a conflict, so your work is still on the shelf if the resolution goes badly. Resolve, stage, thengit stash dropto remove it deliberately.
Reset: Moving the Branch Label Backwards
git reset has a reputation for being dangerous, and the reason is that three very different operations share one command name. Once you see what they have in common, the danger becomes predictable rather than mysterious.
All three do the same core thing: they move the current branch label to point at a different commit, usually an earlier one. Remember from the branching lesson that a branch is only a label. Moving it backwards means the commits after it are no longer on the branch — the commits still exist on disk for a while, but your branch has stopped pointing at them.
What the three modes differ on is what happens to your files and your staging area. That is the entire distinction, and it maps directly onto the three areas from the first lesson: repository, staging area, working directory. --soft rewinds the repository only. --mixed, the default, rewinds the repository and the staging area. --hard rewinds all three, which is why it is the one that can destroy work.
This is much easier to see than to describe, so read the table in the code block carefully — it is the single most useful thing to memorise about reset.
# repository staging area working directory
# git reset --soft moved untouched untouched
# git reset --mixed moved reset untouched (default)
# git reset --hard moved reset RESET <- destroys
# --soft: undo the commit, keep everything staged and ready
# "I committed too early / want to redo the message and content"
git reset --soft HEAD~1
git commit -m "A better message with the same changes"
# --mixed: undo the commit AND unstage, files untouched
# "I committed the wrong set of files; let me re-choose"
git reset HEAD~1
git add only-the-right-file.js
git commit -m "Add only the right file"
# --hard: undo the commit and THROW AWAY the changes
# "That whole experiment was a mistake"
git reset --hard HEAD~1 # DESTRUCTIVE
# Combine three commits into one, keeping all the work
git reset --soft HEAD~3
git commit -m "Add complete registration flow" Why --hard Is the One to Fear
Almost everything in Git is recoverable. Delete a branch and its commits linger, findable through git reflog. Reset a branch backwards and the abandoned commits are still there. Amend, rebase, squash — the originals survive long enough to be retrieved. Git is built so that anything you have committed is very hard to genuinely lose.
git reset --hard is the exception. It overwrites your working directory with the contents of the target commit, so any edit you had made but not committed is simply gone — and because it was never committed, Git has no record of it anywhere. No reflog entry, no dangling object, nothing to recover from. That is the one command in this course that can permanently destroy hours of work with no undo.
Notice the asymmetry: the commits a hard reset moves past are recoverable through the reflog. It is the uncommitted changes it wipes on the way that are not. The risk is entirely about what was sitting in your working directory when you ran it.
Two habits make this safe. Run git status before any hard reset and read what it says you would lose; if there is anything in there, run git stash -u first, which turns an irreversible action into a reversible one. Never run git reset --hard on a branch you have pushed and others have pulled — use git revert, as the next section explains. And note that a hard reset does not remove untracked files; only git clean does, which should always be run with -n first to preview the list.
--soft— safe; nothing is lost, changes stay staged--mixed— safe; nothing is lost, changes return to your working directory unstaged--hard— destructive; uncommitted changes are gone with no recovery path- Committed work survives all three and can be found with
git reflog - Untracked files survive
--hard; onlygit clean -fdremoves them - Never reset a branch that is already pushed and shared — rewrite the history others hold and you will break their repositories
- Before any destructive command, this is the entire safety routine:
git statusto see what is at risk, thengit stash -uif anything is. Two commands, five seconds, and you can never lose an evening's work to a mistyped reset.
Revert: Undoing Safely on a Shared Branch
git revert also undoes a commit, but it works in the opposite direction. Instead of removing the commit from history, it creates a new commit that applies the exact inverse of it. The bad change is neutralised, and both the mistake and its correction remain visible in the history.
That difference is the whole point. Reset rewrites history; revert adds to it. If a branch is only on your machine, rewriting is harmless. If it is pushed and your three teammates have pulled it, rewriting means their repositories disagree with the server about what the history is — and the usual resolution involves force-pushing, confusion, and somebody's commits going missing. Revert never causes that, because it only ever moves forward.
So the rule is simple and worth committing to memory: reset for commits that are still private; revert for anything you have pushed. Reverting also leaves an honest record, which is a virtue rather than an embarrassment — "we tried this, it broke checkout, we backed it out" is useful information for whoever reads the history next.
Reverting a merge commit needs one extra flag, because a merge has two parents and Git has to be told which line of history to keep. -m 1 means "keep the first parent", which is the branch you were on when you merged — that is what you want in almost every case.
# Undo one commit by adding its inverse
git revert a81c9f2
# opens an editor with a pre-filled message: Revert "Add discount logic"
# Undo several, newest first
git revert a81c9f2 4b8e0c3
# Stage the inverse without committing, so you can adjust it first
git revert --no-commit a81c9f2
git commit -m "Back out discount logic pending a fix"
# Reverting a merge commit: -m 1 keeps the branch you merged INTO
git revert -m 1 7d2f1a9
# Abandon a revert that turned out to conflict badly
git revert --abort
# The decision, in one line each:
# commit only on your machine -> git reset
# commit already pushed and shared -> git revert - A revert can conflict, exactly like a merge, if the code has moved on since the commit you are undoing. Resolve the markers the usual way,
git add, and thengit revert --continue.
Reflog: Getting Back Work You Thought Was Gone
This is the section to remember when something goes badly wrong. Git keeps a private log of every position HEAD has occupied in your repository — every commit, every branch switch, every merge, every reset. It is called the reflog, it is local to your machine, and it is not part of the history that gets pushed anywhere.
Its value is that it records commits even after nothing points at them any more. Deleted a branch with -D and realised it had two days of work on it? Reset five commits back and immediately regretted it? Committed on a detached HEAD and switched away? In every one of those cases the commits still exist, and the reflog knows their hashes.
Recovery is two steps. Run git reflog and read the list — each line shows a hash, a HEAD@{n} reference, and what you did. Find the line describing the moment before things went wrong. Then either create a branch at that hash to inspect it safely, or reset your branch back to it. Creating a branch is the calmer option, because it changes nothing else.
Two limits are worth knowing. Reflog entries expire — around ninety days by default — so this is a rescue tool, not an archive. And it can only recover things that were committed at some point. Uncommitted changes destroyed by git reset --hard or git restore were never objects in the repository, so nothing can bring them back. That is one more reason to commit often: committing is what makes your work recoverable.
git reflog
# a81c9f2 HEAD@{0}: reset: moving to HEAD~3
# 7d2f1a9 HEAD@{1}: commit: Add payment confirmation page
# 4b8e0c3 HEAD@{2}: commit: Add order summary
# 9a1d5f7 HEAD@{3}: checkout: moving from main to feature/checkout
# Safest recovery: put a branch on the lost commit and look at it
git switch -c recovered 7d2f1a9
git log --oneline
# Or move the current branch back to where it was
git reset --hard HEAD@{1} # still destructive to uncommitted work
# Recovering a branch you deleted with -D
git reflog
# find the last commit that was on it, then
git switch -c feature/checkout <hash>
# The reflog of one specific branch
git reflog show main - Work through this once now, while nothing is at stake: make a test repository, commit three times, run
git reset --hard HEAD~2, then bring the commits back with the reflog. Ten minutes of practice today turns a future disaster into a two-minute inconvenience. - The reflog is local. It cannot help a teammate recover from your force-push — but their own reflog might, if they had pulled the commits before you overwrote them.
