Insert a commit before the root commit in Git?
Asked Answered
I

16

292

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?

Iso answered 14/3, 2009 at 5:31 Comment(8)
From one obsessive, insane history editor to another, thanks for posting the question! ;DTobacco
In particular, I'm looking at some of the SVN repositories I'm converting to Git.Imprint
In @kch's defense, one perfectly legitimate reason is one that I find myself in: Adding a snapshot of a historical version that was never captured in the repo.Redundancy
I have another legitimate reason! Adding an empty commit before the first in order to be able to rebase to the first commit and remove binary bloat added in the initial commit of a repository (:Sentry
Wait, how often are you going to have to squash the first two commits in a project? I don't understand this questionPhosphoric
Related: Edit/amend/modify/change the first/root/initial commit in Git?.Orrery
In fact, I just add .gitignore as my root git commitCorelli
If you find yourself using rebase as I did, you may also find the --rebase-merges and --committer-date-is-author-date flags useful in order to preserve merge history (default rebase behavior is to drop merge commits and flatten everything into a linear structure) and original commit dates.Demagnetize
C
408

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.

Cluny answered 15/3, 2009 at 7:45 Comment(24)
Nice. Looks like you missed the initial empty commit, or am I missing something? Also, I tried a bit without creating a new repository, but rebase went berserk with changesets dealing with submodules. You noticed any of that?Iso
Thanks for the pointer – yes, I did miss the empty commit. Fixed. Haven’t used submodules, so I don’t know how that turns out.Cluny
I have now used this solution with submodules and didn't have a problem with it. As with everything: not to say there isn't any, just that I didn't hit any.Iso
That --onto newroot option is redundant; you can do without it because the argument you pass it, newroot, is the same as the upstream argument -- newroot.Perineum
cherry-pick and rebase can be simplified into one command: git rebase --onto newroot --root master, see my answer belowEvensong
Wow! Neat and impressive solution. Everyday I discover my knowledge of git is tinier and tinier compared to what the tool's possibilities. I guess that makes me a bit defensive too :-). Thanks for the question, thanks for the neat answer!Drive
Why not use porcelain instead plumbing commands?. I'd replace git symbolic-ref HEAD refs/heads/newroot with git checkout --orphan newrootMintun
@nenopera: because this answer was written before git-checkout had that switch. I’ve updated it to mentioned that approach first, thanks for the pointer.Cluny
The new root commit's time stamps are ahead of its descendants, so in some Git UI implementations (such as SourceTree) it looks as though the new root is separated from the rest of the commits, and at the very top of a chronologically-ordered commit view. If this makes you twitch, you can rebase your branches again on the new root to update their time stamps, or update the new root's timestamps to a time before all other commits were originally made. There are several heavily voted upon solutions on SO for this.Bujumbura
I tried this solution, but it didn't work. I need to insert not empty commit before first, but all other commit trees must stay the same. But git rebase is trying to merge inserted commit with root and changes all other commit trees.Byers
Beware that git rm -rf . modifies your worktree. To avoid this, pull in a suitable empty commit of a suitable repository like this: git fetch https://github.com/hilbix/empty.git rc; git rebase --onto FETCH_HEAD --keep-empty --root masterObnubilate
Or you can always just add --cached to git rm if you don’t want it to touch your working copy.Cluny
@AlexanderKuzin: you need an approach based on git filter-branch then. You can use Kent’s answer, and I’ll also cover this (but slightly differently and a little better IMO) in this answer when I have the time.Cluny
The first paragraph after "Historical answer" worked great for me! It seems much easier than what's labeled the "Mid-2017 answer". ConfusingQuaternion
If your newroot is not empty, use git rebase --merge -s recursive -X theirs --onto newroot --root master to resolve all conflicts automatically (see this answer). @AlexanderKuzinAetolia
@Aetolia Why does one have to rebase? Why doesn't adding files by amending the root commit work?Jamnis
@Jamnis You can amend only the last commit, so if your repository only contains the root commit, it may work, otherwise you will have to rebase all other commits in the repo on top of the amended root commit anyway. But even then, the topic implies that you don't want to change the root commit, but want to insert another one before the existing root instead.Aetolia
@Aetolia thanks. I didn't know amending only works with last commit.Jamnis
Oddly, I got rebase conflicts using either one of the approaches.Fredel
@tartaruga_casco_mole: Try the history rewrite approach I just added.Cluny
Each commit in the master now has a new hash after following the "History rewrite" path. If this is expected (why?), let's explicitly point this out in the answer.Carmella
Adding the empty lone commit works fine. But the option to rewrite history seems to duplicate all commits, I get one correct master branch and a second branch just like the master branch but labelled with "(refs/original/refs/heads/master)" at the leaf and "(replaced)" next to the duplicate of the original commit. The rebase-option worked fine on my tests repo, but now that I launched it on my actual repo it looks like it's going to take ages. Edit: Saw https://mcmap.net/q/11905/-insert-a-commit-before-the-root-commit-in-git how to remove duplicates. Maybe add that to your answer.Lilialiliaceous
This is probably a dumb question but what goes in the place of currentroot? The hash to the first ever commit?Collotype
@BharathRadhakrishnan Exactly.Cluny
E
37

Merge of Aristotle Pagaltzis's and Uwe Kleine-König's answers and Richard Bronosky's comment.

git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d
# touch .gitignore && git add .gitignore # if necessary
git commit --allow-empty -m 'initial'
git rebase --onto newroot --root master
git branch -d newroot

(just to put everything in one place)

Evensong answered 16/3, 2012 at 11:2 Comment(6)
This is excellent. It'd be nice if this could be what a git rebase -i --root did internally.Fidellia
Yep, I was surprised to find out that it doesn't.Evensong
I had to change rebase command to git rebase newroot master, because of error.Assail
@antony-hatchkins thanks for this. I have an existing git repo and (for various reasons which I wont go into here) I am trying to append a NON-EMPTY git commit as my first commit. So I replaced git commit --allow-empty -m 'initial' with git add .; git commit -m "initial laravel commit"; git push; And then this rebase step: git rebase --onto newroot --root master is failing with a TON of merge conflicts. Any advice? :((Ubana
@Ubana try an empty commit :)Evensong
Can you explain more what this does and how it does it?Rota
T
12

I like Aristotle's answer. But found that for a large repository (>5000 commits) filter-branch works better than rebase for several reasons 1) it's faster 2) it doesn't require human intervention when there's a merge conflict. 3) it can rewrite the tags -- preserving them. Note that filter-branch works because there is no question about the contents of each commit -- it is exactly the same as before this 'rebase'.

