Git - Splitting Commits

When you first discover the importance of logically structured Git commits, which is by the way a core fundamental expectation & design characteristic of Git and how it is intended to be used. You are probably still following your bad habits and creating commits that aren't logical commits.

This naturally leads to the question, "How do I split a commit up into multiple commits?" Below I present a simple contrived example so that you can learn the mechanics and process of doing this, as the mechanics & process don't change.

The other topic related to this which this post does not cover is the process of taking code and splitting it up into logical chunks. This generally takes an understanding of the application architecture involved & the dependency relationship between the various elements.

So lets get to it.

TLDR

For those who just want a quick reminder reference here is the TLDR. For those who need a bit more context and detail through the walk through read the sections below.

  • git rebase -i - do an interactive rebase & mark the commit you want to split for edit, it will drop you out into the shell at that commit
  • git reset HEAD^ - soft reset the commit
  • git add -p - stage just the parts you want in the first commit
  • git commit - commit the first commit
  • git add -p - stage just the parts you want in the next commit
  • git commit - commit the next commit
  • git rebase --continue - continue the rebase to play the other commits on top of the new commits you created

Initial State

For this example lets assume that we have a Git tree that has the following commits.

49d9eb4 - (HEAD -> main) Add first paragraph of the README
d3e4eee - Add Subtitle & Description of README
56fa164 - Add title of README
8da3dd9 - (base) Initial commit

As we can see from the first commit on base, 8da3dd9 - (base) Initial commit, it adds the title to the README.

❯ git show 56fa164
commit 56fa1649afa886c309ce8e83c5518b48c8600b6f
Author: Andrew De Ponte <[email protected]>
Date:   Tue Jan 19 23:48:13 2021 -0500

		Add title of README

diff --git a/README b/README
index e79c5e8..4b3b7a8 100644
--- a/README
+++ b/README
@@ -1 +1,2 @@
 initial
+Title of README

The second commit, d3e4eee - Add Subtitle & Description of README, adds the subtitle and the description as seen in the diff below.

❯ git show d3e4eee
commit d3e4eeec96f9c3cd3eadeb4d95256a6326485ed4
Author: Andrew De Ponte <[email protected]>
Date:   Tue Jan 19 23:48:52 2021 -0500

		Add Subtitle & Description of README

diff --git a/README b/README
index 4b3b7a8..cf3aba6 100644
--- a/README
+++ b/README
@@ -1,2 +1,5 @@
 initial
 Title of README
+Subtitle of README
+
+Description of the README

The third commit, 49d9eb4 - (HEAD -> main) Add first paragraph of the README adds the first paragraph in the diff below.

❯ git show 49d9eb4
commit 49d9eb41bd3a0031f537b3865f6c90329f173bc4 (HEAD -> main)
Author: Andrew De Ponte <[email protected]>
Date:   Tue Jan 19 23:49:51 2021 -0500

		Add first paragraph of the README

diff --git a/README b/README
index cf3aba6..d325bcb 100644
--- a/README
+++ b/README
@@ -3,3 +3,5 @@ Title of README
 Subtitle of README

 Description of the README
+
+First paragraph of the README

Non-Logically Structured Commit

Looking at the commit summaries & the diffs themselves we can see that the second commit, d3e4eee - Add Subtitle & Description of README is actually doing two logical things. First it is adding the subtitle. Secondly it is adding the description to the README.

❯ git show d3e4eee
commit d3e4eeec96f9c3cd3eadeb4d95256a6326485ed4
Author: Andrew De Ponte <[email protected]>
Date:   Tue Jan 19 23:48:52 2021 -0500

		Add Subtitle & Description of README

diff --git a/README b/README
index 4b3b7a8..cf3aba6 100644
--- a/README
+++ b/README
@@ -1,2 +1,5 @@
 initial
 Title of README
+Subtitle of README
+
+Description of the README

Instead of the above what we really wanted to have was one commit that adds the subtitle and a separate commit that adds the description as two isolated logical chunks.

Edit Mode

