Advanced Git Workflows: Rebase, Worktree, Bisect, and Recovery
Advanced Git Workflows: Rebase, Worktree, Bisect, and Recovery
Most developers use about 10% of Git's capabilities: add, commit, push, pull, branch, merge. That's enough to get by, but Git has powerful features that solve real problems -- cleaning up messy history, working on multiple branches simultaneously, finding which commit introduced a bug, and recovering from mistakes.
Here are the advanced Git workflows worth learning, with practical examples.
Interactive Rebase: Clean Up Before You Share
Interactive rebase lets you rewrite commit history before pushing. You can squash commits, reorder them, edit messages, and combine related changes into clean, logical units.
# Rebase the last 5 commits
git rebase -i HEAD~5
This opens your editor with a list of commits:
pick a1b2c3d feat: add user authentication
pick d4e5f6a fix: typo in auth middleware
pick 7g8h9i0 wip: debugging session stuff
pick j1k2l3m feat: add password reset flow
pick n4o5p6q fix: edge case in password reset
Change pick to an action:
pick a1b2c3d feat: add user authentication
fixup d4e5f6a fix: typo in auth middleware
drop 7g8h9i0 wip: debugging session stuff
pick j1k2l3m feat: add password reset flow
fixup n4o5p6q fix: edge case in password reset
Actions:
- pick: Keep the commit as-is
- squash: Merge into the previous commit, combine messages
- fixup: Merge into the previous commit, discard this message
- reword: Keep the commit but edit the message
- drop: Delete the commit entirely
Autosquash: Automated Fixups
If you know a commit is a fix for a previous commit, mark it at creation time:
# Create a fixup commit for a specific commit
git commit --fixup=a1b2c3d
# Later, autosquash during rebase
git rebase -i --autosquash main
Git automatically reorders the fixup commit and marks it with fixup, so you just save and close the editor. This workflow is perfect for code review feedback -- make fixes as --fixup commits, then autosquash before merging.
Enable autosquash by default:
git config --global rebase.autoSquash true
Git Worktree: Multiple Branches, No Stashing
git worktree lets you check out multiple branches simultaneously in separate directories. No more stashing your work to review a PR or fix a hotfix on another branch.
# Create a worktree for a feature branch
git worktree add ../project-feature feature-branch
# Create a worktree for a new branch
git worktree add ../project-hotfix -b hotfix/fix-login
# List active worktrees
git worktree list
# Remove a worktree when done
git worktree remove ../project-feature
Practical workflow:
You're deep in a feature branch. A critical bug report comes in. Instead of stashing your work and switching branches:
# Create a worktree for the hotfix (from your feature branch, no context switch)
git worktree add ../myproject-hotfix -b hotfix/critical-bug main
# Work on the fix in the other directory
cd ../myproject-hotfix
# ... fix the bug, commit, push ...
# Come back to your feature branch (still exactly as you left it)
cd ../myproject
git worktree remove ../myproject-hotfix
Each worktree shares the same Git repository (objects, refs) but has its own working directory and index. It's fast and uses minimal extra disk space.
Bisect: Find the Commit That Broke Things
git bisect performs a binary search through commit history to find which commit introduced a bug. If you have 1000 commits between "worked" and "broken," bisect finds the culprit in about 10 steps.
# Start bisect
git bisect start
# Mark current commit as bad (the bug exists here)
git bisect bad
# Mark a known good commit (the bug didn't exist here)
git bisect good v2.1.0
# Git checks out a commit halfway between. Test it, then:
git bisect good # if the bug is not present
# or
git bisect bad # if the bug is present
# Repeat until Git identifies the first bad commit
# When done:
git bisect reset
Automated Bisect
If you have a test script that exits 0 for good and non-zero for bad, automate the entire process:
git bisect start HEAD v2.1.0
git bisect run ./test-script.sh
Git runs the script on each commit and finds the offending commit automatically. This is incredibly powerful for regressions caught by automated tests.
# Example: find which commit broke a specific test
git bisect start HEAD v2.1.0
git bisect run bun test -- tests/auth.test.ts
Reflog: Your Safety Net
The reflog records every change to HEAD, even operations that "rewrite history" like rebase and reset. It's your undo button for nearly any Git mistake.
# View recent reflog entries
git reflog
# Output:
# a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/main
# f4e5d6c HEAD@{1}: rebase (pick): feat: add auth
# 7g8h9i0 HEAD@{2}: rebase (start): checkout main
# j1k2l3m HEAD@{3}: commit: wip: stuff I just lost
Recovery Scenarios
Undo a bad rebase:
# Find the commit before the rebase started
git reflog
# HEAD@{3} is where you were before
git reset --hard HEAD@{3}
Recover a deleted branch:
# Find the commit the branch pointed to
git reflog | grep "checkout: moving from deleted-branch"
# Recreate the branch
git branch recovered-branch a1b2c3d
Recover a dropped commit from interactive rebase:
git reflog
# Find the commit hash of the dropped commit
git cherry-pick a1b2c3d
Important: Reflog entries expire after 90 days by default (30 days for unreachable commits). Don't wait too long to recover.
Stash: Beyond the Basics
Most developers know git stash and git stash pop. But stash has features that make it much more useful.
Named Stashes
# Stash with a descriptive message
git stash push -m "WIP: auth refactor, waiting on API changes"
# List stashes (now you can tell them apart)
git stash list
# stash@{0}: On main: WIP: auth refactor, waiting on API changes
# stash@{1}: On main: debugging session output
Partial Stashing
Stash only some files, or even some changes within a file:
# Stash specific files
git stash push -m "just the config changes" config.ts settings.ts
# Stash interactively (choose hunks)
git stash push -p -m "partial stash"
The -p (patch) flag shows each change hunk and asks whether to stash it. This is invaluable when you've made unrelated changes in the same file and want to separate them.
Stash Without Losing Staged Work
# Stash only unstaged changes (keep staged changes)
git stash push --keep-index -m "unstaged only"
This is useful when you've carefully staged specific changes for a commit and want to temporarily hide everything else to test that the staged changes work in isolation.
Create a Branch from a Stash
# If applying a stash would cause conflicts, create a branch instead
git stash branch new-feature-branch stash@{0}
Practical Tips
Commit often, rebase before sharing. Make small, frequent commits while developing (even "wip" commits). Use interactive rebase to clean them up before pushing or opening a PR. This gives you a safety net during development and clean history for reviewers.
Set up useful aliases:
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.st "status --short --branch"
git config --global alias.unstage "reset HEAD --"
git config --global alias.last "log -1 HEAD --stat"
git config --global alias.wt "worktree"
Always use --force-with-lease instead of --force:
git push --force-with-lease
This force-pushes your branch but refuses if someone else has pushed commits you haven't seen. It prevents accidentally overwriting a teammate's work. Set it as default:
git config --global push.forceWithLease true
The Bottom Line
These workflows solve specific, recurring problems: messy commit history (interactive rebase), context switching (worktree), regression hunting (bisect), mistake recovery (reflog), and partial work management (stash). You don't need all of them every day, but knowing they exist means you'll reach for the right tool when the situation comes up instead of fumbling with error-prone workarounds.