Learning Objectives
By the end of this module, you will be able to:
- Use
git cherry-pickto apply specific commits from one branch to another - Resolve cherry-pick conflicts and understand how cherry-pick relates to rebase
- Navigate the reflog to recover "lost" commits, deleted branches, and undone work
- Run
git bisectto perform a binary search through history and find the commit that introduced a bug - Investigate code history with
git blame,git log -S(pickaxe), andgit log -G - Generate release summaries with
git shortlog
1. git cherry-pick — Surgical Commit Transplanting
Cherry-picking lets you take a single commit from anywhere in your history and replay it on your current branch. Think of it as a mini-rebase — you're copying a change, not moving it.
How It Works Under the Hood
Before cherry-pick (on main):
C --- D (feature)
/
A --- B (main) ← HEAD
git cherry-pick D
After:
C --- D (feature) ← D still here
/
A --- B --- D' (main) ← HEAD
Key observations:
- D' is a new commit with a different SHA than D — same changes, different identity
- The original commit D remains untouched on
feature - Git computes the diff that D introduced (the delta between C and D) and applies it on top of B
Basic Usage
# Cherry-pick a single commit by SHA
git cherry-pick abc1234
# Cherry-pick the tip of a branch
git cherry-pick feature
# Cherry-pick multiple commits (applied in order)
git cherry-pick abc1234 def5678 ghi9012
# Cherry-pick a range of commits (exclusive start, inclusive end)
git cherry-pick abc1234..ghi9012
# Cherry-pick a range inclusive of both ends
git cherry-pick abc1234^..ghi9012The -n (No-Commit) Option
Sometimes you want to cherry-pick changes but not create a commit immediately:
# Stage the changes without committing
git cherry-pick -n abc1234
# Now you can modify, combine with other changes, then commit manually
git commit -m "Combined cherry-picked changes with local fixes"This is useful when you want to:
- Cherry-pick multiple commits and squash them into one
- Modify the cherry-picked changes before committing
- Combine cherry-picked code with additional fixes
The -x Option — Leaving a Trail
git cherry-pick -x abc1234This appends a line to the commit message:
(cherry picked from commit abc1234567890abcdef1234567890abcdef12345)
Always use -x when cherry-picking between long-lived branches. It creates a paper trail that helps your team understand where a change came from and prevents accidental duplicate work.
Cherry-Pick vs. Rebase — Three Key Differences
| Aspect | Cherry-Pick | Rebase |
|---|---|---|
| Direction | You're on the destination branch and pull commits in (like merge) | You're on the source branch and replay onto a target (reverse) |
| Scope | One commit at a time (or a few) | Entire branch from fork point |
| Original commits | Stay where they are | Become unreachable (replaced by new copies) |
Under the hood, cherry-pick IS a rebase — just for a single commit. Git's internal machinery is the same.
2. Cherry-Pick Conflicts
Cherry-pick conflicts work exactly like rebase conflicts. When the patch can't be applied cleanly, Git pauses and asks for help.
Conflict Resolution Workflow
git cherry-pick abc1234
# CONFLICT! Git pauses.
# 1. Check what's happening
git status
# 2. Open conflicted files — resolve the markers
# <<<<<<< HEAD
# (your current branch's code)
# =======
# (the cherry-picked commit's code)
# >>>>>>> abc1234
# 3. After resolving, stage the fixes
git add <resolved-files>
# 4. Continue the cherry-pick
git cherry-pick --continue
# OR abort entirely — your branch returns to its pre-cherry-pick state
git cherry-pick --abort
# OR skip this commit and move to the next (if cherry-picking a range)
git cherry-pick --skip
Understanding "Ours" vs. "Theirs" in Cherry-Pick
During a cherry-pick:
- Ours = the branch you're currently on (destination)
- Theirs = the commit being cherry-picked (source)
This is the same as merge (and opposite to rebase, where they swap).
# Keep your current branch's version of conflicted files
git checkout --ours path/to/file
# Keep the cherry-picked commit's version
git checkout --theirs path/to/fileWhen Cherry-Pick Produces an Empty Commit
If the changes from the cherry-picked commit already exist on your branch (perhaps someone already applied the fix), Git will tell you:
The previous cherry-pick is now empty, possibly due to conflict resolution.
Your options:
# Create the empty commit anyway (preserves history)
git cherry-pick --allow-empty
# Skip it
git cherry-pick --skip3. Common Cherry-Pick Use Cases
Hotfix Pattern
The most common real-world use case: you find a bug in production, fix it on a feature branch, and need that fix on main immediately.
Before:
E --- F --- G (feature) ← bug fix is in F
/
A --- B --- C --- D (main)
git checkout main
git cherry-pick F
After:
E --- F --- G (feature)
/
A --- B --- C --- D --- F' (main) ← hotfix applied
Backporting to Release Branches
# You're on main, the fix is commit abc1234
git checkout release/2.3
git cherry-pick -x abc1234
git checkout release/2.2
git cherry-pick -x abc1234Salvaging from an Abandoned Branch
You started an experiment, it went sideways, but one commit has a useful utility function:
# Copy the good commit to main
git checkout main
git cherry-pick abc1234
# Now safely delete the abandoned branch
git branch -D failed-experiment4. git reflog — Your Safety Net
The reflog (reference log) records every time HEAD moves — every commit, checkout, reset, rebase, merge, cherry-pick, pull, and more. It's your undo history for Git itself.
How the Reflog Works
┌─────────────────────────────────────────────────────────────────┐
│ REFLOG │
│ │
│ HEAD@{0} checkout: moving from feature to main │
│ HEAD@{1} commit: Add login validation │
│ HEAD@{2} rebase (finish): onto abc1234 │
│ HEAD@{3} rebase (pick): Update API endpoint │
│ HEAD@{4} rebase (start): checkout abc1234 │
│ HEAD@{5} commit: WIP: experimenting with auth │
│ HEAD@{6} checkout: moving from main to feature │
│ HEAD@{7} reset: moving to HEAD~3 │
│ HEAD@{8} commit: Add user dashboard │
│ ... │
└─────────────────────────────────────────────────────────────────┘
Every entry has:
- A reference —
HEAD@{n}wherenis how many moves ago - A SHA — the commit HEAD pointed to at that moment
- A description — what operation caused the move
Viewing the Reflog
# Full reflog for HEAD
git reflog
# Same thing, more explicit
git reflog show HEAD
# Reflog for a specific branch
git reflog show main
# With timestamps
git reflog --date=iso
# With relative time
git reflog --date=relative
# Limit output
git reflog -n 20Recovery Scenario 1: Undo a Bad git reset --hard
# Oops! You just did:
git reset --hard HEAD~3 # Three commits are "gone"
# They're not gone! Check the reflog:
git reflog
# abc1234 HEAD@{0} reset: moving to HEAD~3
# def5678 HEAD@{1} commit: The commit you want back
# ...
# Recover:
git reset --hard def5678
# All three commits are back!Recovery Scenario 2: Recover a Deleted Branch
# Oops! You deleted a branch with unmerged work:
git branch -D experiment
# Deleted branch experiment (was abc1234).
# Git tells you the last commit SHA! But even if you missed it:
git reflog
# Look for the last commit on that branch
# Recreate the branch at that commit:
git branch experiment abc1234
# It's back, with all its commits!Recovery Scenario 3: Undo a Bad Rebase
# You rebased and everything went wrong
git rebase main
# ... conflicts, bad resolutions, mess
# Find where your branch was before the rebase:
git reflog
# Look for "rebase (start)" — the entry BEFORE it is your old state
# Reset back:
git reset --hard HEAD@{5} # whatever the pre-rebase entry isORIG_HEAD — The Quick Undo
Git automatically saves the previous HEAD position in ORIG_HEAD after operations that move HEAD dramatically (reset, rebase, merge). It's a shortcut for the most recent reflog entry:
git reset --hard HEAD~5 # Move back 5 commits
git reset --hard ORIG_HEAD # Undo — jump back to where you wereReflog Expiration
Reflog entries don't live forever:
- Reachable entries (commits still on a branch): 90 days default
- Unreachable entries (orphaned commits): 30 days default
After expiration, git gc removes them. Configure with:
git config gc.reflogExpire 180.days
git config gc.reflogExpireUnreachable 60.daysRule of thumb: If you realize you lost something, recover it sooner rather than later.
5. git bisect — Binary Search for Bugs
git bisect performs a binary search through your commit history to find exactly which commit introduced a bug (or any behavioral change). Instead of checking commits one by one, it halves the search space with each step.
The Algorithm
Given: 1024 commits between "good" and "bad"
Step 1: Check commit 512 → "still good" (512 remain)
Step 2: Check commit 768 → "bad!" (256 remain)
Step 3: Check commit 640 → "still good" (128 remain)
Step 4: Check commit 704 → "bad!" (64 remain)
Step 5: Check commit 672 → "still good" (32 remain)
Step 6: Check commit 688 → "bad!" (16 remain)
Step 7: Check commit 680 → "still good" (8 remain)
Step 8: Check commit 684 → "bad!" (4 remain)
Step 9: Check commit 682 → "still good" (2 remain)
Step 10: Check commit 683 → "bad!" (1 remains — FOUND IT!)
10 steps to search 1024 commits! (log₂ 1024 = 10)
Manual Bisect
# 1. Start bisecting
git bisect start
# 2. Mark the current commit as bad (it has the bug)
git bisect bad
# 3. Mark a known good commit (before the bug existed)
git bisect good v1.0 # Can be a tag, SHA, or branch
# Git checks out a commit in the middle. Test it, then:
git bisect good # This commit doesn't have the bug
# OR
git bisect bad # This commit has the bug
# Repeat until Git says:
# "abc1234 is the first bad commit"
# 4. When done, return to your original branch
git bisect resetAutomated Bisect with a Script
The real power: let a script decide good/bad automatically.
# Start and define the range
git bisect start HEAD v1.0
# Run automatically — script exit code 0 = good, 1-124 = bad, 125 = skip
git bisect run ./test-for-bug.shExample test script (test-for-bug.sh):
#!/bin/bash
# Test if the bug exists
# Option 1: Run a specific test
npm test -- --grep "login validation" 2>/dev/null
# Exit code 0 = test passes = commit is "good"
# Exit code 1 = test fails = commit is "bad"
# Option 2: Check if a file exists
if [ -f "src/problematic-file.js" ]; then
exit 1 # bad — file shouldn't exist yet
else
exit 0 # good — file doesn't exist
fi
# Option 3: Check for a string in source code
if grep -q "buggy_function" src/app.js; then
exit 1 # bad
else
exit 0 # good
fiExit code 125 is special — it tells bisect to skip this commit (e.g., it doesn't compile):
#!/bin/bash
# Build the project first
make build 2>/dev/null || exit 125 # Can't build? Skip this commit.
# Run the test
make test-specific 2>/dev/nullBisect with Terms (Renaming Good/Bad)
When you're not searching for a bug, "good" and "bad" can be confusing. Git lets you rename them:
# Searching for when a feature was ADDED (not a bug)
git bisect start --term-old=before --term-new=after
git bisect after # Current commit has the feature
git bisect before v1.0 # This old commit doesn't
# Now use your custom terms
git bisect before # Feature not present here
git bisect after # Feature is present hereBisect Tips
# See the remaining suspects
git bisect visualize # Opens gitk or log
# View the bisect log (useful for reporting)
git bisect log
# Replay a bisect session
git bisect log > bisect-log.txt
git bisect replay bisect-log.txt
# Skip a commit that can't be tested
git bisect skip
# Skip a range of untestable commits
git bisect skip v2.0..v2.16. git blame — Line-by-Line Attribution
git blame shows who last modified each line of a file, when, and in which commit.
Basic Usage
# Blame an entire file
git blame src/app.js
# Blame specific lines (35 to 45)
git blame -L 35,45 src/app.js
# Blame starting from a line to end of function
git blame -L 35,+20 src/app.js
# Blame from a regex pattern
git blame -L '/function login/,/^}/' src/app.jsOutput Format
abc1234a (Alice Smith 2024-03-15 14:22:30 +0100 42) function validate(input) {
def5678b (Bob Jones 2024-01-08 09:15:00 -0500 43) if (!input) return false;
abc1234a (Alice Smith 2024-03-15 14:22:30 +0100 44) return input.length > 0;
ghi9012c (Carol Lee 2023-11-20 16:45:00 +0000 45) }
Each line shows: commit SHA, author, date, line number, code.
Useful Flags
# Ignore whitespace changes (VERY useful)
git blame -w src/app.js
# Show the original commit that introduced the line
# (follows renames and copies)
git blame -C src/app.js
# Follow across file copies (-C -C for more aggressive detection)
git blame -C -C src/app.js
# Follow lines moved within the same file (-C -C -C for all files)
git blame -C -C -C src/app.js
# Blame as of a specific commit (not just HEAD)
git blame abc1234 -- src/app.js
# Show email instead of name
git blame -e src/app.js
# Suppress boundary commits (root commit markers)
git blame -b src/app.jsGoing Deeper: Blame Recursion
Found a "blame" commit but it was just a reformatting? Look at the blame before that commit:
# Blame the file as it was at the commit BEFORE the reformat
git blame abc1234^ -- src/app.jsOr use --ignore-rev to skip known bulk-formatting commits:
# Ignore a specific formatting commit
git blame --ignore-rev abc1234 src/app.js
# Ignore commits listed in a file (one SHA per line)
git blame --ignore-revs-file .git-blame-ignore-revs
# Set this up project-wide
git config blame.ignoreRevsFile .git-blame-ignore-revsCreate a .git-blame-ignore-revs file in your repo root:
# Prettier formatting migration (2024-01-15)
abc1234567890abcdef1234567890abcdef12345
# ESLint auto-fix pass (2024-02-20)
def5678901234567890abcdef1234567890abcde
7. Pickaxe Search — git log -S and git log -G
While git blame tells you who last touched a line, pickaxe search tells you when a string was added or removed across the entire history.
git log -S — Find When a String Was Introduced or Deleted
# When was "validateEmail" added or removed?
git log -S "validateEmail"
# With patch to see the actual changes
git log -S "validateEmail" -p
# Limit to specific files
git log -S "validateEmail" -- "*.js"
# With one-line output for scanning
git log -S "validateEmail" --oneline-S counts occurrences: it shows commits where the count of "validateEmail" changed (i.e., a line containing it was added or removed).
git log -G — Regex Search in Diffs
# Find commits where any line matching a regex was changed
git log -G "validate.*Email" -p
# Find function signature changes
git log -G "function\s+login\(" --oneline
# Find TODO additions/removals
git log -G "TODO|FIXME|HACK" --onelineKey difference: -S checks if the number of occurrences changed. -G checks if any line matching the regex was part of the diff (even if the count didn't change, e.g., modifying a line that contains the string).
Practical Examples
# When was this configuration key added?
git log -S "API_TIMEOUT" --oneline --all
# Who removed the deprecated function?
git log -S "oldHelperFunction" --diff-filter=D --oneline
# Find all commits that touched error handling patterns
git log -G "catch\s*\(" --oneline -- "src/**/*.ts"
# Track a variable rename
git log -S "oldVarName" --oneline # Find when it disappeared
git log -S "newVarName" --oneline # Find when it appeared8. git shortlog — Summarizing History
git shortlog groups commits by author — perfect for release notes, contribution summaries, and team metrics.
Basic Usage
# Group commits by author
git shortlog
# Output (interactive, grouped):
# Alice Smith (15):
# Add user authentication
# Fix login redirect bug
# Update password validation
#
# Bob Jones (8):
# Implement dashboard API
# Add unit tests for dashboard
# Count only (who committed the most?)
git shortlog -s -n
# 15 Alice Smith
# 8 Bob Jones
# 3 Carol LeeUseful Flags
# Summary + numbered (sorted by count)
git shortlog -sn
# Include email
git shortlog -sne
# For a specific range (e.g., since last release)
git shortlog v1.0..HEAD
# Since a date
git shortlog --since="2024-01-01"
# Group by committer instead of author
git shortlog -snc
# Only merge commits (who is merging the most PRs?)
git shortlog -sn --mergesGenerating Release Notes
# Changes since last tag
git shortlog $(git describe --tags --abbrev=0)..HEAD
# Formatted for a changelog
git log --oneline $(git describe --tags --abbrev=0)..HEAD | sort
# Group by conventional commit type (if your team uses them)
git log --oneline v1.0..HEAD | grep "^[a-f0-9]* feat:" | sed 's/^[a-f0-9]* //'Mailmap — Consolidating Authors
Contributors sometimes commit with different names or emails. A .mailmap file normalizes them:
# .mailmap (in repo root)
Alice Smith <alice@company.com> <alice.smith@old-email.com>
Alice Smith <alice@company.com> <asmith@personal.com>
Bob Jones <bob@company.com> Robert Jones <bob@company.com>
After adding this, git shortlog (and git blame) will consolidate entries:
# Before .mailmap:
# 5 Alice Smith <alice@company.com>
# 3 alice.smith <alice.smith@old-email.com>
# 2 asmith <asmith@personal.com>
# After .mailmap:
# 10 Alice Smith <alice@company.com>Command Reference
| Command | Description |
|---|---|
git cherry-pick <commit> | Apply a specific commit to the current branch |
git cherry-pick -n <commit> | Apply changes without committing (stage only) |
git cherry-pick -x <commit> | Cherry-pick and note the source commit in the message |
git cherry-pick --continue | Continue after resolving conflicts |
git cherry-pick --abort | Cancel and return to pre-cherry-pick state |
git cherry-pick A..B | Cherry-pick a range (exclusive of A) |
git reflog | Show HEAD movement history |
git reflog show <branch> | Show reflog for a specific branch |
git reflog --date=relative | Show reflog with relative timestamps |
git bisect start | Begin a bisect session |
git bisect bad [commit] | Mark a commit as bad (has the bug) |
git bisect good [commit] | Mark a commit as good (no bug) |
git bisect run <script> | Automate bisect with a test script |
git bisect reset | End bisect and return to original branch |
git bisect skip | Skip an untestable commit |
git blame <file> | Show line-by-line attribution |
git blame -L 10,20 <file> | Blame specific line range |
git blame -w <file> | Ignore whitespace in blame |
git blame -C <file> | Detect lines moved/copied from other files |
git log -S "string" | Find commits that added/removed a string |
git log -G "regex" | Find commits with diff lines matching regex |
git shortlog -sn | Summarize commits by author, sorted by count |
Hands-On Lab
Setup
mkdir cherry-pick-lab && cd cherry-pick-lab
git init
# Create initial file with content
cat > app.js << 'EOF'
function greet(name) {
return "Hello, " + name;
}
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { greet, add, multiply };
EOF
git add app.js
git commit -m "Initial: add core utility functions"
# Build up some history on main
cat > app.js << 'EOF'
function greet(name) {
return "Hello, " + name + "!";
}
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { greet, add, multiply };
EOF
git add app.js
git commit -m "feat: add exclamation to greeting"
cat > config.js << 'EOF'
const config = {
port: 3000,
host: "localhost",
debug: false
};
module.exports = config;
EOF
git add config.js
git commit -m "feat: add configuration file"
# Tag this as v1.0
git tag -a v1.0 -m "Version 1.0 release"Part 1: Cherry-Pick Basics
# Create a feature branch with multiple commits
git checkout -b feature/math
cat > app.js << 'EOF'
function greet(name) {
return "Hello, " + name + "!";
}
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
module.exports = { greet, add, subtract, multiply, divide };
EOF
git add app.js
git commit -m "feat: add subtract and divide functions"
cat > app.js << 'EOF'
function greet(name) {
return "Hello, " + name + "!";
}
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
function power(a, b) {
return Math.pow(a, b);
}
module.exports = { greet, add, subtract, multiply, divide, power };
EOF
git add app.js
git commit -m "feat: add power function"
# Note the SHAs
echo "Feature commits:"
git log --oneline feature/math ^mainExercise: Cherry-pick only the "divide" function commit to main (the divide function has a critical zero-check you need in production).
# Get the SHA of the first feature commit
DIVIDE_SHA=$(git log --oneline feature/math ^main | tail -1 | awk '{print $1}')
echo "Cherry-picking: $DIVIDE_SHA"
git checkout main
git cherry-pick -x $DIVIDE_SHACheckpoint: Run git log --oneline -3. You should see the cherry-picked commit on main with the original message. Run git log -1 to see the (cherry picked from commit ...) note from -x.
Part 2: Cherry-Pick Conflict Resolution
# Create a conflicting situation
git checkout main
# Modify the same area on main
cat > app.js << 'EOF'
function greet(name) {
if (!name) return "Hello, stranger!";
return "Hello, " + name + "!";
}
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
module.exports = { greet, add, subtract, multiply, divide };
EOF
git add app.js
git commit -m "feat: add null check to greet function"
# Now try to cherry-pick the power function commit (will conflict on exports line)
POWER_SHA=$(git log --oneline feature/math | head -1 | awk '{print $1}')
git cherry-pick $POWER_SHA
# CONFLICT!Exercise: Resolve the conflict — you want BOTH the null check in greet AND the power function. After resolving:
git add app.js
git cherry-pick --continueCheckpoint: git log --oneline -5 should show both cherry-picked commits on main.
Part 3: Using git bisect to Find a Bug
# Create a history with a bug hidden somewhere
git checkout main
for i in $(seq 1 10); do
echo "// Version $i update" >> app.js
git add app.js
git commit -m "update: version $i changes"
done
# Introduce a "bug" at a known point (we'll pretend we don't know which commit)
# The bug is: config.js has debug: true (it should be false in production)
git log --oneline -5
# Let's edit config.js to introduce the "bug"
cat > config.js << 'EOF'
const config = {
port: 3000,
host: "localhost",
debug: true
};
module.exports = config;
EOF
git add config.js
git commit -m "update: adjust config settings"
# Add a few more commits to bury it
for i in $(seq 11 15); do
echo "// Version $i update" >> app.js
git add app.js
git commit -m "update: version $i changes"
done
echo "Total commits:"
git log --oneline | wc -lExercise: Use git bisect to find when debug: true was introduced.
# Create the test script
cat > /tmp/test-debug.sh << 'SCRIPT'
#!/bin/bash
if grep -q 'debug: true' config.js 2>/dev/null; then
exit 1 # bad — debug should not be true
else
exit 0 # good — debug is false (or file doesn't exist yet)
fi
SCRIPT
chmod +x /tmp/test-debug.sh
# Run the automated bisect
git bisect start
git bisect bad HEAD
git bisect good v1.0
git bisect run /tmp/test-debug.shCheckpoint: Bisect should identify the "adjust config settings" commit. Note how few steps it took compared to the total number of commits.
# Don't forget to exit bisect!
git bisect resetPart 4: Recovering "Lost" Work with Reflog
# Create a branch with important work
git checkout -b experiment
cat > experiment.js << 'EOF'
// This took 3 hours to write
function complexAlgorithm(data) {
return data.sort((a, b) => a.priority - b.priority)
.filter(item => item.active)
.reduce((acc, item) => ({ ...acc, [item.id]: item }), {});
}
module.exports = { complexAlgorithm };
EOF
git add experiment.js
git commit -m "feat: implement complex sorting algorithm"
echo "// Additional optimization" >> experiment.js
git add experiment.js
git commit -m "perf: optimize algorithm"
# Record the current SHA for later verification
EXPERIMENT_TIP=$(git rev-parse HEAD)
echo "Experiment branch tip: $EXPERIMENT_TIP"
# Now simulate accidentally deleting the branch
git checkout main
git branch -D experiment
echo "Branch deleted! Oh no!"Exercise: Recover the deleted branch using the reflog.
# Check the reflog
git reflog | head -10
# Find the SHA — look for "commit: perf: optimize algorithm"
# Or use the saved SHA:
git branch experiment $EXPERIMENT_TIP
# Verify recovery
git log --oneline experiment -3
git checkout experiment
cat experiment.jsCheckpoint: The experiment branch should be fully restored with both commits.
Part 5: Blame and Pickaxe Investigation
git checkout main
# Investigate the codebase
# 1. Who last modified each line of app.js?
git blame app.js
# 2. When was the divide function added?
git log -S "divide" --oneline
# 3. When was "debug: true" introduced?
git log -S "debug: true" --oneline
# 4. Find all commits that touched the greet function
git log -G "function greet" --oneline
# 5. Generate a shortlog summary
git shortlog -snCheckpoint: Each command should return meaningful results. The -S "divide" search should show the cherry-pick commit. The -S "debug: true" should match the bisect result.
Challenge
Create a situation where:
- You make 3 commits on a branch
- Rebase it (simulating a "messy" rebase that goes wrong)
- Use
git reflogto recover the pre-rebase state - Use
git cherry-pickto selectively bring just one of those commits to main - Use
git blameon the result to verify the cherry-picked change shows the correct author
Common Pitfalls
| Pitfall | What Happens | Prevention |
|---|---|---|
Cherry-picking without -x | Team doesn't know the commit was copied; may cherry-pick it again | Always use -x on shared branches |
| Cherry-picking a merge commit | Fails with "commit is a merge but no -m option given" | Use git cherry-pick -m 1 <merge-sha> to specify parent |
Trusting reflog after git gc | Old unreachable entries may have been pruned | Recover lost work within 30 days |
Forgetting git bisect reset | Stuck in detached HEAD from bisect | Always reset when done |
| Blaming after a reformat | Every line shows the reformatting commit | Use --ignore-revs-file |
Using -S when you need -G | -S counts occurrences; line modifications won't show | Use -G for regex matching within diffs |
| Cherry-picking in the wrong direction | Applying old code on top of new code | Double-check which branch you're on |
Pro Tips
-
Cherry-pick to stage, then amend: Use
git cherry-pick -nto bring changes in without committing, then combine with your current work usinggit commit --amend. -
Reflog-driven confidence: Once you understand the reflog, you'll never fear
git reset --hardorgit rebaseagain. Everything is recoverable (within 30 days). -
Bisect with test suites: If your project has good tests,
git bisect run npm testorgit bisect run pytestwill automatically find the breaking commit. This is one of the best arguments for maintaining comprehensive tests. -
Blame ignore file in CI: Add
.git-blame-ignore-revsto your repo and configure it in your team's Git config. GitHub automatically respects this file in its blame UI. -
Shortlog for PR descriptions: When writing release notes,
git shortlog v1.0..v2.0gives you a ready-made summary grouped by contributor. -
Pickaxe for archeology: When you find mysterious code,
git log -S "mysteriousFunction" -pshows you the exact commit that introduced it — including the full diff and commit message explaining why. -
Bisect skip for broken builds: If bisect lands on a commit that doesn't compile, use
exit 125in your script orgit bisect skipmanually. Bisect will work around it.
Quiz / Self-Assessment
- What is the key difference between
git cherry-pickandgit rebasein terms of what happens to the original commits?
Answer
With cherry-pick, the original commit stays exactly where it is — you're copying the changes to a new location. With rebase, the original commits become unreachable because the branch pointer moves to the new copies. Cherry-pick creates a duplicate; rebase replaces.
- You cherry-pick a commit and get a conflict. Which side is "ours" and which is "theirs"?
Answer
In a cherry-pick, ours is the branch you're currently on (the destination), and theirs is the commit being cherry-picked (the source). This is the same as merge semantics. (Note: during a rebase, they are swapped.)
- What does the
-xflag do ingit cherry-pick -x?
Answer
It appends a line to the commit message: (cherry picked from commit <original-sha>). This creates a traceable record of where the change originated, which is essential when cherry-picking between shared branches to prevent duplicate work.
- You accidentally ran
git reset --hard HEAD~5and lost five commits. How do you recover them?
Answer
Use the reflog: git reflog to find the SHA of the commit before the reset (it will be HEAD@{1}). Then git reset --hard HEAD@{1} or git reset --hard <sha> to restore your branch. Alternatively, git reset --hard ORIG_HEAD works immediately after the reset since Git saves the previous position.
- What's the difference between
git bisect good/badandgit bisect run <script>?
Answer
git bisect good/bad is manual — you test each commit yourself and tell Git the result. git bisect run <script> automates the process: Git runs your script at each step, uses the exit code (0 = good, 1-124 = bad, 125 = skip) to decide, and finds the culprit without human intervention. Automated bisect is faster and more reliable for large histories.
- How long do reflog entries survive by default? What can you do to extend that time?
Answer
Reachable entries (commits still on a branch) survive 90 days. Unreachable entries (orphaned by reset, rebase, etc.) survive 30 days. After that, git gc removes them. Extend with:
git config gc.reflogExpire 180.days
git config gc.reflogExpireUnreachable 60.days
- What's the difference between
git log -S "text"andgit log -G "text"?
Answer
-S (pickaxe) looks for commits where the number of occurrences of the string changed — meaning a line containing it was added or removed. -G uses regex to find commits where any diff line matches the pattern, even if the total count didn't change (e.g., modifying a line that contains the string). Use -S for "when was this introduced/removed?" and -G for "when was code matching this pattern touched?"
- You ran
git blameon a file and every line shows a commit from last week titled "Apply prettier formatting." How do you see the real authors?
Answer
Create a .git-blame-ignore-revs file containing the SHA of the formatting commit, then configure Git to use it:
echo "abc1234..." > .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revsNow git blame will skip that commit and show the previous meaningful change for each line. GitHub also respects this file automatically.
- During
git bisect, you land on a commit that doesn't compile. What should you do?
Answer
Run git bisect skip. This tells Git that you can't test this commit, and it will select a nearby commit instead. In an automated bisect script, return exit code 125 to signal "skip." Bisect handles skipped commits by adjusting its binary search to work around the untestable range.
- You want to generate a summary of all changes between v1.0 and v2.0, grouped by contributor. What command do you use?
Answer
git shortlog v1.0..v2.0Add -sn for just counts sorted by contribution:
git shortlog -sn v1.0..v2.0Add -e to include email addresses:
git shortlog -sne v1.0..v2.0If contributors used multiple emails, add a .mailmap file to consolidate entries.