You are viewing a preview of this course. Sign in to start learning

Lesson 8: Advanced Patterns — Stash, Cherry-pick, Bisect

Master advanced Git techniques for context switching, selective commits, bug hunting, and recovery. Learn stash, cherry-pick, bisect, and reflog.

Lesson 8: Advanced Patterns — Stash, Cherry-pick, Bisect 🧰

Introduction

You're deep in a feature branch when suddenly: "Emergency! Production bug needs fixing NOW!" But your working directory is a mess of half-finished changes. Or maybe you need just one commit from another branch, not the entire merge. Perhaps a bug appeared somewhere in the last 50 commits, and you need to find it fast.

Welcome to Git's power tools. These aren't commands you'll use daily, but when you need them, they're lifesavers. Git stash lets you temporarily shelve changes. Git cherry-pick plucks individual commits between branches. Git bisect performs binary search through history to find bugs. And git reflog acts as your ultimate safety net, tracking everything you've done.

These patterns separate Git beginners from Git masters. Let's build that mastery. 🎯


Core Concept 1: Git Stash — The Shelf for Unfinished Work 📦

What Problem Does Stash Solve?

You're working on a feature, but your changes aren't ready to commit. You need to:

  • Switch branches to fix a bug
  • Pull latest changes from remote
  • Try something experimental without losing current work

You could make a messy "WIP" commit and undo it later, but that pollutes history. Git stash is better: it temporarily saves your uncommitted changes and gives you a clean working directory.

The Mental Model

Think of stash as a clipboard or shelf where you temporarily store work:

Your Working Directory:
[Modified files, staged changes] ──stash──> [Saved to stash stack]
                                           (Working directory now clean)

Later:
[Clean working directory] <──stash pop── [Retrieve saved changes]

Stash creates a special commit-like object that's not part of any branch. It's stored in a stack (last-in, first-out), so you can stash multiple times.

Essential Stash Commands

Basic stashing:

git stash              # Save working directory + staged changes
git stash save "message"  # Save with descriptive message
git stash -u           # Include untracked files too

Retrieving stashed work:

git stash pop          # Apply most recent stash and delete it
git stash apply        # Apply stash but keep it in stack
git stash apply stash@{2}  # Apply specific stash

Managing the stash stack:

git stash list         # View all stashes
git stash show         # See what's in latest stash
git stash show -p      # See full diff of stash
git stash drop         # Delete most recent stash
git stash clear        # Delete ALL stashes

💡 Pro tip: Use git stash save "message" to describe what you're stashing. When you return to git stash list, you'll thank yourself:

stash@{0}: On main: fixing navbar bug
stash@{1}: WIP on feature: trying new approach
stash@{2}: On main: experiment with caching

Stash Under the Hood

When you stash, Git creates a special commit with your changes and stores the reference in .git/refs/stash. The stash "stack" is actually a reflog of this special ref. Understanding this helps you realize: stashed changes are commits, so they're safe and recoverable (until you explicitly delete them).


Core Concept 2: Git Cherry-pick — Selective Commit Importing 🍒

What Problem Does Cherry-pick Solve?

Sometimes you need one specific commit from another branch without merging everything:

  • A bug fix made on develop that production needs NOW
  • An experimental feature you want to try on your branch
  • A commit accidentally made on the wrong branch

Merging would bring all commits from that branch. Cherry-pick lets you be selective.

The Mental Model

Cherry-pick duplicates a commit from one branch to another:

Branch A:  A---B---C---D---E
                       ^
                       |
                   (want this)
                       |
Branch B:  X---Y---Z---D'
                       ^
                       |
                  (cherry-picked copy)

Important: Cherry-pick creates a new commit with a new SHA. It copies the changes (the diff), not the commit itself. The original commit stays on its branch unchanged.

Cherry-pick Commands

Basic cherry-picking:

git cherry-pick <commit-sha>     # Apply commit to current branch
git cherry-pick <branch-name>    # Cherry-pick latest commit from branch
git cherry-pick abc123 def456    # Multiple commits in order

Advanced options:

git cherry-pick -n <sha>         # Apply changes but don't commit
git cherry-pick -x <sha>         # Add "cherry picked from" note
git cherry-pick --continue       # After resolving conflicts
git cherry-pick --abort          # Cancel cherry-pick in progress

