Career OS

Git 04 — Undo & Recovery: There Is Almost Always A Way Back

It’s 11pm, you ran a command you half-understood, and now git log looks wrong — a commit you needed is gone, or main points somewhere strange. Your stomach drops. Here is the single most calming fact about git, and the whole point of this module: git almost never deletes anything immediately. It moves labels around. The commits are still sitting on disk, and there is a log of every place HEAD has ever pointed. Once you know that log exists, git stops being scary and becomes the most forgiving tool you own.

The Goal

By the end of this module you can:

  • Trust that lost work is recoverable, because you’ve recovered it yourself
  • Read git reflog and use it to jump back to any state HEAD was ever in
  • Choose correctly between reset --soft, --mixed, and --hard from what each touches
  • Decide between revert and reset using one rule — shared history vs local-only
  • Restore a branch you deleted by accident
  • Use commit --amend and stash for the everyday small undos

The Lesson

The one fact that makes git safe

Recall the graph from the mental model: commits are snapshots, each pointing at its parent, and branches/tags/HEAD are just movable labels pointing at commits. Most “destructive” git commands don’t destroy commits — they move a label off a commit. The commit is still there, parentless and unlabelled, but on disk.

A commit with no label pointing at it (directly or through a chain) is called dangling or unreachable. Git keeps unreachable commits for about 90 days by default before garbage-collecting them. That’s your window. Ninety days is a very long time to notice you need something back.

flowchart LR
    A[commit A] --> B[commit B]
    B --> C[commit C lost work]
    B --> D[commit D]
    M[main label] --> D
    H[HEAD] --> D
    C -.->|no label points here<br/>but still on disk for ~90 days| C

commit C looks gone — no branch points at it. But it’s on disk, and git wrote down the moment HEAD stopped pointing at it. That written-down history is the reflog, and it’s how you walk back.

The reflog — git’s CCTV footage

git log shows the history of commits (the graph). git reflog shows the history of where HEAD has been — every checkout, commit, reset, merge, rebase, every time the label moved. It’s local-only, it’s not part of the repo you push, and it is your undo button for almost everything.

$ git reflog
a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
f4e5d6c HEAD@{1}: commit: add refund endpoint
9a8b7c6 HEAD@{2}: commit: add expense validation
1122334 HEAD@{3}: commit: initial expense model

Read it top to bottom as “most recent thing I did, going back in time”:

PieceWhat it means
a1b2c3dThe commit HEAD pointed at after this action
HEAD@{0}A name you can use: “where HEAD was 0 moves ago.” HEAD@{1} = one move ago
reset: moving to HEAD~2What the action was — here, a reset that threw away two commits

See line HEAD@{1}? That commit: add refund endpoint is the work the reset “lost.” It’s right there. To get back to it:

git reset --hard HEAD@{1}

HEAD jumps back to that commit, and your “lost” refund endpoint is back. You did not lose it. You moved a label, and the reflog let you move it back. Run git reflog right now in any repo you have — seeing your own history listed is the moment the fear starts to lift.

reset — three flavours, and what each one touches

git reset moves the current branch label to a commit you name. The flag decides what happens to the two other trees you met in the mental model: the index (staging area) and your working directory (the files on disk).

flowchart TD
    R[git reset to an earlier commit] --> S{which flag}
    S -->|--soft| A[moves branch label only<br/>index kept, working dir kept<br/>changes sit STAGED]
    S -->|--mixed default| B[moves branch label + resets index<br/>working dir kept<br/>changes sit UNSTAGED]
    S -->|--hard| C[moves branch label + index + working dir<br/>changes GONE from disk]
FlagBranch labelIndex (staging)Working dir (your files)Use it when
--softmoveduntoucheduntouchedYou want to redo the last commit(s) — changes stay staged, ready to re-commit
--mixed (default)movedreset to targetuntouchedYou want to unstage / re-think what goes in the commit; files safe on disk
--hardmovedresetreset to targetYou truly want the files gone — the only dangerous one

The everyday “squash my last 3 commits into one” trick is --soft:

git reset --soft HEAD~3      # un-commit the last 3, but keep all the changes staged
git commit -m "add refund flow"   # one clean commit instead of three messy ones

The everyday “I committed too early, let me unstage and redo” is --mixed:

git reset HEAD~1             # un-commit; changes are now unstaged edits in your files
# ...edit more, git add the right bits...
git commit -m "add refund flow with validation"

