gitのsubmoduleを含んだリポジトリのgit操作
gitのsubmoduleは、リポジトリの中に別のリポジトリを埋め込む仕組みです。 ライブラリや共通コンポーネントを複数プロジェクトで使い回すとき、地味に便利。
ただ、普段のgit操作とちょっとだけ勝手が違うので、知らずにハマるポイントがあります。 今回は、submoduleを含んだリポジトリを扱うときに押さえておきたい操作をまとめました。
そもそもsubmoduleとは
あるリポジトリの中に、別のリポジトリを「特定のコミットを指した状態で」取り込む機能です。
たとえば、自社の共通UIコンポーネントを libs/ui-components として複数のプロダクトに組み込みたい、みたいなケース。
npmに上げるほどでもないけど、コピペで管理するのはしんどい。そういうときにsubmoduleが選択肢に入ってきます。
ポイントは、submoduleは「リポジトリへの参照」であって、コードそのものを取り込んでいるわけではないこと。 親リポジトリが持っているのは「どのリポジトリの、どのコミットを使うか」という情報だけです。
cloneするときは —recursive を忘れない
submoduleを含んだリポジトリを普通にcloneすると、submoduleのディレクトリは空っぽです。
git clone https://github.com/your-org/your-project.git
この状態で libs/ui-components を覗くと、ディレクトリはあるけど中身がない。
ビルドが通らなくて「え、なんで?」となるやつです。
正しくはこう。
git clone --recursive https://github.com/your-org/your-project.git
--recursive をつけると、submoduleの中身まで一緒にcloneしてくれます。
すでにclone済みで、submoduleだけ後から取得したい場合はこちら。
git submodule update --init --recursive
CIのパイプラインでも、このオプションの付け忘れでビルドがコケるのはあるあるなので、.gitlab-ci.yml や GitHub Actions の設定で明示しておくのが無難です。
GitHub Actionsなら actions/checkout の submodules オプションで対応できます。
- uses: actions/checkout@v4
with:
submodules: recursive
submoduleのバージョン更新は「意図的に」やる
ここがsubmodule運用で一番見落とされがちな話。
submoduleは独立したリポジトリなので、submodule側でコミットが進んでも、親リポジトリは自動的に追従しません。 親が「このコミットを使う」と指定しているポインタを、明示的に進める必要があります。
cd libs/ui-components
git pull origin main
cd ../..
git add libs/ui-components
git commit -m "update ui-components to latest"
もしくは、まとめて最新に追従させるなら。
git submodule update --remote
git add .
git commit -m "update submodules"
ここで git add と commit をしないと、ローカルでは最新のsubmoduleを見ているのに、pushしても他のメンバーの環境では古いままという状態になります。
「自分の環境では動くんだけど?」の典型的な原因のひとつがこれ。
逆に言えば、意図しないバージョンアップが勝手に入らないのはsubmoduleの利点でもあります。 「いつ、どのバージョンに上げたか」がコミット履歴にちゃんと残るので、トレーサビリティは高い。
よく使うsubmoduleコマンド
# submoduleの追加
git submodule add https://github.com/your-org/ui-components.git libs/ui-components
# 状態確認(現在どのコミットを指しているか)
git submodule status
# 全submoduleを最新に更新
git submodule update --remote --merge
# submoduleの削除(ちょっと手順が多い)
git submodule deinit libs/ui-components
git rm libs/ui-components
rm -rf .git/modules/libs/ui-components
submoduleの削除だけ、やたら手順が多いのが玉にキズ。
.gitmodules ファイルと .git/config と .git/modules/ の3箇所にsubmoduleの情報が散らばっているので、全部消す必要があります。
部品管理としてのsubmodule
submoduleに対する世間の評判は、正直あまりよくないです。 「複雑」「忘れる」「ハマる」と、ネガティブな声のほうが目立つ。
ただ、用途を絞れば話は変わります。
複数リポジトリで共通のコンポーネントやスキーマ定義、設定ファイル群を共有したいとき、submoduleは理にかなった選択肢です。 npm packageやmonorepo化するほどの規模ではないが、コピペで散らかすのは避けたい。そのちょうど中間の粒度で、submoduleはうまくハマります。
具体的には、こんなケースと相性がいい。
- 社内共通のlintルールやCI設定
- マイクロサービス間で共有するProtobuf定義やOpenAPIスキーマ
- デザインシステムのトークン定義
- ドキュメントテンプレート
逆に、頻繁に変更が入るコードを複数リポジトリにsubmoduleで配るのは、更新の手間が掛け算で増えるのでおすすめしません。 そういうケースはmonorepoかpackage化を検討したほうがいいです。
まとめ
submoduleは「別リポジトリへのポインタ」であるという本質を押さえておけば、操作で迷うことは減ります。
忘れがちなポイントを3つだけ。
- cloneするときは
--recursiveをつける(CIも) - submoduleのバージョン更新は自動では反映されない。意図的にcommitする
- 削除は3箇所の掃除が必要
運用のコツは、submoduleに「変更頻度の低い共通部品」を入れること。 更新が少ないからこそ、明示的なバージョン管理が活きてくる。
逆に「毎日のように変わるコード」をsubmoduleで管理すると、更新忘れと差分の嵐でチームが疲弊するので、そこは素直にmonorepoに寄せましょう。