You’re halfway through refactoring a Terraform module — files changed everywhere, nothing in a committable state — when Slack lights up. Production is broken. A bad variable got merged into main twenty minutes ago and the deployment pipeline is failing. You need to drop everything, switch to main, and push a fix. Right now.
If you’ve been in DevOps long enough, you’ve lived this exact moment. The question is: what happens to the work on your screen? Do you frantically copy files to your desktop? Make a half-baked commit with the message “WIP pls ignore”? Or do you reach for the right tool?
Today we’re covering three Git commands that handle situations exactly like this: stash, cherry-pick, and bisect. These aren’t commands you use every day, but when you need them, nothing else will do.
Git Stash: Your Work-in-Progress Safety Net
git stash takes your uncommitted changes — both staged and unstaged — and saves them on a stack, leaving you with a clean working directory. Think of it as a clipboard for your repo state.
The Basics
# Stash your current changes
git stash
# Stash with a descriptive message (highly recommended)
git stash push -m "refactor: halfway through network module rewrite"
# List all stashes
git stash list
# stash@{0}: On feature/network-rewrite: refactor: halfway through network module rewrite
# stash@{1}: WIP on main: 4a2c9f1 fix: correct subnet CIDR
# Bring back the most recent stash and remove it from the stack
git stash pop
# Bring back a stash but keep it on the stack
git stash apply stash@{1}
# Delete a specific stash
git stash drop stash@{0}
# Nuclear option: clear all stashes
git stash clear
Always use a message. When you have five stashes and they’re all called “WIP on feature/xyz,” you’ll be guessing which one has the work you need. The -m flag takes two seconds and saves you real headaches.
Stashing Specific Files
You don’t always want to stash everything. Maybe you’ve changed six files but only need to set aside two while you test something.
# Stash only specific files
git stash push -m "parking the provider config changes" providers.tf variables.tf
# Stash interactively — choose individual hunks within files
git stash push -p
The -p (patch) flag walks you through each change and lets you pick which ones to stash. It’s the surgical option.
Including Untracked and Ignored Files
By default, git stash only touches tracked files. New files you haven’t git add-ed yet? They stay right where they are.
# Include untracked files (new files not yet added)
git stash push -u -m "include the new monitoring config"
# Include everything — untracked AND ignored files
git stash push -a -m "full workspace snapshot"
The -u flag is the one I use most. When I’m mid-feature and I’ve created new files that aren’t staged yet, a plain git stash leaves those orphaned files sitting in my working directory. That’s bitten me more than once when switching branches.
Creating a Branch from a Stash
Sometimes you stash work, come back to it a week later, and realize it deserves its own branch rather than being applied back where it started.
# Create a new branch from a stash and apply it
git stash branch feature/network-monitoring stash@{0}
This creates the branch, checks it out, applies the stash, and drops it from the stack — all in one command. Clean.
Real Scenario: The Urgent Hotfix
Here’s the full workflow from the scenario I described at the top:
# You're on feature/refactor-modules with uncommitted changes
git stash push -u -m "refactor: network module WIP - do not lose"
# Switch to main, create hotfix branch
git checkout main
git pull origin main
git checkout -b hotfix/fix-variable-typo
# Make the fix, commit, push
vim modules/networking/variables.tf
git add modules/networking/variables.tf
git commit -m "fix(networking): correct subnet_prefix variable name"
git push origin hotfix/fix-variable-typo
# After the PR is merged, go back to your feature work
git checkout feature/refactor-modules
git stash pop
# Your changes are back exactly where you left them
Git Cherry-Pick: Surgical Commit Transfers
Where git merge brings entire branch histories together, git cherry-pick lets you grab individual commits and replay them onto your current branch. It creates a new commit with the same changes but a different hash.
Basic Syntax
# Cherry-pick a single commit
git cherry-pick abc1234
# Cherry-pick without committing (stage the changes only)
git cherry-pick --no-commit abc1234
# Cherry-pick a range of commits (inclusive)
git cherry-pick abc1234^..def5678
That ^ in the range syntax is important — abc1234^..def5678 means “from the parent of abc1234 through def5678,” which includes abc1234 itself. Without the caret, you’d skip the first commit in the range.
The Hotfix Workflow
This is the bread-and-butter use case. You fixed a bug on develop, but production is running off release/v2.1 and needs that fix now — without getting every other commit from develop.
# Find the commit hash of the fix on develop
git log --oneline develop
# e4f5a6b fix(auth): handle expired token refresh
# c3d4e5f feat(dashboard): add latency chart
# a1b2c3d chore: update dependencies
# Switch to the release branch
git checkout release/v2.1
# Cherry-pick just the fix
git cherry-pick e4f5a6b
# [release/v2.1 f7g8h9i] fix(auth): handle expired token refresh
# 1 file changed, 3 insertions(+), 1 deletion(-)
One commit, surgically applied. The release branch gets the fix without the new dashboard feature or the dependency update.
Handling Cherry-Pick Conflicts
Conflicts happen when the code around your cherry-picked changes has diverged between the source and target branches. Git will pause and let you resolve them.
git cherry-pick e4f5a6b
# CONFLICT (content): Merge conflict in src/auth/token.py
# error: could not apply e4f5a6b...
# Resolve the conflicts in your editor, then:
git add src/auth/token.py
git cherry-pick --continue
# Or if you decide it's not worth it:
git cherry-pick --abort
When NOT to Cherry-Pick
Cherry-pick is powerful, but it’s not always the right call.
| Situation | Better Alternative | Why |
|---|---|---|
| Bringing a full feature branch up to date | git merge or git rebase | Cherry-picking every commit duplicates history |
| Keeping two long-lived branches in sync | Regular merges | Duplicate commits cause confusing diffs in PRs |
| Replaying a sequence of 20+ commits | git rebase --onto | Rebase handles sequences and dependencies better |
| Collaborating on a shared branch | git merge | Cherry-pick creates new hashes; teammates won’t see them as the “same” commits |
The core issue: cherry-pick duplicates commits. The original and the cherry-picked copy have different SHAs. If you later merge the branches, Git may see both and create conflicts or confusing history. For one-off hotfixes, that’s fine. For regular workflow, use merge or rebase.
Git Bisect: Binary Search for Bugs
git bisect is the command I wish I’d learned five years earlier than I did. It uses a binary search across your commit history to find exactly which commit introduced a bug. Instead of checking commits one by one — which could mean testing dozens — bisect cuts the search space in half with every step.
For 100 commits, that’s roughly 7 tests instead of 100. For 1,000 commits, about 10.
Manual Bisect
# Start bisecting
git bisect start
# Tell Git the current commit is broken
git bisect bad
# Tell Git a known-good commit (e.g., last week's release tag)
git bisect good v2.0.0
# Git checks out a commit halfway between good and bad
# Bisecting: 47 revisions left to test after this (roughly 6 steps)
# [a1b2c3d4...] chore: update provider versions
Now you test. Does the bug exist at this commit? Tell Git:
# If the bug is present at this commit
git bisect bad
# If the bug is NOT present at this commit
git bisect good
# Git narrows the range and checks out the next candidate
# Bisecting: 23 revisions left to test after this (roughly 5 steps)
Repeat until Git identifies the exact commit:
# After several rounds:
# e4f5a6b7 is the first bad commit
# commit e4f5a6b7
# Author: [email protected]
# Date: Thu Mar 19 14:22:11 2026 -0600
#
# refactor(networking): simplify route table logic
# Clean up when you're done
git bisect reset
Now you know exactly which commit broke things and who wrote it. More importantly, you can look at the diff and understand what changed rather than staring at symptoms.
Automated Bisect with a Script
Manual bisecting works, but the real power is automation. Write a script that returns exit code 0 for “good” and non-zero for “bad,” and Git will drive the entire search unattended.
Example: Finding which commit broke terraform plan
#!/usr/bin/env bash
# test-terraform.sh — exit 0 if plan succeeds, exit 1 if it fails
set -euo pipefail
terraform init -backend=false -input=false > /dev/null 2>&1
terraform plan -input=false -detailed-exitcode > /dev/null 2>&1
# -detailed-exitcode: 0 = success (no changes), 2 = success (changes present)
exit_code=$?
if [ "$exit_code" -eq 0 ] || [ "$exit_code" -eq 2 ]; then
exit 0 # good commit
else
exit 1 # bad commit
fi
chmod +x test-terraform.sh
git bisect start
git bisect bad HEAD
git bisect good v2.0.0
# Let Git run the script at each step automatically
git bisect run ./test-terraform.sh
# Git does all the work and reports:
# e4f5a6b7 is the first bad commit
# bisect run success
git bisect reset
You just walked away while Git tested your entire commit range. For infrastructure code, where a terraform plan might take 30 seconds, this still beats manual testing by a mile.
Pro tip: If a commit can’t be tested (maybe it’s a merge commit or has a syntax error unrelated to your bug), your script can exit with code 125 to tell bisect to skip that commit.
Real Scenario: Broken Infrastructure Pipeline
Your team’s nightly terraform plan started failing three days ago. Nobody noticed until the Monday morning alert. There have been 40 commits since the last clean run.
# Find the last known-good pipeline run and its commit
git log --oneline --since="5 days ago" | tail -5
# The nightly run passed on Thursday at commit f1a2b3c
git bisect start
git bisect bad HEAD
git bisect good f1a2b3c
git bisect run ./test-terraform.sh
# 5–6 iterations later:
# d4e5f6a is the first bad commit
# Author: [email protected]
# "feat(storage): add lifecycle rules to blob containers"
git bisect reset
# Now you can review the exact diff that broke the plan
git show d4e5f6a
In ten minutes, you’ve pinpointed a problem that could have taken half a day of manual archaeology.
Hands-On Lab: Practice All Three
Set up a local repo and walk through stash, cherry-pick, and bisect in one session.
Step 1: Create a Test Repository
mkdir git-lab && cd git-lab
git init
# Create an initial file and commit history
echo "v1" > app.txt && git add app.txt && git commit -m "feat: v1 release"
echo "v2" > app.txt && git add app.txt && git commit -m "feat: v2 release"
echo "v3" > app.txt && git add app.txt && git commit -m "feat: v3 release"
echo "BROKEN" > app.txt && git add app.txt && git commit -m "refactor: simplify app logic"
echo "v5" > app.txt && git add app.txt && git commit -m "feat: v5 release"
Step 2: Practice Stash
# Make some uncommitted changes
echo "work in progress" > notes.txt
echo "v5-modified" > app.txt
# Stash everything including the untracked notes.txt
git stash push -u -m "lab: WIP changes for testing"
# Verify working directory is clean
git status
# On branch main — nothing to commit, working tree clean
# Check the stash list
git stash list
# stash@{0}: On main: lab: WIP changes for testing
# Pop the stash back
git stash pop
# Verify your changes are restored
cat notes.txt
# work in progress
Step 3: Practice Cherry-Pick
# Create a feature branch from v3
git stash push -u -m "parking WIP for cherry-pick exercise"
git checkout -b feature/experiment HEAD~2
echo "experiment" > experiment.txt
git add experiment.txt && git commit -m "feat: add experiment file"
# Grab the commit hash
PICK_HASH=$(git rev-parse HEAD)
# Go back to main and cherry-pick it
git checkout main
git stash pop
git cherry-pick $PICK_HASH
# Verify the file exists on main now
ls experiment.txt
# experiment.txt
Step 4: Practice Bisect
# We know "BROKEN" was introduced somewhere in history
# Let's use bisect to find it
git bisect start
git bisect bad HEAD # Current state has the problem in history
git bisect good HEAD~4 # The very first commit was fine
# Git checks out a middle commit — test it
cat app.txt
# If it says "BROKEN", mark bad; otherwise mark good
# Continue until bisect identifies the culprit
# Or automate it:
git bisect reset
git bisect start
git bisect bad HEAD~1 # The commit with "BROKEN"
git bisect good HEAD~4 # First commit
git bisect run bash -c '! grep -q "BROKEN" app.txt'
# Git will identify the exact commit that introduced "BROKEN"
git bisect reset
Troubleshooting Guide
Stash Issues
| Problem | Cause | Fix |
|---|---|---|
| New files not stashed | Untracked files excluded by default | Use git stash push -u |
| Stash pop causes conflicts | Branch diverged since stashing | Resolve conflicts manually, then git add the files |
| Can’t find the right stash | Generic “WIP” messages | Always use git stash push -m "description" |
Accidentally ran git stash clear | All stashes deleted permanently | Check git fsck --unreachable for dangling commits — recovery is possible but not guaranteed |
| Stash apply to wrong branch | Stashes are branch-independent | This is actually fine — stashes can be applied to any branch |
Cherry-Pick Issues
| Problem | Cause | Fix |
|---|---|---|
| Merge conflict during cherry-pick | Target branch has diverged | Resolve conflicts, git add, then git cherry-pick --continue |
| Duplicate commits in PR diff | Same change exists as different SHAs | Squash before merging, or use merge/rebase instead |
| Empty commit after cherry-pick | Change already exists on target | Use --keep-redundant-commits or skip it |
| Cherry-pick of a merge commit fails | Merge commits have multiple parents | Use -m 1 to specify the mainline parent |
Bisect Issues
| Problem | Cause | Fix |
|---|---|---|
| Can’t test a particular commit | Build is broken for unrelated reasons | git bisect skip or exit 125 in your script |
| Bisect gives wrong result | Test script doesn’t accurately detect the bug | Verify your script works on known-good and known-bad commits first |
| Forgot to reset after bisecting | Still in bisect mode | git bisect reset returns you to your original branch |
Quick Reference
# --- Stash ---
git stash push -m "message" # Stash with a label
git stash push -u -m "message" # Include untracked files
git stash list # Show all stashes
git stash pop # Apply and remove latest stash
git stash apply stash@{2} # Apply specific stash, keep it
git stash drop stash@{0} # Delete a specific stash
git stash branch new-branch # Create branch from latest stash
# --- Cherry-Pick ---
git cherry-pick abc1234 # Apply one commit
git cherry-pick abc1234^..def5678 # Apply a range (inclusive)
git cherry-pick --no-commit abc # Stage changes without committing
git cherry-pick --abort # Cancel in-progress cherry-pick
git cherry-pick --continue # Continue after resolving conflicts
# --- Bisect ---
git bisect start # Begin bisect session
git bisect bad # Mark current commit as bad
git bisect good <commit> # Mark a known-good commit
git bisect run ./test.sh # Automate with a script
git bisect skip # Skip untestable commit
git bisect reset # End bisect, return to branch
Happy automating!