--hard is the only one that touches your actual files. Even then — if the commits you reset off of were ever committed, they’re in the reflog. What --hard can truly destroy is uncommitted work (edits you never committed), because that was never a commit, so there’s no reflog entry for it. The rule: committed work survives --hard via the reflog; uncommitted work does not. So commit often — every commit is a save point.

revert vs reset — the most important choice in this module

Both “undo a commit.” They are opposites, and picking wrong on a shared branch causes the classic team disaster.

  • reset moves the branch label backward, rewriting history — it pretends the commit never existed.
  • revert creates a new commit that is the inverse of the one you’re undoing — history is added to, nothing is rewritten. The bad commit stays in the log; a “this cancels that” commit sits on top.
flowchart LR
    subgraph reset rewrites history
    A1[A] --> B1[B] --> C1[C bad]
    A1b[A] --> B1b[B label moved here<br/>C erased]
    end
    subgraph revert adds history
    A2[A] --> B2[B] --> C2[C bad] --> D2[D undoes C]
    end

The deciding rule is dead simple:

SituationUseWhy
The commit is local only — never pushed/sharedresetNo one else has it; rewriting your private history is free
The commit is already pushed / on a shared branchrevertOthers have it. Rewriting shared history forces everyone into the pain you saw threatened by the rebase golden rule

If you reset --hard a branch others have pulled and then force-push, their next git pull diverges, conflicts, and panics — you’ve rewritten history under their feet. revert never does that: it’s just a normal new commit that everyone pulls cleanly.

git revert a1b2c3d           # makes a new commit that undoes a1b2c3d, on shared branches
git reset --hard HEAD~1      # erases the last local-only commit, never pushed

Say it out loud once: shared history → revert; local history → reset. This single sentence is asked in interviews and saves real teams real outages.

Restoring a deleted branch

You ran git branch -D feature-refunds and immediately regretted it. The branch label is gone — but a branch is only a label. The commits it pointed at are still on disk and still in the reflog.

git reflog                              # find the tip commit of the deleted branch
# spot the line: a1b2c3d HEAD@{5}: commit: add refund endpoint
git branch feature-refunds a1b2c3d      # re-create the label pointing at that commit

That’s it. You made a new label pointing at the commit that was the branch’s tip, and the whole branch is back because every commit chains to its parents. (If you can’t find it in git reflog, try git fsck --lost-found, which scans for dangling commits — the deeper safety net.)

commit —amend — fix the last commit

You committed, then noticed a typo in the message, or you forgot to stage one file. Don’t make a sloppy “fix typo” commit — amend the last one:

git commit --amend -m "add refund endpoint"     # fix just the message
# or: stage the forgotten file, then:
git add ForgottenFile.java
git commit --amend --no-edit                      # add it to the last commit, keep the message

The catch: --amend rewrites the last commit (it makes a new commit and moves the label). So the same golden rule applies — only amend a commit you haven’t pushed. Amending a pushed commit is rewriting shared history, same problem as reset.

stash — park your work without committing

You’re mid-change on feature-refunds, a production bug needs a hotfix on main now, but your current files are too messy to commit. git stash pockets your uncommitted changes and gives you a clean working tree:

git stash                  # save uncommitted changes, working dir goes clean
git checkout main          # go fix the fire
# ...fix, commit, push the hotfix...
git checkout feature-refunds
git stash pop              # bring your parked changes back, exactly as they were

git stash list shows your stashes; git stash pop applies the most recent and removes it; git stash apply applies it but keeps it in the list. Stash is for temporary parking — not a substitute for committing. And yes, even dropped stashes are usually recoverable from the reflog for a while. There’s almost always a way back.

Real error, real repo: the day a stuck mid-merge blocked a branch switch — see Needs Merge / Resolve Index First. Notice that doc’s own reassurance: merge --abort and the reflog meant nothing was actually lost. That’s this module in the wild.

Check The Concept

How This Shows Up At Work

  • The 11pm reset panic. A teammate messages “I think I deleted three commits with a hard reset, are they gone?” You reply “run git reflog, find the commit before the reset, git reset --hard HEAD@{1}.” Two minutes later they’re whole again, and you look like a wizard. This exact exchange happens constantly.
  • The force-push that broke the team. Someone reset --hard + force-pushed main to undo a bug. Everyone’s next pull diverged. The postmortem lesson is always the same: on shared branches, revert, never reset. Knowing this before the outage is the seniority signal.
  • The interview classic. “How do you undo a commit that’s already pushed?” The right answer is revert with the shared-vs-local reasoning. Say reset and the interviewer knows you’ve never worked on a team.
  • The clean-history habit. In code review, “please squash those five WIP commits into one” — you reset --soft HEAD~5 and re-commit. Reviewers trust people whose history reads like a story, not a diary.

