# Your AI Just Force-Pushed Over a Day of Work. Now What? Let me paint you a picture I've actually lived. You hand an agent a tidy little instruction ("clean up the branch history before we open the PR") and walk off to refill your coffee. You come back, glance at `git log`, and a commit you definitely made an hour ago is just… not there. The agent decided "clean up" meant `git reset --hard`, helpfully threw away the thing you cared about, and reported success. Your pulse does a thing. Here's what I want you to take from this post: that moment is survivable, and which command you reach for *next* is the entire ballgame. Recovery is its own discipline: not a vibe, not Ctrl-Z mashing, but a small set of tools where picking the right one is the difference between a clean five-second fix and force-pushing your teammate's work into the void. This is the last stop in Unit 2 of [The Workflow](https://git.jpaul.io/justin/ai-workflow-course), my free course for IT folks who can already get an AI to write code but keep getting bitten by everything *around* it. Back in the earlier posts we installed the safety net: version control as undo for the AI. This is the day you learn to actually *use* the net when you fall. ## Three undos, three blast radii The first thing nobody tells you about Git is that it has more than one "undo," and the failure mode is using the wrong one. They differ on two questions: *what do they touch*, and *are they safe once history is shared* (i.e., once someone else has pulled it). | Command | Undoes | Rewrites history? | Safe once shared? | |---------|--------|-------------------|--------------------| | `git restore ` | Uncommitted edits in your working tree | No | Yes, nothing shared to break | | `git revert ` | An already-committed change, by writing a *new* inverse commit | No, it *adds* | **Yes**, the team-safe undo | | `git reset ` | Moves your branch pointer backward, un-committing | **Yes** | **No**, dangerous once others pulled | `restore` you've probably already met: it's for the mess that hasn't been committed yet. This post is about the bottom two rows, because the AI's worst messes are the ones that already made it into a commit, a merge, or a merged PR. ## `revert`: undo by adding, not erasing Mental model: a commit is a diff, a set of line changes. `git revert ` computes the *opposite* diff and commits it. The bad change is still in your history, but a new commit immediately after it cancels it out. ```bash git log --oneline # a1b2c3d Add "export to CSV" command <- turned out to be broken git revert a1b2c3d # opens an editor for the message, then commits the inverse git log --oneline # 9f8e7d6 Revert "Add export to CSV command" # a1b2c3d Add "export to CSV" command ``` Why is this the one you reach for first? Because it never rewrites history. Anyone who already pulled `a1b2c3d` just pulls one more commit on top and they're back in sync with you. Nobody's clone breaks. Nobody has to force-anything. And, this is the part I love, your `git log` now tells the *truth*: "we tried this, then we deliberately pulled it, and here's why." Six months from now that's a gift to whoever's reading the history, human or agent. A `revert` writes the project's memory honestly instead of quietly editing the past. ## Reverting a bad *merge*: the headline case Here's the one that actually bites people, because it's exactly what a bad merged PR looks like. You don't have one bad commit; you have a *merge commit* that dragged in a whole branch's worth of them. Naively reverting it fails: ``` 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) and Git won't guess which side is "the one to keep." You tell it: ```bash git show --format="%P" --no-patch # prints the two parent SHAs, in order git revert -m 1 # keep parent #1 (main), undo what the other side brought in ``` For "a bad feature got merged into main," it's almost always `-m 1`. Now the gotcha, up front, because honesty is the whole point of this section: 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, decides those commits are already accounted for, and brings in **nothing**, silently leaving your fix half-applied. The counterintuitive cure is to **revert the revert** first (`git revert `), then stack your new work on top, then merge. This is a real, recurring source of "why didn't my merge do anything," and now it'll never cost you an afternoon. ## `reset`: moving the pointer (and why it's sharp) `git reset` doesn't write an inverse commit. It **moves your branch to point at an older commit**, un-committing everything after. That's rewriting history, which is both its power and its danger. Three flavors: ```bash git reset --soft HEAD~1 # un-commit, KEEP changes staged (redo the message / squash) git reset --mixed HEAD~1 # un-commit, keep changes unstaged (the default) git reset --hard HEAD~1 # un-commit AND delete the changes (the one that ruins days) ``` `reset` is correct on exactly one kind of history: the kind *you have not shared.* Squashing three "wip" commits before you push, fixing a botched last commit: perfect, that's what it's for. But the instant a commit has been pushed and someone pulled it, `reset` becomes a way to rewrite history out from under them, and the only way to publish your rewrite is `--force`. On a shared branch, that's how you delete a teammate's (or an agent's) work. The rule, plainly: > **Already shared? `revert`. Only ever local? `reset` is fine. When unsure, assume shared.** ## `reflog`: the net under the net Now the reassuring part, the thing that saves the coffee-break disaster from the intro. `reset --hard` *feels* permanent. It almost never is. Git keeps a private, local log of everywhere `HEAD` has ever pointed (every commit, reset, checkout, merge) in the *reflog*. A commit you "lost" is no longer reachable from your branch, but it's still in the object database, and the reflog still knows its SHA. ```bash git reflog # 9f8e7d6 HEAD@{0}: reset: moving to HEAD~1 # a1b2c3d HEAD@{1}: commit: Add the feature I just "lost" <- there it is git branch recovered a1b2c3d # cautious: park it on a branch and inspect # or just snap straight back: git reset --hard a1b2c3d ``` That's the answer to "an agent ran `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 almost nobody knows it exists until the day they desperately need it. [insert a screenshot referencing a `git reflog` output with the "lost" commit highlighted here] ## Tags: named recovery points SHAs are unmemorable. A **tag** is a permanent, human-readable name pinned to a commit: ```bash git tag -a v1.0 -m "Last known-good before the big AI refactor" git push origin v1.0 # tags don't push by default git diff v1.0 # later: everything that changed since the known-good point ``` The habit worth building: **before you turn an agent loose on a large, sweeping change, tag the known-good state.** It turns "I think it was working yesterday" into a named anchor you can diff against in one command. On your git host, a *release* is the same idea dressed up: a tag plus notes and artifacts the whole team can point at. Tags are the durable, *shareable* recovery points the reflog is not. ## Try it for real (the part that sticks) Reading about this is nothing like doing it, so the [course lab](https://git.jpaul.io/justin/ai-workflow-course) has you stage the disaster on purpose, on the little `tasks-app` we use throughout. The short version, abridged: ```bash # Part A: merge a bad change, then revert the merge git switch main git merge --no-ff bad-clear -m "Merge branch 'bad-clear'" # what a merged PR looks like git revert HEAD # refuses: "is a merge but no -m option was given" git revert -m 1 HEAD # writes a NEW commit undoing the whole merge git log --oneline # bad merge STILL there, revert sitting on top, history intact # Part B: "lose" a commit, get it back git reset --hard HEAD~1 # commit vanishes from the branch git reflog # find: "... commit: Add version command" git reset --hard # fully recovered ``` Do it once, deliberately, while the stakes are zero. Then the day it happens for real, your hands already know the moves. ## Where it breaks (the part that earns your trust) This is the second half of a backup-and-recovery thread (pushing to a remote was the *backup* half, this is *recovery*) and the most valuable thing it teaches is **where the analogy stops.** Git gives you near-perfect point-in-time logical recovery for *versioned text*. It is emphatically **not** a general backup system, and treating it like one is exactly how people lose data they thought was safe. - **Not a backup for your database, or any runtime state.** Your app's data lives in a database, in object storage, on a running server. `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. - **Not a backup for secrets, which shouldn't be in there anyway.** And here's the trap: if a key *did* leak into a commit, `revert` does **not** remove it 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. (There's a whole module on keeping them out in the first place; foreshadowing.) - **It only recovers what was committed.** `reset --hard` and `git restore` both destroy *uncommitted* edits, and the reflog **cannot** bring those back: there's no object to recover because nothing was ever committed. The defense is the one this whole course keeps repeating: commit often, so "uncommitted" is always a tiny window. - **Poor backup for large binaries.** Git versions text beautifully and binaries terribly: every change stores a whole new copy and the "diff" is useless noise. Datasets, video, model weights: real artifact storage, not your Git history. - **The reflog is local and temporary.** Not pushed, empty in a fresh clone, and garbage-collected in roughly 30 days. A net for *recent local* mistakes, not an offsite archive. The offsite durability comes from pushing to a remote, a different power. You need both. The honest summary: Git is a beautiful time machine for the text you committed, and nothing more. Know that boundary and you'll trust it exactly as far as it deserves, which, used right, is pretty far. ## You're done when You can say, without looking, which undo fits an uncommitted mess, a bad change already pushed to a shared branch, and three local "wip" commits you want to squash, and why the wrong pick is wrong each time. You've reverted a real merge with `-m 1` and watched both the bad merge and the revert sit in your log. You've "lost" a commit to `reset --hard` and pulled it back from the reflog. And you can name, in one breath, four things Git is *not* a backup for: your database, your secrets, your uncommitted changes, your large binaries. That completes Unit 2: the whole team layer: hosting, issues, review, collaboration, and now recovery. Next up we start Unit 3, where we stop checking things by hand and let the machine do it: tests. Because the best recovery story is the one where the broken change never merges in the first place. If you've got your own "the AI nuked my work and here's how I clawed it back" war story, or a recovery trick I didn't cover, drop it in the comments. I read them, and the scars you've collected are exactly what makes this stuff land for the next person.