My steps are:

# first you need a new empty branch; let's call it `newroot`
git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d

# then you apply the same steps
git commit --allow-empty -m 'root commit'

# then use filter-branch to rebase everything on newroot
git filter-branch --parent-filter 'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat master

Note that the '--tag-name-filter cat' options means that tags will be rewritten to point to the newly created commits.

Thorazine answered 29/3, 2013 at 15:55 Comment(2)
This doesn't help to create non empty commits that is also an interesting use case.Earthiness
In comparison with other solutions, yours has just one insignificant side-effect: it changes hashes, but the whole history stays untouched. Thank you!Correggio
P
12

To add an empty commit at the start of a repository, if you forgot to create an empty commit immediately after "git init":

git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
Popp answered 9/12, 2017 at 21:0 Comment(2)
4b825dc... is the hash of the empty tree: stackoverflow.com/questions/9765453/…Caliban
Don't forget to change the date of root commit if that matters to you, otherwise you will use the present date.Demagnetize
T
7

I think that using git replace and git filter-branch is a better solution than using a git rebase:

  • better performance
  • easier and less risky (you could verify your result at each step and undo what you did...)
  • work well with multiple branches with guaranteed results

The idea behind it is to:

  1. Create a new empty commit far in the past
  2. Replace the old root commit by a commit exactly similar except that the new root commit is added as a parent
  3. Verify that all is as expected and run git filter-branch
  4. Once again, verify that all is OK and clean the no more needed git files

