Dev / Git

Git Recovery.
No drama.

Practical guides for when Git does the thing you did not expect. Free content from Sundeck Dev.

5 Git Mistakes Every Developer Makes (And How to Fix Them)

Everyone commits the wrong file. Everyone pushes to the wrong branch. The commands to undo these things exist — they are just buried under a mental model nobody fully explains. Here are the five most common Git mistakes and the exact commands to recover from them.

  • 01
    Committed to the wrong branch

    You wrote code on main that was meant for a feature branch. Fix it without losing your work:

    git branch feature/my-work # create the right branch git reset HEAD~1 --soft # undo the commit, keep staged changes git stash # stash those changes git checkout feature/my-work # switch to correct branch git stash pop # restore your work git commit -m "where this belongs"

    The key is --soft: it rewinds the commit pointer but leaves your changes staged so nothing is lost.

  • 02
    Committed a file you should not have (env vars, secrets)

    The file is staged and committed. Remove it from the commit without deleting it from disk:

    git rm --cached .env # unstage from tracking echo ".env" >> .gitignore # make sure it stays out git commit --amend --no-edit # rewrite the last commit
    Warning

    If you have already pushed this commit, rotate the secrets. The commit history is not safely rewritable once others have pulled it — treat any pushed credential as compromised.

  • 03
    Pushed a bad commit to a shared branch

    Do not rewrite history on a shared branch. Use a revert commit instead — it creates a new commit that undoes the damage cleanly:

    git log --oneline # find the bad commit hash git revert abc1234 # creates an undo commit git push

    Revert is safe for shared branches because it adds to history rather than rewriting it. Every collaborator sees what happened.

  • 04
    Accidentally deleted a branch with unmerged work

    Git keeps commits for 30–90 days in the reflog even after you delete the branch. Find and recover them:

    git reflog # find the last commit hash of your branch git checkout -b recovered abc1234 # recreate it

    The reflog is your safety net. It records every HEAD movement — even across branch deletes and hard resets.

  • 05
    Messed up a merge and want to start over

    Mid-merge, your working tree is a conflict graveyard. To abort and return to the pre-merge state:

    git merge --abort

    If you already finished the merge commit and want to revert it:

    git revert -m 1 HEAD # -m 1 = keep the first parent (your branch)
Git Unfucked — the full recovery reference

30+ scenarios. Every reset, revert, reflog, and stash pattern documented with context on when to use each. One PDF, buy once.

Get Git Unfucked →

The Git Recovery Cheatsheet

The commands you forget when you need them most. This is the condensed version — a map from "what just happened" to "what to run." The full reference with context, edge cases, and dangerous-mode variants is in Git Unfucked.

Undoing commits

Command What it does
git reset HEAD~1 --soft Undo last commit, keep changes staged
git reset HEAD~1 --mixed Undo last commit, unstage changes (default)
git reset HEAD~1 --hard Undo last commit, discard changes permanently
git commit --amend Rewrite last commit message or add forgotten files
git revert <hash> Create a new commit that undoes a specific commit (safe for shared branches)
Rule of thumb

Use reset only on commits you have not pushed. Use revert on anything that has already left your machine.

Recovering lost work

Command What it does
git reflog Show all recent HEAD positions — find commits before a hard reset
git checkout -b <branch> <hash> Restore a commit from reflog onto a new branch
git stash list List all stashed changesets
git stash pop Apply most recent stash and remove it
git stash apply stash@{2} Apply a specific stash without removing it

Branch management

Command What it does
git branch -D <name> Force-delete a branch (recoverable via reflog for ~30 days)
git cherry-pick <hash> Apply a specific commit to the current branch
git merge --abort Abort in-progress merge and restore pre-merge state
git rebase --abort Abort in-progress rebase and restore pre-rebase state
git push origin <branch> --force-with-lease Force push safely — aborts if remote has new commits

Inspecting history

Command What it does
git log --oneline --graph Compact visual branch graph
git diff HEAD~1 HEAD What changed in the last commit
git blame <file> Who changed which line and when
git show <hash> Full diff and metadata for a specific commit
git bisect start Binary search to find which commit introduced a bug
The full version has 30+ scenarios

Dangerous rebase recovery, bisect workflows, submodule disasters, remote tracking fixes, and more — all with the context on when each approach is safe and when it will hurt you.

Get Git Unfucked →

How to Undo a Git Commit (The Right Way)

Searching "how to undo a git commit" returns 10 Stack Overflow answers with different commands and no explanation of which to use when. Here is the decision tree.

Step 1: Did you push the commit?

No, it is only local. You have the most options. Use git reset:

