エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

gitの履歴を維持したまま、リポジトリを1つに統合する

こんにちは。AI・機械学習チームの苅野です。

このブログはエムスリー Advent Calendar 2025 10 日目の記事です。API と Batch でリポジトリが分かれているプロダクトを 1 つのリポジトリに統合しようと考えて検証を行い、 「既存リポジトリで git mv してから merge --allow-unrelated-histories する」 方法を採用することに決めました。

git filter-repo など他の手法も検証しましたが、この方法だと 「Commit ID を変えずに(過去の PR リンク等を維持したまま)、--follow オプションでファイル移動前の履歴も追える」 ため、この方針で統合を進める予定です。

具体的な検証過程と手順を以下に紹介します。

リポジトリを 1 つに統合したかった理由

今回リポジトリ統合を行いたいと考えたプロダクトはこれまで Batch と API でリポジトリを分けて管理していました。 BigQuery にデータをクエリし整形して Elastic Cloud に投入する Batch と、クライアントからリクエストを受けて Elastic Cloud に問い合わせて結果を返す API から成り立っています。

「モノレポ」というと全社のコードを 1 つにまとめる大規模なものを指すことがありますが、本記事で目指しているのはあくまで「密結合な関係にある API と Batch を 1 つのリポジトリで管理する(プロジェクト単位での集約)」状態です。

もちろんビルド時間の短縮や権限管理の分離といったリポジトリ分割(ポリレポ)ならではのメリットもあり全社的にモノレポを推進しているわけではなくあくまで適材適所だと考えていますが、今回のケースでは以下の理由からリポジトリを統合するメリットが大きいと判断しその方法を検証しました。

リポジトリ統合のイメージ図

まとめて変更を加えることができる

同一リポジトリであれば Batch, API 両方に対して同時に変更を加えることができます。例えば新しいインデックスを作成する場合に Batch では新しいインデックスとそのインデックスにドキュメントを投入し、API では新しいインデックスからもドキュメントを検索するコードを書いて 1 つの Pull Request にまとめることができます。

API の integration_test で Batch の定義ファイルをそのまま使える

API の CI では integration_test が走るようになっています。このプロダクトでは定義ファイルから Elasticsearch のインデックス管理を行う eskeeper を利用しており、これまでは Batch リポジトリにある定義ファイルを API リポジトリにもコピーしてコミットし、テストで使用していました。Batch リポジトリの定義ファイルに変更を加えた場合 API リポジトリに忘れずにコピーする必要があり、定義ファイルを二重管理する必要があります。リポジトリを 1 つにすれば同一リポジトリの Batch ディレクトリに存在する定義ファイルを API の integration_test で利用できます。

チームのプロジェクト構成で単一リポジトリ構成が増えてきた

我々は開発の効率化のため、プロジェクトの雛形を自動的に生成するプロジェクトテンプレートを利用しています。下記はAPI開発で利用している cookiecutter テンプレートについての記事ですが、gokart を使った Batch のテンプレートも存在します。

www.m3tech.blog

AI・機械学習チームでは数多くのプロダクトが稼働しており(なんと去年一年で 28 個リリースしました)、API と Batch をセットにした構成のプロダクトも増えて知見が溜まってきました。チームでの最新の知見は先述した cookiecutter テンプレートに反映されており、cruft を使って追従することができます。CI の関係で統合リポジトリ時の cruft 追従が面倒でしたが、知見が溜まってきて解消されてきました。

リポジトリを統合する具体的な手順

今回の API と Batch はこれまで別々のリポジトリとして開発されてきたため、それぞれの main ブランチには共通のコミット履歴(共通の祖先)が存在しません。

このように履歴のつながりがない(unrelatedな)ブランチ同士は通常マージできませんが、git merge コマンドに --allow-unrelated-histories オプションを付与することで、強制的に統合することができます。API リポジトリのコードを api/ に、Batch リポジトリのコードを batch/ に移動し、その後でマージすれば 1 つのリポジトリに集約にできます。

また今回は完全移行が目的で、移行後の履歴の追いやすさを優先しました。スパッと切り替えて以後は統合リポジトリのみを正とする(旧リポジトリはアーカイブする)想定です。

git subtree add を使う(ファイルごとの履歴が断絶してしまう)

最初に考えたのは git subtree を使う方法です。git subtree を使うと git リポジトリの中で複数のリポジトリの履歴を管理できるので、API リポジトリに Batch リポジトリを取り込めるのではないかと考えました。