Here is a script for the 2 first steps:

#!/bin/bash
root_commit_sha=$(git rev-list --max-parents=0 HEAD)
git checkout --force --orphan new-root
find . -path ./.git -prune -o -exec rm -rf {} \; 2> /dev/null
git add -A
GIT_COMMITTER_DATE="2000-01-01T12:00:00" git commit --date==2000-01-01T12:00:00 --allow-empty -m "empty root commit"
new_root_commit_sha=$(git rev-parse HEAD)

echo "The commit '$new_root_commit_sha' will be added before existing root commit '$root_commit_sha'..."

parent="parent $new_root_commit_sha"
replacement_commit=$(
 git cat-file commit $root_commit_sha | sed "s/author/$parent\nauthor/" |
 git hash-object -t commit -w --stdin
) || return 3
git replace "$root_commit_sha" "$replacement_commit"

You could run this script without risk (even if doing a backup before doing action you never did before is a good idea ;) ), and if the result is not the one expected, just delete the files created in the folder .git/refs/replace and try again ;)

Once you have verified that the state of the repository is what you expect, run the following command to update the history of all branches:

git filter-branch -- --all

Now, you must see 2 histories, the old one and the new one (see help on filter-branch for more information). You could compare the 2 and check again if all is OK. If you are satisfied, delete the no more needed files:

rm -rf ./.git/refs/original
rm -rf ./.git/refs/replace

You could return to your master branch and delete the temporary branch:

git checkout master
git branch -D new-root

Now, all should be done ;)

Tarpeia answered 31/5, 2015 at 13:57 Comment(1)
Using git replace is an excellent approach. But now git filter-repo should be used instead of git-filter-branch.Summerly
F
7

To switch the root commit:

First, create the commit you want as the first.

Second, switch the order of the commits using:

git rebase -i --root

An editor will appear with the commits until the root commit, like:

pick 1234 old root message
pick 0294 A commit in the middle
pick 5678 commit you want to put at the root

You can then put the commit you want first, by placing it in the first line. In the example:

pick 5678 commit you want to put at the root
pick 1234 old root message
pick 0294 A commit in the middle

Exit the editor the commit order will have changed.

PS: To change the editor git uses, run:

git config --global core.editor name_of_the_editor_program_you_want_to_use
Fredella answered 18/2, 2017 at 16:17 Comment(3)
Now that rebase has --root, this is by far the neatest solution.Sequel
Wish I would have read farther down the page to see this the first time. Great answer!Moore
This is the simplest and easiest to understand answer. Thanks!Gallipot
B
6

I used pieces of Aristotle's and Kent's answer successfully:

# first you need a new empty branch; let's call it `newroot`
git checkout --orphan newroot
git rm -rf .
git commit --allow-empty -m 'root commit'
git filter-branch --parent-filter \
'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat -- --all
# clean up
# pre- git 2.28...
git checkout master
# or git 2.28 and later...
git checkout $(git config --get init.defaultBranch)
git branch -D newroot
# make sure your branches are OK first before this...
git for-each-ref --format="%(refname)" refs/original/ | \
xargs -n 1 git update-ref -d

This will also rewrite all branches (not just master or init.defaultBranch) in addition to tags.

Bah answered 17/4, 2014 at 22:44 Comment(3)
what does this last line do?Sky
It searches through refs/original/ and deletes each ref. The refs that it deletes should already be referenced by some other branch, so they don't really go away, just refs/original/ gets removed.Bah
This worked for me. Additionally I used timedatectl set-time '2017-01-01 00:00:00' to give newroot an old timestamp.Krauss
B
4

git rebase --root --onto $emptyrootcommit

should do the trick easily

Benjamin answered 17/1, 2011 at 19:27 Comment(2)
$emptyrootcommit is a shell variable which expands to nothing, surely?Macroclimate
@Flimm: $emptyrootcommit is the sha1 of an empty commit that the original poster already seems to have.Fructificative
T
4

