# Git Disaster Recovery - Getting Back What You Thought You Lost

> The advanced rescue kit: recover 'lost' commits with the reflog, use rebase safely, and undo work you've already pushed - calmly, even at 2am.


---

# Git Disaster Recovery - Getting Back What You Thought You Lost

This is the guide the others kept promising. Something has gone genuinely wrong - a `reset --hard` ate your
afternoon, a rebase turned into a wall of conflicts, you pushed a commit that never should have left your
laptop. Your heart rate is up. Let's bring it down.

Here is the single most important fact in this entire guide, and you can lean your whole weight on it:

> **Git almost never destroys your commits. It loses track of them.** A commit you "deleted" is, in the vast
> majority of cases, still sitting in the repository - Git just moved a label off it. Recovery is usually a
> matter of *finding* it again, not resurrecting it from nothing.

Once you believe that - and by the end of Phase 1 you will, because you'll do it yourself - these
situations stop being catastrophes and become procedures. That's the whole shift this guide delivers:
from panic to procedure.

> ⏭️ **This is the advanced guide.** It assumes you're fluent with commits, branches, HEAD, `reset`,
> merging, and remotes. If any of those are shaky, the earlier guides build the foundation:
> [Git From Zero](/guides/git-from-zero) →
> [Git, Explained Like You're a Human](/guides/git-explained-like-a-human) →
> [Git With Other People](/guides/git-with-other-people).

## How to read this
- **On fire right now?** Go straight to the [recovery cheat-card in Phase 1](01-the-reflog.md) - the
  reflog rescues most "I lost it" disasters, and it's the first thing you'll reach for.
- **Want to wield the sharp tools safely?** Read in order. Phase 1 is the safety net that makes Phases 2
  and 3 safe to attempt.

## The phases
1. **[The Reflog - Your Safety Net](01-the-reflog.md)** - why almost nothing is truly gone, and how to
   recover "lost" commits, a bad `reset --hard`, and even a deleted branch.
2. **[Rebase Without Fear](02-rebase-without-fear.md)** - what rebase really does, cleaning up history
   before a PR, the one rule that keeps it safe, and rescuing a rebase gone wrong.
3. **[Undoing What You've Already Pushed](03-undoing-pushed-history.md)** - the safe public undo
   (`revert`), when rewriting pushed history is OK, and how to do it without clobbering your team.

> This is the final rung of the Git track. After it, you'll have the complete picture - from your first
> commit to rescuing history under pressure - and very little in Git will be able to genuinely scare you.


---

# The Reflog - Your Safety Net

If you only ever learn one recovery tool in Git, make it this one. The **reflog** is the difference between
"I lost a day of work" and "I lost ninety seconds." It's also the thing that proves the promise from the
overview - that your commits are almost never truly gone - so we lead with it, because once you've used it,
every other rescue in this guide feels safe to try.

## The recovery cheat-card

> **Lost something? Find it here, breathe, then read the section. The reflog has your back.**

| "Oh no…" | The calm fix |
|---|---|
| `git reset --hard` threw away commits I wanted | `git reflog`, find the commit, `git reset --hard <hash>` (§3) |
| I made commits on a detached HEAD and they "vanished" | `git reflog`, find them, `git branch <name> <hash>` (§4) |
| I deleted a branch that wasn't merged | Use the hash from the delete message: `git switch -c <name> <hash>` (§5) |
| I have no idea what I just did, but it's bad | `git reflog` first - it shows your last moves so you can step back (§2) |

---

## What the reflog actually is

**What it actually is.** Every time `HEAD` moves - every commit, checkout, switch, reset, merge, rebase -
Git scribbles a line in a private diary called the **reflog** ("reference log"): *where HEAD was, and what
moved it.* It's local to your machine, it's not shared, and it records moves that aren't part of any
branch's visible history.

**Why this is the whole game.** When you "lose" a commit, what really happened is that a label (a branch,
or HEAD) stopped pointing at it, so it fell out of `git log`. But the commit object is still in the
repository - and the reflog still remembers the hash it used to be at. The reflog is the map back to
commits that `git log` can no longer see.

```mermaid
flowchart LR
  C1 --> C2 --> C3
  main(main) -.->|reset moved it back| C2
  reflog(reflog) -.->|still has its hash| C3
  C3 -.- note["'lost': no label, so git log hides it - not gone, just unlabeled"]
```

📝 **Terminology.** *Reachable* means "you can get to this commit by following labels and parent pointers."
`git log` shows reachable commits. The reflog can reach commits that nothing else can - which is exactly
why it rescues you.

## 2. Reading the reflog

Run it any time you're disoriented - it's read-only and changes nothing:
```console
$ git reflog
9a1b2c3 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
e5f6a7b HEAD@{1}: commit: Add checkout validation
4d3c2b1 HEAD@{2}: commit: Wire up promo codes
9a1b2c3 HEAD@{3}: checkout: moving from feature/x to main
```
*What just happened:* Each line is one move of HEAD, newest at the top. `HEAD@{0}` is where you are now;
`HEAD@{1}` is where you were one move ago, and so on. Read the right-hand text - `reset: moving to...`,
`commit: ...`, `checkout: ...` - and you can literally see your own recent history of actions, each tied to
the commit hash HEAD sat on at the time. Those hashes are your handholds back.

💡 **Key point.** `HEAD@{1}` means "wherever HEAD was 1 move ago." So `git reset --hard HEAD@{1}` often
means "put me back to right before my last action" - the universal undo for "I just did something bad."

## 3. Recover from a `git reset --hard` that went too far

The classic disaster. You meant to undo one commit and fat-fingered three - `--hard`, so the changes are
gone from your files and `git log` no longer shows them:
```console
$ git reset --hard HEAD~3
HEAD is now at 9a1b2c3 Older work
$ git log --oneline -1
9a1b2c3 Older work          # the three newer commits are nowhere in sight
```
Don't touch anything else. Open the reflog and find the commit you were on *before* the reset:
```console
$ git reflog
9a1b2c3 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
1a2b3c4 HEAD@{1}: commit: The work I just nuked
...
$ git reset --hard 1a2b3c4
HEAD is now at 1a2b3c4 The work I just nuked
```
*What just happened:* The reflog showed that one move ago (`HEAD@{1}`), `main` pointed at `1a2b3c4` - the
tip of the work you thought you destroyed. `git reset --hard 1a2b3c4` slid `main` back onto it, and your
files came right back with it. The commits were never deleted; the label had just moved off them, and you
moved it back.

⚠️ **Gotcha.** Do the recovery *promptly* and avoid piling on new actions first. Unreachable commits aren't
kept forever - Git's garbage collection eventually prunes them (the default grace period is generous, about
90 days for reachable-from-reflog commits, but don't gamble on it). The reflog itself is also per-machine
and per-clone: it can't rescue something that only ever happened on a teammate's computer.

## 4. Recover commits made on a detached HEAD

Sometimes you check out a specific commit to look around (a "detached HEAD" - HEAD pointing straight at a
commit instead of a branch), make a couple of commits, then switch away. Because no branch label was
following you, those commits become unreachable the moment you leave:
```console
$ git switch main
Warning: you are leaving 2 commits behind, not connected to any of your branches:
  7f8e9d0 Experimental fix
  6e7d8c9 Try another approach
```
Git even warns you and prints the hashes - but if you missed it, the reflog still has them:
```console
$ git reflog
... HEAD@{1}: commit: Experimental fix     (7f8e9d0)
$ git branch rescued-work 7f8e9d0
```
*What just happened:* `git branch rescued-work 7f8e9d0` dropped a brand-new label on that orphaned commit,
making it reachable again - it now shows up in `git log` and is safe. (Creating the label is the fix:
unreachable commits become safe the instant something points at them.)

## 5. Restore a branch you deleted

You deleted a branch that turned out to still have unmerged work on it:
```console
$ git branch -D feature/checkout-redo
Deleted branch feature/checkout-redo (was 3c4d5e6).
```
**Look at that output:** Git printed the tip commit's hash - `(was 3c4d5e6)`. That's all you need to bring
the branch back:
```console
$ git switch -c feature/checkout-redo 3c4d5e6
Switched to a new branch 'feature/checkout-redo'
```
*What just happened:* You recreated the branch label on its old tip commit, restoring the branch exactly as
it was - every commit on it is reachable again. (Scrolled past the delete message? `git reflog` still lists
where that branch's HEAD was; find the tip hash there instead.)

## Why this changes everything

Notice the shape of every fix in this phase: *find the hash in the reflog, then point a label at it.* The
commits were never the problem - they were sitting safe in the repository the whole time. The only thing
you ever lost was a label's aim, and labels are cheap to re-point.

That's why the reflog is the safety net under the rest of this guide. The next two phases use genuinely
sharp tools - `rebase` and rewriting pushed history - and the reason you can wield them calmly is that if
either goes wrong, you already know the way back.

## Your turn: three commits just vanished

Reading the cheat-card is the easy part. Doing it with the afternoon's work missing from `git log` is the
job. There is no single right answer below and nothing is scored right or wrong - but every move costs real
minutes, and the fastest fix is also the calmest one.

```scenario
{
  "title": "reset --hard HEAD~3, when you meant HEAD~1",
  "brief": "It's 4:40pm. You meant to undo your last commit - git reset --hard HEAD~1 - and typed HEAD~3 instead. git log now shows the branch sitting three commits earlier than you left it: the retry logic you've been building since lunch is gone from the tree, and none of it was ever pushed. Nobody else has a copy.",
  "prompt": "What do you do first?",
  "clock": { "unit": "min", "running": "work still missing", "resolved": "back in the tree" },
  "resolvedHeading": "Work's back. Here's how it went.",
  "actions": [
    {
      "id": "reflog",
      "label": "Run git reflog",
      "cost": 1,
      "reveals": "$ git reflog\n9a1b2c3 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3\n1a2b3c4 HEAD@{1}: commit: Add retry backoff and jitter\n7f6e5d4 HEAD@{2}: commit: Handle idempotency key on retry\n3c2b1a0 HEAD@{3}: commit: Retry queue skeleton\n9a1b2c3 HEAD@{4}: commit: Fix invoice rounding",
      "note": "HEAD@{1} is exactly where you stood one move ago - the tip of the work you just reset past. That hash is the whole rescue."
    },
    {
      "id": "status",
      "label": "Run git log again to confirm exactly what's missing",
      "cost": 1,
      "reveals": "$ git log --oneline -1\n9a1b2c3 Fix invoice rounding\n$ git status\nOn branch main\nnothing to commit, working tree clean",
      "note": "Confirms the branch is three commits short and the tree is clean - which the reset's own output already told you. Cheap, but it doesn't get you any closer to the fix."
    },
    {
      "id": "stash-check",
      "label": "Check git stash list, in case the changes are stashed",
      "cost": 1,
      "reveals": "$ git stash list\n(nothing)",
      "note": "--hard doesn't stash what it discards, it throws the changes away outright. The commits themselves are fine - they just aren't in the stash. A quick, cheap dead end."
    },
    {
      "id": "guess-reset",
      "label": "Reset again, guessing how far back you need to go",
      "cost": 5,
      "reveals": "$ git reset --hard HEAD@{4}\nHEAD is now at 9a1b2c3 Fix invoice rounding\n$ git log --oneline -1\n9a1b2c3 Fix invoice rounding      # further from the work than before",
      "note": "Guessing an index instead of reading the reflog first moved you past your own rescue point. You're now one more reset away from the work, not one closer."
    },
    {
      "id": "reclone",
      "label": "Rename the folder aside and clone a fresh copy to compare",
      "cost": 12,
      "reveals": "$ mv checkout-api checkout-api.bak\n$ git clone git@github.com:acme/checkout-api.git\nCloning into 'checkout-api'...\n$ git log --oneline -1\n9a1b2c3 Fix invoice rounding      # last thing that was ever pushed",
      "note": "The three commits were never pushed, so no clone anywhere has them - only the reflog in your original folder does. This time you renamed it instead of deleting it, so you're still fine. rm -rf first and this would not have been recoverable."
    },
    {
      "id": "retype",
      "label": "Stop trusting Git and retype the retry logic from memory",
      "cost": 40,
      "reveals": "// retry_backoff.js (reconstructed from memory)\nfunction retryWithBackoff(fn, attempts = 3) {\n  // ...missing the idempotency-key check you added after lunch\n}",
      "note": "Forty minutes to reconstruct roughly what you remembered - and you dropped the idempotency-key fix, because you're recreating code, not recovering it. The original three commits, exactly as written, were one git reflog away the whole time."
    },
    {
      "id": "reset-recover",
      "label": "Reset back onto the hash the reflog showed",
      "cost": 1,
      "resolves": true,
      "reveals": "$ git reset --hard 1a2b3c4\nHEAD is now at 1a2b3c4 Add retry backoff and jitter\n$ git log --oneline -3\n1a2b3c4 Add retry backoff and jitter\n7f6e5d4 Handle idempotency key on retry\n3c2b1a0 Retry queue skeleton",
      "note": "All three commits are back, exactly as you left them. The reset never deleted anything - it just moved a label. You moved it back."
    }
  ],
  "debrief": {
    "ideal": 2,
    "text": "The reset didn't delete anything - it moved a label backward, and a label is cheap to move back. git reflog, find the hash from one move ago, git reset --hard onto it: two minutes, and the work is exactly as you left it, bugs included. Every other move either burns time confirming what you already knew, or - like re-cloning - trades a recoverable mistake for one that might not be.",
    "notes": [
      { "when": "if-taken", "action": "guess-reset", "text": "Guessing an index instead of reading the reflog first moved you further from the work, not closer - the exact 'do something' panic instinct this phase exists to interrupt." },
      { "when": "if-taken", "action": "reclone", "text": "You got away with it because you renamed the folder instead of deleting it. The reflog that saves you here lives only in that one local folder - delete it, and even the correct fix stops working." },
      { "when": "if-taken", "action": "retype", "text": "Forty minutes rebuilding from memory got you a rough approximation with at least one dropped fix. The real commits, exactly as written, were sitting in the reflog the whole time." },
      { "when": "if-not-taken", "action": "reflog", "text": "You reset straight onto the hash without ever reading the reflog line. It happened to be the right hash this time - reading it first is what makes that a certainty instead of a guess." }
    ]
  }
}
```

## Recap

1. The **reflog** is Git's local diary of everywhere `HEAD` has been - it can reach commits `git log`
   can't.
2. **`git reflog`** is read-only; run it the instant you're disoriented and read your recent moves.
3. Recover a bad `reset --hard` with **`git reset --hard <hash>`** from the reflog.
4. Rescue detached-HEAD or orphaned commits by **putting a label on them** (`git branch <name> <hash>`).
5. Restore a deleted branch from the **`(was <hash>)`** message (or the reflog): `git switch -c <name> <hash>`.


---

# Rebase Without Fear

`rebase` has a fearsome reputation, and it's half-earned: it's a genuinely sharp tool that *rewrites
history*, and used carelessly it can hand you a confusing mess. But the fear mostly comes from not knowing
what it actually does. Once you do - and once you have the reflog from [Phase 1](01-the-reflog.md) as your
undo - rebase becomes a precise, friendly tool you'll reach for often.

## What rebase actually does

**What it actually is.** Rebase picks up the commits on your branch and **re-plays them onto a different
starting point**, one by one. The key word is *re-plays*: it doesn't move your original commits, it makes
**brand-new commits** with the same changes and messages but new parents - and therefore new hashes. The
originals become unreachable (and yes, the reflog still has them).

Compare it with merge, which you already know. Say `main` moved forward while you worked:

**Merge `main` into your branch** - a merge commit `M` ties the two histories together:
```mermaid
gitGraph
  commit id: "C1"
  commit id: "C2"
  branch feature
  checkout feature
  commit id: "A"
  commit id: "B"
  checkout main
  commit id: "C5"
  checkout feature
  merge main id: "M"
```

**Rebase your branch onto `main`** - your `A`,`B` are re-created as `A'`,`B'` on top of `C5`, a straight line with no merge commit:
```mermaid
gitGraph
  commit id: "C1"
  commit id: "C2"
  commit id: "C5"
  branch feature
  checkout feature
  commit id: "A'"
  commit id: "B'"
```

**Why reach for it.** The result is a *linear* history - your work sits cleanly on top of the latest
`main`, as if you'd started from there this morning. No merge bubbles. Many teams prefer that tidiness, and
it makes a PR easier to read.

📝 **Terminology.** "Rebase your branch *onto* `main`" means: use the current tip of `main` as the new base
your commits sit on. Your commits move; `main` doesn't.

## Rebasing your branch onto the latest `main`

This is the everyday use - the rebase-flavored alternative to the "fold `main` in" merge from the team
guide:
```console
$ git switch feature/cart
$ git fetch
$ git rebase origin/main
Successfully rebased and updated refs/heads/feature/cart.
```
*What just happened:* Git set your branch's commits aside, fast-forwarded your starting point to the latest
`origin/main`, then re-applied your commits on top as new commits. Your branch is now a clean line on top
of current `main`.

**When a conflict interrupts it.** Because rebase re-applies your commits one at a time, a conflict stops it
mid-flight:
```console
$ git rebase origin/main
Auto-merging pricing.js
CONFLICT (content): Merge conflict in pricing.js
error: could not apply 2b3c4d5... Add tax line
Resolve all conflicts manually, mark them resolved with "git add",
then run "git rebase --continue". You can instead skip this commit: "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
```
Resolve the file as usual, then *don't commit* - tell the rebase to carry on:
```console
$ git add pricing.js
$ git rebase --continue
```
*What just happened:* You resolved the clash for that one replayed commit and `--continue` resumed applying
the rest. (Conflicts can recur for each commit - keep resolving and continuing.) Lost the thread entirely?
**`git rebase --abort`** returns you to exactly where you stood before the rebase, no harm done. That escape
hatch is why you can always try a rebase.

## Cleaning up history before a PR (interactive rebase)

Here's where rebase earns real love. Before opening a pull request, you can polish your messy
work-in-progress commits into a clean story with **interactive rebase**. Say your last three commits are:
```console
$ git log --oneline -3
3c4d5e6 Add tax line
2b3c4d5 fix typo
1a2b3c4 Add subtotal calc
```
Start an interactive rebase over those three:
```console
$ git rebase -i HEAD~3
```
Git opens an editor listing them oldest-first, with a menu of actions:
```text
pick 1a2b3c4 Add subtotal calc
pick 2b3c4d5 fix typo
pick 3c4d5e6 Add tax line

# Commands:
# p, pick   = use the commit as-is
# r, reword = use the commit, but edit its message
# s, squash = meld into the previous commit (keep both messages)
# f, fixup  = like squash, but discard this commit's message
# d, drop   = remove the commit entirely
```
Edit the verbs to reshape history - here, fold the typo fix into the commit it belongs to, and reword the
last one:
```text
pick  1a2b3c4 Add subtotal calc
fixup 2b3c4d5 fix typo
reword 3c4d5e6 Add tax line
```
Save and close. The result:
```console
$ git log --oneline -2
8f9e0d1 Add tax line and rounding
7a8b9c0 Add subtotal calc
```
*What just happened:* Git replayed the commits applying your instructions - the "fix typo" commit got
absorbed into "Add subtotal calc" (`fixup`), and you got a chance to rewrite the final message (`reword`).
Three scrappy commits became two clean ones. Reviewers see a tidy story instead of your thought process.

## The one rule that keeps rebase safe

Everything above is safe because those commits lived only on your machine. The danger is rewriting commits
that *other people already have*. Burn this in:

> ⚠️ **The Golden Rule of Rebase: never rebase commits that exist outside your own repository.** If you've
> pushed them to a shared branch and others may have pulled them, rebasing creates *different* commits with
> the same content - and now their history and yours disagree, in a way that's painful for everyone to
> untangle.

The safe zone is your own un-pushed (or solo) branch - clean it up all you like before sharing. `main` and
any branch teammates build on are off-limits to rebase. (Rebasing a branch *only you* use, even after
pushing it, is fine - but it requires a force-push, which is exactly what [Phase 3](03-undoing-pushed-history.md)
covers safely.)

## Rescuing a rebase that already finished badly

`--abort` only works *during* a rebase. What if it completed and *then* you realized it mangled things? This
is where Phase 1 pays off - the reflog remembers where you were before the rebase started:
```console
$ git reflog
8f9e0d1 (HEAD -> feature/cart) HEAD@{0}: rebase (finish): returning to refs/heads/feature/cart
...
3c4d5e6 HEAD@{5}: rebase (start): checkout origin/main
e1f2a3b HEAD@{6}: commit: Add tax line        ← the branch tip BEFORE the rebase
$ git reset --hard e1f2a3b
```
*What just happened:* The reflog clearly brackets the rebase with `rebase (start)` and `rebase (finish)`
entries. The commit just before the start (`HEAD@{6}` here) is your branch exactly as it was pre-rebase.
`git reset --hard` to it and the rebase is undone - original commits and all. Nothing was ever lost.

## Recap

1. **Rebase replays your commits onto a new base as new commits** (new hashes) - the originals become
   unreachable but the reflog keeps them.
2. **`git rebase origin/main`** gives a clean linear history; resolve conflicts then `--continue`, or
   `--abort` to bail out safely.
3. **`git rebase -i`** lets you squash, fixup, reword, reorder, and drop commits to tidy history before a
   PR.
4. **The Golden Rule:** never rebase commits others already have - keep it to your own un-shared work.
5. **Undo a finished bad rebase** via the reflog: `git reset --hard <pre-rebase-hash>`.

Watch it animated: [rebasing](/explainers/Rebasing.dc.html)

## Try it yourself

Run commands and watch the history graph build - `commit -m "first"`, `branch dev`, `checkout dev`, `commit -m "work"`, `checkout main`, `merge dev`:

```playground-git
```

## Watch a rebase happen

Step through what "replay the commits on a new base" actually means, one commit at a time:

```explainer-rebase
```


---

# Undoing What You've Already Pushed

Everything got easier in the earlier guides because of one quiet assumption: the commit lived only on your
machine, so you could rewrite it freely. The moment you `push`, that assumption breaks. Your commits now
exist somewhere else, and other people may have pulled them. Undoing work at that point is less about Git
mechanics and more about *not yanking the rug out from under your teammates*.

The good news: there's a clear rule, and once you know it, this stops being scary.

## The pushed-history cheat-card

> **The first question is always: has anyone else got these commits? Then pick your move.**

| Situation | The calm move |
|---|---|
| Undo a commit on a **shared** branch (`main`, anything others build on) | `git revert <hash>` - a new commit that undoes it (§2) |
| You rewrote **your own** pushed branch (rebase/amend) and need to push | `git push --force-with-lease` (§3) |
| Tempted by plain `git push --force` | Don't, on anything shared - it clobbers teammates' commits (§3) |
| You pushed a secret (key, password) | `revert`/rewrite **won't** erase it from history - rotate the secret now (§4) |

---

## The one distinction that decides everything

```mermaid
flowchart TD
  Q1{Is the commit only on MY machine?} -->|Yes| Free(Rewrite freely:<br/>reset, commit --amend, rebase)
  Q1 -->|No, it's pushed| Q2{Has anyone else pulled it,<br/>or do they build on this branch?}
  Q2 -->|Yes - shared branch| Revert(DON'T rewrite.<br/>Use git revert §2)
  Q2 -->|No - a branch only I use| Lease(Rewrite, then<br/>--force-with-lease §3)
```

Memorize the spirit: **rewriting history is fine until that history is shared; after that, you undo by
*adding*, not by rewriting.** That's the whole phase.

## 2. The safe public undo: `git revert`

**What it actually is.** `git revert` doesn't delete or rewrite anything. It creates a **brand-new commit**
that is the exact *inverse* of a previous one - whatever that commit added, the revert removes, and vice
versa. History stays intact; you've appended "...and now undo that."

**Why it's the safe choice.** Because it only adds a commit, everyone else's history still matches yours.
There's nothing to force, nothing to clobber. This is how you undo something on `main` without causing a
team-wide headache.

```console
$ git revert a1b2c3d
[main 9z8y7x6] Revert "Add promo logic that broke checkout"
 1 file changed, 2 insertions(+), 40 deletions(-)
$ git push
```
*What just happened:* Git made a new commit `9z8y7x6` that undoes everything `a1b2c3d` did, opened an
editor with a pre-filled "Revert ..." message (or use `git revert --no-edit` to skip it), and you pushed
it like any normal commit. The bad change is neutralized and no teammate's repository was disturbed.

⚠️ **Gotcha - reverting a merge commit.** A merge commit has two parents, so Git needs to know which side to
keep. A plain `git revert <merge-hash>` fails asking for a `-m` (mainline) option; the usual form is
`git revert -m 1 <merge-hash>` (keep the first parent - normally `main`). It's a known sharp edge; if you
hit it, that's the fix.

## 3. Rewriting your own pushed branch - with a lease, never a hammer

Sometimes you legitimately need to rewrite history you've already pushed - you rebased your *own* feature
branch (Phase 2) to tidy it before review, and now your local branch and its remote copy disagree. A normal
push gets rejected, because you've rewritten commits the remote still has.

This is the *one* time a force-push is appropriate - but use the safe form:
```console
$ git push --force-with-lease
To github.com:acme/shop.git
 + 8f9e0d1...e1f2a3b feature/cart -> feature/cart (forced update)
```
*What just happened:* `--force-with-lease` overwrote the remote branch with your rewritten history - **but
only after checking that the remote was still where you last saw it.** If a teammate had pushed to that
branch in the meantime, the lease check fails and Git refuses, instead of silently destroying their work:
```console
$ git push --force-with-lease
 ! [rejected]        feature/cart -> feature/cart (stale info)
error: failed to push some refs to 'github.com:acme/shop.git'
```
*What just happened:* The lease caught that the remote had moved (someone else pushed). Git stopped you from
clobbering them. Now you `git fetch`, look at what they did, and reconcile - exactly the protection you
want.

> ⚠️ **Why not plain `git push --force`?** It overwrites the remote *unconditionally* - no check, no mercy.
> If a teammate pushed since your last fetch, `--force` erases their commits permanently. `--force-with-lease`
> does the same job but refuses when it would destroy unseen work. Make it your default; reserve bare
> `--force` for never.

🪖 **War story.** A teammate once ran `git push --force` on `main` to "clean up" his branch, not realizing
two other people had merged that morning. Their commits vanished from the remote - recoverable only because
someone still had them in a local reflog, costing everyone an hour. `--force-with-lease` would have refused
the push; the lease isn't training wheels, it's the seatbelt seniors keep on.

## 4. Communicate - the part Git can't do for you

If you rewrite *any* history other people might share, tell them before and after. The mechanical fix is
easy; the damage comes from a teammate who pulls mid-rewrite and ends up with a tangled branch. A thirty-second
"heads up, I'm force-pushing `feature/cart` in a minute, re-pull after" prevents the whole mess.

And the plain caveat this guide owes you: **none of these tools truly erase data from history.** A
`revert` leaves the original commit in place; even a rewrite leaves the old commits reachable via reflogs
and any clone that already fetched them. So if you pushed a password or key, *do not* assume revert or
force-push hides it - **rotate the secret immediately** (invalidate it and issue a new one). Treat anything
that ever reached a remote as public. (Genuinely scrubbing a secret from all history requires special tools
and coordinating every clone - a deliberate, heavy operation beyond this guide.)

## Recap

1. **The deciding question is "does anyone else have this commit?"** Local-only → rewrite freely; shared →
   undo by adding.
2. **`git revert <hash>`** appends a new commit that inverts an old one - the safe way to undo on `main` or
   any shared branch.
3. To revert a **merge** commit, pick the mainline: `git revert -m 1 <merge-hash>`.
4. Rewriting your **own** pushed branch needs a force-push - always **`--force-with-lease`**, never plain
   `--force`.
5. **Communicate** before rewriting shared history, and **rotate any secret** that ever got pushed - undo
   commands don't erase the past.

---

## That's the whole Git track

Look back at the road you've walked. You started not knowing what a commit was; now you can recover work the
tools themselves had given up on, reshape history deliberately, and undo even pushed mistakes without
hurting anyone. The fear that Git began as - the stomach-drop when something went sideways - is gone,
replaced by a set of calm procedures and the deep knowledge that *your work is almost always still there.*

There's always more Git (submodules, hooks, the plumbing underneath) - but you now have everything the day
job demands, from your first commit to a steady hand in a crisis. That's what the senior who actually cares
would have sat down and shown you. Now someone has.
