2020-01-27 Splitting a past commit in two, and a bonus regex trick

More than a year ago I described a very simple Git rebase workflow, where all we were interested in was just fixing some mistakes in a past commit. Let us now go a little bit deeper. One thing I had always trouble with was splitting a commit in two (or more). While there are many tutorials about this on the internet, I wrote my own, even though it turned out that it is not better than the other ones. Go figure. (One advantage is that I have it on my website, so that I won’t need to do much searching in case I need it.)

(Of course, if we mean the last commit, we don’t really need fancy rebase – we can just git reset HEAD^, stage some things, make the first commit, and then proceed to the second one (or more). The tricky part is editing earlier commits.)

Let us begin almost like previously.

cd /tmp
rm -rf rebase-edit-tutorial	# in case you run this again
git init rebase-edit-tutorial
cd rebase-edit-tutorial
echo 'Initial commit' > file.txt
git add file.txt
git commit -m 'Zeroth commit'

echo -e 'This commit\nwill be split in two.' >> file.txt
git add file.txt
git commit -m 'First commit to split'

echo -e 'This commit will stay.' >> file.txt
git add file.txt
git commit -m 'Another commit'

We now want the two lines of the First commit to split to be added in two subsequent commits. Here’s how we can do it. Start with git rebase -i HEAD^^. Change the pick in the first line to edit, save and exit the editor, and then say git reset HEAD^. (Note that this is a different HEAD this time! To see what exactly HEAD is every time, say e.g. git rev-parse HEAD.) Here’s the explanation: rebasing puts us (more or less) in a state “right after HEAD^^=”. Since we want to change =HEAD^^, we need to go back just a bit further, to the moment before we committed it. This is what git reset --mixed HEAD^ does – and by the way, --mixed is the default, so we can omit it. (Actually, this takes us back even further, to the point before git add, i.e., not only does it reset the HEAD, but also the staging area.)

We can now add and commit the first line we introduced in our commit. Say git add -p and then e. Delete the second line beginning with + and exit the editor. Say git commit -m "First part". Now you can just git add file.txt and git commit -m "Second part". Finish off with git rebase --continue and you’re done.

One thing that is not mentioned in many tutorials is that this way, you completely lose the commit message of the commit you have just split in two (or more) parts. If that is a problem, there is a -c option to the git-commit command, which accepts a commit hash and uses the commit message from that commit as the basis for the new message. (There is also a variant, -C, which does not let you edit the message – it just takes another one and applies it.) Of course, the commit is not visible in the log now, but it is not gone from the repository – you can look it up in git reflog, or git rebase --abort, make a note of the commit’s hash and repeat the rebase.

But whenever you use a computer and feel the need to “make a note of something” manually to reuse it later, it is a sign that either you are doing something wrong, or the author of the tool you are using did something wrong. Now guess who is smarter, you or Git developers? (Well, that was mean, sorry.) Here is a trick: you can say git rev-parse :/'First commit to split', or even just git rev-parse :/First. What is this? Well, whenever Git wants you to give a commit hash, you can use the :/regex syntax, which gives you the newest commit reachable from any ref (like HEAD or branches) whose commit message matches regex. How cool is that!? The :/ syntax can do even more – say man gitrevisions to learn about it and other ways to specify revisions. You may thank me later. ;-)

CategoryEnglish, CategoryBlog, CategoryGit