Scenario
Section titled “Scenario”Three commands, all of which “undo” something, all of which people confuse. The confusion is
expensive in one specific direction: reset --hard is the only one that can destroy work Git
never saw, and it is the one people reach for first because the name sounds decisive.
This lab puts you in five situations and asks you to pick. Each one is a thing that happens weekly on a real team.
Objective
Section titled “Objective”Choose correctly between restore, restore --staged, reset --soft and revert; see why
reverting a commit in the middle of history can conflict; and see exactly what reset --hard
throws away.
Prerequisites
Section titled “Prerequisites”- Git installed
- The working tree and the index — the three places a change can live are the whole subject here
Starting state
Section titled “Starting state”mkdir -p /tmp/lab-rrr && cd /tmp/lab-rrrgit init -q -b main .git config user.email "lab@example.com"git config user.name "Lab User"
printf 'line1\nline2\n' > config.txtgit add . && git commit -q -m "Initial config"printf 'line1\nline2\nline3\n' > config.txtgit add . && git commit -q -m "Add line3"printf 'line1\nline2\nline3\nline4\n' > config.txtgit add . && git commit -q -m "Add line4"
git log --oneline189b403 Add line4685027c Add line35b94860 Initial configFor each situation, decide which command applies before running it.
-
Situation A — you edited a file and want the edit gone. Nothing staged, nothing committed.
Terminal window echo "oops" >> config.txtgit status --shortWhich command discards the edit and leaves everything else alone?
-
Situation B — you staged a file by mistake. The edit should stay; it just should not be in the next commit.
Terminal window echo "staged" >> config.txtgit add config.txtgit status --shortWhich command unstages without touching the working tree? (Then discard the edit as in A.)
-
Situation C — you committed too early. The last commit should not exist, but its changes should stay staged so you can recommit properly.
Terminal window git reset --soft HEAD~1git status --shortgit log --onelineThen recommit:
git commit -q -m "Add line4". -
Situation D — a commit is already pushed and must be undone. Rewriting is not an option. Undo the tip commit:
Terminal window git revert HEAD --no-editgit log --onelinecat config.txt -
Situation E — undo a commit in the middle of history. Reset the tip revert first so
line4is back, then revertAdd line3:Terminal window git reset --hard HEAD~1 # remove the revert from D; line4 returnsgit revert HEAD~1 --no-editIt does not go cleanly. Read the output, then
cat config.txt. Resolve it so the file containsline1,line2,line4, and continue the revert. -
The destructive one. Make an uncommitted edit, then:
Terminal window echo "unsaved" >> config.txtgit status --shortgit reset --hard HEADgit status --shortgit reflog | head -3Where did the edit go?
Validation
Section titled “Validation”After step 5, git log --oneline shows a Revert "Add line3" commit on top of the three
originals and config.txt has three lines. After step 6, git status is clean and the word
unsaved appears nowhere — not in the file, not in any commit, not in the reflog.
A. The edit is only in the working tree. git restore <file> overwrites the working tree from
the index.
B. git restore --staged <file> copies the index entry back from HEAD. The working tree is
untouched, so the edit survives.
C. --soft moves the branch pointer and leaves both the index and working tree as they were.
--mixed (the default) would also unstage; --hard would also discard.
D. revert creates a new commit whose changes are the inverse. History only grows.
E. line4 was added on the line after line3. Removing line3 while line4 stays touches
adjacent lines, and Git cannot tell whether line4 depended on line3. That is a conflict, and it
is a judgement call — which is why Git stops.
6. It went nowhere. The reflog records commits; this edit was never committed.
Solution
Section titled “Solution”A
git restore config.txtB
git restore --staged config.txt # status: " M config.txt" — unstaged, edit keptgit restore config.txt # then discard itC
M config.txt685027c Add line35b94860 Initial configThe commit is gone; its change is staged (M in the first column).
D
[main 399dfa2] Revert "Add line4" 1 file changed, 1 deletion(-)config.txt now has three lines. The Add line4 commit is still in history.
E — the conflict:
Auto-merging config.txtCONFLICT (content): Merge conflict in config.txterror: could not revert 685027c... Add line3hint: After resolving the conflicts, mark them withhint: "git add/rm <pathspec>", then runhint: "git revert --continue".hint: You can instead skip this commit with "git revert --skip".hint: To abort and get back to the state before "git revert",hint: run "git revert --abort".line1line2<<<<<<< HEADline3line4=======>>>>>>> parent of 685027c (Add line3)Resolve by writing the file you want, then continue:
printf 'line1\nline2\nline4\n' > config.txtgit add config.txtgit revert --continue --no-editgit log --onelineb32d4d0 Revert "Add line3"189b403 Add line4685027c Add line35b94860 Initial config(git revert --abort at any point returns you to before the revert began.)
6
M config.txtHEAD is now at b32d4d0 Revert "Add line3"Status is clean. The reflog’s top entry is the revert — there is no entry for the edit, because the reflog records commits and this was never one.
Explanation
Section titled “Explanation”Three places, three commands. A change can be in the working tree, the index, or a commit.
restore operates on the first two. reset moves the branch pointer and optionally resets the
first two to match. revert creates a commit. Choosing correctly is a matter of asking where is
the change now, and where should it be?
| Where the change is | Where you want it | Command |
|---|---|---|
| Working tree only | Gone | git restore <file> |
| Staged | Working tree only | git restore --staged <file> |
| Last commit | Staged | git reset --soft HEAD~1 |
| Last commit | Working tree | git reset HEAD~1 |
| Any shared commit | Undone, history kept | git revert <commit> |
| Working tree, staged, or unshared commits | Gone | git reset --hard — unrecoverable for uncommitted work |
Revert of a middle commit is a merge. Git applies the inverse patch on top of everything that came after. If later commits touched the same region, that patch does not apply cleanly. The conflict markers show HEAD’s version (with both lines) against the reverted parent’s version (with neither), and you decide.
Troubleshooting
Section titled “Troubleshooting”Step 5’s revert applied cleanly with no conflict. You skipped the reset --hard HEAD~1 that
brings line4 back. Without line4 present there is nothing adjacent to conflict with.
git revert --continue opens an editor. Add --no-edit, or save and close the editor.
Step 3 left the working tree with the change but nothing staged. You ran git reset HEAD~1
(mixed) rather than --soft. Both are valid; --soft keeps it staged.
Clean up
Section titled “Clean up”cd /tmp && rm -rf lab-rrrRelated lessons
Section titled “Related lessons”Next lab
Section titled “Next lab”Parallel work with worktrees — fix a production bug without stashing, switching, or losing your place.