style(no-slop): remove every em-dash + banned words across all modules + capstone
Apply the no-ai-slop standard (now binding in AGENTS.md): the em-dash character is banned outright (restructured, not blind-replaced), plus the banned word/phrase list (delve, leverage, robust, seamless, truly, unlock, etc.). 0 em-dashes remain in modules + capstone; the only "robust" left is the planted M10 ai-change.patch trap. Module H1 titles use a colon separator. All deliberate teaching devices preserved; labs compile/parse (py/sh/yaml/json); no junk. AGENTS.md updated with the hard no-slop rules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Module 12 — When It Goes Wrong: Revert, Reset, and Recovery
|
||||
# Module 12: When It Goes Wrong: Revert, Reset, and Recovery
|
||||
|
||||
> **A bad change already shipped. Now what?** Recovery is its own skill. Knowing the *right* undo for
|
||||
> the situation is the difference between a clean five-second fix and force-pushing over your
|
||||
@@ -8,15 +8,15 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Module 2 — Version Control as a Safety Net.** You can commit, read a `diff`, and `git restore`
|
||||
- **Module 2: Version Control as a Safety Net.** You can commit, read a `diff`, and `git restore`
|
||||
uncommitted changes. This module is the rest of the undo toolkit: undoing things that are *already
|
||||
committed*, including things already shared.
|
||||
- **Module 6 — Branches: Sandboxes for Experiments.** You merge branches. The headline example here
|
||||
- **Module 6: Branches: Sandboxes for Experiments.** You merge branches. The headline example here
|
||||
is undoing a bad *merge*, which only makes sense once you've made one.
|
||||
- **Module 8 — Remotes and Hosting.** You've pushed history somewhere others can pull it. That's what
|
||||
makes "shared history" real — and it's the dividing line between the safe undo and the dangerous
|
||||
- **Module 8: Remotes and Hosting.** You've pushed history somewhere others can pull it. That's what
|
||||
makes "shared history" real, and it's the dividing line between the safe undo and the dangerous
|
||||
one. Module 8 was the *backup* half of the backup-and-recovery thread; this is the *recovery* half.
|
||||
- **Modules 10–11 — Reviewing Code You Didn't Write / Collaboration.** A bad change usually arrives
|
||||
- **Modules 10–11: Reviewing Code You Didn't Write / Collaboration.** A bad change usually arrives
|
||||
as a merged PR, and other people (and agents) are pulling from the same branch. Recovery has to be
|
||||
safe for *them*, not just you.
|
||||
|
||||
@@ -29,13 +29,13 @@ If you've parachuted in: you minimally need to be comfortable with commits, bran
|
||||
|
||||
By the end of this module you can:
|
||||
|
||||
1. Choose the correct undo for a situation — `restore`, `revert`, or `reset` — and explain why the
|
||||
1. Choose the correct undo for a situation (`restore`, `revert`, or `reset`) and explain why the
|
||||
other two would be wrong.
|
||||
2. Cleanly undo a change that's already on shared history with `git revert`, including the hard case:
|
||||
reverting a merge commit.
|
||||
3. Recover commits you thought you'd destroyed using `git reflog`, even after a `reset --hard`.
|
||||
4. Drop named recovery points with tags (and host releases) before risky work.
|
||||
5. State precisely where Git's recovery powers end — what it is *not* a backup for, and why that
|
||||
5. State precisely where Git's recovery powers end: what it is *not* a backup for, and why that
|
||||
matters before you trust it.
|
||||
|
||||
---
|
||||
@@ -45,23 +45,23 @@ By the end of this module you can:
|
||||
### Three undos, three blast radii
|
||||
|
||||
Git has more than one "undo," and the failure mode is using the wrong one. They differ by *what they
|
||||
touch* and *whether they're safe once history is shared*. Hold this table in your head — the rest of
|
||||
touch* and *whether they're safe once history is shared*. Hold this table in your head; the rest of
|
||||
the module is just filling it in:
|
||||
|
||||
| Command | Undoes | Touches history? | Safe on shared history? |
|
||||
|---------|--------|------------------|--------------------------|
|
||||
| `git restore <file>` | **Uncommitted** edits in your working tree | No | Yes — there's nothing shared to break |
|
||||
| `git revert <commit>` | An **already-committed** change, by writing a *new* inverse commit | No — it *adds* | **Yes** — this is the team-safe undo |
|
||||
| `git reset <commit>` | Moves your branch pointer **backward**, un-committing | **Yes — it rewrites** | **No** — dangerous once others have pulled |
|
||||
| `git restore <file>` | **Uncommitted** edits in your working tree | No | Yes; there's nothing shared to break |
|
||||
| `git revert <commit>` | An **already-committed** change, by writing a *new* inverse commit | No; it *adds* | **Yes**; this is the team-safe undo |
|
||||
| `git reset <commit>` | Moves your branch pointer **backward**, un-committing | **Yes; it rewrites** | **No**; dangerous once others have pulled |
|
||||
|
||||
`restore` you already met in Module 2 — it's for the mess that hasn't been committed yet. This module
|
||||
`restore` you already met in Module 2; it's for the mess that hasn't been committed yet. This module
|
||||
is the other two rows, because the AI's worst messes are the ones that already made it into a commit,
|
||||
a merge, or a PR.
|
||||
|
||||
### `git revert` — undo by adding, not erasing
|
||||
### `git revert`: undo by adding, not erasing
|
||||
|
||||
The mental model: a commit is a diff (a set of line changes). `git revert <commit>` computes the
|
||||
*opposite* diff and commits it. The bad change is still in the history — but a new commit immediately
|
||||
*opposite* diff and commits it. The bad change is still in the history, but a new commit immediately
|
||||
after it cancels it out. The net effect on your files is "as if it never happened"; the net effect on
|
||||
your *history* is "we tried it, then we deliberately undid it," which is honest and readable.
|
||||
|
||||
@@ -84,7 +84,7 @@ This also maps straight back to the Module 2 reframe: the repo is durable memory
|
||||
is *more* informative than a silent erase. Six months later, `git log` tells you the feature was
|
||||
tried and pulled, and the message says why. You're writing the project's memory, not editing it.
|
||||
|
||||
### Reverting a bad **merge** — the headline case
|
||||
### Reverting a bad **merge**: the headline case
|
||||
|
||||
This is the one that bites people, because it's exactly what happens when a bad PR gets merged
|
||||
(Modules 10–11): you don't have one bad commit, you have a *merge commit* that pulled in a whole
|
||||
@@ -95,14 +95,14 @@ error: commit abc123 is a merge but no -m option was given.
|
||||
fatal: revert failed
|
||||
```
|
||||
|
||||
A merge commit has **two parents** — the branch you were on, and the branch you merged in. Git can't
|
||||
A merge commit has **two parents**: the branch you were on, and the branch you merged in. Git can't
|
||||
guess which side is "the mainline you want to keep." You tell it with `-m`:
|
||||
|
||||
```bash
|
||||
git revert -m 1 <merge-sha>
|
||||
```
|
||||
|
||||
`-m 1` means "treat parent #1 — the branch I was sitting on when I merged, i.e. `main` — as the line
|
||||
`-m 1` means "treat parent #1 (the branch I was sitting on when I merged, i.e. `main`) as the line
|
||||
to keep, and undo everything the *other* side brought in." `-m 2` would mean the opposite. For "a bad
|
||||
feature got merged into main," it's almost always `-m 1`. You can confirm the parents before you act:
|
||||
|
||||
@@ -118,11 +118,11 @@ re-merge a branch whose merge you reverted, **revert the revert** first (`git re
|
||||
then add your new work on top, then merge. This is a real, recurring source of "why didn't my merge
|
||||
do anything," and now you know the cause.
|
||||
|
||||
### `git reset` — moving the branch pointer (and why it's sharp)
|
||||
### `git reset`: moving the branch pointer (and why it's sharp)
|
||||
|
||||
`git reset <commit>` doesn't write an inverse commit. It **moves your current branch to point at an
|
||||
older commit**, effectively un-committing everything after it. Because it changes *which commits the
|
||||
branch contains*, it rewrites history — and that's both its power and its danger.
|
||||
branch contains*, it rewrites history, and that's both its power and its danger.
|
||||
|
||||
It comes in three flavors that differ only in what they do to your files:
|
||||
|
||||
@@ -138,7 +138,7 @@ git reset --hard HEAD~1 # un-commit AND throw the changes away entirely
|
||||
- `--hard` deletes the changes from your working tree too. This is the one that ruins days.
|
||||
|
||||
**When `reset` is correct:** *only on history you have not shared.* Cleaning up your own local
|
||||
commits before you push — squashing three "wip" commits into one, fixing a botched last commit — is
|
||||
commits before you push (squashing three "wip" commits into one, fixing a botched last commit) is
|
||||
exactly what it's for. The moment a commit has been pushed and someone else has pulled it, `reset`
|
||||
becomes a way to *rewrite history out from under them*: your branch and theirs now disagree about
|
||||
what happened, and the only way to push your rewritten version is `--force`, which overwrites the
|
||||
@@ -148,11 +148,11 @@ The rule, stated plainly:
|
||||
|
||||
> **Already shared? Use `revert`. Only ever local? `reset` is fine.** When unsure, assume shared.
|
||||
|
||||
### `git reflog` — recovering commits you thought you destroyed
|
||||
### `git reflog`: recovering commits you thought you destroyed
|
||||
|
||||
Here's the reassuring part. `reset --hard` *feels* like it nukes commits permanently. It almost
|
||||
never does. Git keeps a private, local log of **everywhere `HEAD` has ever pointed** — every commit,
|
||||
reset, checkout, merge, rebase — in the *reflog*. A commit you "lost" with `reset --hard` is no
|
||||
never does. Git keeps a private, local log of **everywhere `HEAD` has ever pointed**: every commit,
|
||||
reset, checkout, merge, and rebase lands in the *reflog*. A commit you "lost" with `reset --hard` is no
|
||||
longer reachable from your branch, but it's still in the object database, and the reflog still knows
|
||||
its SHA.
|
||||
|
||||
@@ -161,7 +161,7 @@ git reflog
|
||||
# 9f8e7d6 HEAD@{0}: reset: moving to HEAD~1
|
||||
# a1b2c3d HEAD@{1}: commit: Add the feature I just "lost" <- there it is
|
||||
# ...
|
||||
git reset --hard a1b2c3d # branch pointer back to the lost commit — fully recovered
|
||||
git reset --hard a1b2c3d # branch pointer back to the lost commit, fully recovered
|
||||
# or, more cautiously, inspect it first on a throwaway branch:
|
||||
git branch recovered a1b2c3d
|
||||
```
|
||||
@@ -173,13 +173,13 @@ don't know it exists until the day they need it.
|
||||
Two limits, because they matter: the reflog is **local only** (it's not pushed; a fresh clone
|
||||
has an empty reflog), and entries **expire**. Unreachable ones are garbage-collected after roughly
|
||||
30 days by default, reachable ones after about 90. The reflog is a recovery net for *recent* mistakes
|
||||
on *your* machine, not an archive. (And it can only recover what was *committed* — see "Where it
|
||||
on *your* machine, not an archive. (And it can only recover what was *committed*; see "Where it
|
||||
breaks.")
|
||||
|
||||
### Tags and releases — named recovery points
|
||||
### Tags and releases: named recovery points
|
||||
|
||||
Commits have SHAs; SHAs are unmemorable. A **tag** is a human-readable, permanent name pinned to a
|
||||
specific commit — a recovery point you can actually find later.
|
||||
specific commit, a recovery point you can actually find later.
|
||||
|
||||
```bash
|
||||
git tag -a v1.0 -m "Last known-good before the big AI refactor" # annotated tag on HEAD
|
||||
@@ -192,7 +192,7 @@ git checkout v1.0 # inspect the exact known-good state
|
||||
Use them as deliberate checkpoints: **before you turn an agent loose on a large, sweeping change, tag
|
||||
the known-good state.** If the refactor goes wrong, `v1.0` is a named anchor you can diff against or
|
||||
return to without spelunking through `log` for the right SHA. On your git host, a **release** is a tag
|
||||
plus notes and downloadable artifacts — the same idea, dressed up as a thing the rest of the team can
|
||||
plus notes and downloadable artifacts, the same idea dressed up as a thing the rest of the team can
|
||||
point at. Tags are the durable, *shareable* recovery points the reflog is not.
|
||||
|
||||
---
|
||||
@@ -201,16 +201,16 @@ point at. Tags are the durable, *shareable* recovery points the reflog is not.
|
||||
|
||||
Recovery was always a real skill. AI raises its value on every axis:
|
||||
|
||||
- **AI makes bigger, bolder changes faster — and lands them through the same PR door.** A sweeping
|
||||
- **AI makes bigger, bolder changes faster, and lands them through the same PR door.** A sweeping
|
||||
"refactor the whole module" that *looks* right, passes a human skim (Module 10), gets merged
|
||||
(Module 11), and only then reveals it broke something. That's a bad *merge* on shared history — the
|
||||
(Module 11), and only then reveals it broke something. That's a bad *merge* on shared history, the
|
||||
exact case `git revert -m 1` exists for. The faster code merges, the more you need the clean,
|
||||
team-safe undo.
|
||||
- **Agents run destructive git commands.** An agent told to "clean up the branch history" can reach
|
||||
for `reset --hard` or a force-push and vaporize work. `reflog` is your net for precisely this —
|
||||
for `reset --hard` or a force-push and vaporize work. `reflog` is your net for precisely this,
|
||||
which is why an IT pro supervising agents needs it *cold*, not as trivia.
|
||||
- **Recovery is durable memory, done right.** A `revert` commit records that something was tried and
|
||||
pulled, and why — readable by the next session (Module 2's reframe) and by the next teammate. A
|
||||
pulled, and why, readable by the next session (Module 2's reframe) and by the next teammate. A
|
||||
silent `reset` erases that memory. On a project where agents reconstruct state from `git log`,
|
||||
preferring `revert` over `reset` keeps the history honest for the next agent that reads it.
|
||||
- **The "tag before the risky thing" habit is an AI habit.** The riskiest changes in your week are
|
||||
@@ -236,7 +236,7 @@ do them once on purpose now.
|
||||
command, so everyone produces the *same* bad merge instead of relying on the AI to misbehave on cue.
|
||||
|
||||
> **A note on realism.** By now (post–Module 4) your AI edits files directly. We hand you the exact
|
||||
> broken snippet anyway so the lab is deterministic — the point is practicing the *recovery*, not
|
||||
> broken snippet anyway so the lab is deterministic; the point is practicing the *recovery*, not
|
||||
> waiting for a model to break something on demand.
|
||||
|
||||
You direct the agent to do the git work and you verify the result. The whole point of this lab is
|
||||
@@ -244,7 +244,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
|
||||
|
||||
1. Get the repo onto a clean `main`. Tell your agent:
|
||||
|
||||
> Make sure `~/ai-workflow-course/tasks-app` is on a clean `main` — switch to it and confirm
|
||||
> Make sure `~/ai-workflow-course/tasks-app` is on a clean `main`; switch to it and confirm
|
||||
> there's nothing uncommitted.
|
||||
|
||||
Verify before you go further:
|
||||
@@ -284,7 +284,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
|
||||
|
||||
```bash
|
||||
python cli.py add "ship it"
|
||||
python cli.py clear # prints "cleared all tasks" — looks fine!
|
||||
python cli.py clear # prints "cleared all tasks", looks fine!
|
||||
python cli.py list # CRASHES: it corrupted tasks.json, load() blows up
|
||||
```
|
||||
|
||||
@@ -312,7 +312,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
|
||||
git revert -m 1 <merge-sha> # writes a NEW commit that undoes the whole merge
|
||||
```
|
||||
|
||||
6. **Verify and decide — this is the part you own.** Don't take "I reverted it" on faith. Confirm the
|
||||
6. **Verify and decide; this is the part you own.** Don't take "I reverted it" on faith. Confirm the
|
||||
agent kept the *right* parent: parent 1 is the old `main` tip, parent 2 is `bad-clear`, and `-m 1`
|
||||
keeps parent 1. If it had used `-m 2` it would have kept the broken side.
|
||||
|
||||
@@ -326,7 +326,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
|
||||
```bash
|
||||
rm -f tasks.json # drop the corrupted state file the bug wrote
|
||||
python cli.py add "back to normal"
|
||||
python cli.py list # works again — the clear command is gone
|
||||
python cli.py list # works again, the clear command is gone
|
||||
git log --oneline # the bad merge is STILL there, with a revert after it
|
||||
```
|
||||
|
||||
@@ -337,7 +337,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
|
||||
That last point is the whole lesson: you undid the effect **without rewriting history**. Anyone who
|
||||
pulled the bad merge just pulls your revert on top and they're fine.
|
||||
|
||||
### Part B — "Lose" a commit, recover it with the reflog
|
||||
### Part B: "Lose" a commit, recover it with the reflog
|
||||
|
||||
1. Make a small real commit you'd be sad to lose. Tell your agent:
|
||||
|
||||
@@ -380,7 +380,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
|
||||
**not** have saved those, because they were never committed. Recovery covers committed history, not
|
||||
unsaved scratch work.
|
||||
|
||||
### Part C (optional) — Drop a named recovery point
|
||||
### Part C (optional): Drop a named recovery point
|
||||
|
||||
Before you hand the agent something sweeping, have it tag the current known-good state:
|
||||
|
||||
@@ -405,27 +405,27 @@ important thing it teaches is **where the analogy stops.** Git gives you excelle
|
||||
logical recovery for versioned text*. It is emphatically **not** a general backup system. Treating it
|
||||
like one is how people lose data they thought was safe.
|
||||
|
||||
- **It is not backup for your database — or any runtime state.** Your app's data lives in a database,
|
||||
- **It is not backup for your database, or any runtime state.** Your app's data lives in a database,
|
||||
in object storage, on a running server. None of that is in the repo (and shouldn't be). `git revert`
|
||||
rolls back *code*; it does nothing for the rows your buggy migration already mangled. Restoring data
|
||||
is a different discipline with different tools — Git has no opinion on it.
|
||||
- **It is not backup for secrets — which shouldn't be in there anyway.** API keys, tokens, and
|
||||
is a different discipline with different tools; Git has no opinion on it.
|
||||
- **It is not backup for secrets, which shouldn't be in there anyway.** API keys, tokens, and
|
||||
credentials don't belong in the repo in the first place (Module 17 is the whole story). If they *did*
|
||||
leak in, note the trap: `revert` does **not** remove them from history — the secret is still sitting
|
||||
leak in, note the trap: `revert` does **not** remove them from history; the secret is still sitting
|
||||
in the old commit for anyone with the repo. A committed secret is a *leaked* secret; rotate it, don't
|
||||
just revert it.
|
||||
- **It only recovers what was committed.** This is Module 2's limit, sharpened. `reset --hard` and
|
||||
`git restore` both destroy *uncommitted* working-tree changes, and **the reflog cannot bring those
|
||||
back** — there's no object to recover because nothing was ever committed. The defense is the same one
|
||||
back**; there's no object to recover because nothing was ever committed. The defense is the same one
|
||||
the whole course keeps repeating: commit often, so "uncommitted" is always a small window.
|
||||
- **It is poor backup for large binaries.** Git versions text beautifully and binaries terribly
|
||||
(Module 3): every change to a big binary stores a whole new copy, bloating the repo, and the "diff"
|
||||
is useless noise you can't review or merge. Datasets, video, compiled artifacts, model weights —
|
||||
is useless noise you can't review or merge. Datasets, video, compiled artifacts, model weights:
|
||||
these need real artifact/object storage, not your Git history.
|
||||
- **The reflog is local and temporary.** It's your machine only — not pushed, empty in a fresh clone —
|
||||
- **The reflog is local and temporary.** It's your machine only (not pushed, empty in a fresh clone),
|
||||
and it's garbage-collected (roughly 30 days for unreachable entries). It's a recovery net for recent
|
||||
local mistakes, not an offsite archive. The *offsite, distributed* durability comes from pushing to
|
||||
remotes — which is exactly Module 8's half of this thread. Recovery (this module) and backup
|
||||
remotes, which is exactly Module 8's half of this thread. Recovery (this module) and backup
|
||||
(Module 8) are two different powers; you need both.
|
||||
- **Reverting a merge has a sting in the tail.** As covered above: once you `revert -m 1` a merge,
|
||||
re-merging that branch later quietly does nothing useful until you *revert the revert*. Forget this
|
||||
@@ -442,13 +442,13 @@ more. Know that boundary and you'll trust it exactly as far as it deserves.
|
||||
|
||||
- You can state, without looking, which undo to use for (a) an uncommitted mess, (b) a bad change
|
||||
already pushed to a shared branch, and (c) three local "wip" commits you want to squash before
|
||||
pushing — and why the wrong choice is wrong in each case.
|
||||
pushing, and why the wrong choice is wrong in each case.
|
||||
- You have reverted a real merge commit with `git revert -m 1` on your `tasks-app`, and your `git log`
|
||||
shows both the bad merge and the revert sitting on top of it (history preserved, effect undone).
|
||||
- You have "lost" a commit with `reset --hard` and recovered it from `git reflog`.
|
||||
- You can explain, in one breath, four things Git is *not* a backup for: your database, your secrets,
|
||||
your uncommitted changes, and your large binaries — and why the reflog wouldn't have saved the third.
|
||||
your uncommitted changes, and your large binaries, and why the reflog wouldn't have saved the third.
|
||||
|
||||
When `revert` vs. `reset` is automatic, the reflog feels like a safety net instead of a rumor, and you
|
||||
can name where Git's recovery stops, you've got the recovery half of the thread. That completes the
|
||||
team layer (Unit 2) — next, Unit 3 starts automating the checking and shipping, beginning with tests.
|
||||
team layer (Unit 2); next, Unit 3 starts automating the checking and shipping, beginning with tests.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Module 12 lab — the deliberately BROKEN `clear` command.
|
||||
# Module 12 lab: the deliberately BROKEN `clear` command.
|
||||
#
|
||||
# Paste the elif block below into cli.py's main(), alongside the other
|
||||
# `elif command == "..."` branches (e.g. right after the "done" branch).
|
||||
# Do NOT paste this header or the import line into cli.py if json is already
|
||||
# imported there (it is) — just the elif block.
|
||||
# imported there (it is); just the elif block.
|
||||
#
|
||||
# Why it's broken: it "works" once (prints a friendly message), but it writes
|
||||
# the state file in the WRONG SHAPE. The next time the app loads tasks.json,
|
||||
|
||||
Reference in New Issue
Block a user