# Keep your changes staged (safest — nothing is lost) git reset HEAD~1 --soft # Keep your changes unstaged but on disk git reset HEAD~1 --mixed # Discard your changes completely (no recovery without reflog) git reset HEAD~1 --hard

Yes, it is already pushed. Do not rewrite history on a shared branch. Use git revert instead:

git revert HEAD # undo the most recent commit git push # push the undo commit

Revert creates a new commit that inverts the changes. Your teammates see the history intact — they just also see the fix.

Step 2: Was it multiple commits ago?

If the commit you want to undo is not the latest, you need its hash:

git log --oneline # find the hash git revert abc1234 # revert that specific commit
Tip

If you reverted the wrong commit, just revert the revert: git revert HEAD again will undo the undo.

Step 3: What if you used --hard and lost work?

The reflog still has it. Git keeps a log of every HEAD position change, even across hard resets. Run:

git reflog # look for your commit in the list git checkout -b recovered abc1234 # put it on a new branch

The reflog retains entries for 30–90 days (configurable). As long as garbage collection has not run, the data is there.

Stop searching Stack Overflow mid-incident

Git Unfucked is a single reference PDF with every recovery scenario mapped out. The right command, the right flags, the edge cases explained — before you need to look them up under pressure.

Get Git Unfucked →
Git branch recovery diagram

How to Recover a Deleted Git Branch

You deleted a branch. Maybe it had unmerged work. Maybe you ran git branch -D thinking you were done, then realized you were not. Git almost certainly still has your commits — here is how to get them back.

The fast path: git reflog

Git's reflog is a local log of every position HEAD has been in. Even after a branch is deleted, the commits that were on it remain in the object store for 30–90 days. The reflog tells you where to find them.

git reflog

You'll see output like this:

abc1234 HEAD@{0}: checkout: moving from feature/auth to main def5678 HEAD@{1}: commit: add OAuth provider support 9ab0123 HEAD@{2}: commit: wire up login endpoint ...

Find the last commit that was on your deleted branch — usually the most recent entry before you switched away. Then restore it:

git checkout -b feature/auth def5678 # recreate the branch at that commit
Tip

If you are not sure which commit to restore, create the branch at each candidate and check git log --oneline to confirm the history looks right.

If you know the branch name but not the commit

Use git reflog with a grep to narrow it down:

git reflog | grep "feature/auth"

This shows every reflog entry that mentions your branch name — including checkouts onto it, commits made while on it, and the moment it was deleted.

Recovering a branch that was pushed to remote

If the branch existed on a remote before it was deleted locally, it may still exist there:

git fetch origin # update remote refs git checkout -b feature/auth origin/feature/auth

If the remote branch was also deleted, check if any teammate still has it locally. Their reflog has a copy of the commit history. A git push from their machine restores it.

Last resort: git fsck

If the reflog does not help (e.g. you cleared it or the commit is very old), git fsck lists all unreachable objects:

git fsck --lost-found # Look for "dangling commit" entries git show <dangling-commit-hash> # inspect each one git checkout -b recovered <hash> # restore if it's the right one
Warning

Commits only survive until Git's garbage collector runs. The default window is 30 days for unreachable commits, 90 days for those referenced by the reflog. If git gc ran recently, data may be gone permanently.

32 recovery scenarios. Every edge case covered.

Git Unfucked documents remote branch disasters, multi-author recovery, reflog deep dives, and force-push undos — all in one reference you can search offline when you're mid-panic.

Get Git Unfucked →

How to Fix git stash pop Conflicts

You ran git stash pop and got a conflict. Now your working tree is a mess and your stash is in an ambiguous state. Here is exactly what happened and how to get out of it cleanly.

What just happened

When git stash pop hits a conflict, it applies the stash as much as it can and leaves conflict markers in the affected files — just like a merge conflict. Critically, the stash entry is NOT removed when a conflict occurs. It stays in the stash list until you manually drop it.

git stash list # confirm stash@{0} is still there

Resolve the conflicts normally

Open the conflicting files and resolve the markers:

<<<<<<< Updated upstream function login() { ... } // current branch version ======= function login() { ... } // your stashed version >>>>>>> Stashed changes

Edit to keep what you want, then mark each file as resolved:

git add <resolved-file> git add <other-resolved-file>
Note

Do not run git commit here. You are resolving a stash conflict, not completing a merge. Just stage the resolved files.

Drop the stash entry

Since the stash was not automatically removed on conflict, drop it manually once your files are in the state you want:

git stash drop stash@{0} # remove the stash entry

The safer approach: stash apply + manual drop

Next time, prefer git stash apply over git stash pop. They behave identically, except apply never auto-removes the stash — which means you have time to verify the result before deciding to drop:

git stash apply # apply stash, keep it in list # resolve conflicts, verify output git stash drop # only drop when you are sure

