How to split last commit into two in Git

GitVersion Control

Git Problem Overview


I have two working branches, master and forum and I've just made some modifications in forum branch, that I'd like to cherry-pick into master. But unfortunately, the commit I want to cherry-pick also contains some modifications that I don't want.

The solution would probably be to somehow delete the wrong commit and replace it with two separate commits, one with changes I want to pick in master, and others that doesn't belong there.

I've tried doing

git reset --hard HEAD^

which deleted all changes, so I had to go back with

git reset ORIG_HEAD

So my question is, what is the best way to split last commit into two separate commits?

Git Solutions


Solution 1 - Git

You should use the index. After doing a mixed reset ("git reset HEAD^"), add the first set of changes into the index, then commit them. Then commit the rest.

You can use "git add" to put all changes made in a file to the index. If you don't want to stage every modification made in a file, only some of them, you can use "git add -p".

Let's see an example. Let's suppose I had a file called myfile, which contains the following text:

something
something else
something again

I modified it in my last commit so that now it looks like this:

1
something
something else
something again
2

Now I decide that I want to split it into two, and I want the insertion of the first line to be in the first commit, and the insertion of the last line to be in the second commit.

First I go back to HEAD's parent, but I want to keep the modifications in file system, so I use "git reset" without argument (which will do a so-called "mixed" reset):

$ git reset HEAD^
myfile: locally modified
$ cat myfile
1
something
something else
something again
2

Now I use "git add -p" to add the changes I want to commit to the index (=I stage them). "git add -p" is an interactive tool that asks you about what changes to the file should it add to the index.

$ git add -p myfile
diff --git a/myfile b/myfile
index 93db4cb..2f113ce 100644
--- a/myfile
+++ b/myfile
@@ -1,3 +1,5 @@
+1
 something
 something else
 something again
+2
Stage this hunk [y,n,a,d,/,s,e,?]? s    # split this section into two!
Split into 2 hunks.
@@ -1,3 +1,4 @@
+1
 something
 something else
 something again
Stage this hunk [y,n,a,d,/,j,J,g,e,?]? y  # yes, I want to stage this
@@ -1,3 +2,4 @@
 something
 something else
 something again
+2
Stage this hunk [y,n,a,d,/,K,g,e,?]? n   # no, I don't want to stage this

Then I commit this first change:

$ git commit -m "Added first line"
[master cef3d4e] Added first line
 1 files changed, 1 insertions(+), 0 deletions(-)

Now I can commit all the other changes (namely the numeral "2" put in the last line):

$ git commit -am "Added last line"
[master 5e284e6] Added last line
 1 files changed, 1 insertions(+), 0 deletions(-)

Let's check the log to see what commits we have:

$ git log -p -n2 | cat
Commit 5e284e652f5e05a47ad8883d9f59ed9817be59d8
Author: ...
Date: ...

    Added last line

Diff --git a/myfile b/myfile
Index f9e1a67..2f113ce 100644
--- a/myfile
+++ b/myfile
@@ -2,3 +2,4 @@
 something
 something else
 something again
+2

Commit cef3d4e0298dd5d279a911440bb72d39410e7898
Author: ...
Date: ...

    Added first line

Diff --git a/myfile b/myfile
Index 93db4cb..f9e1a67 100644
--- a/myfile
+++ b/myfile
@@ -1,3 +1,4 @@
+1
 something
 something else
 something again

Solution 2 - Git

Goals:
  • I want to split a past commit (splitme) into two.
  • I want to maintain the commit message.
Plan:
  1. rebase interactive from one before splitme.
  2. edit splitme.
  3. Reset the files to split into a second commit.
  4. Amend commit, maintaining message, modify as necessary.
  5. Add back the files split out from the first commit.
  6. Commit with a new message.
  7. Continue rebase.

The rebase steps (1 & 7) can be skipped if the splitme is the most recent commit.

git rebase -i splitme^
# mark splitme commit with 'e'
git reset HEAD^ -- $files
git commit --amend
git add $files
git commit -m "commit with just some files"
git rebase --continue

If I wanted the split files to be committed first, I'd then rebase -i again and switch the order

git rebase -i splitme^
# swap order of splitme and 'just some files'

Solution 3 - Git

To change the current commit into two commits, you can do something like the following.

Either:

git reset --soft HEAD^

This undoes the last commit but leaves everything staged. You can then unstage certain files:

git reset -- file.file

Optionally restage parts of those files:

git add -p file.file

Make a new first commit:

git commit

The stage and commit the rest of the changes in a second commit:

git commit -a

Or:

Undo and unstage all of the changes from the last commit:

git reset HEAD^

Selectively stage the first round of changes:

git add -p

Commit:

git commit

Commit the rest of the changes:

