Insert a commit before the root commit in Git?

kch picture kch · Mar 14, 2009 · Viewed 54.4k times · Source

I've asked before about how to squash the first two commits in a git repository.

While the solutions are rather interesting and not really as mind-warping as some other things in git, they're still a bit of the proverbial bag of hurt if you need to repeat the procedure many times along the development of your project.

So, I'd rather go through pain only once, and then be able to forever use the standard interactive rebase.

What I want to do, then, is to have an empty initial commit that exists solely for the purpose of being the first. No code, no nothing. Just taking up space so it can be the base for rebase.

My question then is, having an existing repository, how do I go about inserting a new, empty commit before the first one, and shifting everyone else forward?

Answer

Aristotle Pagaltzis picture Aristotle Pagaltzis · Mar 15, 2009

There are 2 steps to achieving this:

  1. Create a new empty commit
  2. Rewrite history to start from this empty commit

We’ll put the new empty commit on a temporary branch newroot for convenience.

1. Create a new empty commit

There is a number of ways you can do this.

Using just plumbing

The cleanest approach is to use Git’s plumbing to just create a commit directly, which avoids touching the working copy or the index or which branch is checked out, etc.

  1. Create a tree object for an empty directory:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Wrap a commit around it:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Create a reference to it:

    git branch newroot $commit
    

You can of course rearrange the whole procedure into a one-liner if you know your shell well enough.

Without plumbing

With regular porcelain commands, you cannot create an empty commit without checking out the newroot branch and updating the index and working copy repeatedly, for no good reason. But some may find this easier to understand:

git checkout --orphan newroot
git rm -rf .
git clean -fd
git commit --allow-empty -m 'root commit'

Note that on very old versions of Git that lack the --orphan switch to checkout, you have to replace the first line with this:

git symbolic-ref HEAD refs/heads/newroot

2. Rewrite history to start from this empty commit

You have two options here: rebasing, or a clean history rewrite.

Rebasing

git rebase --onto newroot --root master

This has the virtue of simplicity. However, it will also update the committer name and date on every last commit on the branch.

Also, with some edge case histories, it may even fail due to merge conflicts – despite the fact that you are rebasing onto a commit that contains nothing.

History rewrite

The cleaner approach is to rewrite the branch. Unlike with git rebase, you will need to look up which commit your branch starts from:

git replace <currentroot> --graft newroot
git filter-branch master

The rewriting happens in the second step, obviously; it’s the first step that needs explanation. What git replace does is it tells Git that whenever it sees a reference to an object you want replaced, Git should instead look at the replacement of that object.

With the --graft switch, you are telling it something slightly different than normally. You are saying don’t have a replacement object yet, but you want to replace the <currentroot> commit object with an exact copy of itself except the parent commit(s) of the replacement should be the one(s) that you listed (i.e. the newroot commit). Then git replace goes ahead and creates this commit for you, and then declares that commit as the replacement for your original commit.

Now if you do a git log, you will see that things already look as you want them to: the branch starts from newroot.

However, note that git replace does not actually modify history – nor does it propagate out of your repository. It merely adds a local redirect to your repository from one object to another. What this means is that nobody else sees the effect of this replacement – only you.

That’s why the filter-branch step is necessary. With git replace you create an exact copy with adjusted parent commits for the root commit; git filter-branch then repeats this process for all the following commits as well. That is where history actually gets rewritten so that you can share it.