If the conflict is too messy and you want to start over

git checkout -- . # discard all working tree changes git stash drop stash@{0} # remove the stash entry you applied
Warning

git checkout -- . discards all uncommitted changes permanently. Only use it if you want to throw away the conflict resolution attempt entirely and go back to a clean working tree.

Inspect stash contents before popping

To avoid surprise conflicts in the first place, preview what a stash contains before applying it:

git stash show -p stash@{0} # full diff of the stash git stash show --stat stash@{0} # just the changed files

If the stash touches the same files that have changed on your current branch, expect a conflict. Switch to a clean branch before applying, or resolve proactively.

Stash disasters, merge conflicts, and 30+ other recovery patterns

Git Unfucked covers every variation: conflicting stashes, dropped stash recovery, stash on wrong branch, and partial stash apply — with the exact commands and the context to know when each is safe.

Get Git Unfucked →
Git stash recovery guide

How to Recover a Lost Git Stash (Even After drop or clear)

You ran git stash drop on the wrong entry, or git stash clear wiped everything. The stash list is empty. The work appears gone. It is almost certainly not — Git keeps the underlying commits around until garbage collection runs. Here is how to get them back.

The quick recovery

Stashes are stored as commit objects. When you drop a stash, the commit still exists in the object store — it just loses its ref. Find it with git fsck:

git fsck --no-reflogs | grep "dangling commit" git show <hash> # inspect each one to find your stash git stash apply <hash> # restore it
Tip

The dangling commit list can be long. Look for commits with "WIP on" in the message — that is Git's default stash commit format.

Read the full guide →

32 recovery scenarios. Every edge case covered.

Git Unfucked covers dropped stash recovery, stash on wrong branch, partial stash apply, and more — all with exact commands and the context to know when each approach is safe.

Get Git Unfucked →
Git reflog recovery guide

Git Reflog: The Complete Recovery Guide

The reflog is Git's private flight recorder. Every time HEAD moves — commit, reset, rebase, checkout, merge — Git logs it. If you have ever lost work to a git reset --hard or a gone-wrong rebase, the reflog is almost certainly your path back. This guide explains how to read it and how to use it to recover from the most common disasters.

Reading the reflog

git reflog # show all HEAD movements git reflog show feature/auth # show movements for a specific branch

Each entry shows: the commit hash, the position index (HEAD@{n}), and what triggered the movement. Use the hash to restore any previous state.

Read the full guide →

Stop searching Stack Overflow mid-incident

Git Unfucked is a single reference PDF with every recovery scenario mapped out. The right command, the right flags, the edge cases explained — before you need to look them up under pressure.

Get Git Unfucked →

How to Debug a Bash Script (set -x, trap, and Common Mistakes)

Bash scripts fail silently. The default behavior is to keep running past errors, swallow exit codes in pipes, and give you no stack trace. Here are the tools that actually help.

The three modes every bash debugger uses

set -x          # xtrace: print each command before executing
set -e          # errexit: exit on any error
set -u          # nounset: error on undefined variables
set -o pipefail # catch failures inside pipelines

Put set -euo pipefail at the top of every script you write. It turns bash from "continue past errors" into "stop and tell me what broke."

Reading xtrace output

The default xtrace prefix is +. Change it to include file and line number:

export PS4='+(${BASH_SOURCE}:${LINENO}): '
set -x

Now each traced line shows exactly where in which file it came from.

Read the full guide →

30 Bash rescue scenarios — $2

Bash Unfucked covers the real mistakes: unquoted variables, pipe errors, cron failures, line ending disasters. Full interactive reference $2.

Get Bash Unfucked — $2 →

Free sampler on GitHub →

Bash Quoting: The Complete Guide (Single, Double, and No Quotes)

Quoting in bash controls three things: variable expansion, word splitting, and glob expansion. Getting it wrong causes intermittent bugs that only appear on filenames with spaces, or variables that are sometimes empty.

The three quoting modes

echo $var        # word splitting + glob expansion
echo "$var"      # expansion only, no splitting
echo '$var'      # literal — no expansion at all

The rule: always double-quote variables. Use single quotes only for literal strings. The only exception is arithmetic: ((count++)) doesn't need quotes.

Why unquoted variables break on spaces

file="my project/config.yaml"
cp $file /backup/    # runs: cp my project/config.yaml /backup/
cp "$file" /backup/  # runs: cp "my project/config.yaml" /backup/

Read the full guide →

30 Bash rescue scenarios — $2

Bash Unfucked covers the real quoting mistakes: unquoted arrays, heredoc traps, SSH double-quoting, eval hazards. Full interactive reference $2.

Get Bash Unfucked — $2 →

Free sampler on GitHub →