Jakub Arnold
Jakub Arnold

Reputation: 87270

How to split the last commit into two in Git?

I have two branches, master and forum, and I've just made some modifications in forum 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, the other with the remaining changes.

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 the last commit into two separate commits?

Upvotes: 291

Views: 93217

Answers (10)

spazm
spazm

Reputation: 4809

Goals:

  • I want to split a past commit $splitme into two.
  • I want to maintain the commit message.

Plan:

  1. Rebase interactive on the parent commit of $splitme, marking $splitme as edit.
  2. Unstage the files that should not be part of the first commit.
  3. Amend the current commit, maintaining the commit message, modifying if necessary.
  4. Stage back the files that were left out of the first commit.
  5. Commit, with a new message.
  6. Continue rebase.

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

git rebase -i $splitme^  # mark $splitme as edit
git reset HEAD^ -- $files
git commit --amend
git add $files
git commit
git rebase --continue

If I wanted to swap the files in the first commit with the files in the second commit, I would then rebase interactive again, swapping their commit line.

Upvotes: 101

Richard Smith
Richard Smith

Reputation: 14188

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

Upvotes: 1

Jose Cifuentes
Jose Cifuentes

Reputation: 596

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.

Upvotes: 1

ADTC
ADTC

Reputation: 10131

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.
  4. 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.
  3. 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.

Upvotes: 1

user2394284
user2394284

Reputation: 6038

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

Upvotes: 4

Michael Krelin - hacker
Michael Krelin - hacker

Reputation: 143329

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.

Upvotes: 22

hcs42
hcs42

Reputation: 13716

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

Upvotes: 343

dahlbyk
dahlbyk

Reputation: 77620

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.

Upvotes: 13

CB Bailey
CB Bailey

Reputation: 793369

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

Upvotes: 55

semanticart
semanticart

Reputation: 5464

git reset HEAD^

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

Upvotes: 16

Related Questions