Cherry-Pick, Bisect, and Blame — Tracking Down Bugs with Git
Table of Contents
- Introduction
- Git Blame — Who Changed This and Why?
- Git Bisect — Binary Search for Bugs
- Git Cherry-Pick — Surgical Commit Transplants
- Combining All Three in Practice
- Common Pitfalls
- Conclusion
Introduction
A bug has landed in production. Users are reporting it. Your PM is pinging you. The clock is ticking. You need to answer three questions fast: when was the bug introduced, who made the change (and what was their intent), and how do you ship a targeted fix without merging an entire branch?
Git has three commands built precisely for this workflow: blame, bisect, and cherry-pick. Most developers have heard of them. Far fewer use them effectively. This guide covers all three — what they do, when to reach for each one, and the real-world workflow that ties them together.
Git Blame — Who Changed This and Why?
Despite its accusatory name, git blame isn't about pointing fingers. It annotates every line of a file with the commit hash, author, and date of the last change. It answers the question: "What was the context behind this line of code?"
Basic Usage
# Blame an entire file
git blame src/utils/retry.ts
# Output format:
# a1b2c3d4 (Alice Chen 2026-01-15 09:32:11 +0000 42) const MAX_RETRIES = 3;
# e5f6g7h8 (Bob Kumar 2026-02-20 14:17:45 +0000 43) const TIMEOUT = 5000;
# a1b2c3d4 (Alice Chen 2026-01-15 09:32:11 +0000 44) const BACKOFF = 1.5;
Narrowing the Scope
# Blame only specific lines
git blame -L 40,60 src/utils/retry.ts
# Blame starting from a specific line to end of file
git blame -L 40, src/utils/retry.ts
# Blame a function by name (Git tries to find function boundaries)
git blame -L :getRetryConfig src/utils/retry.ts
Seeing Past Refactors
A common frustration: you run blame and every line shows the same commit — a large refactor or code formatting change that touched the whole file. The -w and -M flags help you see through these.
# Ignore whitespace changes
git blame -w src/utils/retry.ts
# Detect lines moved within the file
git blame -M src/utils/retry.ts
# Detect lines moved from other files
git blame -C src/utils/retry.ts
# Combine all three for maximum depth
git blame -w -M -C src/utils/retry.ts
Blame a Historical Version
# Blame the file as it existed at a specific commit
git blame abc123^ -- src/utils/retry.ts
# The ^ means "the commit before abc123" — useful for
# looking past a known refactor commit
💡 TIP: Create a .git-blame-ignore-revs file in your repo listing commits that are pure formatting or refactoring. Then configure Git to skip them automatically:
# .git-blame-ignore-revs
# Prettier formatting migration - 2026-01-10
abc123def456789
# ESLint autofix bulk update
789def012abc345
# Tell Git to use the ignore file
git config blame.ignoreRevsFile .git-blame-ignore-revs
GitHub also respects this file and will filter out those commits from blame views in the browser automatically.
Git Bisect — Binary Search for Bugs
git bisect is one of the most powerful and underused commands in Git. It performs a binary search through your commit history to find the exact commit that introduced a bug. If you have 1,000 commits between "it worked" and "it's broken," bisect will find the culprit in about 10 steps.
Manual Bisect
# Start the bisect session
git bisect start
# Mark the current commit as bad (has the bug)
git bisect bad
# Mark a known good commit (before the bug existed)
git bisect good v2.1.0
# Git checks out a commit halfway between good and bad
# Test it, then tell Git the result:
git bisect good # if this commit doesn't have the bug
# or
git bisect bad # if this commit has the bug
# Git narrows the range and checks out the next midpoint
# Repeat until Git identifies the first bad commit
# When done, reset to your original branch
git bisect reset
After each step, Git tells you how many commits remain and how many steps are left. A typical session looks like:
Bisecting: 64 revisions left to test after this (roughly 6 steps)
[commit hash] Some commit message here
Automated Bisect
If you have a test or script that can determine whether a commit is good or bad, bisect can run fully unattended. This is where it gets truly powerful.
# Automated bisect with a test script
# The script must exit 0 for "good" and 1 (or 125) for "bad"
git bisect start
git bisect bad HEAD
git bisect good v2.1.0
git bisect run npm test
# Or with a custom script
git bisect run ./scripts/check-bug.sh
# Or with a specific test file
git bisect run npx jest src/utils/retry.test.ts
Git will check out each midpoint, run your script, interpret the exit code, and narrow the range automatically. When it's done, it prints the exact commit that introduced the regression. Zero manual intervention.
Special Exit Codes
# Exit code 125 tells bisect to skip a commit
# (e.g., it doesn't compile, so you can't test it)
#!/bin/bash
make || exit 125 # Skip if build fails
./run-test.sh # Exit 0 = good, 1 = bad
💡 TIP: If a commit can't be tested (build is broken, dependencies missing), you can skip it manually too:
git bisect skip
Git Cherry-Pick — Surgical Commit Transplants
git cherry-pick takes a specific commit from one branch and applies it to another. It doesn't merge branches — it copies a single commit's changes and creates a new commit on your current branch. Think of it as transplanting one precise change.
Basic Cherry-Pick
# Apply a single commit to the current branch
git cherry-pick abc123
# Apply multiple commits
git cherry-pick abc123 def456 ghi789
# Apply a range of commits (exclusive of the first, inclusive of the last)
git cherry-pick abc123..ghi789
Cherry-Pick Without Committing
# Apply the changes but don't commit yet
# Useful when you want to combine or modify before committing
git cherry-pick --no-commit abc123
Common Use Cases
Hotfixing production. A bug fix exists on develop but production runs off a release branch. Cherry-pick the fix commit directly onto the release branch without merging all of develop.
# On the release branch
git checkout release/2.3
git cherry-pick abc123
git push origin release/2.3
Backporting fixes. You fixed a bug on main and need the same fix on older release branches that are still supported.
# Apply the fix to multiple release branches
git checkout release/2.2
git cherry-pick abc123
git checkout release/2.1
git cherry-pick abc123
Recovering a commit from a deleted branch. The branch is gone, but the commit hash still exists in the reflog. Cherry-pick it onto a new branch.
git reflog # find the commit hash
git checkout -b recovered-work
git cherry-pick abc123
Handling Cherry-Pick Conflicts
# If a conflict occurs during cherry-pick:
# 1. Resolve the conflict in the file
# 2. Stage the resolved file
git add src/utils/retry.ts
# 3. Continue the cherry-pick
git cherry-pick --continue
# Or abort if it's not working out
git cherry-pick --abort
⚠️ WARNING: Cherry-picked commits get new hashes on the target branch. If you later merge the source branch, Git may see the changes as separate commits, which can occasionally cause duplicate changes or unexpected conflicts. Use cherry-pick for targeted fixes, not as a replacement for merging.
Combining All Three in Practice
These three commands are most powerful when used together. Here's a realistic incident response workflow that demonstrates how they complement each other.
The Scenario
Users report that payment processing fails intermittently. It was working fine last week. You need to find the cause, understand the intent, and ship a fix to production — fast.
Step 1: Bisect to Find the Bad Commit
git bisect start
git bisect bad HEAD
git bisect good v3.4.0 # Last known good release
git bisect run npm test -- --grep "payment"
# Result:
# abc123def is the first bad commit
# Author: Dave Park
# Date: Feb 28
# Message: "Refactor timeout logic for retry module"
Step 2: Blame to Understand the Change
# Look at what exactly changed in that commit
git show abc123def
# Blame the specific file and lines affected
git blame -L 38,55 src/services/payment.ts
# Now you see Dave changed the retry backoff from exponential to linear
# That explains the intermittent failures under load
Step 3: Fix and Cherry-Pick to Production
# Create a fix on your branch
git checkout -b fix/payment-retry
# ... fix the code, write a test ...
git commit -m "fix: restore exponential backoff in payment retry logic"
# Cherry-pick the fix to the release branch
git checkout release/3.5
git cherry-pick <fix-commit-hash>
git push origin release/3.5
# Then merge the fix back to main through a normal PR
Total time from report to fix: minutes, not hours. That's the power of knowing these tools.
Common Pitfalls
1. Using blame to assign fault instead of understanding context.
Blame shows who last touched a line — not who caused the bug. The bug might be in the interaction between two changes by two different people. Use blame as a starting point for investigation, not a verdict.
2. Forgetting to reset after bisect.
Bisect puts you in a detached HEAD state as it checks out intermediate commits. Always run git bisect reset when you're done. If you forget and start making commits, you'll be working on a detached HEAD — recoverable, but confusing.
3. Cherry-picking merge commits.
Cherry-picking a merge commit requires the -m flag to specify which parent to treat as the mainline. If you don't specify, Git errors out. If you specify wrong, you get unexpected results.
# Cherry-pick a merge commit (parent 1 is usually the mainline)
git cherry-pick -m 1 <merge-commit-hash>
When possible, cherry-pick the individual fix commits rather than the merge commit itself. It's simpler and less error-prone.
4. Over-relying on cherry-pick instead of proper merging.
If you're cherry-picking frequently between the same branches, your workflow probably needs restructuring. Cherry-pick is for exceptions — hotfixes, backports, emergencies. Regular feature work should flow through merge or rebase.
5. Not writing testable commits for bisect.
Bisect works best when every commit is buildable and testable. If your history is full of WIP commits that don't compile, automated bisect will need constant skip overrides. This is another reason to squash and clean up commits before merging to main.
6. Ignoring git log as a companion tool.
Before reaching for blame or bisect, a targeted git log can sometimes get you there faster:
# Show commits that touched a specific file
git log --oneline -- src/services/payment.ts
# Show commits that changed a specific function (by text)
git log -p -S "MAX_RETRIES" -- src/utils/retry.ts
# Show commits by a specific author in a date range
git log --author="Dave" --after="2026-02-01" --before="2026-03-01" --oneline
Conclusion
Bug hunting is detective work, and Git gives you a remarkably capable toolkit for it. Blame shows you the history behind any line of code. Bisect narrows thousands of commits down to the one that broke things. Cherry-pick lets you ship a targeted fix without waiting for a full branch merge.
Used individually, each command is useful. Used together, they form a workflow that can take you from "something is broken" to "the fix is in production" faster than most engineers think possible.
The investment is small — learn three commands. The payoff is enormous — you'll debug faster, ship fixes with precision, and understand your codebase's history at a level that makes you the person your team calls when things go wrong.