Cherry-Pick, Revert, and Bisect
A production bug report lands: "the login page is broken." It worked last Monday. It is broken now. You have a stack of 300 commits between those points, written by four people across three teams. You could read 300 diffs. Or you could run
git bisectand let Git find the exact commit that introduced the regression in about 15 minutes and 8 test runs. You run bisect. It finds the culprit. The fix is a one-line PR that reverts that specific commit on main (while rolling forward a corrected version on the feature branch). Cherry-pick ports the fix to a release branch. Three commands, bug fixed, tests green, ship.Cherry-pick, revert, and bisect are Git's precision tools for surgical work on history. This lesson covers each with the scenarios that justify them — when to reach for cherry-pick instead of merging, when revert beats reset, and how bisect collapses bug hunts from hours to minutes.
Cherry-Pick: Apply a Commit Elsewhere
git cherry-pick <commit> takes a specific commit and applies its changes (as a new commit) on top of your current branch.
# I'm on main. I want to apply commit abc123 (which exists on another branch)
git cherry-pick abc123
# A new commit is created on main with the same changes as abc123.
# New SHA, same message, same author.
When to cherry-pick
Good uses:
- Backport a fix to a release branch. You fixed a bug on main; you need the same fix on
release/v1.2without pulling in everything else on main. - Rescue a commit from a botched branch. A teammate's branch has one great commit among many bad ones; you want just that one.
- Move a commit between branches without rebase politics. Especially when the source branch is shared and you do not want to force-push it.
Bad uses:
- As a primary integration mechanism. If you need most of a branch's commits, merge or rebase is cleaner. Cherry-picking many commits is fragile (they often have subtle dependencies).
- To move commits off a branch. Use rebase (
--onto) for that.
Cherry-pick a range
git cherry-pick abc123..def456
# Picks every commit in that range (exclusive of abc123, inclusive of def456)
Handle conflicts
git cherry-pick abc123
# CONFLICT in src/auth.py
# (resolve)
git add src/auth.py
git cherry-pick --continue
# Or bail out:
git cherry-pick --abort
Preserve the original SHA reference
git cherry-pick -x abc123
# Adds a line to the commit message: "(cherry picked from commit abc123)"
# Useful for audit trails — you can always trace back to the origin
Many teams use -x by convention for backports so that future readers can find the original PR/context easily.
Cherry-pick with -x is the professional way to backport. The commit message gets an auto-appended "(cherry picked from commit abc...)" line that lets you trace the fix back to the original PR forever. Without -x, you lose the link and future debugging gets harder.
Revert: Undo a Commit Via a New Commit
git revert <commit> creates a new commit whose changes are the inverse of the target commit.
# Main currently: A - B - C - D (HEAD)
# D introduced a bug
git revert D
# Creates a new commit R: A - B - C - D - R
# R's diff against D is the opposite of D's diff against C
# Net effect: R + D cancel out
Why revert and not reset?
- Reset changes the branch's history (removes D).
- Revert adds a new commit (keeps D, adds its inverse).
On shared branches, reset is dangerous — others have D. Revert is safe — everyone just fetches the new R commit.
# Safe for shared branches (main, release/*, develop):
git revert <bad-commit>
git push
# Only safe for unshared branches:
git reset --hard <good-commit>
git push --force-with-lease # even then, careful
Revert a range
git revert HEAD~5..HEAD
# Reverts the last 5 commits, one new revert commit per target
Each reverted commit becomes its own revert commit (by default). Use --no-commit to make one combined revert:
git revert --no-commit HEAD~5..HEAD
# All the changes are staged but not committed
git commit -m "revert: roll back last week's buggy changes"
Revert a merge commit
Reverting a merge is trickier because Git does not know which parent is "the one to revert to." You must specify:
# Merge commit M had two parents: main (-m 1) and feature (-m 2)
git revert -m 1 <merge-sha>
# "Revert the effects of merging in feature" — keeps main's changes, undoes feature's
And if you later want to re-merge that feature, you will have issues because Git "remembers" that feature was already merged. Usually you need to explicitly rebase feature or use git merge --no-ff again after reverting the revert.
Reverting a merge commit is subtle. Re-merging later is non-trivial because Git considers the feature "already merged, then reverted — do not merge again." For critical merges, consider NOT reverting the merge; instead, revert the specific problematic commits within it, or fix-forward with a new corrective commit.
Revert vs reset: decision matrix
| Situation | Use |
|---|---|
| Local branch, commit not pushed | git reset --hard (clean history) |
| Shared branch, commit pushed | git revert (safe, audit trail) |
| Need to remove commit entirely from history | git rebase -i / filter-repo (only if unshared) |
| Want to "roll back" a feature for now, maybe reintroduce later | git revert (keep commit history; re-revert to restore) |
Bisect: Binary Search for a Bug
git bisect does binary search through commit history to find the exact commit that introduced a regression.
The setup: you have a known-good commit (v1.0, maybe) and a known-bad commit (HEAD). Somewhere between them, a commit introduced a bug. You want to find it.
Manual bisect
# 1. Start bisect
git bisect start
# 2. Mark current HEAD as bad
git bisect bad
# 3. Mark a known-good commit
git bisect good v1.0
# Git checks out a commit in the middle of the range and says:
# Bisecting: 27 revisions left to test after this (roughly 5 steps)
# 4. Test the current state (run your tests, do whatever confirms the bug)
# If the bug is present:
git bisect bad
# If the bug is NOT present:
git bisect good
# Git keeps bisecting, halving the range each step.
# After log2(range) steps:
# ef5a6b7c is the first bad commit
# commit ef5a6b7c...
# Author: Alice <alice@example.com>
# Date: Thu Apr 15 ...
#
# refactor: extract login validation
# 5. Reset when done
git bisect reset
# Returns you to your original HEAD
For a range of 300 commits: log2(300) ≈ 8 test steps to find the exact culprit.
Automated bisect (the killer feature)
If you can write a script that exits 0 for "good" and non-zero for "bad," Git runs it for every step:
git bisect start
git bisect bad
git bisect good v1.0
# Automatic!
git bisect run ./run-test.sh
Where run-test.sh might be:
#!/bin/bash
# run-test.sh
make -s clean
make -s test-login
# exit code: 0 if passing, nonzero if failing
Git runs this at each bisect step automatically. For a 300-commit range, this finishes in 8 × (test duration) and prints the culprit SHA. You can start it, go to lunch, come back to the answer.
Bisect on flaky tests
If a test is flaky (sometimes passes, sometimes fails), bisect can mis-identify. Mitigations:
git bisect skip— when you cannot determine good/bad reliably at a commit (test hangs, build broken). Git skips to a nearby commit.- Custom "known bad" logic — in your script, detect and retry flakiness. If still unclear,
exit 125(bisect treats this as "skip").
Visualize the bisect
git bisect visualize
# Opens `git log` showing the remaining candidates
The mental model
Bisect is binary search. Every commit in your repo is either "good" (pre-bug) or "bad" (post-bug). Git finds the boundary. The cost is O(log N) tests instead of O(N).
Investment in a small "does the bug exist in this commit?" script pays off enormously. Once you have automated the test, git bisect run finds regressions faster than any code review. Even messy tests (curl this endpoint, grep for a string) work — bisect does not care how elegant the test is, only that it returns a reliable good/bad.
The Three Tools in Concert
A realistic scenario:
1. The incident
"Users cannot log in." The regression started sometime in the last week.
2. Bisect to find the commit
git bisect start
git bisect bad HEAD
git bisect good v1.2.0 # last known-good release
git bisect run ./test-login.sh
# ... 7 steps later ...
# ef5a6b7c is the first bad commit
3. Revert the bad commit on main
git bisect reset
git revert ef5a6b7c
git push origin main
# Main is fixed. Production unblocked.
4. Cherry-pick the revert onto release branches
git checkout release/v1.2
git cherry-pick <revert-sha>
git push origin release/v1.2
git checkout release/v1.3
git cherry-pick <revert-sha>
git push origin release/v1.3
5. Fix forward
# On the feature branch that introduced the bug
git checkout feature/new-auth
# Fix the actual bug (maybe in coordination with the original author)
git commit -m "fix: correct validation in login flow"
git push origin feature/new-auth
6. PR the corrected feature and merge
When the corrected feature merges into main, it supersedes the revert. All release branches get the fix via cherry-pick when appropriate.
This flow — bisect → revert → cherry-pick → fix-forward — is a standard production hotfix pattern. Each tool plays its specific role.
Cherry-Pick Gotchas
Commit dependencies
Commits are not always independent. A commit that adds a helper used by a later commit will fail to cherry-pick the later commit alone — the helper is missing.
# Branch: feature
# A: add helper function
# B: use the helper in login flow
# Try to cherry-pick just B:
git cherry-pick <B-sha>
# CONFLICT or error: reference to missing helper
Fix: cherry-pick A first, then B. Or cherry-pick the range A..B (or A~1..B).
Duplicate commits
If you cherry-pick commit X to branch Y, then later merge branch Y into the branch that originally had X, Git sees the change twice. Usually Git detects this via patch-id comparison and skips the duplicate — but not always. Be aware of the possibility.
--no-commit
git cherry-pick --no-commit abc123
# Apply the changes but don't commit; stays in the index
# Useful for combining multiple cherry-picks into one commit
Revert Gotchas
Cannot re-apply a reverted commit
# Main: A - B - C - D - R (where R reverts D)
# Later, you want D's changes back
git cherry-pick D
# Might succeed (re-applying D on top of R's undo)
# Might conflict (if anything changed in the meantime)
The common pattern: revert the bad commit, fix forward with a corrected version (not by "re-applying" the old commit).
Reverting across merges
As noted, reverting a merge is tricky. Re-merging later needs --no-ff or rebase acrobatics. For big merges, prefer "fix forward" over "revert the merge."
Bisect Gotchas
Broken builds in the middle of history
Some commits in the range may not even compile or boot. Bisect offers:
git bisect skip # skip this commit; Git tries another nearby
Or scripted (exit 125 from your run-script).
Side-effect contamination
If your tests leave state between runs (databases, caches, files), a "good" test after a "bad" test might pass misleadingly. Always reset state between bisect tests — often in the script:
#!/bin/bash
# run-test.sh
rm -rf /tmp/test-state
mkdir /tmp/test-state
npm ci # clean install
npm test
Bisecting non-regressions
Bisect works for regressions — "good at A, bad at B, find where it changed." If a bug is intermittent or has always been present in subtle form, bisect finds nothing clean. Improve the test case first; then bisect.
Advanced: git blame + git bisect
Combine them for deep code forensics:
# 1. Find when a specific line was changed
git blame src/auth.py -L 42
# abc123 (Sharon 2026-04-10) def validate(user, pass):
# 2. Bisect to verify that commit is the regression source
git bisect start
git bisect bad HEAD
git bisect good abc123~1 # just before the suspect commit
# Bisect should converge on abc123 quickly
# 3. Check the diff
git show abc123
# Sometimes the line was already wrong; git blame just shows the last commit
# that touched it, not the origin.
For "why does this line exist" archeology, git log -L :function_name:file (Module 2 Lesson 2) shows the full history of a function across its lifetime.
Key Concepts Summary
git cherry-pick <sha>applies a commit's changes on top of the current branch as a new commit.-xadds "(cherry picked from commit...)" to the message — always use for backports.git revert <sha>creates a new commit undoing a previous commit. Safe for shared branches.git revert -m 1 <merge-sha>reverts a merge; specify which parent to preserve.- Reverting merges is subtle: re-merging later is non-trivial; prefer fix-forward when possible.
git bisectbinary-searches history for a regression. Log(N) test runs instead of N.git bisect run <script>automates — Git calls the script at each step; exit 0 = good, nonzero = bad, 125 = skip.- Each tool has a specialty: cherry-pick for porting changes, revert for undoing on shared branches, bisect for finding regressions.
- Dependencies matter for cherry-pick: commits that build on each other cannot be picked independently.
Common Mistakes
- Using cherry-pick as the primary integration method for a whole branch. Fragile and messy; use rebase or merge.
- Forgetting
-xon backports. Future readers cannot trace the fix to its origin PR. - Reverting merge commits without thinking about re-merge. Later integrations get confusing.
- Using
git reseton shared branches to "undo" a pushed commit. Userevert— reset rewrites history. - Running
git bisecton flaky tests without handling flakiness. Wrong commit identified. - Not using
git bisect runwith a script. Manual bisect on 500 commits is 9 test runs of manual work; automated is lunch-break walkable. - Skipping clean state between bisect runs, causing false positives/negatives from test contamination.
- Cherry-picking a commit that depends on a prior unpicked commit. Conflicts or subtle missing references.
- Reverting a revert. Legal, occasionally useful, often confusing. Communicate clearly when you do it.
- Using cherry-pick across large time spans (months). The codebase may have shifted enough that the picked commit no longer makes sense; rewrite as a fresh commit instead.
Production has a regression that started sometime in the last 200 commits. You have a one-line test that reproduces the bug in under 5 seconds. What is the fastest way to find the exact commit that introduced the bug, and how many test runs will it take?