How to split last commit into two in Git

Viewed 86367

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?

10 Answers

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'

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.

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).

Related