Reflog, bisect, and the rescue toolkit
Git's rescue and history-rewriting toolkit, reflog, bisect, cherry-pick, revert, and interactive rebase, governed by where a commit lives.
A rebase goes wrong and four hours of work vanish from your branch. A test that was green last week is red today, and one of the forty commits since then is to blame. A one-line fix sits on a feature branch that won’t merge for a week, and production needs it now. These problems show up once you’ve started shipping like a team: more commits, more branches, more history to get lost in.
The previous lesson taught the everyday loop: small branches, pull --rebase, and a squash-merge so main reads like a changelog. This lesson is what you do when a ship goes sideways, and that same squash-merge discipline makes most of these rescues cheaper. You’ll pick up four power tools, plus the smaller commands each leans on: reflog for getting lost work back, bisect for hunting the commit that broke something, cherry-pick and revert for moving and undoing individual commits, and interactive rebase for cleaning up messy history before review. The skill isn’t typing them; it’s knowing which one a problem calls for, and that always comes back to one question.
Where a commit lives decides what you can do to it
Section titled “Where a commit lives decides what you can do to it”Before reaching for any tool in this lesson, ask: where does this commit live? The three possible answers form a gradient from “do whatever you want” to “look but don’t touch.”
A commit can exist only on your machine. You committed it but haven’t pushed it, so nobody else has a copy. Rewrite its message, fold it into another commit, reorder it, or delete it outright, all safely, because the only history you’re changing is one only you can see.
A commit can be pushed to your feature branch but not yet merged. There’s a copy on the remote now, and a teammate could in principle have pulled it, though on a short-lived branch that’s still yours, almost nobody has. So this zone is mutable with care: you can still rewrite, but then the remote disagrees with your machine and you overwrite it with git push --force-with-lease. The --lease part, from the previous lesson, refuses the push if someone pulled and added a commit in the meantime, so you can’t silently bulldoze their work.
A commit can be merged to main. Others have pulled it, and it’s woven into the shared history the whole team builds on. Rewriting it is off-limits, not because Git stops you, but because it can cost the team a day of untangling. When something on main is wrong, you don’t erase it; you add a new commit that undoes it. That tool is git revert, the one exception in this lesson that touches shared history, and it adds a commit rather than rewriting one.
That gradient rests on one mechanical fact. A commit is a snapshot and a branch is just a movable pointer at one of those snapshots, as the previous lesson showed. The consequence: a commit is not deleted just because no branch points at it anymore. When you reset or rebase, you move pointers; the old snapshots become unreferenced but stay on disk, intact, until Git’s garbage collection (GC) eventually sweeps them away. Git also keeps a journal of where every pointer has been, so even an unreferenced commit is usually one command from coming back.
The interactive Learn Git Branching sandbox makes pointers tangible. Type git commit a couple of times to grow a line of snapshots, then git checkout -b feature to drop a new pointer, then git commit again. The branch labels move while the commit nodes just accumulate: pointers slide, snapshots stay.
Type git commit, then git checkout -b feature, then git commit again. Moving a branch pointer never deletes a commit node, and that’s what makes everything in this lesson recoverable.
One more term. HEAD is Git’s name for where you are right now: the commit you currently have checked out, normally reached through your current branch. Almost everything Git does moves HEAD, and the next tool is the log of every place it has ever been.
The reflog: a recovery journal of every HEAD move
Section titled “The reflog: a recovery journal of every HEAD move”Learn this one before you need it: you reach for the reflog the moment you think “I just lost my work,” the worst time to read docs on a command you’ve never run. Knowing it has your back is also what lets you experiment with rebases and resets, since one wrong move no longer erases an afternoon.
Every time HEAD moves, on every commit, checkout, reset, rebase, and merge, Git appends a line to a private, per-repository journal called the reflog. It records your pointer’s movements, not your project history. Since the snapshots those movements left behind aren’t deleted, the reflog is a list of every state your repository has passed through recently, each reachable by its hash.
Recovery is two steps: read the journal with git reflog to find where you were before the mistake, then jump back to it. Here is the reflog right after a reset --hard discarded two commits, with two ways to recover.
$ git reflog9f2c1ab HEAD@{0}: reset: moving to HEAD~23d8e4c7 HEAD@{1}: commit: Add status filter dropdowna1b9f02 HEAD@{2}: commit: Wire filter to the query7c4d2e9 HEAD@{3}: rebase (finish): returning to refs/heads/feat/invoice-status
# Recover by jumping the current branch back:$ git reset --hard 3d8e4c7
# Or, safer — park the lost work on a new branch first:$ git branch recover-work 3d8e4c7Read top-down. HEAD@{0} is where you are now, and the top line is the last thing you did, here the reset --hard that caused the problem.
$ git reflog9f2c1ab HEAD@{0}: reset: moving to HEAD~23d8e4c7 HEAD@{1}: commit: Add status filter dropdowna1b9f02 HEAD@{2}: commit: Wire filter to the query7c4d2e9 HEAD@{3}: rebase (finish): returning to refs/heads/feat/invoice-status
# Recover by jumping the current branch back:$ git reset --hard 3d8e4c7
# Or, safer — park the lost work on a new branch first:$ git branch recover-work 3d8e4c7The line just below the mistake is the commit you want back. Each entry names its action and message, so you can spot the state you’re hunting for. Copy that hash.
$ git reflog9f2c1ab HEAD@{0}: reset: moving to HEAD~23d8e4c7 HEAD@{1}: commit: Add status filter dropdowna1b9f02 HEAD@{2}: commit: Wire filter to the query7c4d2e9 HEAD@{3}: rebase (finish): returning to refs/heads/feat/invoice-status
# Recover by jumping the current branch back:$ git reset --hard 3d8e4c7
# Or, safer — park the lost work on a new branch first:$ git branch recover-work 3d8e4c7The blunt recovery: move the current branch’s pointer straight back to that commit. Fast, but it discards anything after it, so do this only when you’re sure.
$ git reflog9f2c1ab HEAD@{0}: reset: moving to HEAD~23d8e4c7 HEAD@{1}: commit: Add status filter dropdowna1b9f02 HEAD@{2}: commit: Wire filter to the query7c4d2e9 HEAD@{3}: rebase (finish): returning to refs/heads/feat/invoice-status
# Recover by jumping the current branch back:$ git reset --hard 3d8e4c7
# Or, safer — park the lost work on a new branch first:$ git branch recover-work 3d8e4c7The careful recovery, and the better default: create a new branch at the lost commit without moving where you are. The work is parked safely, and you can inspect it before deciding what to keep.
Default to git branch recover-<something> <hash> over git reset --hard <hash>: parking the work on a fresh branch can’t make a bad situation worse, and you can merge or cherry-pick from it once you’ve confirmed it’s what you wanted.
The sequence below makes the mechanism concrete: reset --hard HEAD~2 slides the pointer back so two commits become unreferenced, those commits still sit in the reflog by hash, and a final reset to that hash brings the pointer, and the work, back.
3d8e4c7. The snapshot was never deleted — only unreferenced.
Two limits keep the reflog honest. It is local: it lives in your clone alone, so it can’t recover something only a teammate had, and a fresh clone starts empty. It is also temporary: reachable entries survive roughly 90 days and unreferenced ones about 30 before garbage collection may prune them. It’s a net for recent mistakes, not an archive, so recover lost work now.
Shelving uncommitted work with git stash
Section titled “Shelving uncommitted work with git stash”The reflog gets work back after you’ve lost it; git stash keeps you from losing it when an interruption forces you to drop what you’re doing. The trigger is specific: you’re mid-change, the working tree is too messy to commit, and something arrives that needs a clean tree, like a hotfix to start. You can’t switch branches with a dirty tree in the way, and you don’t want to freeze half a feature into a commit. Stash saves your uncommitted changes onto a stack and resets the tree to clean; when you return, you pop them back.
$ git stash push -m "wip: invoice status filter"Saved working directory and index state On feat/invoice-status: wip: invoice status filter
$ git switch main # go deal with the interruption
# ...later, back on the feature branch...$ git switch feat/invoice-status$ git stash liststash@{0}: On feat/invoice-status: wip: invoice status filter
$ git stash pop # restore the changes and drop the stash entryAlways label a stash with -m; an unlabeled one is a guessing game a day later. You already use stash without naming it: the rebase.autoStash=true config from the previous lesson stashes your tree around git pull --rebase and pops it back when the rebase finishes.
git bisect finds the commit that broke it by binary search
Section titled “git bisect finds the commit that broke it by binary search”Some bugs you can read your way to. This is the bug you can’t: a test that passed weeks ago fails now, with a regression hiding somewhere in the last forty commits and no idea which one. Checking them one by one is the tedious work Git can do for you, by binary search.
You know an old commit was good and the current one is bad, so the break is between them. Instead of testing every commit, test the one in the middle: if it’s good the bug is in the newer half, if it’s bad it’s in the older half. Either way one test eliminates half the suspects. Repeat, and the window halves each time, so a thousand commits collapse to about ten tests, log₂(1000).
Here is the manual session. You tell Git the two ends, and it checks out midpoints for you to judge:
$ git bisect start$ git bisect bad HEAD # the current commit is broken$ git bisect good a1b9f02 # this old commit was fineBisecting: 19 revisions left to test after this (roughly 4 steps)[<sha>] Wire filter to the query
# Git checked out the midpoint. Run your test, then mark the result:$ pnpm test$ git bisect bad # ...or `git bisect good` if it passed
# repeat until Git prints the first bad commit, then:$ git bisect reset # return to where you startedMarking each step by hand is tedious. Hand the whole search to Git instead: give it a command that succeeds when the code is good and fails when it’s bad, which is what a test command does. Treat this one-liner as the default; fall back to manual marking only for bugs no script can check.
$ git bisect start$ git bisect bad HEAD$ git bisect good a1b9f02$ git bisect run pnpm testA test command exits 0 on pass and non-zero on fail, the signal bisect needs: at each midpoint it reads the exit code, marks the commit good or bad, and stops on the culprit. One more code matters: exit 125 means the commit can’t be evaluated, one that won’t build, and tells bisect to skip it rather than mis-mark it.
This is where squash-merge pays off. Because you squash-merge, every commit on main is one complete, shipped change with a green, deployable tree. So bisect always lands on a whole pull request, and the test it runs is meaningful: that state truly worked or truly didn’t. A history full of merge commits and raw “wip” commits is the opposite, where an intermediate state is broken for reasons unrelated to the bug, so bisect marks it bad for the wrong reason and chases the wrong change.
Copy a commit with cherry-pick, undo one with revert
Section titled “Copy a commit with cherry-pick, undo one with revert”These two tools are a matched pair: one applies a commit somewhere new, the other applies a commit’s inverse. The hard part isn’t the syntax; it’s knowing which zone the commit lives in.
git cherry-pick <sha> replays a single commit onto your current branch. The classic trigger in trunk-based work: a small fix sits on a feature branch that won’t be mergeable for another week, but you need it shipped today. So you cherry-pick that one commit onto a fresh branch off main, open a tiny pull request, and ship it while the larger feature keeps developing. Cherry-pick copies: it creates a new commit with the same changes but a different hash, because it has a different parent and place in history.
git revert <sha> handles the opposite case: a bad commit has already shipped to main. It’s in the third zone and everyone has it, so rewriting is off the table. Instead of erasing it, you add a new commit that is its exact inverse: whatever the bad commit added, the revert removes, and whatever it removed, the revert puts back. The rollback is clean and the audit trail survives, since both the original commit and its undo stay in history. The rule: production rolls back with revert; a mistake still on your own branch gets fixed with rebase -i or amend. A later chapter on deployment revisits this: Vercel re-promotes the previous deployment to fix the running app fast, and git revert undoes the code behind it.
So the choice is the zone question again, on one scenario: “I shipped a bad commit.” Read the first sentence of each tab.
$ git rebase -i origin/main# in the editor, change the bad commit's line from `pick` to `drop`$ git push --force-with-lease # only if you'd already pushed the branchThe bad commit hasn’t left your feature branch (zone 1 or 2), so you can rewrite it away. Drop it with an interactive rebase, and force-push only if the branch was already on the remote.
$ git revert 3d8e4c7# creates a new commit that undoes 3d8e4c7, then push and PR as normalThe bad commit is merged to main (zone 3), so rewriting is off the table and you add its inverse instead. revert makes a new commit that undoes it, keeping the audit trail intact.
Two things to watch with cherry-pick. If you cherry-pick a commit that later gets merged normally too, you’ll have two commits with the same changes but different hashes; that’s usually harmless, but when you want the lineage traceable, git cherry-pick -x <sha> appends a (cherry picked from commit …) line to the new message. And don’t cherry-pick commits from main onto your feature branch to “catch up”: that diverges your history from main’s. To stay current, rebase.
You’re on main with a side branch nearby. Run git cherry-pick <id> on one of side’s commits and a new node appears on main with the same change but a new hash. Cherry-pick copies, it doesn’t move.
One term to recognize. Backporting uses cherry-pick to bring a fix from a newer branch onto an older release branch, the one place cherry-pick shows up routinely outside trunk-based work. You’ll rarely need it on a single-main web app, but it’s worth knowing the word when a team that maintains old releases uses it.
Shaping history before it leaves the branch
Section titled “Shaping history before it leaves the branch”Everything here rewrites commits, so it’s only safe in the first two zones: local, or your own un-merged feature branch. These tools exist for one workflow: you commit messily as you work, with messages like “wip” and “fix the test”, because crafting a perfect commit mid-thought wastes attention. Then, before you push for review, you clean that mess into a sequence of commits that read as a clear story. The messy version was for you; the clean version is for the reviewer.
git commit —amend fixes the most recent commit
Section titled “git commit —amend fixes the most recent commit”Amend is the smallest history edit: it changes only the latest commit. Say you just committed and realized you forgot to stage a file, or the subject line has a typo. Stage the fix if there is one, then run git commit --amend to reopen the editor and fix the message, or git commit --amend --no-edit to fold in the staged changes while keeping the message. The previous commit is replaced by a corrected one. The moment the fix is two or more commits back, amend is the wrong tool and rebase -i, covered next, is the right one.
$ git add src/components/invoice-status-filter.tsx$ git commit --amend --no-edit$ git push --force-with-lease # only needed if the original was already pushedgit rebase -i rewrites a run of commits
Section titled “git rebase -i rewrites a run of commits”This is the core history-shaping tool, the one you’ll use most. git rebase -i origin/main (or git rebase -i HEAD~5 for the last five commits) opens an editor with one line per commit, each prefixed with a verb you can change. You don’t run commands; you edit a to-do list, save it, and Git carries it out. The verbs are the whole vocabulary:
pick: keep the commit as-is. This is the default on every line.reword: keep the commit, but edit its message.edit: stop after applying this commit so you can amend it or split it, then continue.squash: fold this commit into the one above it, and combine both messages.fixup: fold this commit into the one above it, and discard this one’s message.drop: remove the commit entirely.
You can also reorder commits by moving their lines. One detail trips up nearly everyone the first time: the list reads top-to-bottom as oldest-to-newest, the reverse of git log. Read it that way and the verbs make sense; read it like git log and you’ll squash in the wrong direction.
The to-do buffer below turns five scrappy commits into two clean ones.
pick a1b9f02 Add invoice status filterfixup 3d8e4c7 wipreword 7c4d2e9 fixfixup 9f2c1ab fix typo in labeldrop b2e8a14 debug logging, remove laterTop line, so this is the oldest commit and the foundation; pick keeps it untouched. Everything below folds into it or rearranges around it.
pick a1b9f02 Add invoice status filterfixup 3d8e4c7 wipreword 7c4d2e9 fixfixup 9f2c1ab fix typo in labeldrop b2e8a14 debug logging, remove laterTwo fixup lines collapse the wip and the typo-fix into the commit above them, discarding their throwaway messages. This is how scrappy commits become one clean feature commit.
pick a1b9f02 Add invoice status filterfixup 3d8e4c7 wipreword 7c4d2e9 fixfixup 9f2c1ab fix typo in labeldrop b2e8a14 debug logging, remove laterreword keeps this commit’s changes but lets you rewrite its vague fix subject into something a reviewer can read.
pick a1b9f02 Add invoice status filterfixup 3d8e4c7 wipreword 7c4d2e9 fixfixup 9f2c1ab fix typo in labeldrop b2e8a14 debug logging, remove laterdrop deletes the debug-logging commit, so its changes never reach the rebased branch. Cleaner than a follow-up to remove it.
The verb that does more than its name suggests is edit, which splits an over-large commit. Mark the commit edit, and when the rebase stops on it, run git reset HEAD~ to undo the commit while keeping its changes in your working tree. Then stage and commit those changes in logical pieces, and run git rebase --continue. One bloated commit becomes the two or three focused commits it should have been.
The tabs below show the same branch before and after the rebase above, five noisy commits collapsing into two that tell the story.
- drop b2e8a14 debug logging, remove later
- fixup 9f2c1ab fix typo in label
- reword 7c4d2e9 fix
- fixup 3d8e4c7 wip
- pick a1b9f02 Add invoice status filter
- c1f3a07 Validate the status filter against allowed values
- e4d9b21 Add invoice status filter
The embed below drops you into a branch with several commits; squash and reorder them and watch the graph linearize in real time.
This is LGB’s Interactive Rebase Intro. Run git rebase -i HEAD~4, then reorder and drop lines in the dialog and hit confirm. Watch the branch’s commits lift onto a new base and the graph go linear. This is the move you’ll make on every feature branch before review.
Autosquash turns review fixes into the commit they belong in
Section titled “Autosquash turns review fixes into the commit they belong in”Interactive rebase almost solves a recurring friction in the review loop. A reviewer points at a line, and the fix belongs inside that original commit, not in a new “address review feedback” commit that’s pure noise. You could rebase -i and manually move a fixup line under the right commit, but Git can wire that up.
When you make the fix, commit it with git commit --fixup=<sha>, naming the commit the fix belongs in. That creates a fixup! <original subject> commit. Later, git rebase -i --autosquash origin/main reads those markers, reorders each fixup! commit beneath its target, and pre-marks it fixup, so the to-do buffer is already correct and you just save. (--squash=<sha> is the same idea when you want to keep the fix’s message instead of discarding it.)
$ git commit --fixup=a1b9f02 # this fix belongs in commit a1b9f02$ git rebase -i --autosquash origin/main # fixup is pre-sorted and pre-marked; just save
# set it once so plain `git rebase -i` always autosquashes:$ git config --global rebase.autoSquash trueThe previous lesson had you set five global config lines and deliberately left this one out. rebase.autoSquash is the sixth: set it once and plain git rebase -i autosquashes every time. This loop, a fixup commit and a push so the reviewer sees only what changed since their last look, is how pull-request review works in practice, the next lesson’s territory.
Resolving the conflicts these tools create
Section titled “Resolving the conflicts these tools create”Merge, rebase, cherry-pick, and revert can all stop partway through with a conflict. The situation and the fix are identical whichever tool triggered it, so here it is once.
A conflict happens when Git can’t reconcile two changes to the same lines automatically and needs you to decide. It marks the spot with three lines: <<<<<<<, =======, and >>>>>>>. The first chunk is one side, the second is the other. Edit the file into the result you want, one side, the other, or a hand-merged combination, then delete all three marker lines.
<<<<<<< HEADconst STATUSES = ['draft', 'sent', 'paid'];=======const STATUSES = ['draft', 'sent', 'paid', 'void'];>>>>>>> feat/invoice-status
# edit the file to the result you want, deleting all three markers, then:$ git add src/lib/invoice-statuses.ts$ git rebase --continueThe three markers Git writes into the file. Everything between the first two is one side; everything between the last two is the other. During a rebase, the top side (HEAD) is the commit being replayed onto, which can feel inverted, so read the labels.
<<<<<<< HEADconst STATUSES = ['draft', 'sent', 'paid'];=======const STATUSES = ['draft', 'sent', 'paid', 'void'];>>>>>>> feat/invoice-status
# edit the file to the result you want, deleting all three markers, then:$ git add src/lib/invoice-statuses.ts$ git rebase --continueThe conflict itself: two versions of the same line. Keep one, keep the other, or merge them by hand into a single correct line.
<<<<<<< HEADconst STATUSES = ['draft', 'sent', 'paid'];=======const STATUSES = ['draft', 'sent', 'paid', 'void'];>>>>>>> feat/invoice-status
# edit the file to the result you want, deleting all three markers, then:$ git add src/lib/invoice-statuses.ts$ git rebase --continueOnce the file reads the way you want and the markers are gone, git add it to mark the conflict resolved.
<<<<<<< HEADconst STATUSES = ['draft', 'sent', 'paid'];=======const STATUSES = ['draft', 'sent', 'paid', 'void'];>>>>>>> feat/invoice-status
# edit the file to the result you want, deleting all three markers, then:$ git add src/lib/invoice-statuses.ts$ git rebase --continueThen continue the operation that stopped. The verb matches the tool: git rebase --continue, git merge --continue, git cherry-pick --continue, or git revert --continue.
If a conflict is more than you want to take on, every one of these operations has a --abort form, such as git rebase --abort, that backs you out to where you started. And if you get past where --abort helps, you know the net: git reflog, then reset.
For a trivial two-line conflict, the terminal is fine. For a messy one, VS Code’s three-way merge editor is faster: it shows theirs, yours, and the result in side-by-side panes.
Here the rerere (“reuse recorded resolution”) config you enabled in the previous lesson pays off: it remembers how you resolved a conflict. On a long-lived branch you rebase onto a moving main repeatedly, you hit the same conflict over and over, and with rerere on, Git replays your earlier resolution instead of making you redo it. The idea underneath is a three-way merge , comparing both sides against their shared ancestor, which is what lets Git replay a resolution with confidence.
Reading history: log, blame, switch, and restore
Section titled “Reading history: log, blame, switch, and restore”Every tool so far operated on a commit you had to find first: the baseline for bisect, the sha to cherry-pick, the commit a fix belongs in. These read-only commands find those shas, each paired with the question it answers.
Start with git log, which answers most “when did this change?” questions. Here are the invocations worth knowing.
git log --oneline -20 # the last 20 commits, one line each — a quick changeloggit log --graph --oneline --all # the real branch topology, drawn as a graphgit log -p src/lib/invoices.ts # one file's history, with the diff of each changegit log -S "calculateTotal" # the pickaxe: commits that added or removed this stringgit log --author="Dana" # one person's commitsThe pickaxe (-S) answers “when did this exact string enter or leave the codebase?”, often how you find the commit that introduced a bug or deleted a function, and the fastest way to hand bisect or cherry-pick a target sha.
When the question is “who wrote this line, and why,” reach for git blame <file>: it annotates every line with the commit, author, and date that last touched it. git blame -L 40,80 <file> blames only lines 40 through 80, and VS Code’s GitLens shows the same inline as your cursor moves. Blame tells you which commit changed the line; to see the whole change, follow it with git log -p or git show <sha>.
Finally, the two verbs that replaced an overloaded one. git checkout did too many unrelated jobs, switching branches, creating branches, and discarding file changes, which made it easy to run the destructive version by accident. Modern Git split it in two, each verb doing one job:
git switch <branch>changes to an existing branch;git switch -c <branch>creates one and switches to it.git restore <file>discards uncommitted changes to a file (the destructive one, now clearly named);git restore --staged <file>unstages a file without touching its contents.
git checkout still works, but prefer switch and restore: one verb per job means you can’t turn a branch switch into a lost edit by mistake. They shed their “experimental” label in Git 2.51; a tutorial that still calls them experimental is out of date.
Matching an incident to the fix
Section titled “Matching an incident to the fix”The hard part of a real incident is rarely typing the command; it’s the ten-second window where your pulse is up and you have to pick the right one. This section trains that match from situation to move.
Start with the most common case: you committed to the wrong branch, straight onto main or onto the wrong feature branch. Which fix you need is a zone question: have you pushed yet?
If you haven’t pushed, the commit is local-only, so you can rewrite freely. Grab its hash with git log, undo it with git reset --hard HEAD~N (where N is how many commits you misplaced), switch to where it belongs with git switch -c correct-branch, and replay it with git cherry-pick <hash>. If you have pushed but no pull request exists yet, run the same sequence plus one step: after the reset, push the original branch with git push --force-with-lease so the remote drops the misplaced commit too. If a step goes sideways, the reflog is your net.
The walker below is your triage trainer. Start at the top, “what went wrong?”, and click down to the recovery move. Notice the order of the questions: what happened, then where the commit lives, then whether it’s pushed. Walk every branch until the path feels automatic.
The commit isn’t deleted, just unreferenced. Read the reflog, find the line just before the reset, and reset back to that hash. Or run git branch recover <hash> to park it safely first, then inspect before you commit to anything destructive.
Local-only, so rewrite freely. git log for the hash, git reset --hard HEAD~N to undo it here, git switch -c correct-branch, then git cherry-pick <hash> to land it where it belongs.
The exact same sequence as the not-pushed case, but the original branch’s remote still has the misplaced commit, so finish with git push --force-with-lease on it to drop the commit there too.
The moment a secret hit the remote it was leaked, so scrubbing it from history is theater, not a fix. Rotate the credential immediately. Removing it from history with tools like git filter-repo or BFG is a later security-playbook step, never the primary one. This is the secret-in-history rule from the previous lesson.
If you’re still mid-rebase, git rebase --abort rewinds you to exactly where you started, as if it never happened. If you’ve already finished the bad rebase and it’s too late to abort, the reflog has the pre-rebase commit, so reset to it.
A regression hiding in a known-good-to-broken range is the textbook bisect case. Mark a good baseline and bad HEAD, then let git bisect run pnpm test binary-search to the culprit for you.
The secret case deserves a sentence more, because the instinct is wrong. Your reflex is to scrub the secret from history, but it leaked the instant it reached the remote. Anyone watching the repository already has it, so deleting it later changes nothing. The real fix is to rotate the secret: revoke the leaked credential and issue a new one. Cleaning history with git filter-repo or BFG is follow-up hygiene, never the primary response.
To lock in the wrong-branch rescue, put its not-pushed path in order.
You committed to the wrong branch and haven't pushed. Order the rescue. Drag the items into the correct order, then press Check.
git log — copy the hash of the misplaced commit. git reset --hard HEAD~1 — undo the commit on this branch. git switch -c correct-branch — create and move to the right branch. git cherry-pick <hash> — replay the commit where it belongs. The next lesson moves up from the command line to the pull request: how to package a change so a reviewer can actually review it.
Going further
Section titled “Going further”The official Git documentation is solid reference once you know what you’re looking for, and Atlassian’s rewriting-history guide is a readable secondary source on the rebase and amend family.
The authoritative docs for the undo journal, including expiry and pruning behaviour.
Every subcommand, including `run`, `skip`, and the new/old aliases for non-bug searches.
The full verb list and the autosquash and rebase-merge options this lesson leans on.
A readable secondary walkthrough of amend, rebase, and reset with diagrams.