How to Split a Single Git Commit into Several Commits?
Context
You have a Git commit which contains several changes. For example, a commit containing the addition of different features, or a feature with additional fixes. You have put them together in one commit and you want to fix that.
Question
Splitting in several commits can be done by extracting one change from the commit into another commit, then repeating the process for another change. Consequently, we will focus on the extraction of a single change, leading to the question: How to extract a change from a Git commit into a separate commit?
Method
Rewriting a Git history can be done in plenty of ways. Unfortunately, messing it up can be done as well. If you feel unsafe about rewriting the history, give a look here first. Better feel safe than sorry.
Several aspects need to be considered here. We may want to extract a change from the last commit or from an earlier commit. We may want to extract a change that is overlapping with the other changes in the commit or which is completely separate. We may want to extract the change next to the current commit or to a different place in the commits history. We will address all these points and more by starting from the most simple case and growing in complexity progressively.
Just Redo the Last Commit
Let's be honest: the most generic way to fix a mistake is to restart from scratch.
If the last commit you created contains several changes, you can just forget that commit and create several ones instead.
You can do so by removing the last commit while preserving the files with git reset HEAD^
.
The rest is just the common process of creating new commits, as if nothing happened in the first place.
You can also create 3, 4, or more commits while you are at it.
Here is a complete example:
git reset HEAD^ # Delete the current commit (keep the changes)
git add <change 1> # Feed the first commit
git commit -m 'Change 1' # Commit
git add <change 2> # Feed the 2d commit
git commit -m 'Change 2' # Commit
That is fine for the smallest commits, for which there is little to redo. But when there is a lot in the commit, and you just want to extract a piece out of it, spending time redoing the whole stuff can feel like a waste of time and error prone.
Extracting an Independent File after the Last Commit
Actually, you can slightly adapt the previous process to not completely restart from scratch.
If you reset with the --soft
option, you can forget the commit but keep all its changes in the stage area.
Calling git commit
would just create the same commit than before.
Unstage the files you don't want before to commit and create an additional commit for them next:
git reset --soft HEAD^ # Delete the current commit (keep the changes staged)
git rm --cached <change 2> # Unstage the file to extract (keep the file itself)
git commit -m 'Change 1' # Commit
git add <change 2> # Feed the 2d commit
git commit -m 'Change 2' # Commit
Although it is a natural way to go, you can optimize a bit. Indeed, with this procedure you:
- go back to the staging,
- fix the staging,
- commit again.
You can reduce it to two steps only:
- unstage the file for a new commit,
- amend the previous commit instead of creating a new one.
git rm --cached <change 2> # Stage the removal of the file to extract (keep the file itself)
git commit --amend -m 'Change 1' # Amend the last commit to include this removal
git add <change 2> # Feed the new commit with the extracted file
git commit -m 'Change 2' # Commit
The command git rm --cached
, like the git add
, can include several files or be repeated if required.
Extracting an Independent Chunk after the Last Commit
Here you want to remove one or several changes from a file, but not all the changes. The commands we have seen so far can deal with files, but not a smaller granularity. To go further, you need to use the interactive staging.
It can be done in command line with git add -i
.
However, it can be time consuming with complex commits.
I personnally like the feature, but only when done through a graphical interface.
It saves a lot of time.