Build This / Try This

Do all of this in a throwaway repo so the real one is never your lab. PowerShell on Windows.

  1. Make a sandbox and seed a few commits:
mkdir git-recovery-lab; cd git-recovery-lab
git init
"line 1" > notes.txt; git add .; git commit -m "first commit"
"line 2" >> notes.txt; git add .; git commit -m "second commit"
"line 3" >> notes.txt; git add .; git commit -m "third commit"
git log --oneline

You should see three commits. Note their hashes.

  1. Make a mess: nuke two commits with a hard reset.
git reset --hard HEAD~2
git log --oneline

Only “first commit” remains. The other two look gone. Your stomach drops on purpose.

  1. Recover with the reflog — the headline move of this module:
git reflog
git reset --hard HEAD@{1}
git log --oneline

reflog lists every move including the reset; HEAD@{1} is where HEAD was just before it. All three commits are back. Say “nothing was lost” out loud.

  1. soft vs mixed, felt directly. Reset soft, then check status:
git reset --soft HEAD~1
git status            # changes shown as "to be committed" (STAGED)
git commit -m "third commit again"   # re-commit cleanly
git reset HEAD~1      # default = --mixed
git status            # same changes now "not staged" (UNSTAGED), still on disk
git add .; git commit -m "third commit"

Same starting point, two different resting states for your changes. That difference is the whole reset table.

  1. revert — add history instead of rewriting it:
git revert HEAD --no-edit
git log --oneline

The bad commit is still there, with a new “Revert …” commit on top. This is what you’d use on a shared branch. Contrast it in your head with step 2’s reset.

  1. Delete a branch and bring it back:
git switch -c feature-x
"feature work" >> notes.txt; git add .; git commit -m "feature work"
git switch main
git branch -D feature-x         # deleted on purpose
git reflog                       # find the "feature work" commit hash
git branch feature-x <paste-the-hash>
git switch feature-x; git log --oneline   # the branch is whole again
  1. stash — park and restore:
"uncommitted mess" >> notes.txt
git stash                # working tree goes clean
git status               # nothing to commit
git stash pop            # your mess is back, exactly as it was
  1. Break it on purpose — the one thing reflog can’t save. Make an uncommitted edit, then hard-reset:
"never committed" >> notes.txt
git reset --hard HEAD
git status               # the edit is GONE — it was never a commit, so no reflog entry

This is the boundary: committed work is recoverable, uncommitted work isn’t. The lesson lands as a feeling now — commit often, every commit is a save point.

Interview Practice

These are asked, almost word for word, at Indian product companies. Answer out loud first, then check.

1. How do you undo a commit that has already been pushed to a shared branch?

Use git revert <commit>. It creates a new commit that is the inverse of the bad one, so history is added to, not rewritten — everyone pulls it cleanly. You do not use reset + force-push on a shared branch, because that rewrites history under teammates who already pulled it, forcing them into diverged-branch conflicts. The rule: shared history → revert, local history → reset.

2. What is the difference between git reset --soft, --mixed, and --hard?

All three move the current branch label to the commit you name. They differ in what else they touch:

  • --soft: moves the label only. Index and working dir untouched — your changes stay staged.
  • --mixed (default): moves the label and resets the index. Working dir untouched — changes become unstaged edits.
  • --hard: moves the label and resets both index and working dir — changes are wiped from disk.

--hard is the only dangerous one, and even it only truly destroys uncommitted work; committed work is recoverable from the reflog.

3. I think I lost some commits. How would you recover them?

Run git reflog. It lists every position HEAD has been in locally — including the state right before whatever lost the commits. Find that entry (e.g. HEAD@{1}), confirm it’s the commit you want, then git reset --hard HEAD@{1} (or create a branch at that hash with git branch recovered <hash>). Commits aren’t deleted immediately — they’re kept as unreachable objects for about 90 days before garbage collection, so the reflog can almost always reach them. If reflog doesn’t show it, git fsck --lost-found scans for dangling commits.

4. What does git stash do, and when do you use it?

git stash saves your uncommitted changes (staged and unstaged) and reverts your working tree to a clean state, so you can switch contexts without committing half-done work. Classic use: you’re mid-feature when an urgent hotfix is needed on another branch — stash, switch, fix, switch back, git stash pop to restore. It’s for temporary parking, not a replacement for committing real progress.

