Luke Oliff.

How to Split a Messy Git Commit Into Clean Ones

·Git·4 min read·Luke Oliff
TL;DR

You don't fix a messy commit by force. Mark it edit in git rebase -i, run git reset HEAD^ to unstage everything, then rebuild it with git add -p, committing each logical change on its own.

You split a messy git commit by undoing it without losing the work, then rebuilding it in pieces. For the last commit that’s git reset HEAD^. For an older one it’s git rebase -i, mark the commit edit, and do the same reset there. Either way the real trick is git add -p, which stages changes one hunk at a time so each new commit says one thing.

This came back around after a post on blog.gnoack.org hit Hacker News in August 2026. Worth relearning. Most of us know the theory and still end up with a commit called “fixes” that touches nine files.

Splitting a git commit that’s still on top

If the mess is your latest commit, you don’t need rebase at all.

git reset HEAD^

That’s a mixed reset. It moves the branch back one commit and leaves every change sitting in your working tree, unstaged. The commit is gone, the code is not. Now rebuild:

git add -p

Git walks each hunk and asks Stage this hunk [y,n,q,a,d,s,e,?]?. Say y to the hunks that belong together, n to the rest. If a hunk is too coarse, s splits it into smaller ones, and e lets you edit the hunk by hand when even that isn’t fine enough. Stage one logical change, commit it, repeat.

git commit -m "extract the retry helper"
git add -p
git commit -m "fix the off-by-one in the pager"

Keep going until git status is clean. One tangled commit is now three that each read on their own.

Splitting a commit buried in history

The commit you regret is rarely the last one. Say it’s three back. Start an interactive rebase from its parent:

git rebase -i HEAD~3

An editor opens with a list of commits, pick on each. Change pick to edit (or just e) on the one you want to break up, save, and close. Git replays up to that commit and stops, leaving it applied and staged. Undo it in place:

git reset HEAD^

Now you’re in the same spot as before: the changes are unstaged, the commit is gone. Use git add -p and git commit as many times as you need. When you’re done:

git rebase --continue

Git replays the commits that sat on top. If it can’t, it stops for a conflict, you fix it, git add the file, and continue. The commits after the one you split keep their messages and their order.

One caveat that bites people: this rewrites history from that commit forward, so every hash downstream changes. Fine on a branch only you have touched. Not fine on main that ten people have pulled. Rebase before you push, not after.

When you just want to leave something out of a commit

Sometimes you don’t need to split an old commit, you just don’t want to commit everything you changed right now. Same tool, different entry point:

git commit --patch

--patch (or -p) runs the hunk-by-hunk picker straight into the commit, skipping the staging step entirely. You get asked about each change and only the ones you approve go in. The rest stay in your tree for the next commit. I reach for this more than the rebase version, honestly, because catching the mess before it lands beats untangling it later.

The whole point of small commits is the review and the bisect later. A reviewer can reason about “fix the off-by-one” in ten seconds. Nobody can reason about “fixes” that also renamed a module and bumped a dependency. git bisect can’t either. Splitting a git commit is you paying that forward: a few minutes now against a confused afternoon in six months when something breaks and the history is the only witness you’ve got.