It's 6:40 on a Thursday. You meant to run git commit --amend on your own branch, but your terminal was sitting in the wrong tab, and now the last three commits on the shared branch look nothing like they did a minute ago. Your stomach drops. You start typing the message to your team lead in your head before you've even looked at what actually happened.
Here's the thing almost nobody tells you early enough: Git is one of the hardest tools in the world to permanently lose work in. It is very easy to make a mess. It is genuinely hard to destroy anything you have committed. Most "I broke Git" panics are recoverable in under two minutes once you know which of about six commands to reach for.
This is a practical map of those commands — what each one actually does, when to use it, and what the recovery path looks like when you pick the wrong one.
First, stop and take a snapshot
Before undoing anything, do the boring thing: make a copy of where you are right now.
git branch backup-before-i-fix-thisThat's it. It costs nothing, it takes half a second, and it means every commit currently reachable from your branch stays reachable no matter what you do next. If your fix goes sideways, git reset --hard backup-before-i-fix-this puts you exactly back.
The second habit is to actually look before you act:
git status # what's staged, what's modified, what branch am I on
git log --oneline -10
git log --oneline --graph --all -20 # where every branch actually pointsHalf of Git disasters are really navigation errors — you were on a different branch than you thought. Two seconds of
git statusprevents the panic entirely.
Undoing changes you haven't committed yet
This is the most common case and the one where you genuinely can lose work, because uncommitted changes were never recorded anywhere.
To throw away edits in one file and take the version from your last commit:
git restore src/app.jsTo unstage something you added by accident, but keep the edits:
git restore --staged src/app.jsNote the split: --staged moves it out of the staging area, no --staged overwrites your working file. Both are git restore, which replaced the overloaded git checkout -- file form back in Git 2.23. If you're on an older Git, git checkout -- file and git reset HEAD file do the same two jobs.
The pattern worth building a reflex around: when you're half-finished with something and need a clean tree right now — a hotfix, a review, a demo — don't discard, stash.
git stash push -m "half-done search filter"
# ... do the urgent thing ...
git stash popgit stash list shows everything parked. And yes, dropped stashes are usually recoverable too, but that recovery is annoying enough that naming your stashes is the cheaper option.
Fixing the commit you just made
Wrong message, forgot a file, committed a stray console.log — all one command:
git add forgotten-file.js
git commit --amendAmend replaces the last commit with a new one containing the combined result. If you only want to fix the message, git commit --amend -m "better message". If you want to add a file silently without opening an editor, add --no-edit.
The one rule with amend: it rewrites history. The old commit gets a new hash. That's harmless on a branch only you have touched. If you've already pushed and someone else has pulled, you've created a divergence they'll have to clean up. More on that in a moment.
Undoing commits: reset vs. revert
These two get confused constantly, and the difference is simple once you frame it as a question: do you want the history to show that this happened?
| Command | What it does | Use when |
|---|---|---|
git reset --soft HEAD~1 | Removes the commit, keeps changes staged | Recommitting differently right now |
git reset --mixed HEAD~1 | Removes the commit, keeps changes unstaged (default) | You want to re-pick what goes in |
git reset --hard HEAD~1 | Removes the commit and the changes | The work was genuinely wrong |
git revert <hash> | Adds a new commit that undoes an old one | The bad commit is already pushed/shared |
Reset moves your branch pointer backwards and pretends the commits never existed. It's the right tool on a private branch. Revert leaves the original commit in place and stacks an inverse on top — history grows instead of shrinking, and nobody else's clone breaks. That's why revert is the correct answer for anything already on main.
A concrete example. A deploy goes out at 2pm, errors spike at 2:06, and you trace it to commit a1b2c3d on main. Do not reset main. Do this:
git revert a1b2c3d
git pushThirty seconds, no force-push, no coordination message to five other people, and the record of both the mistake and the fix stays in the log — which is exactly what you want when someone asks in three weeks why that feature disappeared.
--hard deserves one specific warning: it is the only reset flag that touches your working directory. Uncommitted changes it wipes are gone for real. Committed changes it "wipes" are not — which brings us to the thing that makes all of this survivable.
The reflog: your actual safety net
Every time your HEAD moves — commit, checkout, merge, rebase, reset — Git writes a line to a local log. Even commits that no branch points to anymore are still sitting in the object database, and the reflog remembers where they were.
git reflogYou'll get something like:
a1b2c3d HEAD@{0}: reset: moving to HEAD~3
9f8e7d6 HEAD@{1}: commit: add pagination to results
4c5b6a7 HEAD@{2}: commit: fix null check in parserYour three "destroyed" commits are right there at HEAD@{1}. Getting back:
git reset --hard HEAD@{1}Or, if you'd rather inspect before committing to it, park them on a scratch branch first:
git branch rescued HEAD@{1}
git log rescuedThis is why the panic at the top of this article is usually unwarranted. A bad rebase, a reset --hard to the wrong place, a branch you deleted an hour ago — reflog has all of it. The entries expire eventually (90 days by default for reachable objects, 30 for unreachable ones), which is more than enough time for any mistake you'll notice.
The one real gap: reflog is local and per-clone. It won't help you recover work from a machine you no longer have, and a fresh git clone starts with an empty reflog. Anything that only ever existed as an uncommitted edit on a laptop that died is genuinely gone.
When you've already pushed
If you rewrote history on a branch that's shared, the fix is communication plus one careful flag.
git push --force-with-leasePrefer --force-with-lease over plain --force, always. Plain force says "make the remote match me, whatever's there." Force-with-lease says "make the remote match me, but only if it still looks the way it did when I last fetched." If a teammate pushed in the meantime, the lease version refuses and you find out before you've overwritten their work rather than after.
And for a colleague on the receiving end of a rewrite, the recovery is two lines:
git fetch origin
git reset --hard origin/feature-branchAssuming they've backed up or stashed anything local first — see the top of this article.
A short decision tree
Next time something goes wrong, run down this list:
- Uncommitted edits you want gone →
git restore <file> - Uncommitted edits you want back later →
git stash push -m "..." - Last commit is slightly wrong, not pushed →
git commit --amend - Last few commits wrong, not pushed →
git reset --soft HEAD~nand recommit - Commit is wrong and already pushed →
git revert <hash> - You already did something drastic →
git reflog, thengit reset --hard HEAD@{n} - You need to overwrite a remote branch →
git push --force-with-lease, after telling people
That's most of it. Not every Git command, just the handful that cover the situations that actually cause the 6:40pm stomach drop.
The mental shift
The developers who look calm around Git aren't the ones who've memorized every flag. They're the ones who internalized two facts: a commit is a snapshot that stays retrievable, and a branch is just a movable label pointing at one. Once those click, "I broke the repository" turns into "the label is pointing at the wrong snapshot" — which is a much smaller, much more fixable sentence.
So commit early and often, even messily. Messy commits can be cleaned up later with an interactive rebase. Work that was never committed can't be cleaned up at all.
And keep git reflog somewhere you'll remember it. Some evening it's going to save you an hour and a very uncomfortable message to your team.


