Add commits before root commit in Git? [duplicate]

Update

I found another, slightly simpler way to do this, using the --root flag of git rebase. I tried it out with a repo of mine, so I know it works (at least for a completely linear history, more on that at the end).

Step 1: Create a backup clone of your repo

Let’s play it safe and make a backup, just in case we end up doing something disastrous and losing your data:

git clone original-repo backup-repo

Step 2: Create orphan branch (this will be your new root)

Checkout the orphan branch. Specifying a start-point commit/branch is optional, if you leave that argument out, then Git will default to using your current commit as the start-point (it follows the pattern of standard branching syntax):

git checkout --orphan new-master firstCommitOfOldMaster

I specified the root of the old master as the start-point, because it will probably make the following step faster (removing the working directory files).

Step 3: Remove working directory files

Creating the orphan branch new-master from your old master might leave behind files in your working directory and index, depending on what the state of the files were in the commit you branched off of:

The index and the working tree are adjusted as if you had previously run git checkout <start_point>. This allows you to start a new history that records a set of paths similar to <start_point> by easily running git commit -a to make the root commit. — git checkout docs

What you’ll want to do now is to completely clear the state of the branch, so that you’re really starting again from a clean slate (run from top level folder):

git rm -rf .

This is the explanation from the docs:

If you want to start a disconnected history that records a set of paths that is totally different from the one of <start_point>, then you should clear the index and the working tree right after creating the orphan branch by running git rm -rf . from the top level of the working tree. Afterwards you will be ready to prepare your new files, repopulating the working tree, by copying them from elsewhere, extracting a tarball, etc.

Step 4: Recommit older work into new-master

Now you’ll want to start commiting your older work into new-master. When you’re done, you’ll rebase your old master on top.

I’m not exactly sure how your backup folders are organized, but I’m going to assume that each one is just a dated copy of your project folder, named in YYYY-MM-DD format, e.g. 2013-01-30. You can add the contents of each folder as a new commit manually, or you can write a script to automate the process.

Let’s assume you have all your backup folders in a top folder called backup that sits in the same directory as your main project folder. Then here is some pseudo-code for a script to automate the process of commiting each folder:

# Get backups and sort them from oldest to newest
backups = open("backups").toList().sort("date ascending")
for each (backup in backups)
    copy files from backup to project folder
    execute `git add "*"`
    execute `git commit --date="#{backup.date}" --message="#{backup.date}"`

The script above will only add and modify files between commits. If any of your backups deleted, moved, or renamed files, the script won’t record that. You’ll need to write a more clever script to do that (maybe something that makes use of git diff or some other diffing tool), or record those changes manually.

Step 5: Rebase old master onto new-master

BEHOLD THE AWESOME POWER THAT IS GIT! Now we’re going to REWRITE YOUR ENTIRE HISTORY! Recite these arcane words of power, and watch the magic happen!:

git rebase --onto new-master --root master

TA-DA! You might have to resolve conflicts between the last-commit of new-master and the first-commit of master, but other than that, that should be it!

The --root flag to rebase gets around the problem we were having earlier about the fact that—normally—rebase won’t operate on the first (root) commit of a Git repo. The --root flag basically tells Git to include it in the rebase:

Rebase all commits reachable from <branch>, instead of limiting them with an <upstream>. This allows you to rebase the root commit(s) on a branch. When used with --onto, it will skip changes already contained in <newbase> (instead of <upstream>) whereas without --onto it will operate on every change. — rebase docs

Step 6: Verify that final commit is still the same

Just to make sure we didn’t mess any of the code up, we’ll want to compare the current, final state of master to its state before we did the rebase. Either of the following commands will work (they’re identical). While on the master branch:

git diff orig_head   # "Original" head
git diff master@{1}  # Master at its 1st prior position

If you get no output from the diff, then that means the final state of your rebased master is identical to what it was before the rebase. Good job!

NOTE: not sure what happens to merge commits…

Now the above steps will work fine if you have a perfectly linear history, i.e. you don’t have any merge commits. I’m not sure if it will still work if you do. In my previous answer below I mentioned that using the --preserve-merges flag with rebase might help you keep those merges, but the docs mention that that flag also interacts with the --onto and --root flags:

When [--root is] used together with both --onto and --preserve-merges, all root commits will be rewritten to have <newbase> as parent instead. — rebase docs

I’m not exactly sure what that means at the moment…I might have to try it out and see what happens. Does it mean that if you have more than 1 root commit (like 10 for example), then 9 of those root commits will end up being rebased onto the last one acting as the new root? That doesn’t seem like the behavior we would want. We just want to preserve merge commits for one root being rebased onto another.


Previous answer

MGA had the right idea with rebase. I’m not sure if this will fix your problem, but maybe you need to make a new root commit in your repo, add your backup files as new commits on top of it, then rebase your old commit graph on top.

For example, git checkout has an --orphan flag according to the documentation:

--orphan <new_branch>
Create a new orphan branch, named <new_branch>, started from <start_point> and switch to it. The first commit made on this new branch will have no parents and it will be the root of a new history totally disconnected from all the other branches and commits.

So maybe you can try

git checkout --orphan <temp_branch>

Then commit your backup files to it. Then with temp_branch checked out, do

git cherry-pick <first commit from master>
git rebase --onto temp_branch <first commit from master> master

I think you would probably need to cherry-pick the first commit because the 2nd argument to rebase --onto is the old base of the branch you want to rebase (in this case master), and the old base isn’t included. So that’s why I figure you need to cherry-pick it in order to get it, since it has no base commit itself, so you can’t specify one for it in the rebase.

Also note that if your commit history isn’t linear (i.e. it has merge commits), then rebase won’t normally preserve those. It has a --preserve-merges flag, but I’ve never used it before, so I’m not really sure of how it will work. From the docs:

-p
--preserve-merges
Instead of ignoring merges, try to recreate them.

This uses the --interactive machinery internally, but combining it with the --interactive option explicitly is generally not a good idea unless you know what you are doing (see BUGS below).

So, maybe all of that will work?

Leave a Comment