Git Internals for Engineers

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 bisect and 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.2 without 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.

PRO TIP

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.

WARNING

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

SituationUse
Local branch, commit not pushedgit reset --hard (clean history)
Shared branch, commit pushedgit revert (safe, audit trail)
Need to remove commit entirely from historygit rebase -i / filter-repo (only if unshared)
Want to "roll back" a feature for now, maybe reintroduce latergit 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).

PRO TIP

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.
  • -x adds "(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 bisect binary-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 -x on 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 reset on shared branches to "undo" a pushed commit. Use revert — reset rewrites history.
  • Running git bisect on flaky tests without handling flakiness. Wrong commit identified.
  • Not using git bisect run with 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.

KNOWLEDGE CHECK

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?