5. When is git commit --amend safe, and when is it dangerous?

--amend rewrites the last commit (new message and/or newly staged files), producing a new commit and moving the branch label. It’s safe and great for fixing a commit you haven’t pushed — typos, a forgotten file. It’s dangerous on a pushed commit because rewriting shared history forces teammates into the same diverged-history pain as a reset + force-push. Same golden rule as rebase: don’t rewrite history others already have.

6. You deleted a branch with git branch -D. Is the work gone?

No. A branch is just a label; deleting it doesn’t delete the commits it pointed at. They remain on disk (unreachable) and the reflog still has the branch’s tip commit. Recover by re-creating the label at that commit: find the hash in git reflog, then git branch <name> <hash>. The whole branch reappears because every commit chains back to its parents.

7. Why is the reflog local and not pushed?

The reflog records where your HEAD and branches have moved on your machine — checkouts, commits, resets, rebases. It’s a personal undo trail, not part of the project’s shared history. The commit graph (git log) is what’s shared and pushed; the reflog is the per-clone safety net layered on top. That’s also why it can’t save a teammate from their mistake — each clone has its own.

8. What kind of work can git genuinely lose forever?

Uncommitted changes. If you edit files and never git add/commit, there’s no commit object and no reflog entry — a git reset --hard or git checkout that overwrites them is unrecoverable. Anything that was ever committed is reachable via the reflog for ~90 days. The practical takeaway: commit early and often; every commit is a save point git will let you return to.

Where to Practice

ResourceWhat to doHow long
learngitbranching.js.orgDo the “reset & revert” levels in the Remote and Advanced sections — you see the labels move on the graph30 min
git-scm.com (Pro Git, ch. 2.4 + 7.1)Read “Undoing Things” and “Reset Demystified” — the canonical explanation of the three trees40 min
git-scm.com docsRead git reflog, git reset, and git revert reference pages with your sandbox repo open beside them25 min

Check Yourself

  1. In one sentence, why is committed work almost never truly lost in git?
  2. What does git reflog record, and how does it differ from git log?
  3. For reset --soft / --mixed / --hard: which trees does each touch?
  4. State the one rule that decides between revert and reset.
  5. Your branch is shared and you force-pushed after a reset --hard. What goes wrong for your teammates?
  6. You deleted a branch by accident. Walk through getting it back.
  7. When is commit --amend safe, and why?
  8. Name the one category of work git can genuinely lose, and the habit that prevents it.
Answers
  1. Because “destructive” commands move labels, not delete commits — unreachable commits stay on disk for ~90 days and the reflog records the state you can return to.
  2. git reflog records every position HEAD/branches moved to on your local machine (checkouts, commits, resets, rebases); git log is the shared commit graph. Reflog is local-only and not pushed.
  3. --soft: branch label only (changes stay staged). --mixed: label + index (changes unstaged, files safe). --hard: label + index + working dir (files wiped).
  4. Shared/pushed history → revert (adds an inverse commit). Local-only history → reset (rewrites it).
  5. Their next pull diverges because you rewrote history they already had — conflicts, confusion, and someone has to reconcile it. That’s why shared branches get revert.
  6. Find the branch’s tip commit in git reflog (or git fsck --lost-found), then git branch <name> <hash> to re-create the label at that commit.
  7. Safe only on an unpushed commit. --amend rewrites the last commit; doing that to shared history forces teammates into diverged-history pain, same as reset + force-push.
  8. Uncommitted changes — never added/committed, so no commit object and no reflog entry. Prevent it by committing early and often; every commit is a recoverable save point.

Explain it out loud: Pretend a panicking teammate just reset --hard’d away a day’s work. Talk them down step by step — why it’s probably fine, what git reflog shows, the exact command to recover, and the one case (uncommitted work) where it wouldn’t have been recoverable. If you stall, re-read the reflog section.

Why AI Can’t Do This For You

AI can recite the reset flags. What it can’t do is be calm and correct at 11pm with your repo in a weird state — it doesn’t know what you actually ran, what was committed versus dangling, or whether the branch is shared. Paste it a vague “I lost my commits” and it’ll guess; the engineer who knows to run git reflog first and read it is the one who recovers the work.

And the judgment that matters most here — revert versus reset — is a decision about your team’s shared state, not a syntax fact. Choosing wrong corrupts other people’s repos. No prompt knows whether that commit is on someone else’s machine. You do, and that’s exactly why this skill stays yours.

Module done? Add it to today’s tracker

Saves your progress on this device.