git clone <api repository> local-api
cd local-api
mkdir api
# .gitと新しく作ったapi以外を移動
git mv $(ls -A | grep -v '^\.git$' | grep -v '^api$') api/
git commit -m 'move to api directory'

git subtree add --prefix=batch <batch repository> main

この方法でも統合自体は可能です。しかし、subtree add した時点でファイルのパスが batch/ 配下に変わってしまうため、ファイル単位のコミット履歴(Blame)が途切れてしまうという課題がありました。

具体的には、普通に git log batch/batch_job.py を実行しても統合以降の履歴しか表示されません。過去の履歴を見るには git log <batch repositoryの最新コミットID> -- <旧ファイル名>のように明示的に指定する必要がありシームレスに辿れないのが難点でした。

この「ファイルごとの履歴が断絶する問題」を解決するために、次は git filter-repo を検討しました。

git filter-repo を使う(コミット ID が変わってしまう)

次に考えたのは git filter-repo を使う方法です。git filter-repo は git の履歴を書き換えるツールで、過去にサイズが大きいファイルをコミットしてしまった場合に履歴から取り除いたり、コミットメッセージを書き換えたりと色々なことができます。ドキュメントの使用例の中にまさしく今回やりたいファイルを別ディレクトリに移動するが含まれています。

moving all files into a subdirectory in preparation for merging with another repo

git filter-repo を使って API と Batch で別リポジトリに分かれていたのを統合してみましょう。

git clone <api repository> local-api
cd local-api
# すべてのファイルをapi/配下に移動(履歴も書き換え)
git filter-repo --to-subdirectory-filter api
# project ディレクトリに戻る
cd ..

# batch をクローン
git clone <batch repository> local-batch
cd local-batch
# すべてのファイルをbatch/配下に移動(履歴も書き換え)
git filter-repo --to-subdirectory-filter batch
cd ..
# local-api で batch のマージを行う
cd local-api

# local-batch ディレクトリを remote に登録
git remote add batch ../local-batch
git fetch batch
# ここでローカルの batch をマージする
git merge batch/main --allow-unrelated-histories

上記の手順を踏むことで git の履歴を書き換えて API リポジトリは最初から api ディレクトリ配下で開発されてきたようにできます。実際統合した後に git log --oneline api を実行すると統合前と同じコミットメッセージが表示され、GitHub の画面上でも各ファイルの履歴を辿ることができます。

一方でこの方法だと git の履歴を書き換えるためコミット ID が変わってしまいます。そのため今までコミット ID でリンクされていた場合リポジトリの URL を書き換えてもコミット ID が異なるので辿れなくなってしまいます。

git mv を使う(コミット ID を変えずにファイルごとの履歴を辿れるのでこの方法を採用)

最後に試したのは既存のコードを api/ や batch/ に移動するコミットを積んでからマージする方法です。

git clone <batch repository> local-batch
cd local-batch
# batch ディレクトリがないと git mv がエラーになる
mkdir batch
# .gitと新しく作ったbatch/以外を移動
git mv $(ls -A | grep -v '^\.git$' | grep -v '^batch$') batch/
git commit -m 'move to batch directory'

cd ..
git clone <api repository> local-api
cd local-api
# api ディレクトリがないと git mv がエラーになる
mkdir api
# .gitと新しく作ったapi以外を移動
git mv $(ls -A | grep -v '^\.git$' | grep -v '^api$') api/
git commit -m 'move to api directory'

git remote add batch ../local-batch
git fetch batch
# ここでローカルの batch をマージする
git merge batch/main --allow-unrelated-histories
git remote remove batch

こうするとコミット ID を書き換えずに統合できます。git log --oneline api/README.md だと 'move to api directory' のように移動したコミットしか表示されませんが、--follow オプションをつけて git log --oneline --follow api/README.md とするとファイルを移動する前の履歴も辿ってくれます。GitHub の画面上でもファイルごとの履歴を見ることができます。これで履歴を保ちながら 1 つのリポジトリに統合できました。

おわりに

本記事では API と Batch でリポジトリが分かれているプロダクトを 1 つのリポジトリに統合する方法を紹介しました。履歴を保ちながらファイルごとの履歴も辿れる git mv してから git merge --allow-unrelated-histories する方法で統合しようと考えています。

We are hiring!

AI・機械学習チームでは、医療現場やWebでの課題解決に取り組むエンジニアを募集しています。

「機械学習モデルの社会実装」や「MLOps基盤の構築・改善」に興味がある方、コスト意識を持った技術選定に関心のある方は、ぜひカジュアル面談でお話ししましょう!

jobs.m3.com