tomono を使ってmonorepoに移行した話を書きます。
(※ monorepo化 = ソフトウェアプロジェクトを複数のgitリポジトリに分けずに、1つのgitリポジトリにおさめること)
monorepo に移行したかった
業務上、あるプロジェクト内でシステムコンポーネントごと(各所で動くAPIやバッチプログラム)に、gitプロジェクトを切っていたのですが、下記の理由により、だんだんmonorepoにしたくなってきました。
- そもそもメンテナが1-2人しかいないのに、リポジトリが10弱存在していて管理が煩雑。
- 依存パッケージやランタイムのバージョンアップについて、それぞれのリポジトリにプルリクだしてマージするのが超煩雑。
- 共通ライブラリもリポジトリが分かれているけれども、半分自動化されているとはいえ、共通ライブラリのアップデートのたびに下記を繰り返すのが苦痛。
- 共通ライブラリの更新とバージョン付け
- 付与したバージョンでリポジトリ(e.g. Sonatype Nexus)へのpublish
- そのバージョンを利用するように各プロジェクトの依存性ファイルを更新する
- (以後、共通ライブラリにミスを発見すると最初のステップからやり直し…)
- それぞれのプロジェクトで共通コードがある場合、共通プロジェクトにくくりだすのが超面倒 (リポジトリ作ってCI置いてpublishして参照して…)
- それぞれのリポジトリごとに開発環境のセットアップ手順があり(
docker-compose up -d
するだけですが)、無駄。 - エディタ切り替える時間が無駄。
リポジトリを用途ごとに細かく分けているのはそれはそれで多人数だと回る面はあるのですが、少人数だと苦痛しか生んでいない状況はおかしいと思いました。
要するにリポジトリの凝集度がおかしかったんです。
monorepo なら俺に任せろー!(バリバリバリバリ) → 失敗
そういうことで、monorepoに下記のように移行しようと思ったのですが、
repo1 (git://example.com/git/repo1.git)
repo2 (git://example.com/git/repo2.git)
:
repoN (git://example.com/git/repoN.git)
↓↓上記を下記に変換↓↓
monorepo/ (git://example.com/git/monorepo.git)
repo1/
repo2/
:
repoN/
「gitのことならくわしいんだ!」と思う私は、下記のように「各プロジェクトをマージしていけばいいんじゃね?」と思い、移行を試みます。
$ git init
$ git remote add repo1 git://example.com/git/repo1.git
$ git fetch repo1
$ git merge --allow-unrelated-histories repo1/master
$ # いそいそとrepo1の全ファイルを子ディレクトリに移動させる
$ git remote add repo2 git://example.com/git/repo2.git
$ git fetch repo2
$ git merge --allow-unrelated-histories repo2/master
$ # いそいそとrepo2の全ファイルを子ディレクトリに移動させる
git merge
は --allow-unrelated-histories
オプションにより、全く関係のないリポジトリのコミットグラフをマージすることができます。普通、gitの履歴をたどると、かならず最初のコミットにたどり着くわけですが、これを行ったリポジトリの場合は2つ以上のコミットにたどり着くことになります。
これまでプロジェクトを統合した時の経験に従って、上記のようにmonorepo統合化を始めたのですが、今回は途中でうまくいかなくなりました。
$ git remote add repo2 git://example.com/git/repo3.git
$ git fetch repo3
# ここで conflict!
$ git merge --allow-unrelated-histories repo3/master
😡 You have unmerged paths. (以下略)
git 「マージしようと思ったけどな、repo2の中身の一部がコンフリクトしてんねん。あ、一部はマージできとるからな! 」 (そして一部が無駄に改変されたrepo2の内容が…!)
ここで裏設定が発覚! なんとrepo3はrepo2からはるか昔に暖簾分けされたリポジトリだったのです! …まあ、だからこそmonorepoに戻したいと思ったわけですが…
「そこは別にマージしなくていいんだよぉ!」と思ったのですが、gitさんからしたらマージせざるをえないですよね。どう見ても、マージですし。
**マージさせながら、させない(謎)**オプションあるかな?ないよな… うーんどうしよう… と思っていました。
tomono
そういうことで tomono の紹介です。同僚の人が見つけてくれました。
上記でやったような作業を、コンフリクトなしで全て自動的にやってくれます。
使い方は簡単で、下記のようにマージ指示のテキストを用意して、リポジトリにある tomono.sh をダウンロードして標準入力に与えてあげればいいです。
$ bash tomono.sh <<EOF
git://example.com/git/repo1.git repo1
git://example.com/git/repo2.git repo2
git://example.com/git/repo3.git repo3
:
git://example.com/git/repoN.git repoN
EOF
# 上記完了後、 core/ ディレクトリの中に全てがおさまったリポジトリができあがっている
$ ls -a core
.git
repo1 repo2 repo3 ... repoN
履歴はもちろん、手作業で諦めていた各リポジトリのタグも、すべてきちんと移行できました。特に魔法を使っているわけではなく、gitコマンドで移行してくれます。
俺たちのmonorepo化は始まったばかりだ
ここでmonorepo化が終わればいいのですが、どちらかというとリポジトリ統合の後からがmonorepo化の本番で、開発環境定義を修正したり、モジュール間の依存性を書き直したり、きちんとCI/CDの設定をしたり、READMEを修正したりなどなど、いろいろ実施する必要がありました。
特にCI/CDは、そもそも分かれていたリポジトリについて、一斉にビルド・テスト・パブリッシュをするようになるので、ビルド時間増大の懸念などあったのですが、特にそこまで問題にならずに統合することができました。(もちろんいくつかトリックは入れたのですが)
みなさんも、リポジトリ多すぎと思ったら、monorepo化してみましょう!