Git Advanced Techniques — Rebase, Cherry-Pick, Bisect, and Stash
Beyond basic git: interactive rebase, cherry-pick workflows, git bisect for bug hunting, stash management, reflog recovery, and worktrees. Real scenarios for each technique.
You know git add, git commit, git push. You might even know git branch and git merge. But there's a tier of git commands that separates developers who fight git from developers who wield it. These commands aren't obscure -- they solve problems that come up weekly on any active team.
Interactive Rebase
Regular rebase replays commits on top of another branch. Interactive rebase lets you rewrite history: squash commits, reorder them, edit messages, or drop commits entirely.
When to Use It
- Clean up messy WIP commits before merging to main
- Combine "fix typo" and "oops forgot file" into the original commit
- Reorder commits so the PR tells a logical story
How It Works
# Rebase the last 4 commits interactively
git rebase -i HEAD~4
This opens your editor with something like:
pick a1b2c3d Add user authentication
pick e4f5g6h Fix typo in auth middleware
pick i7j8k9l Add password reset flow
pick m0n1o2p Fix: forgot to export reset handler
Change the commands to reshape history:
pick a1b2c3d Add user authentication
fixup e4f5g6h Fix typo in auth middleware
pick i7j8k9l Add password reset flow
fixup m0n1o2p Fix: forgot to export reset handler
| Command | What It Does |
|---|---|
pick | Keep the commit as-is |
reword | Keep changes, edit the commit message |
edit | Pause at this commit so you can amend it |
squash | Combine with previous commit, edit the merged message |
fixup | Combine with previous commit, discard this commit's message |
drop | Remove the commit entirely |
The Golden Rule
Never rebase commits that have been pushed to a shared branch. Rebase rewrites commit hashes. If someone else has based work on the original commits, you've just created a parallel timeline. Feature branches that only you work on? Rebase freely.main? Never.
# Safe: rebase your feature branch onto latest main
git checkout feature/auth
git rebase main
# Dangerous: rebase main (never do this)
git checkout main
git rebase feature/auth # DON'T
Resolving Conflicts During Rebase
When a conflict occurs mid-rebase:
# 1. Fix the conflicted files
# 2. Stage the fixes
git add src/auth.ts
# 3. Continue the rebase
git rebase --continue
# Or abort if things go sideways
git rebase --abort
Unlike merge conflicts (which happen once), rebase conflicts can occur at each commit being replayed. If your branch has 20 commits and many touch the same files, consider squashing first to reduce conflict surface.
Cherry-Pick
Cherry-pick copies a specific commit from one branch to another. Not a merge -- just that one commit, applied as a new commit on the current branch.
Real Scenarios
Hotfix backporting: A bug fix lands onmain, but you need it on the release/v2.1 branch too.
git checkout release/v2.1
git cherry-pick abc123f
Pulling one feature from a stale branch: Someone started a feature branch months ago, abandoned it, but one commit in there has useful work.
git cherry-pick def456a
Selective deployment: Your branch has 10 commits but only 3 are ready for production.
git checkout deploy-branch
git cherry-pick commit1 commit2 commit3
Cherry-Pick a Range
# Pick commits A through D (inclusive of D, exclusive of A)
git cherry-pick A..D
# Pick commits A through D (inclusive of both)
git cherry-pick A^..D
When NOT to Cherry-Pick
If you're cherry-picking more than 3-4 commits, you probably want a merge or rebase instead. Cherry-picks create duplicate commits (different hashes, same changes), which makes git history harder to follow and can cause confusing conflicts later.
Git Bisect
Bisect is git's binary search tool. You tell it "this commit is good, this commit is bad" and it walks the history to find the exact commit that introduced a bug.
The Workflow
# Start bisecting
git bisect start
# Mark the current (broken) commit as bad
git bisect bad
# Mark a known-good commit (maybe last week's release)
git bisect good v2.3.0
# Git checks out a commit halfway between good and bad
# Test it, then tell git:
git bisect good # if this commit works
# or
git bisect bad # if this commit is broken
# Repeat until git finds the culprit
# Git will output: "abc123f is the first bad commit"
# Done -- go back to your branch
git bisect reset
For a range of 1000 commits, bisect finds the culprit in about 10 steps (log2(1000) ≈ 10). Way faster than checking commits one by one.
Automated Bisect
If you have a test that reproduces the bug, bisect can run it automatically:
git bisect start HEAD v2.3.0
git bisect run npm test -- --grep "user login"
Git will run the command at each step. Exit code 0 means "good", non-zero means "bad". It'll find the breaking commit without you touching anything.
# You can also use a script
git bisect run ./test-bug.sh
Where test-bug.sh is:
#!/bin/bash
# Returns 0 (good) if the bug is NOT present
# Returns 1 (bad) if the bug IS present
npm run build 2>/dev/null && node -e "
const result = require('./dist/calculate').compute(42);
process.exit(result === 84 ? 0 : 1);
"
This is one of the most underused git features. It's saved me hours on multiple occasions -- especially on large codebases where the bug could be in any of hundreds of recent commits.
Git Stash
Stash temporarily shelves uncommitted changes so you can switch branches without committing half-done work.
Basic Usage
# Stash current changes
git stash
# List stashes
git stash list
# stash@{0}: WIP on feature/auth: abc123f Add login form
# stash@{1}: WIP on main: def456a Update deps
# Apply the most recent stash (keeps it in the stash list)
git stash apply
# Apply and remove from stash list
git stash pop
# Apply a specific stash
git stash apply stash@{1}
# Drop a stash
git stash drop stash@{0}
# Clear all stashes
git stash clear
Named Stashes
Default stash messages are useless. Name them:
git stash push -m "half-done payment integration"
Now git stash list shows something meaningful.
Stash Individual Files
# Stash only specific files
git stash push -m "experimental API change" src/api/routes.ts src/api/middleware.ts
# Stash everything except staged changes
git stash push --keep-index -m "stash unstaged only"
Stash Pitfall
Stashes are local and not pushed to remote. They're also easy to forget about. I've seen developers with 30+ stashes they'll never look at again. If you stash something and it's still relevant after a day, commit it to a branch instead:
# Turn a stash into a branch
git stash branch feature/payment-wip stash@{0}
Reflog -- Your Safety Net
Reflog records every movement of HEAD. Every commit, rebase, reset, checkout -- everything. It's your undo history.
Recovery Scenarios
Accidentally reset --hard and lost commits:# See what HEAD pointed to recently
git reflog
# abc123f HEAD@{0}: reset: moving to HEAD~3
# def456a HEAD@{1}: commit: Add payment processing
# ghi789b HEAD@{2}: commit: Add order validation
# jkl012c HEAD@{3}: commit: Add cart functionality
# Recover the lost commits
git checkout def456a
# Or create a branch from the lost commit
git branch recovery def456a
Rebase went wrong:
# Find where you were before the rebase
git reflog
# ... find the entry before the rebase started
# Reset to that point
git reset --hard HEAD@{5}
Deleted a branch:
git reflog
# Find the last commit on that branch
git branch restored-branch abc123f
Reflog entries expire after 90 days by default (30 days for unreachable commits). It's local only -- not pushed to remote.
Worktrees
Worktrees let you check out multiple branches simultaneously in separate directories. Instead of stashing, switching branches, doing work, switching back, and popping the stash -- just work in both branches at the same time.
Setup
# From your main repo directory
git worktree add ../project-hotfix hotfix/critical-bug
git worktree add ../project-review feature/new-api
# List worktrees
git worktree list
# /home/user/project abc123f [main]
# /home/user/project-hotfix def456a [hotfix/critical-bug]
# /home/user/project-review ghi789b [feature/new-api]
Now you have three separate directories, each on a different branch, sharing the same git history. Changes in one are visible to the others after committing.
When Worktrees Shine
- Code review: Check out the PR branch in a worktree, run it, test it, while your main work continues undisturbed.
- Hotfixes: Urgent fix needed while you're mid-feature. Worktree lets you fix, commit, push, and deploy without touching your feature branch.
- Long builds: Start a build in one worktree, continue coding in another.
- Comparing behavior: Run two versions of the app side by side on different ports.
Cleanup
# Remove a worktree (after merging the branch)
git worktree remove ../project-hotfix
# Prune stale worktree references
git worktree prune
Quick Reference
| Problem | Solution |
|---|---|
| Messy commit history on feature branch | git rebase -i HEAD~N |
| Need one commit from another branch | git cherry-pick |
| Find which commit broke something | git bisect start + good/bad |
| Need to switch branches with uncommitted work | git stash push -m "description" |
| Lost commits after reset/rebase | git reflog + git checkout |
| Work on two branches simultaneously | git worktree add |
| Undo the last commit (keep changes) | git reset --soft HEAD~1 |
| See what changed in a file over time | git log -p -- path/to/file |
| Find who changed a specific line | git blame -L 50,60 file.ts |
| Show what branches contain a commit | git branch --contains |
Decision: Merge vs Rebase
This is the eternal debate. Here's when to use each:
| Scenario | Use | Why |
|---|---|---|
| Updating feature branch with latest main | Rebase | Clean linear history |
| Merging feature into main | Merge | Preserves branch context |
| Shared branch (multiple people) | Merge | Don't rewrite shared history |
| Solo feature branch cleanup | Rebase | Squash WIP commits |
| Long-running release branch | Merge | Need to track what was integrated when |