外部リポジトリを参照・統合するGit submodule/subtree
はじめに
外部リポジトリを参照したり・モノリポとして統合する方法として Git submodule/subtree があります。
ドキュメントを読む限りだと(特に subtree の)使い方が理解できなかったので、Claude Code を利用して Lambda 関数開発の検証用プロジェクトを作成してみました。
結論として、今は 「外部リポジトリの参照には基本的に Submodule で良い」と考えています。
Submodule は更新手順が面倒な側面がありますが、それも含めて「むしろこの厳格さが安心できる」と感じています。
あくまで私個人の検証結果に基づく感想ですので、「うちは Subtree でうまくいってるよ!」という意見があればぜひ教えていただきたいです。
ディレクトリ構成
以下の通りです。
ルートリポジトリ
├── projects/utils/ # 共通ユーティリティ(ルート管理)
├── projects/submodule-app/ # Python Lambda(git submodule で外部リポジトリを参照)
└── projects/subtree-app/ # TypeScript Lambda(git subtree で外部リポジトリを統合)
-
Submodule App: 外部リポジトリを
git submoduleで参照する方式。 -
Subtree App: 外部リポジトリを
git subtreeで統合する方式。
Phase 1: セットアップ
Submodule の場合:外部リポジトリの特定のコミットハッシュを「参照」
Submodule の追加は非常に直感的でした。
git submodule add https://github.com/user/submodule-app.git projects/submodule-app
これを行うと、.gitmodules に設定が書かれ、ディレクトリには「特定のコミットハッシュへの参照(ポインタ)」が置かれます。
「ここには外部のリポジトリがあるよ」ということが明確であり、ルートリポジトリの歴史を汚さない点が、個人的には非常にスッキリしていると感じました。
Subtree の場合:実体をコピーする「混入」
一方、Subtree はコマンドが長く、最初は戸惑いました。
git subtree add \
--prefix=projects/subtree-app \
https://github.com/user/subtree-app.git \
main --squash
実行すると、外部リポジトリのファイル群が実体としてルートリポジトリにコピーされ、コミットログも取り込まれます。
「ファイルが手元にある安心感」はありますが、同時に「ルートリポジトリの履歴に外部の履歴が混ざり込む」ことへの違和感も覚えました。
Phase 2: 開発
ここが評価の分かれ目ですが、私は Submodule の「厳格さ」をポジティブに捉えるようになりました。
サブリポジトリの更新を取り込む (Pull)
Submodule: 明示的な更新
Submodule の更新は、参照先(コミットハッシュ)を更新する作業です。
# Submodule を最新にする
git submodule update --remote
「どのバージョンの外部コードを使っているか」が親リポジトリのコミットとして記録されるため、意図しないライブラリのアップデートによる破壊を防げる安心感があります。
Subtree: 履歴のマージ
Subtree は git subtree pull コマンドを使いますが、これは実質的にマージです。
コンフリクトが発生した場合、ルートリポジトリのファイルとして解消する必要があり、外部リポジトリとの整合性を保つのが少し難しいと感じました。
変更の反映
「Submodule は変更の反映が面倒」というのが最大の懸念点でした。
確かに、(1) Submodule 内でコミット・Push、(2) 親リポジトリで参照更新をコミット、という 2 段階の手順が必要です。
しかし、検証を進めるうちに、「(モノリポにまとめる意思がないなら)変更は取り込まれたリポジトリ(Submodule)内で行う」のが本来あるべき姿で、それが強制されているのはむしろ安心できると感じました。
Subtree の Push の注意点
一方、Subtree の変更を元リポジトリに戻す git subtree push は、subtree 側を直接更新した人がいた場合のコンフリクト時の解決が面倒そうでした。
「手軽に取り込める」ことの代償として、「元のリポジトリに反映する」コストが非常に高いと感じました。
Phase 3: 実際にハマったポイント
Docker Build Context
Submodule/Subtree 共通の課題として、ディレクトリ階層が深くなることによる Docker Build Context の問題がありました。
これは親ディレクトリを Context に指定することで解決しますが、構成管理としては Submodule のように「物理的に分かれている」方が、コンテナビルドの境界線も意識しやすかったです。
Subtree コマンドの複雑さ
Subtree はコマンドが長く、オプション(--squash など)を忘れると履歴が汚れてしまいます。
チーム全員がこの複雑なコマンドを正しく理解し実行するのは、学習コストが高いと感じました。
Submodule なら git add / git commit / git push という基本操作の延長で扱えるため、教育コストも低いのではないでしょうか。
結論: Subtree を選ぶ理由が明確に説明できなければ Submodule で十分
今回の検証を通じて、外部リポジトリを参照・統合する方法についての私の結論は以下のようになりました。
外部リポジトリの参照には基本的に Submodule
- 構成が綺麗: 外部リポジトリは基本的には「参照」として扱うべき。
- バージョン管理が厳密: いつの時点でどのバージョンを使っていたかが明確。
外部リポジトリの統合には Subtree
- モノリポへの統合: 複数のリポジトリを 1 つのリポジトリに完全に統合(履歴ごと移行)したい場合。
とはいえチームの運用ルールや、Git に対する習熟度によっては Subtree の方が適している場合もあるでしょう。
「Subtree の方が ⚪︎⚪︎ では便利だ」など、上記以外の使い方があればぜひコメントいただけますと幸いです。
お読みいただき、ありがとうございました。
検証リポジトリ: https://github.com/monokaai/git-submodule-subtree-demo
Discussion