サブモジュールの更新が手元に反映されない原因は、単なる「pullし忘れ」だけではありません。
“親リポジトリが指すコミット(ポインタ)”と“サブモジュール側のブランチ先端”の概念差、初期化や再帰更新の不足、detached HEAD、URLやブランチ設定の不整合、ローカル変更の衝突などが絡み合って起こります。
本記事では、症状を見極める診断手順から、確実に反映させる実践的な解決策、再発防止の運用までを順に解説します。
- まず把握する:親が指すのは「サブモジュールの特定コミット」
- ① 初期化・取得が不十分(新規クローン後/再帰が足りない)
- ② 親ブランチ切替やpull後にポインタ更新を適用していない
- ③ サブモジュールがdetached HEADのまま手動で進めている
- ④ サブモジュールのブランチ追従を期待している(が、親のポインタは固定のまま)
- ⑤ URLやリモート名の変更が未反映(sync不足)
- ⑥ サブモジュールにローカル変更が残っていて更新が当たらない
- ⑦ 認証・権限・ネットワーク起因でfetchできていない
- ⑧ サブモジュールの中にもサブモジュール(入れ子)がある
- ⑨ 浅いクローン/履歴が浅くて指し先コミットが取得できない
- 変更を「反映したつもり」で終わらせない:親のコミットを忘れず作る
- トラブル時の総合リカバリ手順(安全策)
- 再発防止:運用と設定のポイント
- まとめ
まず把握する:親が指すのは「サブモジュールの特定コミット」
親リポジトリは、各サブモジュールに対して「どのコミットを使うか」というポインタを履歴に保持します。
つまり、たとえサブモジュール側のリモートでブランチ先端が進んでいても、親が指し示すコミットが変わっていなければ、手元の更新は変わりません。
反映されないと感じたら、まず「親が今どのコミットを指しているか」を確認します。
# 親リポジトリ直下で、各サブモジュールの指し先(ハッシュと相対パス)を確認
git submodule status
# サブモジュール下に降りて現在地を確認
cd path/to/submodule
git status
git log --oneline -n 5
① 初期化・取得が不十分(新規クローン後/再帰が足りない)
新規クローンや別環境では、サブモジュールの初期化と取得が必要です。深い入れ子がある場合は再帰オプションを付けます。
# クローン直後にまとめて初期化・取得・チェックアウト
git submodule update --init --recursive
# 既に初期化済みでも、念のため再帰で更新
git submodule update --recursive
以降のクローンは、最初から再帰オプションを付けると楽になります。
git clone --recurse-submodules <repo-url>
② 親ブランチ切替やpull後にポインタ更新を適用していない
親でブランチを切り替えたり、pullしてポインタが変わった場合、サブモジュールの作業ツリーをそのポインタへ合わせる必要があります。
「更新したのに中身が古い」は、この適用を忘れているケースが最多です。
# 親の指し示すコミットへ各サブモジュールを合わせる
git submodule update --recursive
③ サブモジュールがdetached HEADのまま手動で進めている
サブモジュールを開くと、多くの場合は特定コミットを指すdetached HEADです。
そこで手動でpullしても、親のポインタが更新されない限り、親に戻ると元のコミットへ引き戻されます。
サブモジュール側でブランチを使って開発するなら、ブランチをチェックアウトした上で更新し、親側のポインタもコミットして揃えます。
# サブモジュール内でブランチを明示して更新
cd path/to/submodule
git switch main
git pull --ff-only
# 親へ戻り、ポインタ更新(サブモジュールのハッシュ変化)をコミット
cd ../..
git add path/to/submodule
git commit -m "Update submodule to latest main"
git push
④ サブモジュールのブランチ追従を期待している(が、親のポインタは固定のまま)
「サブモジュールは常に特定ブランチの最新にしておきたい」要件なら、.gitmodules
にブランチ設定を記述し、--remote
で更新します。
ただし最終的には親側のポインタを更新コミットしないと、他メンバーに最新が伝わりません。
# 例:.gitmodules にブランチ設定(親リポジトリ直下で編集)
# [submodule "path/to/submodule"]
# path = path/to/submodule
# url = git@github.com:org/sub.git
# branch = main
# 設定後、リモート先端に追従してマージ(またはリベース)
git submodule update --remote --merge --recursive
# 親にポインタ更新をコミット
git add path/to/submodule .gitmodules .git/config
git commit -m "Track submodule branch and update pointer"
git push
⑤ URLやリモート名の変更が未反映(sync不足)
サブモジュールのURLを変更したのに反映されないときは、sync
で設定を同期します。
# .gitmodules→.git/config への反映
git submodule sync --recursive
# その後に再度取得
git submodule update --init --recursive
⑥ サブモジュールにローカル変更が残っていて更新が当たらない
サブモジュール内に未コミット変更があると、submodule update
やチェックアウトが拒否されることがあります。
退避または破棄してから更新します。どうしても破棄でよければ--force
も使えます。
# サブモジュール直下で退避
git stash push -m "wip in submodule"
# 破棄して強制的に親の指すコミットへ合わせる(注意)
cd ../..
git submodule update --recursive --force
⑦ 認証・権限・ネットワーク起因でfetchできていない
サブモジュールがプライベートリポジトリの場合、親の取得は成功してもサブモジュールのfetchで失敗して中身が進まないことがあります。
使用するプロトコル(SSH/HTTPS)と資格情報の設定を統一し、必要に応じてURLを書き換えて同期します。
# URLをSSHに切り替える例
git config -f .gitmodules submodule.path/to/submodule.url git@github.com:org/sub.git
git submodule sync --recursive
git submodule update --init --recursive
⑧ サブモジュールの中にもサブモジュール(入れ子)がある
再帰オプションを付けないと、深い階層が古いままになります。入れ子構造では常に--recursive
を意識します。
git submodule update --init --recursive
⑨ 浅いクローン/履歴が浅くて指し先コミットが取得できない
サブモジュールを浅くクローンしていると、親が指す特定コミットが手元に存在せず、チェックアウトに失敗することがあります。
一時的に深掘りして取得し直します。
cd path/to/submodule
git fetch --unshallow || git fetch --depth=2147483647
git checkout <parent-pinned-commit>
変更を「反映したつもり」で終わらせない:親のコミットを忘れず作る
サブモジュール内で更新して満足しても、親側でポインタの変化をコミットしなければ、他メンバーに反映されません。
最後は必ず親リポジトリでサブモジュールのエントリをadd
し、コミット・pushします。
git add path/to/submodule
git commit -m "Bump submodule pointer"
git push
トラブル時の総合リカバリ手順(安全策)
うまくいかないときは、退路を確保してからクリーンにやり直します。バックアップブランチを切り、同期→強制合わせ→再取得の順で進めます。
# 親で退避ブランチ
git switch -c safety/submodule-$(date +%Y%m%d-%H%M)
# 参照更新と掃除
git fetch --prune
# 設定同期と再帰更新(必要ならforce)
git submodule sync --recursive
git submodule update --init --recursive --force
再発防止:運用と設定のポイント
サブモジュールの更新は「親のポインタを動かす作業」であることをチームで共有します。
日常運用では、親のpull後に必ずgit submodule update --recursive
を実行し、サブモジュール側でブランチを進めたら親でポインタ更新をコミットする、という型を徹底します。
URL変更時はsubmodule sync
をセットにし、入れ子構造は常に再帰更新を使う、といったルール化が有効です。
まとめ
「サブモジュールの更新が反映されない」問題の本質は、親が保持する“指し先コミット”と、サブモジュールの“ブランチ先端”のズレにあります。
初期化・再帰更新・detached HEADの理解・ブランチ追従設定・URL同期・ローカル変更の解消を順に確認し、最後に親のポインタ更新をコミットすれば、確実に反映できます。
運用を型化すれば、環境差や人為ミスによるハマりは大幅に減らせます。