【Git】submoduleの更新が反映されないときの原因と解決策

【Git】submoduleの更新が反映されないときの原因と解決策 Git

サブモジュールの更新が手元に反映されない原因は、単なる「pullし忘れ」だけではありません。
“親リポジトリが指すコミット(ポインタ)”と“サブモジュール側のブランチ先端”の概念差、初期化や再帰更新の不足、detached HEAD、URLやブランチ設定の不整合、ローカル変更の衝突などが絡み合って起こります。
本記事では、症状を見極める診断手順から、確実に反映させる実践的な解決策、再発防止の運用までを順に解説します。

まず把握する:親が指すのは「サブモジュールの特定コミット」

親リポジトリは、各サブモジュールに対して「どのコミットを使うか」というポインタを履歴に保持します。
つまり、たとえサブモジュール側のリモートでブランチ先端が進んでいても、親が指し示すコミットが変わっていなければ、手元の更新は変わりません。
反映されないと感じたら、まず「親が今どのコミットを指しているか」を確認します。

# 親リポジトリ直下で、各サブモジュールの指し先(ハッシュと相対パス)を確認
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同期・ローカル変更の解消を順に確認し、最後に親のポインタ更新をコミットすれば、確実に反映できます。
運用を型化すれば、環境差や人為ミスによるハマりは大幅に減らせます。