My workflow usually involves squashing many commits into a single one in preparation for a pull request. But sometimes, I need to perform the opposite action — splitting a single commit into many.
It may be that I realize one of my commits would better serve future developers as two distinct commits in history (e.g. a refactoring commit that “makes the change easy”, and a feature commit that “makes the easy change”). Or perhaps, I know I can unblock work for others by extracting some of the changes in the commit into a separate pull request (e.g. several features need the same table).
Whatever the case, when I need to split a commit, I rebase, edit, reset, and commit.
Let’s first do an interactive rebase. I like to keep my branch up to date with
the latest and greatest, so we’ll go ahead and fetch and rebase from
$ git fetch origin $ git rebase -i origin/master
We will be given a choice of what to do with our commits. In this case, we have two of them:
pick da8f4d4 Adds greeting to application pick 1c5e3b7 Adds greeting to jobs # Rebase e42f496..1c5e3b7 onto e42f496 (2 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit
1c5e3b7 (“Adds greeting to jobs”) is fine as it is. But suppose we want
to split commit
da8f4d4 (“Adds greeting to application”) to more closely match
the “jobs” commit — separating the changes in views, controllers, and
models to their own commits.
In order to edit commit
da8f4d4, we want the rebase to stop at that commit. So
change that commit’s
edit (or just
edit da8f4d4 Adds greeting to application pick 1c5e3b7 Adds greeting to jobs # Rebase e42f496..1c5e3b7 onto e42f496 (2 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit
Save and exit. We should now be in an editable state for commit
Stopped at da8f4d4... Adds greeting to application You can amend the commit now, with git commit --amend Once you are satisfied with your changes, run git rebase --continue
The output tells us about two options, neither of which are what we want:
❌ amend the commit message with
git commit --amend, or
❌ continue with
git rebase --continue
We don’t want to simply amend the commit message — we want to change the commit itself. And we don’t want to continue with the rebase just yet. What we want is a way to restart this commit as though we were trying to stage files for the first time. And that’s where secret option number three comes in:
✅ reset the code with
git reset HEAD^and re-commit!
$ git reset HEAD^ Unstaged changes after reset: M app/controllers/application_controller.rb M app/models/application_record.rb M app/views/layouts/application.html.erb
Excellent! We now have all of our files unstaged, which we could confirm by
git status. Let’s create the new commits next.
We can now choose to add the changes in as many commits as we want. We could
git add --patch to select a subset of a file’s changes. In this
exercise, we want to add the views, controllers, and models in separate commits:
# first commit $ git add app/views $ git ci -m 'adds greeting to views' [detached HEAD 81c8f2b] adds greeting to views 1 file changed, 1 insertion(+) # second commit $ git add app/controllers $ git ci -m 'adds greeting to controllers' [detached HEAD 3bf0765] adds greeting to controllers 1 file changed, 5 insertions(+) # third commit $ git add app/models $ git ci -m 'adds greeting to models' [detached HEAD d77419c] adds greeting to models 1 file changed, 4 insertions(+)
Now that we’re satisfied with our new commits, we can continue with the rebase:
$ git rebase --continue Successfully rebased and updated refs/heads/splitting-commit.
And voilà! We have split our commit into three, and now each commit has changes for one section of the application:
$ git log --oneline --decorate -4 f5fd17f (HEAD -> splitting-commit) Adds greeting to jobs d77419c adds greeting to models 3bf0765 adds greeting to controllers 81c8f2b adds greeting to views
If you enjoy rewriting history and want to learn more about the many ways to do so, take a look at the Rewriting History docs, this wonderful blog post, or if you prefer videos, you can’t go wrong with Chris Toomey’s Crafting History with Rebase video on Upcase.