⚠️ Important: Cherry-picking can create duplicate commits — same changes, different SHAs. If you later merge the branches, you might get confusing history. Use cherry-pick for:

  • Hotfixes going to production (you'll merge the fix back later)
  • Mistakes (commit on wrong branch)
  • Backporting (applying fixes to older release branches)

Cherry-pick with Conflicts

Just like merging, cherry-picking can have conflicts. The process is identical:

git cherry-pick abc123
# CONFLICT in file.js
# Fix conflicts manually
git add file.js
git cherry-pick --continue

💡 Use case example: You're on main (v1.0) and develop has your new v2.0 features. A critical security fix was committed to develop, but production needs it now without the v2.0 changes:

git checkout main
git cherry-pick <security-fix-sha>
git push origin main  # Deploys fix to production
# Later, when you merge develop→main, Git is smart enough
# to recognize the duplicate and handle it gracefully

Core Concept 3: Git Bisect — Binary Search for Bugs 🔍

What Problem Does Bisect Solve?

You know your code worked 2 weeks ago. Now there's a bug. Somewhere in the last 50 commits, someone broke it. How do you find the culprit?

You could check commits one by one (git log, git checkout, test, repeat). That's 50 tests in the worst case. Git bisect uses binary search, finding the bad commit in ~log₂(50) = 6 tests!

The Mental Model

Bisect divides history in half repeatedly:

Commits:  [Good] ─── 25 commits ─── ??? ─── 25 commits ─── [Bad]
                                    ^
                           Test this middle commit

If middle is Good:  [Good] ─── [Good] ─── ??? ─── 12 commits ─── [Bad]
If middle is Bad:   [Good] ─── 12 commits ─── ??? ─── [Bad] ─── [Bad]

... continue halving until you find the exact commit where Good → Bad

This is a binary search algorithm. If you have N commits, you'll find the bug in approximately log₂(N) tests.

Bisect Commands

Manual bisect process:

git bisect start              # Begin bisect session
git bisect bad                # Current commit is bad (has bug)
git bisect good <old-sha>     # This old commit was good

# Git checks out a middle commit
# You test: run your app/tests, check if bug exists

git bisect good   # If this commit is good
# OR
git bisect bad    # If this commit has the bug

# Git jumps to next middle commit
# Repeat until Git identifies the first bad commit

git bisect reset  # Exit bisect mode, return to original HEAD

Automated bisect (if you have tests):

git bisect start HEAD <good-commit>
git bisect run npm test  # Or any command that returns 0 (pass) or 1 (fail)

# Git automatically tests each commit, finds the bad one!

Bisect Example Walkthrough

Let's say a login feature broke. It worked in commit a1b2c3d but is broken now:

$ git bisect start
$ git bisect bad                    # Current HEAD is broken
$ git bisect good a1b2c3d           # This old commit worked

Bisecting: 25 revisions left to test after this
[checking out middle commit abc123]

# Test the login feature
$ npm run dev   # Manually test
# Bug is present, so:

$ git bisect bad
Bisecting: 12 revisions left to test after this
[checking out another commit]

# Test again
$ npm run dev
# Login works! So:

$ git bisect good
Bisecting: 6 revisions left to test after this

# ... continue until ...

commit def4567 is the first bad commit
Author: Jane <jane@example.com>
Date:   Mon Dec 2
    Refactored authentication logic

$ git bisect reset  # Return to normal
$ git show def4567  # Examine the bad commit

🧠 Mnemonic: Bisect = Binary section. You're sectioning history in two repeatedly.

💡 Pro tip: Bisect works best when:

  • Each commit builds successfully
  • The bug is reproducible (consistent pass/fail)
  • You can test relatively quickly

If commits don't build, use git bisect skip to skip that commit.


Core Concept 4: Git Reflog — Your Safety Net 🛟

What Problem Does Reflog Solve?

You just ran git reset --hard and lost 3 hours of work. Or accidentally deleted a branch. Or rebased away important commits. Is the work gone forever?

No! Git's reflog (reference log) tracks every movement of HEAD and branch pointers. It's your "undo for Git commands" — a complete history of where you've been.

The Mental Model

While git log shows commit history (parent→child relationships), reflog shows your history — every checkout, commit, reset, rebase, merge you've performed:

Git Log (commit history):     Reflog (your actions):
    E                          HEAD@{0}: commit: Add feature
    |                          HEAD@{1}: checkout: moving to main  
    D                          HEAD@{2}: commit: Fix bug
    |                          HEAD@{3}: reset: moving to HEAD~1
    C                          HEAD@{4}: commit (amend): Update docs
   / \                         HEAD@{5}: commit: Update docs
  B   F                        ...
  |                      
  A                      

Reflog is local (not pushed to remotes) and entries expire after 90 days by default. It's a safety net, not permanent storage.

Reflog Commands

Viewing reflog:

git reflog                    # Show HEAD movements
git reflog show main          # Show reflog for main branch
git reflog --all              # Show all refs

Using reflog to recover:

# You accidentally reset and lost commits
git reflog
# Find the SHA before the mistake (e.g., HEAD@{3})

git reset --hard HEAD@{3}     # Go back to that state
# OR
git cherry-pick abc123        # Cherry-pick lost commits
# OR
git branch recovery-branch abc123  # Create branch at old commit

Recovery Example

Disaster scenario:

$ git log --oneline
abc123 Add payment feature     ← You're here
def456 Update user model
789abc Initial commit

$ git reset --hard 789abc      # Oops! Meant to soft reset
$ git log --oneline
789abc Initial commit           ← Lost 2 commits!

# Don't panic. Check reflog:
$ git reflog
789abc HEAD@{0}: reset: moving to 789abc
abc123 HEAD@{1}: commit: Add payment feature
def456 HEAD@{2}: commit: Update user model
...

# Recovery:
$ git reset --hard HEAD@{1}     # Back to abc123
$ git log --oneline
abc123 Add payment feature      ← Recovered!
def456 Update user model
789abc Initial commit

🤔 Did you know? Even if you delete a branch, as long as you committed your work, it's in the reflog. You can always recover it (within ~90 days).

⚠️ Limitation: Reflog can't save uncommitted changes. If you modified files but never committed or stashed, then ran git reset --hard, those changes are truly gone. Always commit or stash before risky operations!


Example 1: Context Switching with Stash 🔄

Scenario: You're implementing a new dashboard feature on feature/dashboard when your manager asks you to fix a critical bug on main.

# On feature/dashboard, working directory has uncommitted changes
$ git status
Modified: dashboard.js
Modified: dashboard.css
Untracked: dashboard-test.js

# Can't switch branches with uncommitted changes
$ git checkout main
error: Your local changes would be overwritten by checkout

# Solution: Stash your work
$ git stash save "Dashboard WIP - adding charts"
Saved working directory and index state

$ git status
nothing to commit, working tree clean

# Now switch branches
$ git checkout main
$ git pull origin main

# Fix the bug
$ vim bug-fix.js
$ git add bug-fix.js
$ git commit -m "Fix critical auth bug"
$ git push origin main

# Return to feature work
$ git checkout feature/dashboard
$ git stash list
stash@{0}: On feature/dashboard: Dashboard WIP - adding charts

$ git stash pop
On branch feature/dashboard
Changes not staged for commit:
  modified:   dashboard.js
  modified:   dashboard.css
Untracked files:
  dashboard-test.js
Dropped refs/stash@{0}

# Continue working where you left off!

💡 Why pop instead of apply?

  • Pop = apply + delete stash (you're done with it)
  • Apply = apply but keep stash (you want to try it on multiple branches)

Example 2: Cherry-picking a Hotfix 🚑

Scenario: Your team works on develop for v2.0 features. Production runs main (v1.0). A security fix was committed to develop, but you need it in production NOW without the v2.0 changes.

# On develop, find the security fix commit
$ git log --oneline develop
abc123 Add new dashboard (v2.0)
def456 Security: Fix XSS vulnerability  ← Need this!
789abc Refactor auth (v2.0)
...

# Switch to production branch
$ git checkout main
$ git log --oneline
old123 Last production release (v1.0)
...

# Cherry-pick just the security fix
$ git cherry-pick def456
[main xyz789] Security: Fix XSS vulnerability
 Date: ...
 1 file changed, 5 insertions(+), 2 deletions(-)

$ git log --oneline
xyz789 Security: Fix XSS vulnerability  ← New commit (different SHA!)
old123 Last production release (v1.0)

# Deploy to production
$ git push origin main

What happened?

  • The changes from def456 were copied to a new commit xyz789 on main
  • develop still has the original def456 commit unchanged
  • When you eventually merge developmain, Git recognizes the duplicate content and handles it gracefully

⚠️ Common mistake: Cherry-picking lots of commits. If you're cherry-picking more than 2-3 commits, you probably should merge instead. Cherry-pick is for exceptional cases.


Example 3: Finding a Bug with Bisect 🐛

Scenario: Your test suite passed last week. Today, 40 commits later, a test fails. You need to find which commit introduced the failure.

# Start bisect
$ git bisect start
$ git bisect bad                     # Current HEAD fails test
$ git bisect good HEAD~40            # 40 commits ago, tests passed

Bisecting: 20 revisions left to test after this
[abc123def...] Refactored payment module

# Test this commit
$ npm test
# Test fails, so:
$ git bisect bad

Bisecting: 10 revisions left to test after this
[def456abc...] Updated dependencies

$ npm test
# Test passes!
$ git bisect good

Bisecting: 5 revisions left to test after this
[789abcdef...] Added caching layer

$ npm test
# Fails
$ git bisect bad

Bisecting: 2 revisions left to test after this
# ... continue ...

commit 456def789abc
Author: Bob <bob@example.com>
Date:   Tue Dec 3
    Optimized database queries

# This is the first bad commit!

$ git bisect reset  # Return to HEAD
$ git show 456def789abc  # Examine the culprit

Automated version (if your test command is consistent):

$ git bisect start HEAD HEAD~40
$ git bisect run npm test

# Git automatically tests each commit
# Finds the bad commit in ~6 tests instead of 40!

running npm test
✓ All tests passed
Bisecting: ...
✗ Test failed
...
456def789abc is the first bad commit

🔧 Try this: Create a test script that returns exit code 0 (success) or 1 (failure), then use git bisect run ./test.sh to fully automate bug hunting!


Example 4: Recovering from Reflog 🛟

Scenario: You rebased your feature branch, but realized you lost important commits. Panic!

# Before rebase
$ git log --oneline
a1b2c3 Add feature X
d4e5f6 Add feature Y
g7h8i9 Base commit

# Rebase gone wrong
$ git rebase -i HEAD~3
# You accidentally dropped commits during interactive rebase

$ git log --oneline
g7h8i9 Base commit  ← Features X and Y are gone!

# Don't panic! Check reflog
$ git reflog
g7h8i9 HEAD@{0}: rebase finished
a1b2c3 HEAD@{1}: rebase: Add feature X
d4e5f6 HEAD@{2}: rebase: Add feature Y
a1b2c3 HEAD@{3}: commit: Add feature X  ← This is before rebase!
...

# Recovery option 1: Reset to before rebase
$ git reset --hard HEAD@{3}
$ git log --oneline
a1b2c3 Add feature X   ← Recovered!
d4e5f6 Add feature Y
g7h8i9 Base commit

# Recovery option 2: Create a backup branch
$ git branch backup-before-disaster HEAD@{3}
$ git checkout backup-before-disaster
# Now you can cherry-pick commits you need

💡 Best practice: Before risky operations (rebase, reset --hard, filter-branch), create a backup branch:

git branch backup-$(date +%Y%m%d)

Even with reflog, having an explicit branch makes recovery easier!


Common Mistakes ⚠️

1. Stashing untracked files and losing them

# Wrong: default stash doesn't save untracked files
$ git stash
$ git stash pop
# Your new-file.js is gone!

# Right: include untracked files
$ git stash -u  # or --include-untracked

2. Cherry-picking merge commits

# Cherry-picking a merge commit is ambiguous (which parent?)
$ git cherry-pick <merge-commit-sha>
error: commit is a merge but no -m option was given

# You need to specify which parent to use:
$ git cherry-pick -m 1 <merge-commit-sha>  # Use first parent

Better: don't cherry-pick merges. Cherry-pick the individual commits instead.

3. Forgetting to reset after bisect

$ git bisect start
# ... testing ...
# Find bad commit, but forget:
$ git bisect reset

# Now you're stuck on a detached HEAD!
# Always end with:
$ git bisect reset

4. Thinking reflog is pushed to remote

# Reflog is LOCAL only
# If you clone a repo, you don't get the original reflog
# If a coworker deletes a branch, you can't use YOUR reflog to recover THEIR work

5. Stash pop with conflicts and panicking

$ git stash pop
Auto-merging file.js
CONFLICT (content): Merge conflict in file.js

# Don't panic! The stash is still there
# Fix conflicts, then:
$ git add file.js
$ git stash drop  # Manually drop stash after resolving

6. Using bisect with broken builds

If many commits in your history don't build/run, bisect becomes frustrating. Use:

git bisect skip  # Skip commits that won't build

Or maintain better commit hygiene: every commit should build and pass tests!


Real-World Workflow: Combining These Tools 🎯

Scenario: Monday morning chaos:

  1. Context switch:
git stash save "Feature: half-done API integration"
git checkout main
git pull origin main
  1. Cherry-pick a fix:
# Someone fixed a bug on develop, you need it on main
git cherry-pick abc123
git push origin main
  1. Hunt a bug:
# Tests failing since last week
git bisect start HEAD HEAD~30
git bisect run npm test
# Found: commit def456
git bisect reset
git revert def456  # Undo the bad commit
  1. Recover from mistake:
# Oops, that revert was wrong
git reflog
git reset --hard HEAD@{1}  # Before revert
  1. Return to original work:
git checkout feature/api-integration
git stash pop
# Continue where you left off!

This is real-world Git mastery! 🏆


Key Takeaways 🎓

Git stash temporarily saves uncommitted work, giving you a clean slate for context switching

Stash stack can hold multiple stashed states; use descriptive messages with git stash save "message"

Git cherry-pick copies individual commits between branches (creates new commits with new SHAs)

Cherry-pick is for exceptions: hotfixes, backports, or mistakes. Don't cherry-pick dozens of commits—merge instead

Git bisect uses binary search to find bug-introducing commits in O(log n) time

Bisect can be automated with git bisect run <test-command> if you have consistent pass/fail tests

Git reflog is your safety net, tracking every HEAD movement for ~90 days

Reflog is local only and can't recover uncommitted changes—always commit or stash before risky operations

Combine these tools: stash to switch context, cherry-pick urgent fixes, bisect to find bugs, reflog to recover from mistakes

Create backup branches before risky operations: git branch backup-today


📚 Further Study

  1. Git Stash Documentation: https://git-scm.com/docs/git-stash
  2. Git Bisect Guide: https://git-scm.com/docs/git-bisect
  3. Understanding Reflog: https://www.atlassian.com/git/tutorials/rewriting-history/git-reflog

📋 Quick Reference Card

╔══════════════════════════════════════════════════════════════╗
║                  ADVANCED GIT PATTERNS                       ║
╠══════════════════════════════════════════════════════════════╣
║ STASH (temporary storage)                                    ║
║  git stash save "msg"    Save with description               ║
║  git stash -u            Include untracked files             ║
║  git stash list          View all stashes                    ║
║  git stash pop           Apply + delete latest stash         ║
║  git stash apply         Apply but keep stash                ║
║  git stash drop          Delete latest stash                 ║
║  git stash clear         Delete all stashes                  ║
╠══════════════════════════════════════════════════════════════╣
║ CHERRY-PICK (copy commits)                                   ║
║  git cherry-pick <sha>   Copy commit to current branch       ║
║  git cherry-pick -x <sha> Add "cherry picked from" note      ║
║  git cherry-pick -n <sha> Apply without committing           ║
║  git cherry-pick --continue  After resolving conflicts       ║
║  git cherry-pick --abort     Cancel operation                ║
╠══════════════════════════════════════════════════════════════╣
║ BISECT (find bugs)                                           ║
║  git bisect start        Begin binary search                 ║
║  git bisect bad          Mark current commit as bad          ║
║  git bisect good <sha>   Mark commit as good                 ║
║  git bisect skip         Skip broken commit                  ║
║  git bisect reset        Exit bisect mode                    ║
║  git bisect run <cmd>    Automate with test command          ║
╠══════════════════════════════════════════════════════════════╣
║ REFLOG (safety net)                                          ║
║  git reflog              View HEAD history                   ║
║  git reflog show <branch> View branch reflog                 ║
║  git reset --hard HEAD@{n} Return to old state              ║
║  git branch recover <sha>  Create branch at old commit      ║
╠══════════════════════════════════════════════════════════════╣
║ WORKFLOW TIPS                                                ║
║  • Always use stash -u to include untracked files            ║
║  • Cherry-pick sparingly (use merge for multiple commits)    ║
║  • Create backup branches before risky operations            ║
║  • Reflog is local and expires after ~90 days                ║
║  • Bisect works best with consistent builds/tests            ║
╚══════════════════════════════════════════════════════════════╝

🧠 Remember: These are power tools. You won't use them daily, but when you need them, they're indispensable. Practice in a test repo to build confidence! 🚀