Module 7 — Undoing Work (git reset, revert, restore, reflog)
Updated 8 September 2026
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
Part A — Undoing uncommitted work: restore
A1. Throwing away working-tree edits
git restore <file> overwrites the working-tree file with the copy in the index — "make it look like it did the last time I staged or committed it." You have seen git status advertise it since Module 2 (use "git restore <file>..." to discard changes). One property makes it different from nearly everything else in this module: the edits it discards were never committed and never staged, so they exist in no object anywhere — this is the one Git command in daily use that destroys work unrecoverably. Treat the command like rm: read the filename twice.
🧪 Exercise 7.1 — ruin a file, restore it
cd ~/git-course/kitchen # on main, clean, post-Module 6
echo "RUINED" > method.txt # simulate a bad edit / fat-fingered redirect
git status -s
git restore method.txt
cat method.txt
git status -s # prints nothing: clean again✅ Expected result — click to reveal
M method.txt
knead 12 minutesWhat to read out of it: M — modified, unstaged; then after restore, the file's content is back to the committed version and short status prints nothing (silence = clean, Module 2). The RUINED content is gone forever — no blob was ever written for it, so no recovery tool in this module or any other can bring it back. That asymmetry is the deep lesson of this Part: Git protects what you gave it; it cannot protect what you never gave it. Committing early and often is not bureaucracy — it is how edits become recoverable objects.
A2. Unstaging: restore --staged
git restore --staged <file> is the opposite direction of git add: it copies the HEAD version of the file's entry back into the index, un-staging your change — while leaving the working-tree file untouched. Nothing is lost; the change simply exits the next-commit draft. (Status has been advertising this one too: use "git restore --staged <file>..." to unstage.) The two flags compose: git restore --staged --worktree <file> does both at once, and --source <commit> restores from any commit instead of the default — the write-flavored sibling of Module 4's read-only git show <commit>:<file>.
🧪 Exercise 7.2 — stage, change your mind, unstage
cd ~/git-course/kitchen
echo "cool on wire rack" > cooling.txt
git add cooling.txt
git status -s # staged new file
git restore --staged cooling.txt
git status -s # file untouched, no longer staged
rm cooling.txt # tidy up: we decided against it entirely✅ Expected result — click to reveal
A cooling.txt
?? cooling.txtWhat to read out of it: A␣ — a new file staged for the next commit; after unstaging, ?? — Git no longer plans to commit it, but the file sits on disk exactly as written (untracked again, since HEAD has no version of it; for a tracked file the code would go M␣ → ␣M). Column 1 changed, column 2's world did not — --staged touches only the index. This is the safe half of restore: unstaging is always reversible (just add again).
🎯 Interview questions — Part A
🎯 "How can you undo changes in Git? Name a few commands." — asked verbatim in LabEx's Git Interview Questions and Answers, 2025
Organized by where the change lives. Uncommitted, working tree: git restore <file> overwrites from the index — the only common undo that destroys unrecoverable data. Staged: git restore --staged <file> unstages, touching nothing on disk. Committed, local-only: git commit --amend fixes the last commit; git reset (soft/mixed/hard) moves the branch pointer back, with the mode choosing how much else follows. Committed and shared: git revert <commit> adds a new commit that applies the inverse — history grows instead of changing. And behind all of it, git reflog finds any commit a pointer ever touched, making most "disasters" ten-second recoveries.
The details that separate candidates: an average answer lists commands. A strong answer organizes by the change's location (working tree → index → local history → shared history), because the location determines the tool — and states the two boundary rules: never-staged edits are the only unrecoverable class, and shared history calls for revert, not reset. Mentioning that older material spells the first two git checkout -- <file> and git reset HEAD <file> (same operations, pre-2.23 names) shows range across codebases.
Part B — reset: moving the branch itself
B1. One command, three trees, three modes
git reset <commit> does something no command so far has done: it moves the current branch's pointer to an arbitrary commit — Module 3's 41-byte file, rewritten to a hash of your choosing. The commits after that point do not vanish (they are immutable objects); the branch simply stops naming them. The mode flag answers one question: after the pointer moves, which of the other two trees follow it?
| Mode | Branch/HEAD | Index | Working tree | Your changes end up… |
|---|---|---|---|---|
| --soft | moves | untouched | untouched | staged, ready to re-commit |
| --mixed (default) | moves | reset to target | untouched | in the working tree, unstaged |
| --hard | moves | reset to target | reset to target | gone from all three trees |
Read the table as a progression: soft touches one tree, mixed two, hard all three. That is the entire secret — the modes are not three unrelated behaviors but one operation with an increasing blast radius, named by which trees it reaches (exactly the payoff Module 3's C3 promised).
Diagram source
flowchart LR
A["reset --soft<br>branch only"] --> B["reset --mixed<br>branch + index"] --> C["reset --hard<br>branch + index + working tree"]🧪 Exercise 7.3 — soft: uncommit, keep everything staged
cd ~/git-course/kitchen
echo "proof 1 hour" > proofing.txt
git add proofing.txt && git commit -m "WIP: proofing notes"
git log --oneline -1
git reset --soft HEAD~1 # undo the COMMIT, keep the work staged
git status -s
git log --oneline -1✅ Expected result — click to reveal
ff6f254 WIP: proofing notes
A proofing.txt
01756f0 Merge branch 'seeds'What to read out of it: the WIP commit is out of the branch's history (log's newest entry is the seeds merge again), yet A␣ proofing.txt — the work sits staged, precisely as it was the moment before you committed. This is the "uncommit" from Module 5's hotfix capstone, delivered as promised: soft reset is how you unwrap a commit without disturbing its contents — re-commit under a better message, or split it into pieces. Nothing in the index or working tree moved; only the 41-byte pointer did.
🧪 Exercise 7.4 — mixed (the default): uncommit AND unstage
cd ~/git-course/kitchen
git commit -m "WIP again" # re-commit the staged file from 7.3
git reset HEAD~1 # no flag = --mixed
git status -s✅ Expected result — click to reveal
?? proofing.txtWhat to read out of it: compare with 7.3's A␣ — same undo, one tree further: the index was reset to the target commit too, so the file is no longer staged; it survives only in the working tree (?? because HEAD~1 never contained it; a previously-committed file would show ␣M). This is LabEx's interview question "undo the last commit without losing the changes" answered live: git reset HEAD~1. Finish the exercise: git add proofing.txt && git commit -m "Add proofing notes" — committed properly this time.
🧪 Exercise 7.5 — hard: make the working tree match HEAD, discarding edits
cd ~/git-course/kitchen # after committing "Add proofing notes"
echo "junk edit" >> proofing.txt
git reset --hard HEAD # target = current commit: pointer doesn't move, trees sync to it
cat proofing.txt✅ Expected result — click to reveal
HEAD is now at 0f35617 Add proofing notes
proof 1 hourWhat to read out of it: HEAD is now at … — reset always announces the destination; with HEAD as the target the pointer stays put and the other two trees are forced to match it, discarding the junk edit from both index and disk. This is the bulk version of Exercise 7.1 ("make everything look committed again"), and it shares 7.1's danger exactly: the discarded edits were never objects, so they are unrecoverable. git reset --hard deserves a one-second pause every single time you type it — the next exercise shows why the pause is usually, but not always, survivable.
🎯 Interview questions — Part B
🎯 "What is the difference between git reset --soft, --mixed, and --hard?" — asked verbatim in LabEx's Git Interview Questions and Answers, 2025; also as "Soft vs mixed vs hard reset." in Greenroom, Jun 2026
All three move the current branch's pointer to the target commit; they differ in which of the other trees follow. --soft: pointer only — the undone commits' changes remain staged. --mixed (default): pointer + index — changes remain in the working tree, unstaged. --hard: pointer + index + working tree — changes are removed everywhere, and any uncommitted edits caught in the blast are gone for good (committed work remains recoverable via reflog).
The details that separate candidates: an average answer recites the three lines. A strong answer presents them as one operation with an expanding blast radius across the three trees (working tree / index / HEAD — the model that makes the flags derivable instead of memorized) and attaches a use case to each: soft = re-message or squash locally; mixed = unstage everything and re-pick; hard = deliberately abandon local state. The two safety clauses that mark seniority: reset rewrites the branch, so it is for unshared commits (shared ones get revert); and the only truly unrecoverable loss is never-committed work under --hard — everything committed is one reflog lookup away.
🎯 "How do you undo the last commit without losing the changes?" — asked verbatim in LabEx's Git Interview Questions and Answers, 2025
git reset HEAD~1 — mixed reset one commit back: the branch pointer steps to the parent, the commit's changes drop back into the working tree unstaged, nothing is lost. Prefer git reset --soft HEAD~1 when you want them still staged (ideal for immediately re-committing with a better message or different grouping). If the goal was only a better message or one forgotten file, git commit --amend is the shorter road. All of these rewrite the branch, so they are for commits that have not been shared; a shared bad commit gets git revert instead.
The details that separate candidates: an average answer gives one command. A strong answer chooses between the three (mixed / soft / amend) by stating what each preserves, names the shared-history boundary unprompted, and adds the recovery footnote: even after this reset, the undone commit still exists — git reflog can point you back if you change your mind. Demonstrating that an "undo" is itself undoable is exactly the calm this topic is probing for.
Part C — The reflog: Git's flight recorder
C1. Every place HEAD has ever been
Those logs inside .git (Module 3's floor plan) are the reflog: a local journal recording every time HEAD or a branch pointer moved — commit, switch, merge, reset, revert — with a timestamped entry saying what moved it. git reflog prints HEAD's journal, newest first, each line addressable as HEAD@{N} ("where HEAD was N moves ago"). Two properties define its role. It is strictly local: never pushed, never cloned, private to this repository copy — a journal of your movements, not the project's history. And it is temporary: entries expire (about 90 days; entries for commits now unreachable, about 30 — Module 5's stranded-commit window).
Why it matters: git reset --hard HEAD~1 removes a commit from the branch — but the commit object still exists (immutable, Module 3), and the reflog remembers its address. Which turns the scariest command in Git into something you can walk back.
Branch history is the official route map you publish to customers. The reflog is the GPS tracker in the van: every actual movement, timestamped — including wrong turns you later edited out of the official map. When someone says "we lost the package at some point yesterday," you do not consult the published map; you replay the tracker and drive back to the exact spot.
Where the analogy stops working. A GPS tracker records where the van went; the reflog records where the pointers went. Work that never moved a pointer — uncommitted edits blown away by restore or reset --hard — was never tracked and is genuinely gone (Part A's warning, still true here). The reflog rescues everything that was ever committed; it cannot rescue what never was. And unlike a fleet tracker, nobody else can subpoena it: each clone's reflog is private and unshared.
🧪 Exercise 7.6 — destroy a commit with --hard, then walk it back
cd ~/git-course/kitchen # newest commit: "Add proofing notes"
git log --oneline -2
git reset --hard HEAD~1 # "accidentally" nuke the newest commit
git log --oneline -1 # gone from the branch...
git reflog | head -3 # ...but the flight recorder saw everything
git reset --hard HEAD@{1} # go back to where HEAD was 1 move ago
git log --oneline -1✅ Expected result — click to reveal
0f35617 Add proofing notes
01756f0 Merge branch 'seeds'
HEAD is now at 01756f0 Merge branch 'seeds'
01756f0 Merge branch 'seeds'
01756f0 HEAD@{0}: reset: moving to HEAD~1
0f35617 HEAD@{1}: reset: moving to HEAD
0f35617 HEAD@{2}: commit: Add proofing notes
HEAD is now at 0f35617 Add proofing notes
0f35617 Add proofing notesWhat to read out of it: after the hard reset the log swears Add proofing notes never happened — but the reflog line HEAD@{0}: reset: moving to HEAD~1 records the destructive move itself, and HEAD@{1} records where HEAD stood just before it (your @{1} entry may differ if you experimented — read the journal, pick the entry whose left-hand hash is the commit you want, and reset to that). One more hard reset — this time to a reflog address — and the branch names the commit again. Total recovery time: seconds. This exercise is the single most confidence-building thing in the track: you have now broken history and repaired it yourself.
C2. Recovering a deleted branch
Module 5's Ticket 3 recovered a deleted branch because the hash survived in a deploy log. The reflog removes even that requirement: deleting a branch deletes its pointer and its own journal, but HEAD's journal still contains every commit you made while standing on that branch — with messages attached. Find the commit, plant a branch on it (Module 5's rescue move), done.
🧪 Exercise 7.7 — delete a branch with real work, then resurrect it
cd ~/git-course/kitchen
git switch -c doomed
echo "x" > doomed.txt && git add doomed.txt && git commit -m "Doomed work"
git switch main
git branch -D doomed # force-delete: seatbelt overridden, work stranded
git reflog | head -3 # find the stranded commit in HEAD's journal
git branch doomed-restored HEAD@{1}
git log --oneline -1 doomed-restored
git branch -D doomed-restored # tidy up: the demo is over✅ Expected result — click to reveal
Switched to a new branch 'doomed'
[doomed b8d0ff1] Doomed work
1 file changed, 1 insertion(+)
create mode 100644 doomed.txt
Switched to branch 'main'
Deleted branch doomed (was b8d0ff1).
0f35617 HEAD@{0}: checkout: moving from doomed to main
b8d0ff1 HEAD@{1}: commit: Doomed work
0f35617 HEAD@{2}: checkout: moving from main to doomed
b8d0ff1 Doomed workWhat to read out of it: HEAD's journal shows the whole story in reverse — the switch back to main (@{0}), the doomed commit (@{1}, with its message right there), the switch onto the branch (@{2}). HEAD@{1} addresses the commit no branch names anymore, and git branch doomed-restored HEAD@{1} makes it reachable again — the deleted branch is back in all but name. (If your journal has extra entries from experimenting, grep it: git reflog | grep "Doomed work".) Interviewers ask "how do you recover a deleted branch?" — this exercise is the answer, performed.
🎯 Interview questions — Part C
🎯 "Describe the purpose of git reflog." — asked verbatim in LabEx's Git Interview Questions and Answers, 2025
The reflog is a local, per-repository journal of every movement of HEAD and each branch pointer — commits, switches, merges, resets, rebases — newest first, addressable as HEAD@{N} or <branch>@{N}. Its purpose is recovery and audit-of-self: because commits are immutable objects that reset/rebase merely stop naming, the reflog's record of "where the pointer was before" makes almost any history accident reversible — git reset --hard HEAD@{1} undoes a bad reset; a reflog entry's hash resurrects a force-deleted branch via git branch <name> <hash>. It is strictly local (never pushed or cloned) and entries expire — ~90 days, ~30 for entries pointing at now-unreachable commits.
The details that separate candidates: an average answer says "it shows history of HEAD, used to recover commits." A strong answer states why it works (immutable objects + a journal of pointer positions = nothing referenced is ever really lost until GC), the addressing syntax, and the three boundaries: local-only (a teammate's reflog cannot save you), time-limited, and blind to never-committed changes. Bonus precision: each branch has its own reflog too (git reflog show <branch>), and git log -g renders reflog entries with full log formatting.
🎯 "How do you recover a deleted branch?" — asked verbatim in LabEx's Git Interview Questions and Answers, 2025
Find the deleted branch's tip commit, then plant a new branch on it: git branch <name> <hash>. Sources for the hash, in order of convenience: the deletion message itself (Deleted branch X (was abc1234) — printed by both -d and -D); the reflog (git reflog | grep <commit message> — HEAD's journal keeps every commit you made on the branch even after its own journal died with it); or any external record — CI logs, the deploy system, a teammate's clone where the branch still exists. Verify with git log --oneline <name> that the resurrected branch has the expected history.
The details that separate candidates: an average answer says "use reflog." A strong answer explains why recovery is possible at all — branch deletion removes a 41-byte pointer, never the commits — and ranks the recovery sources, starting with the deletion message everyone scrolls past. The two clock-caveats show operational maturity: unreachable commits are GC'd after the grace window (~30 days by default), so recover promptly; and if the branch was pushed, the server still has it — git fetch beats forensics (Module 8).
Part D — revert: undoing in public
D1. Undo by adding, not removing
git revert <commit> computes the target commit's changes, applies the inverse to your working tree and index, and records that inverse as a new commit (Revert "<original subject>"). Nothing is removed: the bad commit stays in history, followed by its antidote. That makes revert the only undo that is safe for history other people already have — the branch only ever grows, so nobody's copy becomes wrong (the full mechanics of why rewriting shared history hurts arrive with remotes in Module 8 and rebase in Module 10; the rule is usable now). Revert can conflict, exactly like a merge — same markers, same resolve-or---abort exits — because "apply the inverse of an old change to newer code" is the same three-way machinery.
🧪 Exercise 7.8 — revert the proofing commit
cd ~/git-course/kitchen # newest commit: "Add proofing notes" (restored in 7.6)
git revert --no-edit HEAD
git log --oneline -3
ls proofing.txt✅ Expected result — click to reveal (the ls fails on purpose)
[main ec95180] Revert "Add proofing notes"
Date: Mon Sep 7 17:04:20 2026 +0000
1 file changed, 1 deletion(-)
delete mode 100644 proofing.txt
ec95180 Revert "Add proofing notes"
0f35617 Add proofing notes
01756f0 Merge branch 'seeds'
ls: cannot access 'proofing.txt': No such file or directoryWhat to read out of it: the revert is an ordinary commit — auto-messaged Revert "…", with the inverse change (1 deletion, delete mode: the original added the file, so the inverse removes it). The log tells the whole honest story: mistake and correction, both permanent — an auditor sees what happened; a teammate's copy stays valid because nothing they had was altered. Compare the feel with 7.6: reset made history shorter; revert made it longer. Same end state of the files, opposite philosophy — and the philosophy is what interviews probe.
D2. Reverting a merge commit
A merge commit has two parents (Module 6), so "the inverse of this commit" is ambiguous: undo relative to which parent? Git refuses to guess — plain git revert <merge> fails and demands -m <parent-number>. In practice you almost always want -m 1: parent 1 is the branch that received the merge (Module 6's ^1), so -m 1 means "take this feature's arrival back out of my branch." This is Module 6's --no-ff payoff realized: because the feature landed as one merge commit, it leaves as one revert.
🧪 Exercise 7.9 — the failure, then the fix, then the un-undo
cd ~/git-course/kitchen
git revert --no-edit 01756f0 # YOUR seeds merge hash — fails: which parent?
echo "exit code: $?"
git revert --no-edit -m 1 01756f0 # relative to parent 1: main's own line
ls seeds.txt 2>&1
git revert --no-edit HEAD # revert the revert: bring seeds back
git log --oneline -1✅ Expected result — click to reveal (the first command fails on purpose)
error: commit 01756f0b4bb15fc076ec8f1ff5e1f7be6e9c9fbb is a merge but no -m option was given.
fatal: revert failed
exit code: 128
[main 500d96d] Revert "Merge branch 'seeds'"
Date: Mon Sep 7 17:04:20 2026 +0000
1 file changed, 1 deletion(-)
delete mode 100644 seeds.txt
ls: cannot access 'seeds.txt': No such file or directory
[main 9531dc7] Reapply "Merge branch 'seeds'"
Date: Mon Sep 7 17:04:20 2026 +0000
1 file changed, 1 insertion(+)
create mode 100644 seeds.txt
9531dc7 Reapply "Merge branch 'seeds'"What to read out of it: the refusal names the exact problem (is a merge but no -m option was given) — ambiguity, not breakage. With -m 1, the merge's contribution (seeds.txt) is subtracted from main while history keeps both the merge and its removal. The final move — reverting the revert — even auto-titles itself Reapply: Git knows this dance. One subtlety worth carrying into Module 10: after reverting a merge, re-merging the same branch later brings nothing (its commits are already in history — only their content was subtracted), so a feature un-merged this way must be reapplied by reverting the revert, or by new commits. Interviewers who ask about merge reverts are usually fishing for exactly that.
🎯 Interview questions — Part D
🎯 "How do you revert a commit that has already been pushed to a remote repository?" — asked verbatim in LabEx's Git Interview Questions and Answers, 2025
git revert <hash>, then share the result (pushing is Module 8's mechanics; the principle stands alone): the bad commit's inverse lands as a new commit, history only grows, and every copy of the repository stays consistent — nobody has to repair anything. Resolve any conflicts like a merge's. The alternative — resetting the branch and force-sharing the shortened history — invalidates every copy downstream and is reserved for genuine emergencies with team coordination (Module 10's territory).
The details that separate candidates: an average answer gives the command. A strong answer explains why revert is the shared-history tool — append-only means no one else's clones, builds, or in-flight work reference commits that stopped existing — and covers the special cases: a merge commit needs -m 1; a secret in the pushed commit is NOT fixed by revert (the content remains in history — that demands rewriting plus rotation, Modules 10/15); and a range revert (git revert <old>..HEAD) walks back several commits without rewriting anything.
🎯 "reset vs revert vs checkout — and which is safe on shared branches." — asked verbatim in Greenroom's Git Interview Questions, Jun 2026
Three different objects being moved. reset moves the branch pointer (with soft/mixed/hard deciding how far the index and working tree follow) — it rewrites the branch, so it is for commits that exist only in your copy. revert moves history forward — a new commit containing an inverse; the only one of the three that is unconditionally safe on shared branches, because it changes nothing anyone already has. checkout (today: switch/restore) moves you — HEAD to another branch or commit, or a file's content from the index — touching no history at all. Rule of thumb the question wants verbatim: revert for public commits, reset for local cleanup, checkout to move around.
The details that separate candidates: an average answer defines the three. A strong answer names what each one mutates (branch ref / history / HEAD-or-file) — the frame that makes the safety rule derivable — and adds the modern-command mapping (checkout split into switch + restore, Git 2.23) plus the honest edge: even reset is fine on a shared branch if the team explicitly coordinates (protected branches usually forbid it outright — Module 9). Knowing the reflog backstops every local mistake rounds out the answer.
🎯 "How do you revert a merge commit?" — asked verbatim in WeCreateProblems' 100+ GIT Interview Questions, 2026
git revert -m 1 <merge-hash>. A merge has two parents, so Git requires -m to define "inverse relative to which parent"; -m 1 — the branch that received the merge — is almost always correct, subtracting the merged branch's entire contribution as one new commit. Conflicts resolve merge-style. Critical follow-on: the merged branch's commits remain in history — only their content was backed out — so merging that branch again later is a no-op. To land the feature again, revert the revert (git revert <revert-hash> — Git titles it "Reapply") or rebuild it as new commits.
The details that separate candidates: an average answer stops at -m 1. A strong answer explains what the parent numbers are (^1 = receiving branch, ^2 = merged branch — Module 6's anatomy), states the re-merge trap and its two escapes, and connects to workflow: this is why --no-ff/PR merge commits are operationally valuable — one commit to revert rolls back one feature, which is the fastest "get it out of production" move that never rewrites shared history.
Part E — Production practice
E1. Symptom → cause → diagnosis → fix
Click the symptom you're seeing.
⚠️ Ran git reset --hard and a needed commit vanished
What is really happening: The branch stopped naming it; the commit object still exists.
Diagnose: git reflog — find the entry whose left hash is the lost commit
The fix: Run git reset --hard HEAD@{N} (or git branch rescue <hash> to be gentler).
⚠️ Ran git restore <file> / reset --hard and uncommitted edits vanished
What is really happening: Never-staged content has no object anywhere — the one unrecoverable class.
Diagnose: git reflog will NOT help; check editor local-history/backups
The fix: Nothing in Git can bring it back; prevention is to commit early — WIP commits are free and reflog-covered.
⚠️ Deleted a branch, need it back, hash unknown
What is really happening: The pointer is gone; the commits are intact and journaled in HEAD's reflog.
Diagnose: git reflog | grep "<commit subject>" · scrollback for (was <hash>)
The fix: Run git branch <name> <hash> — then push it somewhere durable.
⚠️ error: commit <x> is a merge but no -m option was given.
What is really happening: Reverting a two-parent commit needs a stated baseline.
Diagnose: git cat-file -p <x> | head -3 — see the two parents
The fix: Run git revert -m 1 <x> (parent 1 = the branch that received the merge).
⚠️ Re-merging a branch after reverting its merge brings no changes
What is really happening: The commits are already ancestors; only their content was subtracted.
Diagnose: git log --oneline <branch> --not main — empty: nothing new to merge
The fix: Revert the revert ("Reapply"), or land the work as fresh commits.
⚠️ Undid a "bad" commit on a branch teammates use; now their copies misbehave
What is really happening: Reset rewrote shared history — downstream copies reference commits your branch dropped.
Diagnose: Ask what they see (Module 8 makes the symptoms concrete)
The fix: Restore the old tip from reflog, then git revert the bad commit instead; adopt "revert in public, reset in private."
⚠️ Revert stopped with conflict markers
What is really happening: The inverse patch overlaps changes made since — the same three-way machinery as merge.
Diagnose: git status — unmerged paths list
The fix: Resolve → add → git revert --continue, or git revert --abort.
E2. Capstone — four tickets
Worked answer: git revert 4f2ea11, resolve any conflict with current code, share the result. Why not reset: shortened shared history invalidates three people's in-flight work and removes the evidence — auditors need mistake and correction both visible, which the revert pair provides (D1's philosophy point as compliance requirement). If the bad commit was a merge (feature rollback): git revert -m 1 <merge> — one command, whole feature out, and note in the incident doc that re-landing the feature later requires reverting the revert. Speed note for the interview version: revert is also the fastest safe rollback, because it needs no coordination — appends never break anyone.
Worked answer: unwrap, then re-stage in pieces: git reset HEAD~1 (mixed — commit undone, all changes back in the working tree, unstaged), then three rounds of selective staging: git add <bugfix files> (or git add -p for hunk-level picks within a file, Module 2) → git commit -m "Fix …"; repeat for stub and tweak. --soft would also work but leaves everything pre-staged, which fights the goal of selecting. Everything stays local, so reset is the right tool by D's rule; and if the split goes sideways mid-way, git reflog still holds the original WIP commit — reset back to it and start over. This flow (reset → re-stage → re-commit) is the manual form of what interactive rebase automates later (Module 10).
Worked answer: two minutes, no re-doing. git reflog — the journal shows this morning's commits with messages; identify the newest one that should be the tip (say HEAD@{2}); git reset --hard HEAD@{2} (or, more cautiously, git branch rescue HEAD@{2} first, inspect with git log rescue, then move the real branch). Then the two teaching points that prevent the next incident: the only thing --hard destroys forever is uncommitted work — the moment something is committed, the reflog covers it for weeks; so commit early, even messily (Ticket 2 shows how messy commits become clean ones later). And institute the habit that makes --hard safe: run git status first — a clean tree means --hard can only lose what is already recoverable.
Worked answer (the tree): Not committed yet? — wrong stage: git restore --staged <f>; wrong working-tree edit: git restore <f> (accepting the loss) — nothing else in this tree destroys data, this branch can. Committed, not shared? — message/content fix: commit --amend; unwrap: reset --soft HEAD~1; unwrap + unstage: reset HEAD~1; abandon: reset --hard <good> after a clean git status. Shared? — single commit: git revert <hash>; feature via merge commit: git revert -m 1 <merge> (+ document the re-merge trap); several commits: git revert <old>..HEAD; never reset shared branches without an incident-level decision. Something seems lost? — git reflog first, always, before any re-work; git branch rescue <hash> to make findings durable. Close the runbook with the one-liner that summarizes the module: restore for files, reset for private history, revert for public history, reflog before panic.
E3. Documentation reference
| Topic | Official source | What it covers |
|---|---|---|
| git restore | git-restore manual | Working-tree and --staged restores, --source |
| git reset | git-reset manual | All modes, path-limited reset, examples |
| Reset, deeply | Git Book — Reset Demystified | The three-trees walkthrough this Part is built on |
| git revert | git-revert manual | -m, --no-edit, --continue/--abort, ranges |
| git reflog | git-reflog manual | @{N} syntax, per-branch reflogs, expiry |
| Undoing (tutorial) | Git Book §2.4 — Undoing Things | Amend, unstage, discard — the gentle intro |
E4. Self-assessment
Try each question aloud, from memory, before opening its answer — the gap between your answer and the hidden one is what to study.
1. Which single undo command in this module destroys unrecoverable data, and why exactly is it unrecoverable?
git restore <file> (and reset --hard when it catches uncommitted edits). The edits it discards were never committed and never staged, so they exist in no object anywhere — there is nothing in the object database for any recovery tool to find. Git protects what you gave it; it cannot protect what you never gave it.
2. git restore --staged <f>: which tree changes, which two do not?
Only the index changes: the HEAD version of the file's entry is copied back into the index, un-staging your change. The working tree and HEAD are untouched — nothing is lost, the change simply exits the next-commit draft. This is the safe half of restore: unstaging is always reversible (just add again).
3. What does every git reset do regardless of mode, and what question does the mode answer?
Every reset moves the current branch's pointer to an arbitrary commit — the 41-byte ref file rewritten to a hash of your choosing; the commits after that point do not vanish, the branch simply stops naming them. The mode flag answers one question: after the pointer moves, which of the other two trees follow it? Soft touches one tree, mixed two, hard all three — one operation with an increasing blast radius.
4. Soft vs mixed after HEAD~1: where do the undone commit's changes sit in each?
After --soft, the changes sit staged in the index, precisely as they were the moment before you committed — ready to re-commit under a better message or split into pieces. After --mixed (the default), the index is reset too, so the changes survive only in the working tree, unstaged. Neither touches the files on disk.
5. Why is git reset --hard HEAD (no movement) still useful, and what does it share with Exercise 7.1?
With HEAD as the target the pointer stays put and the other two trees are forced to match it — "make everything look committed again," the bulk version of Exercise 7.1's single-file restore. It shares 7.1's danger exactly: the discarded edits were never objects, so they are unrecoverable — which is why --hard deserves a one-second pause every time you type it.
6. What exactly does the reflog record — and name two things it does not record.
The reflog is a local journal recording every time HEAD or a branch pointer moved — commit, switch, merge, reset, revert — with a timestamped entry saying what moved it. It does not record uncommitted edits (work that never moved a pointer — blown away by restore or reset --hard, it was never tracked), and it does not record anyone else's movements — it is strictly local, never pushed or cloned. Entries also expire: about 90 days, about 30 for entries pointing at now-unreachable commits.
7. HEAD@{1}: read it aloud in words. How does it differ from HEAD~1?
"Where HEAD was one move ago" — an address into the reflog's journal of pointer positions. HEAD~1 is graph ancestry — the first parent of the current commit. One walks the journal of where you have been; the other walks the commit graph's parent chain. After a reset, HEAD@{1} is where you stood before it — which is exactly what makes a bad reset walk-backable.
8. Walk the deleted-branch recovery when you know only the commit message.
HEAD's journal keeps every commit you made while standing on the branch, with messages attached, even after the branch's own journal died with it. So: git reflog | grep "<commit subject>" to find the entry, take its left-hand hash, then git branch <name> <hash> to make it reachable again. Verify with git log --oneline <name> that the resurrected branch has the expected history.
9. State revert's mechanism in one sentence, and the one property that makes it shared-history-safe.
git revert <commit> computes the target commit's changes, applies the inverse to your working tree and index, and records that inverse as a new commit. The safety property: nothing is removed — the branch only ever grows, so nobody's existing copy becomes wrong. Mistake and antidote both stay in history.
10. Why does reverting a merge need -m, which number do you almost always pass, and what is the re-merge trap?
A merge commit has two parents, so "the inverse of this commit" is ambiguous — undo relative to which parent? Git refuses to guess and demands -m <parent-number>. You almost always pass -m 1: parent 1 is the branch that received the merge, so -m 1 takes the feature's arrival back out of your branch. The trap: the merged branch's commits remain ancestors — only their content was subtracted — so re-merging later brings nothing; you must revert the revert (Git titles it "Reapply") or land the work as new commits.
11. Recite the runbook one-liner: which tool for files / private history / public history / panic?
Restore for files, reset for private history, revert for public history, reflog before panic. Choose the tool by where the change lives — the location determines the command.
12. A teammate asks "does revert take us back to how things were at that commit?" — correct the misconception precisely.
No — revert undoes one commit's changes, not everything back to that commit. Reverting a five-commit-old commit leaves the four newer commits fully intact; it subtracts exactly one change from the current state. Reset is time travel; revert is targeted subtraction — and time travel on a shared branch actually needs a sequence of reverts (git revert <old>..HEAD) or a very deliberate team conversation.
E5. Sources
🗒️ Cheat sheet — Module 7
| Command | What it does |
|---|---|
| git restore <file> | Overwrite working-tree file from the index — destroys the uncommitted edit |
| git restore --staged <file> | Unstage (index ← HEAD); working tree untouched; always reversible |
| git restore --source <commit> <file> | Restore a file's content from any commit |
| git reset --soft HEAD~1 | Uncommit; changes stay staged (re-message / regroup) |
| git reset HEAD~1 | Uncommit and unstage (mixed, the default); changes stay on disk |
| git reset --hard <commit> | Move branch + index + working tree; uncommitted edits are gone for good |
| git reflog · git reflog | grep "<msg>" | HEAD's movement journal · find a lost commit by its message |
| git reset --hard HEAD@{1} · git branch rescue <hash> | Walk back the last pointer move · make any found commit reachable again |
| git revert <commit> · --no-edit | Commit the inverse of one commit — the shared-history undo |
| git revert -m 1 <merge> | Back out a whole merged feature relative to the receiving branch |
| git revert <old>..HEAD · --continue / --abort | Revert a range, newest first · finish or bail out of a conflicted revert |
Key concepts: choose the tool by where the change lives — working tree (restore), index (restore --staged), private history (reset/amend), public history (revert) · reset = move the branch pointer; modes soft/mixed/hard = how many trees follow (1/2/3) · commits are never deleted by reset — the branch just stops naming them; the reflog remembers every pointer position (~90 days; ~30 for unreachable), locally only · the single unrecoverable loss: content that was never staged or committed · revert = append the inverse; history only grows, so shared copies stay valid · merge revert needs -m 1, and re-merging afterwards is a no-op until the revert is reverted · reflog before panic, always.