Git basics
April 2026
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 →
Cheatsheet
April 2026
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 →
Quick guide
April 2026
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 →
Recovery
April 2026
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 →
Conflicts
April 2026
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 →
Recovery
April 2026
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 →
Deep dive
April 2026
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 →
April 27, 2026
Bash
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 →
April 27, 2026
Bash
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 →