Reframe sweep M7-27 + capstone (AI drives git, lesson=theory, de-slop) (#93)
Sync course wiki / sync-wiki (push) Successful in 11s
Sync course wiki / sync-wiki (push) Successful in 11s
Co-authored-by: claude <claude@jpaul.io> Co-committed-by: claude <claude@jpaul.io>
This commit was merged in pull request #93.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Module 12 — When It Goes Wrong: Revert, Reset, and Recovery
|
||||
|
||||
> **A bad change already shipped. Now what?** Recovery is its own skill — and knowing the *right*
|
||||
> undo for the situation is the difference between a clean five-second fix and force-pushing over
|
||||
> your teammates' work.
|
||||
> **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
|
||||
> teammates' work.
|
||||
|
||||
---
|
||||
|
||||
@@ -81,7 +81,7 @@ nobody has to force-anything. On a branch other people (or agents) share, `rever
|
||||
the correct answer.
|
||||
|
||||
This also maps straight back to the Module 2 reframe: the repo is durable memory. A `revert` commit
|
||||
is *more* informative than a silent erase — six months later, `git log` tells you the feature was
|
||||
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
|
||||
@@ -110,9 +110,9 @@ feature got merged into main," it's almost always `-m 1`. You can confirm the pa
|
||||
git show <merge-sha> --format="%P" --no-patch # prints the two parent SHAs, in order
|
||||
```
|
||||
|
||||
**The gotcha you must know about (honesty up front):** reverting a merge tells Git "the content of
|
||||
**The gotcha you must know about:** reverting a merge tells Git "the content of
|
||||
that branch is undone." If you later fix the branch and try to merge it again, Git looks at the
|
||||
*reverted* merge and decides those commits are already accounted for — so it brings in **nothing**,
|
||||
*reverted* merge and decides those commits are already accounted for, so it brings in **nothing**,
|
||||
or only the new commits, silently leaving your fix half-applied. The fix is counterintuitive: to
|
||||
re-merge a branch whose merge you reverted, **revert the revert** first (`git revert <revert-sha>`),
|
||||
then add your new work on top, then merge. This is a real, recurring source of "why didn't my merge
|
||||
@@ -148,7 +148,7 @@ The rule, stated plainly:
|
||||
|
||||
> **Already shared? Use `revert`. Only ever local? `reset` is fine.** When unsure, assume shared.
|
||||
|
||||
### `git reflog` — the net under the net
|
||||
### `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,
|
||||
@@ -167,12 +167,11 @@ git branch recovered a1b2c3d
|
||||
```
|
||||
|
||||
This is the answer to "an agent ran `git reset --hard` and ate an hour of my commits." As long as
|
||||
the work was *committed at some point*, the reflog can almost certainly get it back. It's the single
|
||||
most reassuring command in Git, and most people don't know it exists until the day they desperately
|
||||
need it.
|
||||
the work was *committed at some point*, the reflog can almost certainly get it back. Most people
|
||||
don't know it exists until the day they need it.
|
||||
|
||||
Two honest 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
|
||||
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
|
||||
breaks.")
|
||||
@@ -231,43 +230,54 @@ do them once on purpose now.
|
||||
**You'll need:**
|
||||
|
||||
- The `tasks-app` Git repo from Module 2 (with a few commits in its history).
|
||||
- Git installed, and your AI assistant available.
|
||||
- The starter file `lab/bad-clear-snippet.py` from this module — a deliberately broken `clear`
|
||||
- Git installed, and your agent in the repo. We use **Claude Code** as the worked example
|
||||
(`claude # sub your own agent`); the directing-and-verifying pattern is the same for any of them.
|
||||
- The starter file `lab/bad-clear-snippet.py` from this module, a deliberately broken `clear`
|
||||
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
|
||||
> waiting for a model to break something on demand.
|
||||
|
||||
### Part A — Merge a bad change, then revert the merge
|
||||
You direct the agent to do the git work and you verify the result. The whole point of this lab is
|
||||
that *you* hold the judgment: which undo, which parent, whether it actually worked.
|
||||
|
||||
1. Make sure you're on a clean `main`:
|
||||
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
|
||||
> there's nothing uncommitted.
|
||||
|
||||
Verify before you go further:
|
||||
|
||||
```bash
|
||||
cd ~/ai-workflow-course/tasks-app
|
||||
git switch main
|
||||
git status # should be clean
|
||||
git status # should be clean, on main
|
||||
```
|
||||
|
||||
2. Branch, and add the broken `clear` command. Open `cli.py`, and inside `main()`'s command dispatch
|
||||
(next to the other `elif command == ...` branches), paste the block from
|
||||
`lab/bad-clear-snippet.py`. It *looks* reasonable and even "works" once — the bug is that it
|
||||
corrupts the saved state so the **next** command crashes.
|
||||
2. Stage the broken change. The snippet in `lab/bad-clear-snippet.py` *looks* reasonable and even
|
||||
"works" once; the bug is that it corrupts the saved state so the **next** command crashes. Hand it
|
||||
to your agent:
|
||||
|
||||
> Create a branch `bad-clear`. Add the `elif command == "clear"` block from
|
||||
> `lab/bad-clear-snippet.py` into `cli.py`'s command dispatch inside `main()`, next to the other
|
||||
> `elif command == ...` branches. Commit it with the message `Add clear command`.
|
||||
|
||||
Verify the agent did exactly that, on the branch:
|
||||
|
||||
```bash
|
||||
git switch -c bad-clear
|
||||
# ...paste the snippet into cli.py, save...
|
||||
git add cli.py
|
||||
git commit -m "Add clear command"
|
||||
git log --oneline -1 # "Add clear command", on bad-clear
|
||||
git show HEAD -- cli.py | grep clear # the clear branch is in the diff
|
||||
```
|
||||
|
||||
3. Merge it into `main` with a real merge commit (the `--no-ff` forces a merge commit even though a
|
||||
fast-forward was possible — this is what a merged PR looks like):
|
||||
3. Merge it into `main` as a real merge commit (a merged PR is a merge commit, not a fast-forward):
|
||||
|
||||
> Switch to `main` and merge `bad-clear` with a real merge commit (no fast-forward), message
|
||||
> `Merge branch 'bad-clear'`.
|
||||
|
||||
Verify the shape:
|
||||
|
||||
```bash
|
||||
git switch main
|
||||
git merge --no-ff bad-clear -m "Merge branch 'bad-clear'"
|
||||
git log --oneline --graph -3
|
||||
git log --oneline --graph -3 # a merge commit sitting on main
|
||||
```
|
||||
|
||||
4. **Now feel the bug.** It passes the first skim:
|
||||
@@ -279,29 +289,39 @@ do them once on purpose now.
|
||||
```
|
||||
|
||||
This is the AI plausibility trap made concrete: the change reviewed fine and "worked," and broke
|
||||
the *next* command. It's merged on `main`. You need it gone — safely, because in a real team
|
||||
the *next* command. It's merged on `main`. You need it gone, and safely, because in a real team
|
||||
others may have already pulled.
|
||||
|
||||
5. Try the naive revert and watch it refuse, because a merge has two parents:
|
||||
5. Direct the agent to undo the bad merge, and watch the trap. Reverting a merge is fiddly: a naive
|
||||
`git revert HEAD` refuses, because a merge has two parents and Git won't guess which side to keep.
|
||||
Tell your agent:
|
||||
|
||||
```bash
|
||||
git revert HEAD # error: ... is a merge but no -m option was given
|
||||
> The merge we just put on `main` is bad. Undo it safely on shared history. Note that it's a merge
|
||||
> commit.
|
||||
|
||||
A naive revert hits this, and a competent agent recognizes it:
|
||||
|
||||
```
|
||||
error: commit ... is a merge but no -m option was given
|
||||
fatal: revert failed
|
||||
```
|
||||
|
||||
6. Confirm the parents, then revert the merge properly, keeping the `main` side (`-m 1`):
|
||||
The correct move keeps the `main` side, which is parent 1:
|
||||
|
||||
```bash
|
||||
git show HEAD --format="%P" --no-patch # two SHAs: parent 1 is main, parent 2 is bad-clear
|
||||
git revert -m 1 HEAD # writes a NEW commit that undoes the whole merge
|
||||
git log --oneline -3 # you'll see a "Revert ..." commit on top
|
||||
git revert -m 1 <merge-sha> # writes a NEW commit that undoes the whole merge
|
||||
```
|
||||
|
||||
> `git revert` drops you into your text editor with a pre-filled "Revert …" message — save and
|
||||
> close it (in vim, type `:wq` then Enter; in nano, Ctrl-O then Ctrl-X). Or add `--no-edit` to
|
||||
> keep that default message and skip the editor entirely: `git revert -m 1 HEAD --no-edit`. Either
|
||||
> way you end up with the same "Revert …" commit.
|
||||
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.
|
||||
|
||||
7. Prove you're recovered — and notice nothing was erased:
|
||||
```bash
|
||||
git show <merge-sha> --format="%P" --no-patch # two SHAs: parent 1 is main, parent 2 is bad-clear
|
||||
git log --oneline -3 # a "Revert ..." commit on top
|
||||
```
|
||||
|
||||
7. Prove you're recovered, and notice nothing was erased:
|
||||
|
||||
```bash
|
||||
rm -f tasks.json # drop the corrupted state file the bug wrote
|
||||
@@ -319,16 +339,20 @@ do them once on purpose now.
|
||||
|
||||
### Part B — "Lose" a commit, recover it with the reflog
|
||||
|
||||
1. Make a small real commit you'd be sad to lose:
|
||||
1. Make a small real commit you'd be sad to lose. Tell your agent:
|
||||
|
||||
> Add a trivial `version` command to `cli.py` that prints a version string, and commit it with the
|
||||
> message `Add version command`.
|
||||
|
||||
Verify it's there:
|
||||
|
||||
```bash
|
||||
# with your AI, add a trivial "version" command to cli.py that prints a version string, then:
|
||||
git add cli.py
|
||||
git commit -m "Add version command"
|
||||
git log --oneline -1 # note this commit exists
|
||||
git log --oneline -1 # "Add version command"
|
||||
python cli.py version # prints the version
|
||||
```
|
||||
|
||||
2. Now destroy it the way an over-eager cleanup (or an agent) would — a hard reset:
|
||||
2. Now destroy it the way an over-eager "clean up the history" cleanup (or an agent) would, with a
|
||||
hard reset. Run this one yourself so you feel the floor drop out:
|
||||
|
||||
```bash
|
||||
git reset --hard HEAD~1
|
||||
@@ -338,26 +362,36 @@ do them once on purpose now.
|
||||
|
||||
It's not in `log`. It feels permanently lost. It isn't.
|
||||
|
||||
3. Find it in the reflog and bring it back:
|
||||
3. Direct the agent to recover it from the reflog. You need to know the reflog exists so you can ask
|
||||
for it and check the result:
|
||||
|
||||
> My last commit was destroyed by a `git reset --hard`. Find it in the reflog and restore the
|
||||
> branch to it. Show me the reflog line you used before you reset.
|
||||
|
||||
Then verify. The commit is back, and the app works again:
|
||||
|
||||
```bash
|
||||
git reflog # find the line: "... commit: Add version command"
|
||||
git reset --hard <that-sha> # branch pointer back to the recovered commit
|
||||
# (or, more cautiously: git branch recovered <that-sha> then inspect before resetting)
|
||||
git log --oneline -1 # it's back
|
||||
git log --oneline -1 # "Add version command" is back
|
||||
python cli.py version # works again
|
||||
```
|
||||
|
||||
You just recovered a commit that `log` swore was gone. **That's the net under the net.** Note that
|
||||
step 2's `--hard` would have *also* eaten any uncommitted edits in the working tree at the time —
|
||||
and the reflog could **not** have saved those, because they were never committed. Recovery covers
|
||||
committed history, not unsaved scratch work.
|
||||
You just recovered a commit that `log` swore was gone. Note the honest limit: step 2's `--hard`
|
||||
would have *also* eaten any uncommitted edits in the working tree at the time, and the reflog could
|
||||
**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
|
||||
|
||||
Before you hand the agent something sweeping, have it tag the current known-good state:
|
||||
|
||||
> Tag the current commit as `known-good`, an annotated tag, message "Clean state at end of Module 12
|
||||
> lab".
|
||||
|
||||
Confirm the anchor exists:
|
||||
|
||||
```bash
|
||||
git tag -a known-good -m "Clean state at end of Module 12 lab"
|
||||
git diff known-good # later, this shows everything that changed since this anchor
|
||||
git tag # known-good is listed
|
||||
git diff known-good # later, this shows everything that changed since this anchor
|
||||
```
|
||||
|
||||
Get in the habit of tagging before you hand an agent something sweeping.
|
||||
@@ -397,8 +431,8 @@ like one is how people lose data they thought was safe.
|
||||
re-merging that branch later quietly does nothing useful until you *revert the revert*. Forget this
|
||||
and you'll burn an afternoon wondering why your fix won't merge.
|
||||
|
||||
The honest summary: Git is a near-perfect time machine for the *text you committed*, and nothing more.
|
||||
Know that boundary and you'll trust it exactly as far as it deserves.
|
||||
The boundary in one line: Git is a near-perfect time machine for the *text you committed*, and nothing
|
||||
more. Know that boundary and you'll trust it exactly as far as it deserves.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user