With the command line, you have to remove the last commit with git reset --soft HEAD^
.
You can then update the stage interactively to remove the chunks you don't want before to commit.
With the graphical interface, you can often enter in an "amend mode". It allows you to edit the last commit without removing it and creating it again. The graphical interface can also let you chose which file to look at and show all the chunks of that file at once. It makes it quick to go to the chunk we want and add or remove it. You can often do it on the selected lines rather than a complete chunk too.
We don't go deeper on the chunk aspect.
You can adapt the procedures by selecting chunks with your preferred tool instead of using the git add
and git rm
commands.
Extracting an Overlapping Change after the Last Commit
Now we reach the limits of Git in terms of granularity. Git deals with the syntax, but not with the semantic. If several changes occur in the same chunk, Git only sees a single chunk of change. It cannot discern that several things are happening there.
At this point, we need to help Git by editing the file.
It can be done manually or with git commands to retrieve a previous state, like git checkout <commit> <file>
.
Of course, combine them as you see fit depending on your needs.
The advantage of editing the file is that we don't have to play with the staging.
Since the file is cleaned, just stage it completely, which can be done automatically when commiting with the --all|-a
option.
For example:
<editing> # Remove the changes to extract
git commit --amend -am 'Change 1' # Amend the last commit
<editing> # Redo the extracted changes
git commit -am 'Change 2' # Commit
If the removal of the changes can be done quickly, it is often a satisfying procedure. Just remove the stuff, save and commit, then cancel the removal (often CTRL+Z), save and commit, period.
But the removal may take some time and require to change several places at once.
In this case, we may prefer to avoid doing a manual pass a second time to cancel all of them.
In which case, we can use Git to automatize the cancellation.
We do so by creating a commit that contains the extraction and by exploiting git revert
to cancel it.
However, it creates additional commits that we have then to clean.
Here is the complete procedure:
# Preparation phase
<editing> # Remove the change 2 to extract
MESSAGE_1ONLY='Change 1' # Stores the final name of the commit with change 1
MESSAGE_2ONLY='Change 2' # Stores the final name of the commit with change 2
# Commiting phase
git commit -am 'Remove change 2' # Commit the removal
git revert -n HEAD # Revert the removal and stage it (no commit)
git commit -m "$MESSAGE_2ONLY" # Commit with your own message
# Cleaning phase
COMMIT_1AND2=$(git rev-parse HEAD~2) # Identify the commit with change 1 & 2
COMMIT_REMOVE2=$(git rev-parse HEAD~1) # Identify the commit which removes the change 2
COMMIT_2ONLY=$(git rev-parse HEAD) # Identify the commit with change 2 only
git reset --hard ${COMMIT_REMOVE2} # Reset to the state with only change 1
git reset --soft ${COMMIT_1AND2} # Reset to the first commit, but keeping the state
git commit --amend -m "${MESSAGE_1ONLY}" # Update this commit with that state
git cherry-pick ${COMMIT_2ONLY} # Redo the commit with change 2
Now it becomes ugly, but this is for a good cause. As you can see, there is a preparation phase where we provide all the information, including the change to extract (change 2) by removing it manually. Then, the commiting phase produces all the commits we need. In particular, it produces the commit having only change 2. But for change 1, it keeps the initial commit with change 1 and change 2, and creates an additional commit without change 2. The goal of the cleaning phase that come after is to squash these two commits into one, which contains only change 1. Since changing a commit cannot be done in the middle of a branch, we must recreate all of them, which explains the multiple commands.
Although the procedure is long, it only requires a single manual editing.
After setting the variables MESSAGE_1ONLY
and MESSAGE_2ONLY
, you can copy the remaining commands and execute them all at once.
Extracting a Change before the Last Commit
We have seen so far how to extract a change after the last commit. But extracting it after the original commit means the last commit becomes the one extracted. If you need to remove several things, you cannot repeat the procedure. You first need to reorder the commits to bring back the "fat" commit last.
If your change is not overlapping with the rest, it is easy to do so. You can use the right procedure to extract a file or a chunk after the commit. You can then inverse the two last commits, with no conflict, with these commands:
COMMIT_1=$(git rev-parse HEAD~1) # Identify the commit 1
COMMIT_2=$(git rev-parse HEAD) # Identify the commit 2
git reset --hard ${COMMIT_1}~1 # Reset to the state before commit 1
git cherry-pick ${COMMIT_2} ${COMMIT_1} # Redo commit 2 then commit 1
However, this procedure works without conflict only if your changes do not overlap.
If they overlap, reordering the commits will require to resolve conflicts.
You may prefer instead to extract the change directly before the commit.
To do so, you need to place yourself before the commit, reproduce the change (manually or with git git checkout
), and then create the remaining commit:
COMMIT=$(git rev-parse HEAD) # Identify the last commit
git reset --hard ${COMMIT}~1 # Reset the branch to the previous commit
<editing> # Reproduce change 1
git add . # Stage change 1
git commit -m "Change 1" # Create the commit with change 1 only
git revert -n HEAD # Revert it without creating a commit
git cherry-pick -n ${COMMIT} # Reproduce the commit with changes 1 & 2, no commit again
git commit -m "Change 2" # Create the resulting commit with change 2 only
There is two things to understand here:
-
Why no conflict occurs when cherry-picking after adding two new commits?
The commit with changes 1 & 2 builds on a specific state (with none of these changes). To cherry-pick it without having conflicts, we must be in the same state. By commiting the change 1 and reverting it, we go back to this state, allowing the cherry-pick. -
How reverting the commit with change 1 acts like a revert of change 1 in the commit with both changes?
There is actually no attempt to adapt the revert for the incoming commit. After the commit with change 1, we reach a state with change 1 only. After reverting it and cherry-picking, we reach a state with changes 1 & 2. All we do then, is to ask Git to create a single commit for this new state with 1 & 2. In other words, create a commit to pass from a state with change 1 to a state with changes 1 & 2. The result is, by construction, to produce a commit that contains only change 2.
Extracting a Change from Another Commit
So far, we have focused on extracting a change from the last commit.
The advantage of the last commit is that it can be directly edited with git commit --amend
.
Most of the procedures we have seen previously exploit this command.
At the opposite, changing a commit that is not the last one cannot be done immediately.
We first need to start from the right commit with git reset
, update as needed, then reproduce the remaining commits with git cherry-pick
.
The procedure to extract a change before the last commit shows this aspect by working one commit before the last one.
Extracting a change before any other commit is a generalisation of this case. The difference is that we need to reproduce a sequence of remaining commits instead of only one. Here is the procedure:
COMMIT_LAST=$(git rev-parse HEAD) # Identify the last commit
COMMIT_SPLIT=<commit SHA-1> # Identify the commit to split
git reset --hard ${COMMIT_SPLIT}~1 # Reset the branch before the commit to split
<editing> # Reproduce change 1
git add . # Stage change 1
git commit -m "Change 1" # Create the commit with change 1 only
git revert -n HEAD # Revert it without creating a commit
git cherry-pick -n ${COMMIT_SPLIT} # Reproduce the commit with changes 1 & 2, no commit again
git commit -m "Change 2" # Create the resulting commit with change 2 only
git cherry-pick ${COMMIT_SPLIT}..${COMMIT_LAST} # Reproduce all the remaining commits until the last
This procedure can also be adapted to extract a change after any commit:
COMMIT_LAST=$(git rev-parse HEAD) # Identify the last commit
COMMIT_SPLIT=<commit SHA-1> # Identify the commit to split
git reset --hard ${COMMIT_SPLIT} # Reset the branch to the commit to split
<editing> # Remove change 2
git add . # Stage the removal
git commit --amend -m "Change 1" # Update the commit, resulting in change 1 only
git revert -n HEAD # Revert it without creating a commit
git cherry-pick -n ${COMMIT_SPLIT} # Reproduce the commit with changes 1 & 2, no commit
git commit -m "Change 2" # Create the resulting commit with change 2 only
git cherry-pick ${COMMIT_SPLIT}..${COMMIT_LAST} # Reproduce all the remaining commits until the last
The difference focuses on the lines 3-6:
# | Extract before | Extract after |
---|---|---|
3 | reset before the commit to split | reset to the commit to split |
4 | add the change to extract (change 1) | remove the change to extract (change 2) |
5 | stage the update | |
6 | create a new commit from it | update the existing commit |
Extracting a Change Elsewhere in the Commits History
Actually, there is not much to say at this point. With the previous procedures, you know how to split the commit on the spot. Once you have extracted the change, you need to move it in the history.
An alternative is to insert the change directly where needed. For instance, rather than splitting and moving it before, you may directly create a commit with your change earlier in the history. However, to reproduce the next commits after it, you need either to:
- resolve the conflicts for each commit
- revert the insertion before to reproduce the commits, and then move the revert throughout the commits, resolving the conflicts anyway
At the end, it is equivalent to extract it on the spot and move it.
Answer
To extract a change from a Git commit, various commands can be used depending on the context, among which:
git reset
to go back to a specific commit, either by using--hard
to forget the changes altogether (and reproduce them later), by using--soft
to keep the changes staged and remove the ones we don't want withgit rm --cached
, or by using none of them to keep the changes but choose which will be staged withgit add
.git cherry-pick
to reproduce a commit, preferably from a state equivalent to the state it is picked from to avoid conflicts, possibly with-n
to not create the commit immediately.git revert
to cancel the last commit, possibly with-n
to not create the commit immediately, unless we plan to squash it later.git commit
to finally create the commit with the staged changes, or with--amend
to update the last commit instead of creating a new one.
A particular care should be provided to managing the potential conflicts between changes. Depending on which is preferable, one can either resolve the conflicts or manually add/remove the change.
Related Questions
Bibliography
- Git Book: https://git-scm.com/book/en/v2