In my case, I had a my-plugin
repository and a main-project
repository, and I wanted to pretend that my-plugin
had always been developed in the plugins
subdirectory of main-project
.
Basically, I rewrote the history of the my-plugin
repository so that it appeared all development took place in the plugins/my-plugin
subdirectory. Then, I added the development history of my-plugin
into the main-project
history, and merged the two trees together. Since there was no plugins/my-plugin
directory already present in the main-project
repository, this was a trivial no-conflicts merge. The resulting repository contained all history from both original projects, and had two roots.
TL;DR
$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty
Long version
First, create a copy of the my-plugin
repository, because we're going to be rewriting the history of this repository.
Now, navigate to the root of the my-plugin
repository, check out your main branch (probably master
), and run the following command. Of course, you should substitute for my-plugin
and plugins
whatever your actual names are.
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
Now for an explanation. git filter-branch --tree-filter (...) HEAD
runs the (...)
command on every commit that is reachable from HEAD
. Note that this operates directly on the data stored for each commit, so we don't have to worry about notions of "working directory", "index", "staging", and so on.
If you run a filter-branch
command that fails, it will leave behind some files in the .git
directory and the next time you try filter-branch
it will complain about this, unless you supply the -f
option to filter-branch
.
As for the actual command, I didn't have much luck getting bash
to do what I wanted, so instead I use zsh -c
to make zsh
execute a command. First I set the extended_glob
option, which is what enables the ^(...)
syntax in the mv
command, as well as the glob_dots
option, which allows me to select dotfiles (such as .gitignore
) with a glob (^(...)
).
Next, I use the mkdir -p
command to create both plugins
and plugins/my-plugin
at the same time.
Finally, I use the zsh
"negative glob" feature ^(.git|plugins)
to match all files in the root directory of the repository except for .git
and the newly created my-plugin
folder. (Excluding .git
might not be necessary here, but trying to move a directory into itself is an error.)
In my repository, the initial commit did not include any files, so the mv
command returned an error on the initial commit (since nothing was available to move). Therefore, I added a || true
so that git filter-branch
would not abort.
The --all
option tells filter-branch
to rewrite the history for all branches in the repository, and the extra --
is necessary to tell git
to interpret it as a part of the option list for branches to rewrite, instead of as an option to filter-branch
itself.
Now, navigate to your main-project
repository and check out whatever branch you want to merge into. Add your local copy of the my-plugin
repository (with its history modified) as a remote of main-project
with:
$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY
You will now have two unrelated trees in your commit history, which you can visualize nicely using:
$ git log --color --graph --decorate --all
To merge them, use:
$ git merge my-plugin/master --allow-unrelated-histories
Note that in pre-2.9.0 Git, the --allow-unrelated-histories
option does not exist. If you are using one of these versions, just omit the option: the error message that --allow-unrelated-histories
prevents was also added in 2.9.0.
You should not have any merge conflicts. If you do, it probably means that either the filter-branch
command did not work correctly or there was already a plugins/my-plugin
directory in main-project
.
Make sure to enter an explanatory commit message for any future contributors wondering what hackery was going on to make a repository with two roots.
You can visualize the new commit graph, which should have two root commits, using the above git log
command. Note that only the master
branch will be merged. This means that if you have important work on other my-plugin
branches that you want to merge into the main-project
tree, you should refrain from deleting the my-plugin
remote until you have done these merges. If you don't, then the commits from those branches will still be in the main-project
repository, but some will be unreachable and susceptible to eventual garbage collection. (Also, you will have to refer to them by SHA, because deleting a remote removes its remote-tracking branches.)
Optionally, after you have merged everything you want to keep from my-plugin
, you can remove the my-plugin
remote using:
$ git remote remove my-plugin
You can now safely delete the copy of the my-plugin
repository whose history you changed. In my case, I also added a deprecation notice to the real my-plugin
repository after the merge was complete and pushed.
Tested on Mac OS X El Capitan with git --version 2.9.0
and zsh --version 5.2
. Your mileage may vary.
References: