Sometimes you want to split a huge monorepo into multiple smaller repositories, like if you were migrating from Subversion. What if you want to do the opposite? I did.

I had a set of tiny git repositories that describe how to build Debian packages in OpenSuse Build Service. The repositories had similar directory structure and even the commits in each repository were alike. Eventually I thought that merging all repositories into a single monorepo would be more efficient.

I started searching if somebody had already achieved something like this. I found few attempts but they all differ in a way how the history is preserved.

Basically all the solutions I found merge repositories with --allow-unrelated-histories option. It allows merging branches that don’t share common ancestor commits. But in result the history of each repository is trapped inside merge commits. Here is how it would look for two repositories:

     A  B  C  D       W  X  Y  Z
     •--•--•--•   +   •--•--•--•

     A  B  C  D       W  X  Y  Z
     •--•--•--•   +   •--•--•--•
    /                /

Which is inconvenient if you still want to reorder or squash some consequent commits across multiple repositories. This is how I wanted the commits, ordered chronologically:

A  W  B  C  X  Y  Z  D

Here is the approach I went with:

mkdir -p ~/merge-attempt/repos
cd ~/merge-attempt/repos
git clone git@githost:user/repo1
git clone git@githost:user/repo9
cd repos
for R in *; do
  pushd $R
  git format-patch --root --src-prefix=a/$R/ --dst-prefix=b/$R/
  for P in *.patch; do
    grep '^Date: ' $P | \
      sed 's/^Date: \(.*\)/\1/' | \
      date -Iseconds -f - | \
      sed 's/[:+]/-/g' | \
      xargs -I{} mv $P {}.patch
  mv *.patch ../..

Here --src-prefix and --dst-prefix arguments allow us to preconfigure the patches, so when applied, the files would be created in subdirectories named according to original repository.

We should get something like this:

mkdir -p ~/merge-attempt/monorepo
cd ~/merge-attempt/monorepo
git init .
git am ../*.patch
git filter-branch --env-filter 'export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"'

And that would be it.