Break a previous commit into multiple commits

Previous answers have covered the use of git rebase -i to edit the commit that you want to split, and committing it in parts.

This works well when splitting the files into different commits, but if you want to break apart changes to the individual files, there's more you need to know.

Having got to the commit you want to split, using rebase -i and marking it for edit, you have two options.

  1. After using git reset HEAD~, go through the patches individually using git add -p to select the ones you want in each commit

  2. Edit the working copy to remove the changes you do not want; commit that interim state; and then pull back the full commit for the next round.

Option 2 is useful if you're splitting a large commit, as it lets you check that the interim versions build and run properly as part of the merge. This proceeds as follows.

After using rebase -i and editing the commit, use

git reset --soft HEAD~

to undo the commit, but leave the committed files in the index. You can also do a mixed reset by omitting --soft, depending on how close to the final result your initial commit is going to be. The only difference is whether you start with all the changes staged or with them all unstaged.

Now go in and edit the code. You can remove changes, delete added files, and do whatever you want to construct the first commit of the series you're looking for. You can also build it, run it, and confirm that you have a consistent set of source.

Once you're happy, stage/unstage the files as needed (I like to use git gui for this), and commit the changes through the UI or the command line

git commit

That's the first commit done. Now you want to restore your working copy to the state it had after the commit you are splitting, so that you can take more of the changes for your next commit. To find the sha1 of the commit you're editing, use git status. In the first few lines of the status you'll see the rebase command that is currently executing, in which you can find the sha1 of your original commit:

$ git status
interactive rebase in progress; onto be83b41
Last commands done (3 commands done):
   pick 4847406 US135756: add debugging to the file download code
   e 65dfb6a US135756: write data and download from remote
  (see more in file .git/rebase-merge/done)
...

In this case, the commit I'm editing has sha1 65dfb6a. Knowing that, I can check out the content of that commit over my working directory using the form of git checkout which takes both a commit and a file location. Here I use . as the file location to replace the whole working copy:

git checkout 65dfb6a .

Don't miss the dot on the end!

This will check out, and stage, the files as they were after the commit you're editing, but relative to the previous commit you made, so any changes you already committed won't be part of the commit.

You can either go ahead now and commit it as-is to finish the split, or go around again, deleting some parts of the commit before making another interim commit.

If you want to reuse the original commit message for one or more commits, you can use it straight from the rebase's working files:

git commit --file .git/rebase-merge/message

Finally, once you've committed all the changes,

git rebase --continue

will carry on and complete the rebase operation.


Use git rebase --interactive to edit that earlier commit, run git reset HEAD~, and then git add -p to add some, then make a commit, then add some more and make another commit, as many times as you like. When you're done, run git rebase --continue, and you'll have all the split commits earlier in your stack.

