The commands you memorise are not the skill. The skill is what you do in the ninety seconds after something looks destroyed. Real development is mostly editing, committing and recovering, in that order of frequency, and almost nobody drills the third one. This chapter is five drills, each run on a throwaway repo where the stakes are zero, so that the first time you meet each failure for real is your second time meeting it.
One idea powers all five, so learn it once:
Almost nothing is deleted, only unreferenced
Git's object store is append-only in practice. Commits you "lost" are nearly always still in .git, just no longer pointed at by any branch. And Git keeps a private diary of everywhere HEAD and every branch tip has been: the reflog. Its documentation is plain about the scope: reference logs record when the tips of branches were updated in the local repository. Local, updated on your machine, per clone; it is not on GitHub and it is not shared.
Unreachable commits are eventually garbage-collected, but only after a grace window (the default gc.pruneExpire is two weeks). Between "I destroyed it" and "it is actually gone" there is almost always a comfortable gap. Recovery is the art of using it calmly.
The Recovery Ladder
When something looks lost, climb this ladder from the top. Stop at the first rung that has your work:
Now the drills. Each starts with git init drill-N && cd drill-N and a couple of throwaway commits.
Drill 1: detached HEAD
You check out a commit hash to look at old code, get interrupted, and later realise you have been committing on top of it. The docs' own description of the state: new commits made here are not owned by any branch, and once HEAD moves away they become candidates for collection.
git checkout <old-hash> # "You are in 'detached HEAD' state…"
# …you make two commits without noticing…
The wrong move is panicking back to git checkout main, which is exactly the "HEAD moves away" the warning describes. The right move costs one command before you leave:
git branch rescue-work # a branch now owns those commits
git checkout main
git merge rescue-work # or cherry-pick, or PR it properly
Already switched away? Rung two: git reflog, find the line for your last detached commit, git branch rescue-work <hash>. Nothing was lost; it was only unowned for a while.
Drill 2: the deleted branch
git branch -D feature/x and your stomach drops. The branch was a pointer; deleting it deleted the pointer, not the commits.
git reflog # find: "checkout: moving from feature/x…"
git branch feature/x <hash> # the branch is back, history intact
If the branch had ever been pushed, rung three works even from a fresh clone: git checkout -b feature/x origin/feature/x. This is also the calm answer when GitHub's UI deletes a branch after a merged PR: the squash landed the content on main, and the remote branch can be restored from the PR page besides.
Drill 3: the force-push that overwrote the remote
Someone (statistically: you) ran git push --force with a stale local branch and rewrote the remote's history. The remote now agrees with the wrong clone. Two recoveries, in order of preference:
# On any machine whose clone still has the good history:
git push --force-with-lease origin main # put truth back
# Otherwise, on the machine that force-pushed:
git reflog # find the pre-push state of the branch
git reset --hard <good-hash>
git push --force-with-lease
The durable lesson is the flag: --force-with-lease refuses to overwrite the remote if it has commits you have not seen, which converts the classic team-destroying force-push into a polite error. Plain --force is for repos where you are the only writer, and barely then.
Drill 4: the merge conflict, demystified
Conflicts are the one failure on this list that is not an accident; they are Git behaving exactly as designed when two branches edited the same lines. The markers are a data format, not an alarm:
<<<<<<< HEAD
your branch's version
=======
the incoming version
>>>>>>> feature/x
The drill: create the conflict deliberately (two branches, same line, merge), then resolve it three ways until each feels boring: hand-edit and git add, git checkout --ours <file> / --theirs <file> for wholesale picks, and git merge --abort for the honourable retreat that returns you to the pre-merge state. The abort is the most underused: a conflict you are not ready to resolve now is not a hostage situation.
Drill 5: the lost commit, found
The capstone, combining everything: commit work, then simulate every disaster at once with git reset --hard HEAD~2. The two commits are unreachable. Recover them:
git reflog # HEAD@{1} is where you just were
git reset --hard HEAD@{1} # …and you are back, both commits restored
Run this drill until it is muscle memory, because reset --hard is also the recovery tool in drill 3, and a tool that can rescue you and destroy you deserves rehearsal in a repo that does not matter.
The meta-drill
Everything above ran on rungs one and two: local Git, no network, exactly the boxes the primer chapter put on your machine. The operational incidents chapter earlier in this course is the same discipline one level up, where the failures involve the remote, Actions and deploys. Same shape either way: know the ladder, climb calmly, write the retro. Recovery is not an interruption to real work. On any project that lives long enough, it is real work.
Run all five drills
Create a throwaway repo and run every drill in this chapter: detach HEAD and rescue the commits, delete and restore a branch, simulate and repair a bad force-push against a second clone, manufacture and resolve a conflict three ways, and reflog-recover from a hard reset.
Expected behaviour
- A rescue branch containing commits made in detached HEAD
- A deleted branch restored from the reflog with history intact
- A conflict resolved by hand, by --ours/--theirs, and once aborted cleanly
- Two commits recovered after git reset --hard HEAD~2
PROVE IT git reflog | head -20
You committed twice in detached HEAD and then checked out main. What is true of those two commits?
Which push flag refuses to overwrite the remote if it holds commits you have not seen?
A teammate hard-reset their branch, lost a day's commits, and is about to re-do the work from memory. Walk them up the Recovery Ladder: what do you check, in what order, and where does the ladder run out?
Show answer
Rung one: the working tree and stash, in case anything uncommitted survived the reset or was stashed earlier. Rung two: git reflog in THEIR clone, because the reflog is per-machine and records every place the branch tip has been; find the pre-reset entry and git reset --hard back to it, or branch off that hash. Rung three: if the commits were ever pushed, restore from origin or any other clone that fetched them. The ladder runs out only for work that was never committed and never stashed on rung one, and for commits that existed solely in a clone whose reflog is gone, which is why the moral is commit early, push often.