I got excited and wrote an 'idempotent' version of this nice script ... it will always insert the same empty commit, and if you run it twice, it doesn't change your commit hashes each time. So, here's my take on git-insert-empty-root:

#!/bin/sh -ev
# idempotence achieved!
tmp_branch=__tmp_empty_root
git symbolic-ref HEAD refs/heads/$tmp_branch
git rm --cached -r . || true
git clean -f -d
touch -d '1970-01-01 UTC' .
GIT_COMMITTER_DATE='1970-01-01T00:00:00 +0000' git commit \
  --date='1970-01-01T00:00:00 +0000' --allow-empty -m 'initial'
git rebase --committer-date-is-author-date --onto $tmp_branch --root master
git branch -d $tmp_branch

Is it worth the extra complexity? maybe not, but I will be using this one.

This SHOULD also allow to perform this operation on several cloned copies of the repo, and end up with the same results, so they are still compatible ... testing ... yes it does, work, but need also to delete and add your remotes again, e.g.:

git remote rm origin
git remote add --track master user@host:path/to/repo
Trisyllable answered 6/2, 2013 at 12:28 Comment(0)
I
2

Well, here's what I came up with:

# Just setting variables on top for clarity.
# Set this to the path to your original repository.
ORIGINAL_REPO=/path/to/original/repository

# Create a new repository…
mkdir fun
cd fun
git init
# …and add an initial empty commit to it
git commit --allow-empty -m "The first evil."

# Add the original repository as a remote
git remote add previous $ORIGINAL_REPO
git fetch previous

# Get the hash for the first commit in the original repository
FIRST=`git log previous/master --pretty=format:%H  --reverse | head -1`
# Cherry-pick it
git cherry-pick $FIRST
# Then rebase the remainder of the original branch on top of the newly 
# cherry-picked, previously first commit, which is happily the second 
# on this branch, right after the empty one.
git rebase --onto master master previous/master

# rebase --onto leaves your head detached, I don't really know why)
# So now you overwrite your master branch with the newly rebased tree.
# You're now kinda done.
git branch -f master
git checkout master
# But do clean up: remove the remote, you don't need it anymore
git remote rm previous
Iso answered 14/3, 2009 at 8:8 Comment(0)
M
2

Here's my bash script based on Kent's answer with improvements:

  • it checks out the original branch, not just master, when done;
  • I tried to avoid the temporary branch, but git checkout --orphan only works with a branch, not detached-head state, so it's checked out long enough to make the new root commit and then deleted;
  • it uses the hash of the new root commit during the filter-branch (Kent left a placeholder in there for manual replacement);
  • the filter-branch operation rewrites only the local branches, not remotes too
  • the author and committer metadata is standardised so that the root commit is identical across repositories.

#!/bin/bash

# Save the current branch so we can check it out again later
INITIAL_BRANCH=`git symbolic-ref --short HEAD`
TEMP_BRANCH='newroot'

# Create a new temporary branch at a new root, and remove everything from the tree
git checkout --orphan "$TEMP_BRANCH"
git rm -rf .

# Commit this empty state with generic metadata that will not change - this should result in the same commit hash every time
export GIT_AUTHOR_NAME='nobody'
export GIT_AUTHOR_EMAIL='[email protected]'
export GIT_AUTHOR_DATE='2000-01-01T00:00:00+0000'
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
git commit --allow-empty -m 'empty root'
NEWROOT=`git rev-parse HEAD`

# Check out the commit we just made and delete the temporary branch
git checkout --detach "$NEWROOT"
git branch -D "$TEMP_BRANCH"

# Rewrite all the local branches to insert the new root commit, delete the 
# original/* branches left behind, and check out the rewritten initial branch
git filter-branch --parent-filter "sed \"s/^\$/-p $NEWROOT/\"" --tag-name-filter cat -- --branches
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
git checkout "$INITIAL_BRANCH"
Maccabean answered 7/5, 2015 at 17:29 Comment(0)
C
2