Important: Note that you can play around and make all the changes you want, and not have to worry about losing old changes, because you can always run git reflog to find the point in your project that contains the changes you want, (let's call it a8c4ab), and then git reset a8c4ab.

Here's a series of commands to show how it works:

mkdir git-test; cd git-test; git init

now add a file A

vi A

add this line:

one

git commit -am one

then add this line to A:

two

git commit -am two

then add this line to A:

three

git commit -am three

now the file A looks like this:

one
two
three

and our git log looks like the following (well, I use git log --pretty=oneline --pretty="%h %cn %cr ---- %s"

bfb8e46 Rose Perrone 4 seconds ago ---- three
2b613bc Rose Perrone 14 seconds ago ---- two
9aac58f Rose Perrone 24 seconds ago ---- one

Let's say we want to split the second commit, two.

git rebase --interactive HEAD~2

This brings up a message that looks like this:

pick 2b613bc two
pick bfb8e46 three

Change the first pick to an e to edit that commit.

git reset HEAD~

git diff shows us that we just unstaged the commit we made for the second commit:

diff --git a/A b/A
index 5626abf..814f4a4 100644
--- a/A
+++ b/A
@@ -1 +1,2 @@
 one
+two

Let's stage that change, and add "and a third" to that line in file A.

git add .

This is usually the point during an interactive rebase where we would run git rebase --continue, because we usually just want to go back in our stack of commits to edit an earlier commit. But this time, we want to create a new commit. So we'll run git commit -am 'two and a third'. Now we edit file A and add the line two and two thirds.

git add . git commit -am 'two and two thirds' git rebase --continue

We have a conflict with our commit, three, so let's resolve it:

We'll change

one
<<<<<<< HEAD
two and a third
two and two thirds
=======
two
three
>>>>>>> bfb8e46... three

to

one
two and a third
two and two thirds
three

git add .; git rebase --continue

Now our git log -p looks like this:

commit e59ca35bae8360439823d66d459238779e5b4892
Author: Rose Perrone <[email protected]>
Date:   Sun Jul 7 13:57:00 2013 -0700

    three

diff --git a/A b/A
index 5aef867..dd8fb63 100644
--- a/A
+++ b/A
@@ -1,3 +1,4 @@
 one
 two and a third
 two and two thirds
+three

commit 4a283ba9bf83ef664541b467acdd0bb4d770ab8e
Author: Rose Perrone <[email protected]>
Date:   Sun Jul 7 14:07:07 2013 -0700

    two and two thirds

diff --git a/A b/A
index 575010a..5aef867 100644
--- a/A
+++ b/A
@@ -1,2 +1,3 @@
 one
 two and a third
+two and two thirds

commit 704d323ca1bc7c45ed8b1714d924adcdc83dfa44
Author: Rose Perrone <[email protected]>
Date:   Sun Jul 7 14:06:40 2013 -0700

    two and a third

diff --git a/A b/A
index 5626abf..575010a 100644
--- a/A
+++ b/A
@@ -1 +1,2 @@
 one
+two and a third

commit 9aac58f3893488ec643fecab3c85f5a2f481586f
Author: Rose Perrone <[email protected]>
Date:   Sun Jul 7 13:56:40 2013 -0700

    one

diff --git a/A b/A
new file mode 100644
index 0000000..5626abf
--- /dev/null
+++ b/A
@@ -0,0 +1 @@
+one

git rebase -i will do it.

First, start with a clean working directory: git status should show no pending modifications, deletions, or additions.

Now, you have to decide which commit(s) you want to split.

A) Splitting the most recent commit

To split apart your most recent commit, first:

$ git reset HEAD~

Now commit the pieces individually in the usual way, producing as many commits as you need.

B) Splitting a commit farther back

This requires rebasing, that is, rewriting history. To specify the correct commit, you have several choices:

  • If it is three commits back, then

      $ git rebase -i HEAD~3
    

    where 3 is how many commits back it is.

  • If it is farther back in the tree than you want to count, then

      $ git rebase -i 123abcd~
    

    where 123abcd is the SHA1 of the commit you want to split up.

  • If you are on a different branch (e.g., a feature branch) that you want to merge into master:

      $ git rebase -i master
    

When you get the rebase edit screen, find the commit you want to break apart. At the beginning of that line, replace pick with edit (e for short). Save the buffer and exit. Rebase will now stop just after the commit you want to edit. Then:

$ git reset HEAD~

Commit the pieces individually in the usual way, producing as many commits as you need.

Finally

$ git rebase --continue

From git-rebase manual (SPLITTING COMMITS section)

In interactive mode, you can mark commits with the action "edit". However, this does not necessarily mean that git rebase expects the result of this edit to be exactly one commit. Indeed, you can undo the commit, or you can add other commits. This can be used to split a commit into two:

  • Start an interactive rebase with git rebase -i <commit>^, where <commit> is the commit you want to split. In fact, any commit range will do, as long as it contains that commit.

  • Mark the commit you want to split with the action "edit".

  • When it comes to editing that commit, execute git reset HEAD^. The effect is that the HEAD is rewound by one, and the index follows suit. However, the working tree stays the same.

  • Now add the changes to the index that you want to have in the first commit. You can use git add (possibly interactively) or git gui (or both) to do that.

  • Commit the now-current index with whatever commit message is appropriate now.

  • Repeat the last two steps until your working tree is clean.

  • Continue the rebase with git rebase --continue.

Tags:

Git