You pushed five commits to your feature branch. The messages are garbage. Before you fix them, you pull the latest changes from main to stay current. Now you try the standard git rebase -i HEAD~5 to clean up those messages, and Git asks you to process forty commits, resolve conflicts you did not create, and manage merge commits you never wrote.
This is not a skill issue. This is Git trying to replay an entire merge operation because your recent history now includes everything that came from main. A standard interactive rebase becomes unworkable the moment you merge your target branch.
What You Actually Want
The goal is simple. You want the same five commits with identical code. You want brand new commit messages that actually describe what you did. You want a linear history that looks like you branched off main five minutes ago, with no merge bubbles cluttering the graph.
You do not want to spend an hour resolving conflicts just to change some text strings. You do not want to risk breaking the code. You do not want Git to make you think through every commit that happened on main while you were working on your feature.
The standard tools fail here because they were not built for this specific scenario. Interactive rebase is powerful but it assumes you have not already tangled your history with a merge. Once that merge exists, the mental model breaks down. You need a different approach.
Why Standard Rebase Fails After a Merge
When you merge main into your feature branch, Git creates a merge commit that ties the two histories together. Your feature branch now contains not just your five commits but also every commit that was added to main since you branched. The commit graph has two parents at the merge point.
When you run git rebase -i HEAD~5, Git looks at the last five commits in your history. Those five commits now include things from main, not just your feature work. Git tries to replay all of them, including the merge commit itself. Replaying a merge means re-resolving all the conflicts that were already resolved. It means processing commits that have nothing to do with your feature. It means descending into a special kind of hell where you question every life choice that led you to this terminal window.
The standard advice is to never rebase after merging, and that advice is correct for most situations. But sometimes you already did the merge, and you still need those commit messages fixed. Telling someone “you should not have done that” does not help them now. You need a way forward, not a lecture.
The Clean Room Strategy
Instead of trying to untangle the mess on your existing branch, you build a new branch in a completely separate workspace. You use Git worktrees to create that workspace, cherry-pick to extract just your commits, and then rebase on the clean branch where it actually works. Finally, you force push the clean branch to replace the messy one.
This works because you never try to rebase the merged history. You extract the valuable parts, put them on a clean foundation, fix what needs fixing, and discard the rest. It is surgical. It is precise. It does not require you to understand or replay the merge.
The key insight is that your five commits still exist as distinct objects in Git’s database. They are just buried in a complicated history graph. Cherry-pick lets you pluck them out and apply them to a different base. Once they are on a clean base with no merge history, interactive rebase works exactly as intended.
Step One: Create the Operating Room
Git worktrees let you check out a branch in a completely different directory while keeping your current directory untouched. This is crucial because you need to keep your messy branch exactly as it is while you work in a separate space.
git worktree add ../repair-shop -b clean-fix origin/sprint-target
cd ../repair-shop
This command does several things at once. It creates a new directory called repair-shop one level up from your current repo. It creates a new branch called clean-fix based on your target branch, which might be main or develop or whatever your team uses. It checks out that branch in the new directory. Now you have a completely clean workspace with none of your feature work and none of the merge history.
The reason this matters is isolation. Your original working directory still has all your uncommitted changes, your editor state, your terminal history. Nothing about that environment changes. You are working in a parallel universe where the merge never happened.
Step Two: Extract Your Commits
Cherry-pick is how you grab specific commits and apply them somewhere else. You need to identify the range of commits that represent your actual feature work, then apply them to the clean branch.
git cherry-pick <oldest-hash>^..<newest-hash>
The ^ means “include the oldest commit in the range.” Without it, Git would start from the commit after the one you specified, which is usually not what you want. The .. syntax specifies a range. You are telling Git to take every commit from your oldest feature commit through your newest feature commit and apply them in order to the current branch.
If you do not remember the exact hashes, run git log --oneline in your original directory and find where your feature commits start and end. Copy those hashes. You can also use git log --oneline --graph to see the structure visually if that helps.
When the cherry-pick completes, your clean branch now has your five commits applied on top of the target branch. No merge commits. No extra history. Just your work on a clean foundation.
Step Three: Fix the Messages
Now you have exactly the scenario that interactive rebase was designed for. A linear series of commits with no merge complications.
git rebase -i HEAD~5
Git opens your editor with a list of your five commits. Change pick to reword for each commit you want to fix. Save and close. Git reopens the editor for each commit, letting you write a better message. Save and close each time. When it finishes, you have five commits with identical code but clean, descriptive messages.
This is why the clean room strategy works. You moved the problem into an environment where the tools function as expected. No conflicts. No surprises. No processing dozens of unrelated commits. Just you and your five commits and the text editor.
Step Four: Replace the Old Branch
Now you have a perfect branch in your worktree. The old messy branch still exists on the remote. You need to replace it.
git push origin clean-fix:story/your-branch-name --force-with-lease
This command pushes your local clean-fix branch to the remote but tells the remote to treat it as your original branch. The : syntax means “push my local branch to this remote branch name.” The --force-with-lease flag force pushes but only if nobody else has pushed to that branch since you last fetched. This protects you from overwriting someone else’s work.
If you try to push directly from the worktree to update the local branch pointer, Git refuses because the branch is checked out elsewhere. The remote push syntax sidesteps this issue entirely. Your remote branch is now clean. Your teammates who pull will get the rewritten history. If you need to update your local branch pointer, switch to it in your original directory and run git reset --hard origin/your-branch-name.
The Gotchas
The locked worktree error happens if you try to force update a branch that is currently checked out in another worktree. Git will not let you do this because it would yank the branch out from under an active working directory. The solution is to push to the remote reference like I showed above, or switch your original directory to a different branch before updating the pointer.
Force pushing is destructive. If someone else pushed commits to your branch while you were performing surgery, you will overwrite their work. The --force-with-lease flag mitigates this somewhat by checking that the remote ref matches what you last fetched, but it is not foolproof. The real protection is communication. Make sure nobody else is working on your branch before you rewrite its history.
This strategy eliminates merge commits from your history. Your final branch will be linear, as if you rebased regularly and never merged main into your feature. For most teams and most workflows, this is exactly what you want. Merge commits clutter the graph and make bisect harder. But if your team explicitly requires merge commits for audit trails or policy reasons, check with your lead before doing this.
If you have uncommitted changes in your original directory when you create the worktree, they stay in the original directory. The worktree starts clean. This is usually what you want, but it can be confusing if you expect the worktree to mirror your current state. Worktrees and working directories are independent. Plan accordingly.
When to Use This
This technique is specifically for fixing commit messages after you have already merged your target branch into your feature branch. If you have not merged yet, standard interactive rebase works fine. If you have not pushed yet, you have even more flexibility because force push is not a concern.
Use this when the merge already happened, commits are already public, and you still need to clean up the history. Use this when you realize too late that your commit messages are terrible and your team uses a tool that enforces conventional commits or parses commit messages for changelogs. Use this when the alternative is leaving the mess in place because standard rebase is too painful.
Do not use this if other people are actively working on the same branch. Coordinate first. Do not use this if you are not comfortable with force pushing and the risks involved. Do not use this if your team forbids history rewriting on shared branches. Some teams have policies, and those policies exist for good reasons.
Cleaning Up
After you successfully push and verify everything works, clean up the worktree.
cd /path/to/original/repo
git worktree remove ../repair-shop
This removes the worktree directory and tells Git it no longer exists. If you already deleted the directory manually, run git worktree prune to clean up the references. Leaving stale worktree references around causes confusion later when Git thinks a directory exists that has been deleted.
Delete the temporary branch if you do not need it anymore. Since you pushed it to replace your original branch, the local clean-fix branch served its purpose.
git branch -D clean-fix
Your original branch now points to the clean history on the remote. Everything is tidy. The surgery is complete.
The Real Lesson
Git gives you incredible power to manipulate history, but that power comes with complexity. Tools like interactive rebase work beautifully in the scenarios they were designed for, but they fall apart when you violate their assumptions. Merging and then trying to rebase is one of those violations.
The clean room strategy works because it respects how the tools actually function. Instead of fighting Git’s behavior, you create an environment where the behavior makes sense. You isolate the problem, use the right tool for each step, and produce a clean result.
This is a pattern worth internalizing beyond just this specific Git scenario. When a tool is not working the way you expect, sometimes the issue is not the tool. Sometimes the issue is that you are using it in a context it was not built for. The solution is not always to learn more flags or read more documentation. Sometimes the solution is to change the context.
🎯 Surgical History Rewriting
- ✓ Merging main into your branch breaks standard interactive rebase by forcing you to replay the entire merge
- ✓ Use git worktree to create a clean workspace based on your target branch without touching your current work
- ✓ Cherry-pick extracts just your commits and applies them to the clean base where rebase actually works
- ✓ Push with --force-with-lease to replace the remote branch while protecting against concurrent changes
- ✓ This strategy only works solo - coordinate with your team before rewriting shared history
- ✓ Clean up worktrees when done to avoid stale references cluttering your repository
Enjoyed this article? Share it with others!