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-this

That'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 points

Half of Git disasters are really navigation errors — you were on a different branch than you thought. Two seconds of git status prevents 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.js

To unstage something you added by accident, but keep the edits:

git restore --staged src/app.js

Note 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 pop

git 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 --amend

Amend 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?

CommandWhat it doesUse when
git reset --soft HEAD~1Removes the commit, keeps changes stagedRecommitting differently right now
git reset --mixed HEAD~1Removes the commit, keeps changes unstaged (default)You want to re-pick what goes in
git reset --hard HEAD~1Removes the commit and the changesThe work was genuinely wrong
git revert <hash>Adds a new commit that undoes an old oneThe 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:

AD
git revert a1b2c3d
git push

Thirty 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 reflog

You'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 parser

Your 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 rescued

This 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-lease

Prefer --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-branch

Assuming 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 gonegit restore <file>
  • Uncommitted edits you want back latergit stash push -m "..."
  • Last commit is slightly wrong, not pushedgit commit --amend
  • Last few commits wrong, not pushedgit reset --soft HEAD~n and recommit
  • Commit is wrong and already pushedgit revert <hash>
  • You already did something drasticgit reflog, then git reset --hard HEAD@{n}
  • You need to overwrite a remote branchgit 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.