To accomplish this we need to utilize an interactive rebase to enter "edit" mode in the correct place in the Git history. In this particular case we want to rebase the 3 commits we looked at onto base.

git rebase -i base

This will bring up the following in your editor.

pick 56fa164 Add title of README
pick d3e4eee Add Subtitle & Description of README
pick 49d9eb4 Add first paragraph of the README

In the interactive rebase buffer we can change the action for the middle commit to edit so it as follows.

pick 56fa164 Add title of README
edit d3e4eee Add Subtitle & Description of README
pick 49d9eb4 Add first paragraph of the README

When you save & quit the editor it will run the specified interactive rebase commands. In this case pick (meaning keep) the first commit and then stop on the second commit allowing for editing because we specified, edit. When it does this will drop you back to the console with a message similar to the following:

Stopped at d3e4eee...  Add Subtitle & Description of README
You can amend the commit now, with

	git commit --amend '-S'

Once you are satisfied with your changes, run

	git rebase --continue

Split the Commit

We want to split the changes currently held in this commit into multiple commits. To do this we need to reset the commit that we are currently on and then partially stage the changes back commit, then stage the other part and commit, and then continue the rebase.

So first we have to reset just the commit that we are checked out on and we want to do a soft reset. So we do the following:

git reset HEAD^

Now if we run git diff to see the now local ustagged changes we see the following.

❯ git di
diff --git a/README b/README
index 4b3b7a8..cf3aba6 100644
--- a/README
+++ b/README
@@ -1,2 +1,5 @@
 initial
 Title of README
+Subtitle of README
+
+Description of the README

As we can see we now have the changes that add both the subtitle & the description locally.

So we first want to stage a commit with just the Subtitle of README portion. To do this we need to use git add -p README to do a partial stage of the README files changes to just stage the Subtitle of README portion. See my post, git add patch won't split for details on how to accomplish this.

+
+Description of the README

If you run git status to check on things at this point it should look like this.

❯ git status
interactive rebase in progress; onto 8da3dd9
Last commands done (2 commands done):
	 pick 56fa164 Add title of README
	 edit d3e4eee Add Subtitle & Description of README
Next command to do (1 remaining command):
	 pick 49d9eb4 Add first paragraph of the README
	(use "git rebase --edit-todo" to view and edit)
You are currently splitting a commit while rebasing branch 'main' on '8da3dd9'.
	(Once your working directory is clean, run "git rebase --continue")

Changes to be committed:
	(use "git restore --staged <file>..." to unstage)
				modified:   README

Changes not staged for commit:
	(use "git add <file>..." to update what will be committed)
	(use "git restore <file>..." to discard changes in working directory)
				modified:   README

If we specifically check out the staged changes with git diff --staged we get the following.

❯ git diff --staged
diff --git a/README b/README
index 4b3b7a8..70e7d0a 100644
--- a/README
+++ b/README
@@ -1,2 +1,3 @@
 initial
 Title of README
+Subtitle of README

This is exactly what we wanted. But lets make sure the unstaged changes also represent what we want by running git diff.

❯ git diff
diff --git a/README b/README
index 70e7d0a..cf3aba6 100644
--- a/README
+++ b/README
@@ -1,3 +1,5 @@
 initial
 Title of README
 Subtitle of README
+
+Description of the README

Yep looks like they do. So now we just need create the first of the two commits that will replace the commit we marked for edit. This can be done as follows.

git commit -m "Add Subtitle to README"

From there we can stage the rest of the changes with git add README and create the second commit with the following.

git commit -m "Add Description to README"

Finish the Rebase

Now that the changes have been split up into separate commits like we wanted we now need to instruct it to finish the interactive rebase that we started with the edit. This is done as follows.

git rebase --continue

Once it is complete if we checkout our tree it will look as follows.

fe12bd6 - (HEAD -> main) Add first paragraph of the README
3566dec - Add Description to README
f676187 - Add Subtitle to README
56fa164 - Add title of README
8da3dd9 - (base) Initial commit

Exactly what we wanted!