Merge of Ivy's answer and mrks'es comment on it with VonC's answer from here:

git rebase --root --onto $(git commit-tree -m 'root_commit' $(git hash-object -t tree /dev/null))

This covers the situation of some near future: when future versions of Git won't have

sha1:4b825dc642cb6eb9a060e54bf8d69288fbee4904

for its empty tree node at all, but

sha256:6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321

only instead. This command should work even after the SHA-2 transition the same as it does today.

Codling answered 26/6, 2023 at 10:0 Comment(2)
Don't forget to change the date of root commit if that matters to you, otherwise you will use the present date.Demagnetize
@Demagnetize that's a good point. One may achieve it by executing git rebase -i --root after the command in the answer, then changing pick for edit for the newly created root commit, and then git commit --amend --no-edit --date='1999-10-10 10:10:10' --allow-empty to change its date, and finish the operation with git rebase --continuePractitioner
S
1

Combining the latest and greatest. No side effects, no conflicts, keeping tags.

git log --reverse

tree=`git hash-object -wt tree --stdin < /dev/null`
commit=`git commit-tree -m 'Initialize empty repository' $tree`
echo $commit # copy below, interpolation didn't work for me

git filter-branch --parent-filter 'sed "s/^\$/-p <commit>/"' --tag-name-filter cat master

git log --reverse

Note that on GitHub you will lose CI run data and PR might get messed up unless other branches are fixed as well.

Saccharide answered 12/9, 2019 at 9:24 Comment(0)
I
0

Following answer Aristotle Pagaltzis and others but using more simple commands

zsh% git checkout --orphan empty     
Switched to a new branch 'empty'
zsh% git rm --cached -r .
zsh% git clean -fdx
zsh% git commit --allow-empty -m 'initial empty commit'
[empty (root-commit) 64ea894] initial empty commit
zsh% git checkout master
Switched to branch 'master'
zsh% git rebase empty
First, rewinding head to replay your work on top of it...
zsh% git branch -d empty 
Deleted branch empty (was 64ea894).

Note your repo shouldn't contain no local modifications waiting to be commited.
Note git checkout --orphan will work at new versions of git, I guess.
Note most of the time git status gives useful hints.

Indeclinable answered 18/2, 2013 at 8:28 Comment(0)
S
-6

Start a new repository.

Set your date back to the start date you want.

Do everything the way you wish you'd done it, adjusting the system time to reflect when you'd wished you'd done it that way. Pull files from the existing repository as needed to avoid a lot of needless typing.

When you get to today, swap the repositories and you're done.

If you're just crazy (established) but reasonably intelligent (likely, because you have to have a certain amount of smarts to think up crazy ideas like this) you will script the process.

That will also make it nicer when you decide you want the past to have happened some other way a week from now.

Sweltering answered 14/3, 2009 at 5:49 Comment(1)
I have bad feelings about a solution that requires you to mess around with the system date, but you did give me an idea, which I developed a bit and, alas, it worked. So, thanks.Iso
T
-8

I know this post is old but this page is the first one when Googling "inserting commit git".

Why make simple things complicated?

You have A-B-C and you want A-B-Z-C.

  1. git rebase -i trunk (or anything before B)
  2. change pick to edit on the B line
  3. make your changes: git add ..
  4. git commit (git commit --amend which will edit B and not create Z)

[You can make as many git commit as you want here to insert more commits. Of course, you may have troubles with step 5, but resolving merging conflict with git is a skill you should have. If not, practice!]

  1. git rebase --continue

Simple, isn't it?

If you understand git rebase, adding a 'root' commit should not be a problem.

Have fun with git!

Tubulate answered 27/6, 2011 at 9:40 Comment(1)
The question asks for inserting a first commit: from A-B-C you want Z-A-B-C. A straightforward git rebase can't do this.Hoxha

© 2022 - 2024 — McMap. All rights reserved.