hisham hm

🔗 Splitting a Git commit into one commit per file

Sometimes when working on a branch, you end up with a “wip” or “fixup” commit that contains changes to several files:

01a25e6 introduce raccoon library
bd197ac modify core to use raccoon
02890e3 add --raccoon option to the CLI
f938740 fixes
fab9379 add documentation on raccoon features

Our f938740 fixes commit has changes that really belong in the three previous commits. Before merging, we want to squash those changes in the original commits where the correct code should have been in the first place.

The typical way to do this is to use interactive rebase, using git rebase -i.

This is not a post explaining interactive rebase, so check out some other sources before proceeding if you are not familiar with it!

Splitting things from a “fixup” commit can get tedious using git rebase -i in conjunction with the edit option and git add -p, especially when you really know that all changes to a file belong to a certain commit.

Here’s a quick script for the rescue: it is designed to be used during an interactive rebase, and splits the current commit into multiple commits, one with the contents of each file:

#!/usr/bin/env bash

message="$(git log --pretty=format:'%s' -n1)"

if [ `git status --porcelain --untracked-files=no | wc -l` = 0 ]
then
   git reset --soft HEAD^
fi

git status --porcelain --untracked-files=no | while read status file
do
   echo $status $file

   if [ "$status" = "M" ]
   then
      git add $file
      git commit -n $file -m "$file: $message"
   elif [ "$status" = "A" ]
   then
      git add $file
      git commit -n $file -m "added $file: $message"
   elif [ "$status" = "D" ]
   then
      git rm $file
      git commit -n $file -m "removed $file: $message"
   else
      echo "unknown status $file"
   fi
done

Save this as split-files.sh (and make it executable with chmod +x split-files.sh).

Now, we proceed with the interactive rebase. When doing an interactive rebase, Git will open a text editor: in the commit you want to split, replace pick with edit:

pick 01a25e6 introduce raccoon library
pick bd197ac modify core to use raccoon
pick 02890e3 add --raccoon option to the CLI
edit f938740 fixes
pick fab9379 add documentation on raccoon features

# Rebase 01a25e6..fab9379 onto cb370a2 (5 commands)
#
# Commands:
# p, pick  = use commit
# r, reword  = use commit, but edit the commit message
# e, edit  = use commit, but stop for amending
# ...

When you save and exit the text editor launched by Git, you will return to the prompt with the repo's HEAD pointing at the commit we will split. Then run ./split-files.sh and then git rebase --continue.

Now launch the interactive rebase again. Your commits should look like this:

pick 01a25e6 introduce raccoon library
pick bd197ac modify core to use raccoon
pick 02890e3 add --raccoon option to the CLI
pick 8369783 src/lib/racoon.foo: fixes
pick a3c4e42 src/cli/foobar: fixes
pick 108a931 src/core/core.foo: fixes
pick fab9379 add documentation on raccoon features

# Rebase 01a25e6..fab9379 onto cb370a2 (7 commands)
#
# Commands:
# p, pick  = use commit
# r, reword  = use commit, but edit the commit message
# e, edit  = use commit, but stop for amending
# ...

The "fixes" commit in our example was split into three. Now move these new commits around and use the fixup command to merge them to the commit immediately above it:

pick 01a25e6 introduce raccoon library
fixup 8369783 src/lib/racoon.foo: fixes
pick bd197ac modify core to use raccoon
fixup 108a931 src/core/core.foo: fixes
pick 02890e3 add --raccoon option to the CLI
fixup a3c4e42 src/cli/foobar: fixes
pick fab9379 add documentation on raccoon features

# Rebase 01a25e6..fab9379 onto cb370a2 (7 commands)
#
# Commands:
# p, pick  = use commit
# r, reword  = use commit, but edit the commit message
# e, edit  = use commit, but stop for amending
# ...

Save, exit, and we're done! But a word of warning: when moving commits around make sure there are no other commits that change the same part of the file in between your "fixes" commit and the one you're squashing it into. When in doubt, Gitk and similar tools make it easier to check this before you jump into squashing commits.

If everything went well, our history now looks like this:

8370e83 introduce raccoon library
038c5a3 modify core to use raccoon
bb9783a add --raccoon option to the CLI
fab9379 add documentation on raccoon features

The SHA hashes of the commits have changed, because they now contain the fixes merged into them, and the separate catch-all "fixes" commit is now gone for good!

Of course this is a bit of an ideal scenario where each file goes neatly into a separate commit. Sometimes changes made to a single file belong in separate commits. In those cases, the solution is a bit more manual, using edit and then git add -p, which is super useful.

And remember, if any moment you messed up, git reflog is your best friend! But this is a topic for another time. Cheers!


Latest posts


Search


Admin area


Feeds