Recover From the 7 Most Common Git Mistakes (Easy Fixes)
It's 6:47 PM on a Thursday. You've been heads-down for the last three hours, finally in that rare state of flow where the code just works. You type git, hit a few keys on autopilot, press Enter... and then you see it. That one line of terminal output that makes your stomach drop.
Maybe it's HEAD is now at a1b2c3d. Maybe it's a wall of <<<<<<< HEAD conflict markers you didn't ask for. Maybe your branch, the one with three days of unpushed work, is just... gone.
I've been writing software professionally for over eight years, and I can tell you from experience: this feeling never fully goes away, no matter how senior you get. I've seen a staff engineer with fifteen years of experience accidentally force-push over a teammate's work during a "quick fix" at 5 PM on a Friday. I've done it myself — more than once, if I'm being honest. I recall sending urgent messages to my team asking them to temporarily avoid pulling from the main branch while I resolved an unintended commit.
So here's the good news, and I mean this sincerely: Git almost never actually deletes anything. It feels destructive because the interface is unforgiving and the error messages are terse, but underneath the hood, Git is closer to a hoarder than a shredder. It keeps snapshots, logs, and dangling objects around for weeks by default, quietly waiting for you to ask for them back.
This guide walks through the seven Git disasters I see most often — in my own work, in code review threads, and in the increasingly long "help me" messages I get from friends at 11 PM. For each one, I'll explain what actually happened under the hood (because understanding why it broke is what stops you from panicking next time), and then give you the exact commands to fix it.
Grab a coffee. Let's get your work back.
Git Cheatsheet
Bookmark our interactive Git Cheatsheet so you never have to blindly guess a command again.
View CheatsheetFirst: The One Rule That Will Save You Every Time
Before we get into specific mistakes, there's a single habit that will save you more grief than anything else in this article: when something goes wrong, stop typing.
I mean this literally. The instinct when you see something unexpected in your terminal is to immediately try another command to "fix" it. That's usually how a small, recoverable mistake turns into a genuinely messy one. Take your hands off the keyboard for ten seconds. Read the output. Figure out what state you're actually in (git status and git log --oneline -10 are your friends here) before you touch anything else.
Git's safety net is deep, but it's not infinite, and most of the disasters that do become unrecoverable happen because someone panicked and ran three more destructive commands trying to undo the first one.
Okay. Deep breath. Let's go through the list.
Mistake #1: "I committed my changes to the wrong branch!"
This is, without question, the most common mistake I see — especially among people who multitask across several tickets in a day. You're happily coding away, you commit, and then your eyes drift up to the terminal prompt and you realize you're on main, not feature-awesome-new-button.
It usually happens one of two ways: either you forgot to create the feature branch before you started typing, or you switched branches mid-task to quickly check something and never switched back. Either way, now you've got a commit sitting on a branch it has no business being on.
Why this happens
Git doesn't ask "are you sure?" before a commit. It trusts that wherever HEAD is pointing is exactly where you want to be. There's no built-in nudge that says "hey, this is the shared main branch, are you sure about this?" — that responsibility is entirely on you (or on a pre-commit hook, if your team has one configured, which is honestly worth setting up after you've been burned by this once).
The Fix
If you haven't pushed to the remote yet, this is a genuinely easy fix — maybe thirty seconds of typing, tops.
Step 1: Create the new branch and switch to it.
We want to take our current state (which includes the accidental commit) and make that the starting point for our new feature branch.
git checkout -b feature-awesome-new-button
Note: if you're on a newer version of Git, git switch -c feature-awesome-new-button does the same thing and is arguably the more intuitive command — checkout has always tried to do too many jobs at once.
Step 2: Switch back to the original branch.
Now that our changes are safely living on the new branch, we go back to main (or whichever branch caught the accidental commit).
git checkout main
Step 3: Rewind the original branch by one commit.
This is the part that makes people nervous, because --hard has a scary reputation. But stick with me.
git reset --hard HEAD~1
Wait, isn't --hard dangerous? Usually, yes! It's one of the few Git commands that can genuinely discard work. But in this specific case, we already copied those changes over to feature-awesome-new-button in Step 1. We're not deleting anything — we're just telling main to forget a commit it never should have had, while the actual work lives safely somewhere else.
What if I already pushed to the remote?
This is where it gets more delicate, because now other people (or CI, or a deploy pipeline) may have already seen that commit. Rewriting shared history with reset --hard and a force-push is a great way to make enemies on your team. Instead:
git revert <bad-commit-hash>
This creates a new commit that undoes the changes from the bad one, without erasing history. Then cherry-pick your original commit onto the correct branch:
git checkout feature-awesome-new-button
git cherry-pick <bad-commit-hash>
Now main is clean, your feature branch has the work, and nobody has to hear about a force-push in the team retro.
Mistake #2: "I messed up my commit message — or forgot a file!"
You were moving fast. You typed git commit -m "Fixes bug in login", hit Enter, and only afterward noticed you'd misspelled "login" as "loign," or forgotten the ticket number your team's linting bot insists on, or — worse — realized you forgot to git add a file that was very much part of the change.
The Fix
If this is the most recent commit and you haven't pushed it yet, Git has a built-in "let me try that again" button.
git commit --amend -m "Fixes bug in login (Ticket #1234)"
This doesn't stack a new commit on top of the old one — it replaces the last commit entirely with a new one carrying the corrected message. As far as history is concerned, it's as if you typed it correctly the first time.
Forgot to include a file? --amend isn't just for typos in the message. Stage the missing file and reuse the same commit:
git add forgotten-file.ts
git commit --amend --no-edit
The --no-edit flag tells Git "keep the message exactly as it was, just fold this staged file into the commit." It's one of the most quietly useful flags in all of Git, and I use it multiple times a day.
One important caveat: if you've already pushed this commit and someone else may have already pulled it, amending and force-pushing rewrites history they've already built on top of. On a solo branch that only you touch, amend freely. On a shared branch, coordinate with your team first or use a follow-up commit instead.
Mistake #3: "I did a hard reset and lost my uncommitted work!"
You were experimenting, the experiment wasn't working, and you decided to nuke it and start fresh. You typed git reset --hard HEAD. Two seconds later, you remembered there was one small utility function buried in that mess that you actually wanted to keep. Because it was never committed, it feels like it's just... gone.
The Fix (Maybe)
I'll be straight with you: this is the one scenario on this list where you might genuinely be out of luck, and it's worth understanding why. Git only takes a "snapshot" of your work when you explicitly tell it to — via git add (which stages a snapshot as a "blob" object) or git commit. If a file was never staged and never committed, Git never had a copy of it to protect, and a hard reset really can wipe it from existence.
That said, there are two places worth checking before you give up.
Check your editor's local history first. Most modern editors — VS Code, JetBrains IDEs like WebStorm and IntelliJ, even some versions of Sublime — silently keep their own local snapshot history completely separate from Git, precisely for moments like this. In VS Code, right-click the file in the explorer and look for "Open Timeline" or check the Local History panel. This has saved me more times than Git itself has.
Then check Git's dangling blobs. If you did run git add at any point before the reset, even briefly, there's a real chance the underlying blob object is still sitting in Git's database, just unreferenced by any commit or branch. Git won't clean these up until garbage collection runs (which isn't automatic on every single command).
git fsck --lost-found
This creates a .git/lost-found/other directory full of orphaned objects. They won't have their original filenames — you'll be opening anonymous blobs one by one to find the right content — but if the work mattered enough, it's worth the ten minutes of digging.
The real lesson here, and I say this as someone who has learned it the hard way more than once: get in the habit of committing early and often, even with messy "WIP" messages you'll clean up later with rebase -i. A commit you can always undo. Uncommitted work is the one thing Git can't fully promise to protect.
Mistake #4: "I'm trapped in a rebase and everything is broken!"
Rebasing is one of Git's most powerful tools for keeping a clean, linear project history — and also one of its most reliable ways to ruin an afternoon. You run git rebase main, expecting a quiet, uneventful update, and instead you're staring down forty merge conflicts spread across files you're fairly sure you never touched. You resolve a few, run git rebase --continue, and somehow more conflicts appear. It starts to feel like you're arguing with the tool itself.
Why this happens
A rebase doesn't just merge your changes once — it replays every single commit on your branch, one at a time, on top of the new base. If your branch has fifteen commits and commit #3 conflicts with something in main, you'll be asked to resolve that conflict, and then Git moves on to replay commit #4, which might conflict again with the same lines, because commit #3's resolution hasn't fully "settled" yet from Git's perspective. Long-lived branches with dozens of small commits are notorious for this kind of conflict cascade.
The Fix
The single most important command to remember when a rebase goes sideways is the abort switch. You are never obligated to fight through it in the moment.
git rebase --abort
That's it. The rebase is cancelled, every conflict marker disappears, and your branch is restored to exactly how it looked the second before you typed git rebase main. No judgment, no penalty — just a clean slate.
Once you've backed out, it's worth pausing to diagnose why it went so badly before you try again:
- Are you rebasing against the branch you actually think you are? A stray typo in the branch name is more common than you'd expect.
- Did someone rewrite the history of the target branch (via their own rebase or a squash-merge) since you last synced? Rebasing onto a branch whose history moved out from under you is a recipe for exactly this kind of chaos.
- Would a plain
git merge maingenuinely serve you better here? Merges create one extra commit in the history, but they resolve conflicts exactly once, in one sitting, instead of replaying them commit-by-commit. For messy, long-running branches, merge is often the less painful option, and there's no shame in choosing it.
If you do go back in for another attempt, a visual merge tool — VS Code's built-in three-way merge editor is genuinely excellent — makes resolving conflicts far less error-prone than eyeballing <<<<<<< markers in a plain text view.
Mistake #5: "I accidentally deleted a branch with important stuff on it!"
Spring cleaning gone wrong. You're deleting a pile of stale local branches, typing git branch -D, and letting tab-completion do the heavy lifting. Except this time, autocomplete filled in feature-payment-gateway instead of the feature-old-test branch you meant to remove — and you hadn't pushed it to the remote yet.
The Fix
This is where Git's quiet, meticulous logging habit becomes your best friend. Every time HEAD moves — every commit, checkout, merge, or rebase — Git records it in a running log called the reflog. Deleting a branch doesn't delete the commits it pointed to; it just removes the label. The commits themselves stick around in Git's object database for weeks (by default, unreachable objects are kept for 30 days before git gc prunes them), which gives you plenty of time to recover.
Step 1: Check the reflog.
git reflog
You'll see something like this, newest entries at the top:
a1b2c3d HEAD@{0}: checkout: moving from feature-payment-gateway to main
e4f5g6h HEAD@{1}: commit: Add Stripe integration
i7j8k9l HEAD@{2}: checkout: moving from main to feature-payment-gateway
Scan for the last meaningful action on the branch you lost. Here, the last real commit before you switched away was e4f5g6h.
Step 2: Recreate the branch from that commit.
git branch recovered-payment-gateway e4f5g6h
Check it out, and everything — every file, every line — is exactly as you left it. The only thing you've lost is the original branch name, which took you all of five seconds to type back in.
Mistake #6: "I force-pushed and now my teammate's commits are gone!"
This one is scarier because it doesn't just affect you — it affects someone else's afternoon too. You made some local changes, ran git pull, saw a divergence, got impatient, and ran git push --force to "just make it work." A few minutes later a teammate messages you: "hey, where did my commits go?"
The Fix
First, don't spiral — their commits almost certainly still exist somewhere, they're just no longer reachable from the branch pointer on the remote. If your teammate still has their branch locally, the fastest fix is simply for them to push again:
git push --force-with-lease
--force-with-lease is the safer sibling of --force — it checks that the remote branch hasn't changed since you last fetched it, and refuses to push if it has, which is exactly the guardrail that would have prevented this mess in the first place. Make it a habit to use --force-with-lease instead of --force, always.
If your teammate doesn't have a local copy anymore, check the remote's reflog if your Git hosting provider keeps one, or ask whether anyone on the team has a recent git fetch cached locally — remote-tracking branches like origin/feature-x often still reference the "lost" commits even after a force-push, right up until the next fetch overwrites them. This is why, the moment you realize a bad force-push happened, the right move is to tell everyone to stop pulling or fetching that branch until the recovery is sorted — fetching too early can erase the very reference you need.
Mistake #7: "I committed something I really, really shouldn't have — like a secret key."
This one deserves its own section because it's less "annoying" and more "genuinely urgent." You committed a .env file, an API key, or a database password, and possibly already pushed it. Amending the commit or resetting locally doesn't help much here, because the sensitive data is already sitting in Git's history — and if it's already on a public or shared remote, potentially already exposed.
The Fix
Step 1: Rotate the credential immediately. Before you do anything else in Git, go to whatever service issued that key or password and revoke/regenerate it. Removing it from Git history does nothing if the old value is still valid and someone already has a copy.
Step 2: Clean the history, if it hasn't gone fully public. For removing a file from every commit in your history, the modern, actively maintained tool is git filter-repo (the older git filter-branch is officially deprecated and much slower):
git filter-repo --path .env --invert-paths
Step 3: Force-push the cleaned history and have everyone re-clone. Because this rewrites every commit hash downstream of the change, this is one of the rare, legitimate cases where a coordinated force-push across the whole team is the right call — just make sure everyone knows to re-clone rather than pull.
And going forward: add a .gitignore entry for .env and secrets before you start a project, not after. It takes ten seconds and it's the cheapest insurance you'll ever buy.
Final Thoughts: Prevention Is Better Than the Cure
Git rewards a little bit of understanding far more than it rewards memorization. You don't need to know every flag on git rebase — you need to understand what the staging area is, what HEAD actually points to, and the difference between your local history and what's sitting on the remote. Once those three concepts click, most "disasters" stop feeling like disasters and start feeling like Tuesday.
Even so, nobody keeps forty commands in working memory under pressure — and you shouldn't have to. The engineers who look calm during a Git incident aren't the ones who memorized every flag; they're the ones who know exactly where to look something up in the ten seconds it actually matters.
That's really the whole point of keeping a good reference nearby: not to know everything, but to never have to guess.
Git Cheatsheet
Our interactive Git Cheatsheet is categorized and searchable. Find exactly the command you need, right when you need it.
Try the CheatsheetHappy coding, and may your commits always be clean, your rebases always be boring, and your force-pushes always be --with-lease.