深夜2時、チームのCI/CDパイプラインが真っ赤に染まりました。15GBもの巨大なモノレポの奥深くに位置するレガシーな決済モジュールが不要な依存関係を大量に引き連れ、ビルド時間は4分から35分超へと跳ね上がっていたのです。本番環境の危機を救う最短ルートは、packages/payment-core を即座に別リポジトリへと切り離し、独立してhotfixとリリースを行えるようにすることでした。しかし最大の課題は、過去3年分のコミット履歴や git blame を1行たりとも失ってはならないという点です。
Gitでサブディレクトリを切り出す3つのアプローチとそれぞれの代償
この課題に直面した際、エンジニアがよく検討するアプローチは次の3つです。
- アプローチ1:手動で新規リポジトリへコピー&ペースト
空のリポジトリを作成し、packages/payment-coreディレクトリを移動させてgit init && git commit -m "Initial commit"を実行する方法です。手軽ですが、過去の全履歴が完全に消滅します。 - アプローチ2:
git filter-branchまたはgit-filter-repoの使用
コミットツリー全体を書き換える方法です。過去の全コミットをスキャンし、対象ディレクトリ以外のファイルをすべて削除します。 - アプローチ3:
git subtree splitの使用
Git 1.7.0以降に標準搭載されているコマンドです。指定したディレクトリに関連するコミット履歴のみを抽出し、完全に独立した合成ブランチ(synthetic branch)を生成します。
実践比較:本番障害発生時に選ぶべき解決策は?
各手法にはそれぞれ明確なメリットとデメリットがあります。
1. 手動コピー&ペースト
- メリット: 30秒で完了し、Gitコマンドの打ち間違いリスクがありません。
- デメリット: コミットログ、作成者、更新日時がすべて失われます。深夜に誤請求バグが発生した際、6か月前に誰がロジックを変更したのかを
git blameで追跡する術がなくなります。
2. git filter-branch または git-filter-repo の使用
- メリット: 切り出し先リポジトリのサイズを劇的に削減でき、新しいコミットハッシュもクリーンになります。
- デメリット: リスクが極めて高い点です。
git filter-branchはコミットハッシュを直接書き換えるため、万が一誤ったブランチへpush --forceしてしまうとチーム全体のmainブランチが壊滅します。さらにgit-filter-repoは外部ツールであり、Pythonパッケージの追加インストールが必要なため、どの環境でもすぐに使えるわけではありません。
3. git subtree split の使用
- メリット: 圧倒的に安全です。履歴を読み取ってサブブランチを生成するだけで、作業中のブランチを上書き・改変しません。Gitのコア機能として組み込まれているため、追加インストールの手間も不要です。
- デメリット: 処理速度が元リポジトリのコミット数に依存します。8万コミットを超えるような大規模リポジトリでは、スキャンに2〜5分程度かかる場合があります。
なぜ git subtree split が最も安全な選択肢なのか?
緊急の障害対応において、最も重要な要素はデータの安全性とトレーサビリティ(追跡性)の2点です。git subtree split はその両方を完璧に満たします。元のリポジトリを100%無傷のまま保ちつつ、完全なコミット履歴を持つ新規リポジトリを生成できます。
実際に10人規模のチームでこの手順を適用した際、履歴のコンフリクトを一切起こすことなく、4つの共通モジュールを独立した個別リポジトリへと無事に切り出すことができました。
詳細な実行手順
ここでは、monorepo-core リポジトリのルートディレクトリにおり、packages/payment-core を新規リポジトリ payment-service-repo へ切り出すシナリオを例に解説します。
ステップ1:ワーキングツリーをクリーンな状態にする
最新のブランチにおり、コミットされていない変更(uncommitted changes)がないことを確認します。
# ステータスを確認
git status
# mainから最新のコードを取得
git checkout main
git pull origin main
ステップ2:対象ディレクトリの履歴を専用ブランチとして抽出する
git subtree split コマンドを実行し、-P(prefix:対象ディレクトリのパス)と -b(新しいブランチ名)フラグを指定します。
git subtree split -P packages/payment-core -b extract-payment-branch
Gitがコミットツリー全体をスキャンします。packages/payment-core 配下のファイルに変更を加えたコミットのみが抽出され、extract-payment-branch ブランチにまとめられます。同時に、このディレクトリ配下の全ファイルが新しいブランチのルート(root)直下に再配置されます。
ステップ3:新しいリポジトリへコードをプッシュする
GitHubやGitLab上で空のリポジトリ(例:[email protected]:your-org/payment-service-repo.git)を作成します。プッシュには以下の2通りの方法があります。
方法1:現在のリポジトリから直接プッシュする(最速)
git push [email protected]:your-org/payment-service-repo.git extract-payment-branch:main
このコマンドにより、抽出した一時ブランチが新しいリポジトリの main ブランチとして直接プッシュされます。
方法2:別ディレクトリにクローンしてローカルで事前検証する
# モノレポの外に新しいディレクトリを作成
cd ..
mkdir payment-service-repo
cd payment-service-repo
# リポジトリを初期化し、切り出したブランチを取り込む
git init
git pull /path/to/monorepo-core extract-payment-branch
# 新しいリモートリポジトリを設定してプッシュ
git remote add origin [email protected]:your-org/payment-service-repo.git
git branch -M main
git push -u origin main
ステップ4:履歴と git blame を検証する
新しいリポジトリを開き、過去のコミット履歴や作成者情報が保持されているかログを確認します。
# 直近10件のコミットを確認
git log --oneline -n 10
# 重要ファイルのblameを確認
git blame src/gateway.js
実行結果として、決済モジュールに関するすべてのコミットログ、タイムスタンプ、作成者のメールアドレスが完全に保持されていることが確認できます。モノレポ内の不要なファイルはきれいに除外されています。
ステップ5:元リポジトリの一時ブランチをクリーンアップする
新しいリポジトリの稼働が確認できたら、元リポジトリに戻って不要になった一時ブランチを削除し、ワークスペースを整理します。
cd /path/to/monorepo-core
git branch -D extract-payment-branch
これで、元リポジトリ側の packages/payment-core ディレクトリを安全に削除したり、npmやComposerを介した独立パッケージ管理へと移行したりする準備が整いました。過去の開発履歴が失われる心配はありません。

