Painless commit splitting in Git

Thursday, 15 Jan 2009

It just occurred to me that since Git stores full tree states, the easiest way to split a commit would be to make a copy of the final tree repeatedly, and in each copy cancel away some changes relative to the previous amended copy of the tree. It turns out you can actually do the procedural steps fairly mechanically.

For the sake of this example, I want to split out the combined changes to the files foo, bar, baz and quux over four commits. The parts you have to fill in are bolded; the rest is mechanical.

  1. Make sure you are at the root level directory of your repository’s working copy.

  2. Set a bookmark on the commit you want to split so you can refer to it easily:

    git branch splitme 007dead
  3. Now, interatively rebase the current commit series, starting at the parent of the commit you want to split:

    git rebase -i splitme^

    When your editor comes up with the rebase todo list, mark the 007dead commit for editing.

  4. Once you have been dropped back into the shell, undo all changes except the ones you want in the first of the split commits, then amend the commit:

    git reset HEAD^ quux baz bar
    git commit -m 'change foo' --amend

    Here, you undid the changes to quux, baz and bar; the amended commit therefore contains only the changes to foo, which the new commit message reflects.

  5. Now comes the point where you create extra commits. Each follows the same steps:

    1. git checkout splitme .
      git reset HEAD^ quux baz
      git commit -m 'change bar'
    2. git checkout splitme .
      git reset HEAD^ quux
      git commit -m 'change baz'
    3. git checkout splitme .
      git commit -m 'change quux'

    This step is where the magic happens: you repeatedly check out a copy of the final state of the tree into the index and working copy (using checkout with a path, which is . i.e. the project root directory) and commit it after undoing progressively fewer changes (using reset). In the last step no changes are undone, you just need it to write a new commit message.

    The secret sauce is that splitme always refers to the final state of the tree and HEAD^ always refers to a state missing some of the changes.

  6. Finally, continue the rebase. Once it’s finished, you can drop the bookmark.

    git rebase --continue
    git branch -D splitme

That’s it.