git commit -a

(In either step, if you undid a commit that added a brand new file and want to add this to the second commit you'll have to manually add it as commit -a only stages changes to already tracked files.)

Solution 4 - Git

Run git gui, select the "Amend last commit" radio button, and unstage (Commit > Unstage From Commit, or Ctrl-U) changes that you do not want to go into first commit. I think that's the easiest way to go about it.

Another thing you could do is cherry-pick the change without committing (git cherry-pick -n) and then either manually or with git gui select desired changes before committing.

Solution 5 - Git

git reset HEAD^

the --hard is what's killing your changes.

Solution 6 - Git

I'm surprised nobody suggested git cherry-pick -n forum. This will stage the changes from the latest forum commit but not commit them - you can then reset away the changes you don't need and commit what you want to keep.

Solution 7 - Git

The double-revert-squash method

  1. Make another commit that removes the unwanted changes. (If it's per file, this is really easy: git checkout HEAD~1 -- files with unwanted changes and git commit. If not, files with mixed changes can be partially staged git reset file and git add -p file as an intermediate step.) Call this the revert.
  2. git revert HEAD – Make yet another commit, that adds back the unwanted changes. This is the double-revert
  3. Of the 2 commits you now made, squash the first onto the commit to split (git rebase -i HEAD~3). This commit now becomes free of the unwanted changes, for those are in the second commit.

Benefits

  • Preserves the commit message
  • Works even if the commit to split is not the last one. It only requires that the unwanted changes do not conflict with later commits

Solution 8 - Git

Since you're cherry-picking, you can:

  1. cherry-pick it with --no-commit option added.
  2. reset and use add --patch, add --edit or just add to stage what you want to keep.
  3. commit the staged changes.
  • To re-use original commit message, you can add --reuse-message=<old-commit-ref> or --reedit-message=<old-commit-ref> options to the commit command.
  1. Blow away unstaged changes with reset --hard.

Another way, preserving or editing the original commit message:

  1. cherry-pick the original commit as normal.
  2. Reverse the changes you don't want and use add to stage the reversal.
  • This step would be easy if you are removing what you added, but a bit tricky if you're adding what you removed or reversing a change.
  1. commit --amend to effect the reversal on the cherry-picked commit.
  • You'll get the same commit message again, which you can keep or revise as necessary.

Solution 9 - Git

This might be another solution targeted for cases where there is a huge commit and a small amount of files needs to be moved into a new commit. This will work if a set of <path> files are to be extracted out of the last commit at HEAD and all moved to a new commit. If multiple commits are needed the other solutions can be used.

First make patches into the staged and unstaged areas that would contain the changes to revert the code to before modification and after modification respectively:

git reset HEAD^ <path>

$ git status
On branch <your-branch>
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   <path>

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

        modified:   <path>

To understand what's gonna happen (arrow and comments are not part of command):

git diff --cached   -> show staged changes to revert <path> to before HEAD
git diff            -> show unstaged changes to add current <path> changes

Revert <path> changes in last commit:

git commit --amend  -> reverts changes on HEAD by amending with staged changes

Create new commit with <path> changes:

git commit -a -m "New Commit" -> adds new commit with unstaged changes

This has the effect of creating a new commit containing the changes extracted out of the last commit.

Solution 10 - Git

You can use git rebase -i <commit>, where <commit> is the latest commit you want to keep as-is. Add a break at each point where you would like to insert a new split-out commit. Then at each break, use git checkout -p <commit containing parts you want> to pull in the parts you want to split out, and commit them. Then git rebase --continue to remove those parts from later commits.

For the simple case of splitting the most recent commit, this looks like:

$ git rebase -i HEAD^
# add 'break' at the start

$ git checkout -p master # or whatever your branch is called
# choose the parts you want to split out

$ git commit
# commit the newly-split-out parts

$ git rebase --continue
# rebase the remaining parts of the change onto the split-out parts

This assumes you want the later commit to retain the original commit message; that's the situation I usually find myself in (factoring out some preparatory change).

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJakub ArnoldView Question on Stackoverflow
Solution 1 - Githcs42View Answer on Stackoverflow
Solution 2 - GitspazmView Answer on Stackoverflow
Solution 3 - GitCB BaileyView Answer on Stackoverflow
Solution 4 - GitMichael Krelin - hackerView Answer on Stackoverflow
Solution 5 - GitsemanticartView Answer on Stackoverflow
Solution 6 - GitdahlbykView Answer on Stackoverflow
Solution 7 - Gituser2394284View Answer on Stackoverflow
Solution 8 - GitADTCView Answer on Stackoverflow
Solution 9 - GitJose CifuentesView Answer on Stackoverflow
Solution 10 - GitRichard SmithView Answer on Stackoverflow