Gitのsubmoduleは「別リポジトリを親リポジトリに埋め込む」仕組みで、OSSのライブラリ参照・社内共通コンポーネント・マルチリポジトリ構成で広く使われています。しかしdetached HEAD・ポインタ更新漏れ・認証・CI/CD連携など、クセの強い挙動が多く「使いこなせる人が少ない」機能でもあります。
公式ドキュメントは淡白、Web記事はgit submodule addで止まっていることが多く、実戦運用に必要な全知識——特にdetached HEADの扱い・ブランチ追随・浅いクローン最適化・認証パターン(SSH/Deploy key/PAT)・CI/CD統合・Dependabot連携・削除/移動までまとまった情報源がほとんどありません。
この記事では、submoduleのライフサイクル全体を押さえた上で、subtree/monorepo/パッケージマネージャーとの比較判断フレーム、チーム運用のベストプラクティス15則、アンチパターン集、そしてGitHub Actions完全YAMLまで網羅する2026年版の完全ガイドとしてまとめました。「更新が反映されない」などのトラブルシューティングは別記事【Git】submoduleの更新が反映されない原因と解決策で9パターン診断まで扱っていますので、併読推奨です。
この記事で学べること
- 30秒で使えるsubmoduleコマンド早見表(20項目)
- submodule vs subtree vs monorepo vs package manager の判断フレーム
- submoduleの内部構造(
.gitmodules/.git/modules//gitlink) - 追加・clone・更新・切替・削除のライフサイクル完全版
- detached HEAD問題の本質と対処(
submodule.recurse設定) - ブランチ追随(
branch = main+--remote)と固定運用の使い分け - 浅いクローン最適化(
submodule.*.shallow/fetchRecurseSubmodules) - private submoduleの認証(SSH/Deploy key/PAT/url上書き)
- CI/CD統合:GitHub Actions完全YAMLとDependabot自動バンプ
- 移動・リネーム・完全削除の手順(
.git/modules/残骸処理) - ベストプラクティス15則とアンチパターン集
- 30秒で使えるsubmoduleコマンド早見表
- submodule vs subtree vs monorepo vs package manager:判断フレーム
- submoduleの内部構造:gitlink・.gitmodules・.git/modules/
- submoduleの追加:基本からshallowまで
- cloneしたリポジトリでsubmoduleを取得
- ブランチ追随 vs コミット固定:どちらを選ぶか
- submoduleの更新フロー:二段階コミット
- submoduleの状態確認:status / summary / diff / foreach
- CI/CD統合:GitHub Actions完全YAML
- Dependabotでsubmoduleの自動バンプ
- submoduleの削除と移動
- アンチパターン集:submoduleで絶対にやってはいけない7つ
- ベストプラクティス15則(2026年版)
- よくある質問
- 関連記事
- まとめ
30秒で使えるsubmoduleコマンド早見表
日常で使う主要コマンドを最初にまとめました。詳細は後続セクションで深掘りします。
最も重要な3設定:git config --global submodule.recurse true(毎回--recursive不要に)、push.recurseSubmodules=check(親pushがsubmodule未push時に警告)、diff.submodule=log(git diffでsubmodule変更が読める)。3つ入れるだけで事故率が激減します。
submodule vs subtree vs monorepo vs package manager:判断フレーム
「別リポジトリのコードを再利用したい」場合、submodule以外にも選択肢があります。最初に選び間違えると後で大きな技術的負債になるため、用途で正しく選択することが重要です。
Q1: そのコードは公開可能? YES → npm/pip/cargoで配布(パッケージ化) NO → Q2 Q2: 同じ組織内のコード? YES → monorepo(Turborepo / Nx / pnpm workspace) NO → Q3 Q3: 参照先の履歴を取り込みたい? YES → subtree(履歴統合、cloneしやすい) NO → submodule(特定コミット固定、軽量)
実務の肌感:新規で始めるならまずmonorepoかpackage managerを検討。submoduleは「OSS本体のコードをそのままの履歴で参照したい」「アクセス権を厳密に分けたい」「fork/パッチを保持したい」など明確な理由がある時に限り採用するのがおすすめ。
submoduleの内部構造:gitlink・.gitmodules・.git/modules/
正しく扱うには内部構造の理解が必須。submoduleは3つの要素で成り立っています。
親リポジトリ/ ├── .gitmodules ← URL/path設定(追跡対象) ├── libs/ │ └── lib/ ← サブモジュールの作業ツリー │ └── .git ← ファイル(.git/modules/libs/libへのポインタ) ├── .git/ │ ├── config ← url上書き(個人用) │ └── modules/ ← サブモジュールの実態(.git配下) │ └── libs/lib/ ← ここに HEAD, refs, objectsが入る そして親のindex/treeには: gitlink: libs/lib → コミットSHA (160000 モード) = "このパスは<他リポ>のこのコミットを指す"という情報のみ記録
3つの要素の役割
.gitmodulesと.git/configの役割分担
.gitmodulesはチーム共有(チェックイン)、.git/configは個人用上書き(ローカルのみ)。forkを使いたい人は.git/configでurlを上書きでき、チーム全体には影響しません。git submodule syncを実行すると.gitmodulesの内容を.git/configに反映します(URL変更時に必要)。
submoduleの追加:基本からshallowまで
# 標準:HEADを参照して固定 git submodule add https://github.com/example/lib.git libs/lib # 特定ブランチを追跡する設定付き git submodule add -b main https://github.com/example/lib.git libs/lib # 浅いクローンで追加(CIで履歴が不要な場合) git submodule add --depth 1 https://github.com/example/lib.git libs/lib # ブランチ設定は後からも変更可能(.gitmodulesを編集+sync) git config -f .gitmodules submodule.libs/lib.branch main git submodule sync libs/lib # 追加の最後にcommitするのを忘れない git add .gitmodules libs/lib git commit -m "chore: add submodule libs/lib"
git submodule addを実行すると親リポジトリに2つのファイル変更が入ります:.gitmodules(新規作成または追加)とgitlink(libs/libのエントリ)。両方をgit commitしないとチームメンバーが取得できません。
cloneしたリポジトリでsubmoduleを取得
# 一番簡単:cloneと同時に再帰取得 git clone --recurse-submodules https://github.com/org/app.git # 短縮(--recurseは同義の省略) git clone --recurse https://github.com/org/app.git # 既にclone済みなら初期化+取得 git submodule update --init --recursive # 浅いクローン(履歴節約、CI向き) git clone --recurse-submodules --shallow-submodules https://github.com/org/app.git # 親と子の両方を--depth 1 git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/org/app.git # 特定のsubmoduleだけ取得 git submodule update --init libs/lib # 並列取得でクローン高速化 git submodule update --init --recursive --jobs 8
submodule自動追随の設定(おすすめ)
# グローバル設定(すべてのリポで有効) git config --global submodule.recurse true # これで以下のコマンドが自動的に--recursive相当になる: # - git pull # - git checkout # - git reset # - git switch # - git rebase # クローン時は別オプション(これだけは都度必要) git config --global clone.recurseSubmodules true
submodule.recurse=trueはsubmodule運用で最も価値のある設定です。ブランチ切替やpull時に自動でsubmoduleもupdateされるため、「親だけ切り替えてsubmoduleが古いまま」という事故が激減します。
ブランチ追随 vs コミット固定:どちらを選ぶか
submoduleはデフォルトで特定コミットに固定されますが、.gitmodulesにbranch設定を入れると「ブランチの最新を追う」運用も可能。ただし用途を間違えると事故のもとです。
# .gitmodules にbranch設定 [submodule "libs/lib"] path = libs/lib url = https://github.com/example/lib.git branch = main # submodule側で追随最新を取得(親のSHAは更新される) git submodule update --remote --recursive # 更新内容を親にコミット("新しいSHAを指せ"という差分) git add libs/lib git commit -m "chore: bump libs/lib to latest main"
ブランチ追随の落とし穴:本番デプロイ時にsubmodule update --remoteを実行すると、毎回異なるSHAのコードで動くことになります。再現性が崩壊するため、本番・CIでは--remoteを絶対に使わない。ブランチ追随は”開発中の統合テスト”限定にしましょう。
submoduleの更新フロー:二段階コミット
submoduleを更新する際は、子リポジトリでコミット→親で参照SHAをコミットの二段階が必要です。この手順を抜かすと「他メンバーが取得しても古いまま」になります。
# ① submoduleへ移動(またはgit -Cで省略) cd libs/lib # ② submodule内で通常のGit操作 git checkout main # ← detached HEAD回避!(超重要) git pull origin main # (あるいは特定のバージョンタグに固定) git fetch --tags git checkout v2.0.0 cd ../.. # ③ 親にはポインタ(gitlink)変更が出る git status # modified: libs/lib (new commits) # ④ 親側に参照SHAをコミット git add libs/lib git commit -m "chore: bump libs/lib to v2.0.0" # ⑤ push(submoduleもpush済みか確認済みなら) git push
detached HEADを防ぐ
submoduleのデフォルト状態はdetached HEADです。submodule updateはブランチではなく「特定コミット」をcheckoutするため、そのまま編集・commitするとどこのブランチにも属さないコミットが生まれます。作業前に必ずgit checkout main(またはトピックブランチ)でブランチに切り替えましょう。
# submodule内で常にブランチへ移動 cd libs/lib git checkout main # または trunk, develop # 作業してcommit→push echo "change" >> file.txt git add file.txt git commit -m "feat: 新機能" git push origin main
push漏れを防ぐ設定
# submoduleが未pushだとwarn or fail git config push.recurseSubmodules check # さらに親push時に自動でsubmoduleもpush git config push.recurseSubmodules on-demand # 失敗を無視(非推奨) git config push.recurseSubmodules no
submoduleの状態確認:status / summary / diff / foreach
# 現在のsubmodule状態を一覧 git submodule status # a1b2c3d libs/lib (v2.0.0) # +d4e5f6c libs/lib2 (heads/main) ← "+"は未コミット変更あり # -9a8b7c6 libs/lib3 ← "-"は未初期化 # Uxxxxxxx libs/lib4 ← "U"はマージ競合 # 親との差分サマリー(追加・削除コミット数) git submodule summary # 特定submoduleだけ git submodule summary libs/lib # git diff でsubmoduleの詳細を表示する設定 git config --global diff.submodule log git diff # Submodule libs/lib a1b2c3d..d4e5f6c: # > feat: 新機能 # > fix: バグ修正
全submoduleで一括操作(foreach)
# 全submoduleで最新のmainに追随 git submodule foreach 'git checkout main && git pull' # 再帰的に(ネストしたsubmodule含む) git submodule foreach --recursive 'git fetch --tags' # 全submoduleの現在ブランチを表示 git submodule foreach 'echo "$name: $(git branch --show-current)"' # 全submoduleでクリーンアップ git submodule foreach --recursive 'git clean -fdx'
CI/CD統合:GitHub Actions完全YAML
CI環境では認証設定+submodule取得オプションがポイント。publicなsubmoduleは簡単ですが、privateなsubmoduleはDeploy keyかPATが必要です。
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout with submodules
uses: actions/checkout@v4
with:
submodules: recursive # または true(1階層のみ)
fetch-depth: 0 # タグ・履歴を全取得(必要な時)
- name: Build
run: npm ci && npm run build
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout with private submodules
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.SUBMODULE_PAT }}
# SUBMODULE_PATは repo:read スコープ付きのPAT
# Settingsの Secrets に事前登録
Deploy keyでの認証(SSH方式・権限分離推奨)
# 1. ssh-keygenでキー生成
ssh-keygen -t ed25519 -C "ci-submodule-key" -f submodule_deploy_key -N ""
# 2. submodule側のGitHub Settings → Deploy keys に公開鍵を追加
# 3. 親リポのSecrets に SSH_PRIVATE_KEY として秘密鍵を登録
# 4. ワークフローでSSH認証を設定
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
submodules: recursive
浅いクローンでCI高速化
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Shallow submodule
run: |
git config submodule.recurse true
git submodule update --init --recursive --depth 1
並列取得で高速化
複数のsubmoduleがあるならgit submodule update --init --recursive --jobs 8で並列取得。ネットワークが律速のCIでは2〜4倍速くなります。actions/checkout@v4は内部でこれを呼ぶため通常は自動最適化されていますが、手動update時は覚えておくと便利。
Dependabotでsubmoduleの自動バンプ
submoduleを手動更新し続けるのは手間で漏れが出ます。GitHubのDependabotがsubmoduleの自動PRに対応しているので、設定ファイル1つで脆弱性対応やバージョン追随を自動化できます。
version: 2
updates:
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
labels:
- "dependencies"
- "submodule"
reviewers:
- "your-org/maintainers"
commit-message:
prefix: "chore"
include: "scope"
Dependabotはweekly scheduleが実用的。daily だとPRが増えすぎ、monthlyだと脆弱性対応が遅れる。重要度の高いsubmoduleはSlack通知+auto-mergeを組み合わせると運用が楽になります。
submoduleの削除と移動
submoduleの削除は3箇所の痕跡を消す必要があります。git rmだけでは.git/modules/にゴミが残り、同名のsubmoduleを再度追加するとエラーになります。
# ① deinit で初期化解除 git submodule deinit -f libs/lib # ② git rm で作業ツリーと.gitmodulesから削除 git rm -f libs/lib # ③ .git/modules/libs/lib のゴミを削除(最も忘れがち) rm -rf .git/modules/libs/lib # ④ コミット git commit -m "chore: remove submodule libs/lib"
③を忘れると、同じパスに新しいsubmoduleを追加する際にA git directory for 'libs/lib' is found locallyエラーが出ます。.git/modules/<path>/を手動で消すのが正解。他メンバーもローカルでrm -rfが必要になります。
submoduleのリネーム・移動
# ① 作業ツリーを新しいパスへmove git mv libs/lib vendor/lib # ② .gitmodulesを編集(pathエントリを修正) # (通常はgit mvが自動で直してくれる) # ③ .git/modules/ の内部パスを更新 git submodule sync # ④ コミット git add .gitmodules vendor/lib git commit -m "chore: rename submodule libs/lib to vendor/lib"
アンチパターン集:submoduleで絶対にやってはいけない7つ
①submoduleにdetached HEADのままcommit。どのブランチにも属さないコミットが生まれ、submoduleをpushしても他メンバーが取得できない(ブランチに紐づかないため)。作業前に必ずgit checkout main。
②本番ビルドでsubmodule update --remoteを実行。毎回異なるSHAでビルドされ、再現性が崩壊。CIのgoldenビルドは親のgitlinkが指すSHAで固定すべき。
③親push時にsubmoduleのpushを忘れる。他メンバーがclone時に「指定されたSHAが存在しない」エラー。push.recurseSubmodules=checkで自動検出を。
④submodule内でgit rm -rf。.git/modules/のゴミが残り、再追加でエラー。必ずdeinit → git rm → rm -rf .git/modules/の手順で。
⑤深いネスト(submodule of submodule of …)。運用コストが指数関数的に増える。2階層までが限界、それ以上はmonorepoかパッケージ化を検討。
⑥URL変更後git submodule syncを実行しない。.gitmodulesだけ変えてもローカルの.git/configは古いまま。git submodule sync --recursiveで同期。
⑦CIで浅いクローンしているのにtagを参照する。--depth 1ではタグが取れない。fetch-depth: 0か明示的なfetch --tagsが必要。
ベストプラクティス15則(2026年版)
## Submodule運用ルール ### clone - `git clone --recurse-submodules <url>` を標準とする - 既にclone済みなら `git submodule update --init --recursive` ### グローバル設定(各自1回) - `git config --global submodule.recurse true` - `git config --global push.recurseSubmodules check` - `git config --global diff.submodule log` ### submodule内での作業 - 作業前に必ず `git checkout <branch>`(detached HEAD禁止) - submodule内でcommit → push - 親で `git add <path>` → commit → push ### 更新の責任 - submoduleを触ったPRは必ず親のポインタ更新もセットでmerge - 本番リリース用は必ずタグ指定(SHA直指定も可) ### 禁止 - 本番CI/CDで `submodule update --remote` を実行 - 3階層以上のネスト - submoduleを `rm -rf` で直接削除(.git/modules/残骸問題)
よくある質問
git submodule updateを実行するとデフォルトで特定コミットがcheckoutされブランチに属さない状態(detached HEAD)になります。この状態でcommitするとコミットはどのブランチからも辿れず、pushもできません。作業前にgit checkout main(などブランチへ切替)が必須です。git statusでmodified: <path> (new commits)と表示されます。詳しくは【Git】submoduleの更新が反映されない原因と解決策参照。push.recurseSubmodules=check設定で自動検出できます。また--recurse-submodules指定忘れ、認証未設定も典型原因。--depth 1の浅いクローンでタグが取れないgit fetch --tags --depth 1で明示取得、またはCIでfetch-depth: 0(全履歴)に切替。タグ参照するsubmoduleでは--shallow-submodulesと--depthの組み合わせに注意。git mv libs/lib vendor/libで作業ツリーと.gitmodulesを同時更新→git submodule syncで.git/configを同期→commit。.git/modules/の内部パスはgit submodule syncで自動追従します。.git/configでurlを上書きできます。git config submodule.libs/lib.url https://github.com/your-fork/lib.git→git submodule sync libs/libで反映。チームには影響せず自分だけforkを参照できます(.gitmodulesは変更しない)。関連記事
- 【Git】submoduleの更新が反映されない原因と解決策 — 9パターン診断・総合リカバリ
- 【Git】よく使うgitコマンド決定版チートシート — submoduleコマンド含む早見表
- 【Git】ブランチが削除できないときの原因と対処法完全ガイド — submoduleブランチ削除の注意点
- 【Git】rebaseとmergeの違いと使い分け完全ガイド — submodule更新時の履歴整形
- 【Git】タグ完全ガイド — submoduleをタグ固定する運用
- 【Git】revertとresetの違いと使い分け完全ガイド — submoduleポインタの巻き戻し
- 【Git】refusing to merge unrelated historiesエラー — subtreeとの比較
- 【Git】管理からファイルを外す方法 — submodule削除の類似操作
- 【Git】bisectでバグコミット特定完全ガイド — submoduleを含むbisectの注意点
まとめ
- submoduleは別リポジトリの特定コミットをポインタで固定する仕組み
- 内部構造:
.gitmodules(共有)+.git/config(個人)+.git/modules/(実体)+gitlink(親のtree内のSHAエントリ) - 採用判断:まずmonorepo/パッケージマネージャー/subtreeを検討、それでダメな時にsubmodule
- 最重要3設定:
submodule.recurse=true、push.recurseSubmodules=check、diff.submodule=log - 更新はsubmoduleでcheckout→commit→push後、親でadd→commit→pushの二段階
- 作業前に必ず
git checkout <branch>でdetached HEAD回避 - 本番はタグ固定、ブランチ追随(
--remote)は開発統合テスト限定 - private submoduleはDeploy keyが権限分離として最適、PAT利用時はscope最小化
- CI/CDは
actions/checkout@v4にsubmodules: recursiveを必ず指定 - 自動バンプはDependabot (gitsubmodule)で週次PR運用
- 削除はdeinit → git rm → rm -rf .git/modules/の3ステップ
- アンチパターン:detached HEADコミット/本番remote追随/push漏れ/深いネスト/直接rm
submoduleは強力な反面、内部構造とライフサイクルを理解せずに使うと”再現性崩壊”や”取得不能”などの深い事故を招きます。本記事の15則とチーム運用テンプレをCONTRIBUTING.mdに書いておけば、新規参加者でも迷わずsubmoduleを扱える環境が整います。「なぜ更新が反映されないか」のトラブル時はsubmoduleの更新が反映されない原因と解決策の9パターン診断と合わせて参照してください。

