Most tutorials about Git history rewriting state that history should never ever be rewritten. Like all principles, it depends mostly on the exact context. The principle should probably be updated like this:
Public Git history should not be rewritten
The reason is that once the Git history has been pushed, it has been made public: other developers might have started working on top of it. Then, and only then, is rewriting the history an issue. It also means that sometimes, there are reasons to rewrite the history. This is fine as long as commits have not been pushed.
There are a lot of different ways to alter the history. Among them, the one I use regularly is to insert a new commit. For example, when I’m writing code for a new talk, I try to make it so that it follows a natural progression: each commit is a step I want to demo in my talk. But sometimes, it happens I forget a step. Or that I realize later the step is not what is required for a future step. In order to display a clean history for attendees who want to access the repo, I need to update steps early in the history.
Here are the methods I use, with their respective contexts.
Straightforward rebase
When the commit to be inserted - and the rest of the history - have nothing in common, the easiest and fastest way is to add the steps after, and then rebase
interactively.
Consider the following Git tree:
A---B---C---D---E ^ master
To rebase interactively, the git rebase -i
command is the one to use:
git rebase -i C^
This displays the following:
pick C Commit files c1.txt and c2.txt pick D Commit files d3.txt and d4.txt pick E Commit files e5.txt and e6.txt
With one’s favorite text editor, change the lines order:
pick E Commit files e5.txt and e6.txt pick C Commit files c1.txt and c2.txt pick D Commit files d3.txt and d4.txt
The final tree looks like this:
A---B---E---C---D ^ master
Split a single commit in two
Another common use-case is to split a single commit step into two different commit steps.
Let’s start from the same Git history as above, and use the same rebase
command.
This time, however, instead of changing the lines order, one needs to edit the commit to be split:
edit C Commit files c1.txt and c2.txt pick D Commit files d3.txt and d4.txt pick E Commit files e5.txt and e6.txt
To move all changes belonging to the C
commit back to the working tree:
git reset HEAD^
Now, files can be committed individually as desired:
git add c1.txt
git commit -m "Commit file c1.txt"
git add c2.txt
git commit -m "Commit file c2.txt"
Finally, the interactive rebase
should continue:
git rebase --continue
This yields the following result:
A---B---C1---C2---D---E ^ master
Branch and rebase
From time to time, rebasing is a bit more complex than the two above examples. In that case, the most important is to avoid wreaking havoc with the history. For that reason, and as a precaution, one should first create a branch at the point of insertion:
git checkout C
git checkout -b insert
This gives out the following:
A---B---C---D---E ^ ^ insert master HEAD
Now, it’s possible to add an additional commit (or several):
touch insert.txt
git add insert.txt
git commit -m "Commit file insert.txt"
This is the resulting tree:
A---B---C---D---E \ ^ \ master \ N ^ insert
Finally, let’s move the master
branch on top of the insert
branch that contains the newly-inserted commit.
It requires another option with the rebase
command, namely --onto
:
this allows to "pluck" a part of the Git tree from a commit point, to "plant" it on top of another commit.
git checkout master
git rebase --onto insert C (1)
1 | Get the commits between the current branch (master ) and C , and move them on top of insert |
This is the resulting tree:
A---B---C \ \ \ N---D---E ^ ^ insert master
Or looking at it in another way:
A---B---C---N---D---E ^ ^ insert master
Ideally, the insert
branch should now be deleted, though it’s not strictly necessary:
git branch -d insert
Conclusion
Git is a huge beast: it allows a lot, and in many different ways. To insert a commit in the history, I showed 3 alternatives I regularly use:
- Simple interactive rebase with a reordering of the commits
- More comple interactive rebase with the split of an existing commit into several different ones
- Rebase onto
There might be more. Know your options, and use the one that best fits your use-case.