Learning Objectives

By the end of this module, you will be able to:

  1. Use git cherry-pick to apply specific commits from one branch to another
  2. Resolve cherry-pick conflicts and understand how cherry-pick relates to rebase
  3. Navigate the reflog to recover "lost" commits, deleted branches, and undone work
  4. Run git bisect to perform a binary search through history and find the commit that introduced a bug
  5. Investigate code history with git blame, git log -S (pickaxe), and git log -G
  6. 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^..ghi9012

The -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 abc1234

This 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

AspectCherry-PickRebase
DirectionYou're on the destination branch and pull commits in (like merge)You're on the source branch and replay onto a target (reverse)
ScopeOne commit at a time (or a few)Entire branch from fork point
Original commitsStay where they areBecome 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/file

When 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 --skip

3. 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 abc1234

Salvaging 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-experiment

4. 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 referenceHEAD@{n} where n is 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 20

Recovery 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 is

ORIG_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 were

Reflog 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.days

Rule 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 reset

Automated 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.sh

Example 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
fi

Exit 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/null

Bisect 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 here

Bisect 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.1

6. 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.js

Output 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.js

Going 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.js

Or 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-revs

Create 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" --oneline

Key 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 appeared

8. 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 Lee

Useful 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 --merges

Generating 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

CommandDescription
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 --continueContinue after resolving conflicts
git cherry-pick --abortCancel and return to pre-cherry-pick state
git cherry-pick A..BCherry-pick a range (exclusive of A)
git reflogShow HEAD movement history
git reflog show <branch>Show reflog for a specific branch
git reflog --date=relativeShow reflog with relative timestamps
git bisect startBegin 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 resetEnd bisect and return to original branch
git bisect skipSkip 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 -snSummarize 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 ^main

Exercise: 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_SHA

Checkpoint: 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 --continue

Checkpoint: 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 -l

Exercise: 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.sh

Checkpoint: 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 reset

Part 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.js

Checkpoint: 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 -sn

Checkpoint: 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:

  1. You make 3 commits on a branch
  2. Rebase it (simulating a "messy" rebase that goes wrong)
  3. Use git reflog to recover the pre-rebase state
  4. Use git cherry-pick to selectively bring just one of those commits to main
  5. Use git blame on the result to verify the cherry-picked change shows the correct author

Common Pitfalls

PitfallWhat HappensPrevention
Cherry-picking without -xTeam doesn't know the commit was copied; may cherry-pick it againAlways use -x on shared branches
Cherry-picking a merge commitFails 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 gcOld unreachable entries may have been prunedRecover lost work within 30 days
Forgetting git bisect resetStuck in detached HEAD from bisectAlways reset when done
Blaming after a reformatEvery line shows the reformatting commitUse --ignore-revs-file
Using -S when you need -G-S counts occurrences; line modifications won't showUse -G for regex matching within diffs
Cherry-picking in the wrong directionApplying old code on top of new codeDouble-check which branch you're on

Pro Tips

  1. Cherry-pick to stage, then amend: Use git cherry-pick -n to bring changes in without committing, then combine with your current work using git commit --amend.

  2. Reflog-driven confidence: Once you understand the reflog, you'll never fear git reset --hard or git rebase again. Everything is recoverable (within 30 days).

  3. Bisect with test suites: If your project has good tests, git bisect run npm test or git bisect run pytest will automatically find the breaking commit. This is one of the best arguments for maintaining comprehensive tests.

  4. Blame ignore file in CI: Add .git-blame-ignore-revs to your repo and configure it in your team's Git config. GitHub automatically respects this file in its blame UI.

  5. Shortlog for PR descriptions: When writing release notes, git shortlog v1.0..v2.0 gives you a ready-made summary grouped by contributor.

  6. Pickaxe for archeology: When you find mysterious code, git log -S "mysteriousFunction" -p shows you the exact commit that introduced it — including the full diff and commit message explaining why.

  7. Bisect skip for broken builds: If bisect lands on a commit that doesn't compile, use exit 125 in your script or git bisect skip manually. Bisect will work around it.


Quiz / Self-Assessment

  1. What is the key difference between git cherry-pick and git rebase in 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.

  1. 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.)

  1. What does the -x flag do in git 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.

  1. You accidentally ran git reset --hard HEAD~5 and 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.

  1. What's the difference between git bisect good/bad and git 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.

  1. 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
  1. What's the difference between git log -S "text" and git 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?"

  1. You ran git blame on 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-revs

Now git blame will skip that commit and show the previous meaningful change for each line. GitHub also respects this file automatically.

  1. 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.

  1. 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.0

Add -sn for just counts sorted by contribution:

git shortlog -sn v1.0..v2.0

Add -e to include email addresses:

git shortlog -sne v1.0..v2.0

If contributors used multiple emails, add a .mailmap file to consolidate entries.