【Git】submoduleの更新が反映されない原因と解決策|9パターン診断・自動化設定・総合リカバリ完全ガイド

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

「submoduleのリモートは更新されているのに、親リポジトリ側のファイルが古いまま」「pullしたのにsubmoduleの中身が変わらない」——submodule運用でもっとも多い困りごとです。原因は単純なpull忘れに見えて、実はsubmoduleの仕組みそのものへの理解不足が背景にあることが多いです。

submoduleが「動かない」時の典型症状
# 親で pull したのに submoduleが古いまま
$ git pull
$ cat submodule/VERSION
1.2.0    # ← 期待は 2.0.0

$ git status
  modified:   submodule (new commits)
# これが「submoduleのポインタが進んでいるが取り込まれていない」合図

この記事では、submoduleの更新が反映されない9つの典型原因を診断フロー付きで整理し、初期化不足・ポインタ未更新・detached HEAD・URLずれ・認証・浅いcloneなどそれぞれの解決手順を解説します。submoduleの概念モデル(親ポインタ vs サブモジュールのブランチ)を押さえれば、今後のハマりが激減します。

この記事で学べること

  • submoduleの本質:親リポジトリが保持するのは「コミットSHA固定ポインタ」
  • git submodule statusによる状態診断と表示記号の読み方
  • 9つの「反映されない」原因と対処
  • --init--recursive--remoteの使い分け
  • detached HEADで作業して親に反映させる手順
  • URL変更時のsubmodule sync必須運用
  • submodule.recurseなど自動化設定と再発防止
スポンサーリンク

submoduleの本質:親は「コミット固定ポインタ」を持つ

submoduleは別リポジトリを親リポジトリから参照する仕組みですが、親が保持するのは「submoduleがどのSHAを指すか」という情報だけ。ブランチ名ではなくコミットSHA固定です。これが「submoduleのmainブランチが進んだのに反映されない」現象の根本原因です。

親が持つ情報の実体
# 親リポジトリが記録しているのは「特定のコミットSHA」
git ls-files --stage | grep "^160000"
# 例: 160000 a1b2c3d4e5f6... 0  submodule/
# 160000 がsubmodule(ファイルモード扱い)
# SHA部分が「親が指すsubmoduleのコミット」

# .gitmodulesには設定(URL/pathなど)
cat .gitmodules
# [submodule "submodule-name"]
#     path = submodule-name
#     url = https://github.com/org/sub.git
#     branch = main  (あれば)

ポイント:submoduleが古いまま見える時、まず疑うのは親が指すポインタ自体が古いままかポインタは新しいがローカル作業ツリーが追従していないかの2点。前者なら「submodule側を更新+親でポインタをcommit」、後者なら「git submodule updateでポインタに合わせる」が解決策です。

「反映されない」9つの典型原因

原因 対処コマンド
① 新規clone後に初期化未実施 git submodule update --init --recursive
② 親pull後にポインタ適用忘れ git submodule update --recursive
③ submodule内detached HEADで手動pull ブランチcheckout→親にポインタcommit
④ ブランチ追従を--remoteで更新せず submodule update --remote --merge
⑤ URL変更後にsync未実施 git submodule sync --recursive
⑥ submodule内に未コミット変更 stash退避または--force
⑦ 認証/SSH鍵/トークン不足 URL方式統一+資格情報確認
⑧ 入れ子submoduleで再帰不足 常に--recursiveを付与
⑨ 浅いcloneで指すSHAが取得できない git fetch --unshallow

ポイント:①②③で全体の7割を占めます。「clone直後ならinit、pullしたらupdate」「submodule内作業はブランチ経由で、親にcommitまで」の2点を徹底すれば多くの問題は予防できます。

STEP 0:git submodule statusで状況を把握する

submodule status の読み方
git submodule status
# 出力例:
#    a1b2c3d4e5f6 submodule/sub1 (v1.2.0)          ← 正常(ポインタ=作業ツリー)
# + e4f5g6a7b8c9 submodule/sub2 (heads/feature)   ← 作業ツリーがポインタと異なる
# - 1a2b3c4d5e6f submodule/sub3                   ← まだ初期化されていない
# U 7j8k9l0m1n2o submodule/sub4                   ← conflict未解決

# 記号の意味:
#   (なし) 正常
#   -       未初期化(submodule update --init が必要)
#   +       ポインタと作業ツリーが不一致(submodule updateで合わせる)
#   U       merge conflict

# 再帰付きで全submodule確認
git submodule status --recursive

# よりリッチな情報
git submodule summary

# submodule内に降りての状態確認
cd path/to/submodule
git status
git log --oneline -5

記号別の対処早見

  • (マイナス):git submodule update --init
  • +(プラス):git submodule updateでポインタに合わせる(または親でcommit)
  • U:conflict解消→git addgit commit
  • 記号なし:正常、反映されないなら親側のpull忘れを疑う

原因①:新規clone後に初期化未実施

submoduleを含むリポジトリをcloneしただけでは、submoduleの中身は空のフォルダのままです。--init --recursiveで初期化と取得を明示する必要があります。

初期化と取得
# clone時に一発で
git clone --recurse-submodules <repo-url>
# 短縮形
git clone --recursive <repo-url>

# clone済みなら別途
git submodule update --init --recursive

# 特定のsubmoduleだけ初期化
git submodule update --init -- path/to/submodule

clone時の自動recurseをデフォルト化

Git 2.13+ならgit config --global clone.recurseSubmodules trueでclone時に自動で--recurse-submodules扱いになります。複数リポで頻繁にcloneするなら設定しておくと楽です。

原因②:親pull後にポインタ適用を忘れている

親リポジトリをpullしてsubmoduleのポインタSHAが更新されても、作業ツリー上のsubmoduleは自動では追従しません。pull後にgit submodule update --recursiveを実行するのがセット。

親pullの正しい手順
# 親をpull
git pull

# submoduleのポインタを作業ツリーに適用
git submodule update --recursive

# これで親が指すSHAにsubmoduleの作業ツリーが揃う

# 確認
git submodule status
# 記号が出ていなければOK
親pullと同時にsubmodule更新を自動化
# submodule.recurseを有効化
git config --global submodule.recurse true

# これでpull/checkout/resetなどでsubmoduleも自動更新される
git pull   # 自動で submodule update --recursive 相当が走る

ポイント:submodule.recurse=trueはsubmodule運用している全てのプロジェクトで恩恵が大きい設定。pullだけでなくcheckoutやresetも自動でsubmoduleを追従させてくれるので、「ポインタ適用忘れ」事故がほぼ消えます。

原因③:submodule内がdetached HEADで手動pullしている

submoduleはgit submodule updateでチェックアウトされるとdetached HEAD状態になります。そこでgit pullして更新した気でも、次回親のupdateで戻される事態に。submodule側で作業するならブランチを切り、親でポインタをcommitするまでやりきる必要があります。

submodule内でブランチ運用
# submodule内に入る
cd path/to/submodule

# detachedから開発ブランチへ
git switch main
git pull --ff-only
# または既存ブランチ
git switch feature/xxx
# 作業してcommit
git add .
git commit -m "feat: 機能追加"
git push

# 親に戻ってポインタ更新をcommit
cd ../..
git add path/to/submodule
git commit -m "Update submodule to latest main"
git push

注意:submodule内でdetached HEADのままcommitすると、どのブランチにも属さない迷子コミットになります。ブランチを切ってから作業する癖を付けましょう。detached HEAD全般はdetached HEAD状態から元の作業ブランチに戻る方法を参照。

原因④:ブランチ追従は--remoteで更新する

「submoduleは常に特定ブランチの最新に追従させたい」要件なら、.gitmodulesにbranchを指定し、--remote付きで更新します。最後に親ポインタをcommitしないとチームには伝わりません。

ブランチ追従設定と更新
# .gitmodulesでブランチ指定
# [submodule "path/to/submodule"]
#     path = path/to/submodule
#     url = git@github.com:org/sub.git
#     branch = main

# リモートブランチの先端に追従
git submodule update --remote --recursive

# --merge オプションでmerge戦略
git submodule update --remote --merge --recursive
# --rebase オプションでrebase戦略
git submodule update --remote --rebase --recursive

# 親にポインタ更新をcommit
git add path/to/submodule
git commit -m "Bump submodule to latest main"
git push

原因⑤:URL変更後にsubmodule syncしていない

submoduleのURLを.gitmodulesで書き換えても、.git/configには自動反映されませんgit submodule syncで設定を同期する必要があります。

URL変更時の手順
# .gitmodulesでURL変更後
git submodule sync --recursive
# → .git/config にも新URLが反映される

# 設定後に再取得
git submodule update --init --recursive

# URLをSSHからHTTPSに切り替える例
git config -f .gitmodules submodule.path/to/sub.url \
  https://github.com/org/sub.git
git submodule sync --recursive
git submodule update --init --recursive

URL切替が必要になる主な場面

  • OSS→enterpriseへの移行
  • SSHからHTTPSへの変更(CI環境など)
  • 組織名変更やリポジトリリネーム
  • GitHub→GitLab等の移行

原因⑥:submodule内にローカル変更が残っている

submodule内に未コミット変更があると、submodule updateで「ワーキングツリーを壊す」と拒否される場合があります。退避か破棄で解消してから更新します。

ローカル変更の処理
# submodule内で確認
cd path/to/submodule
git status

# 退避(untrackedも含める)
git stash push -u -m "wip in submodule"

# 親に戻って更新
cd ../..
git submodule update --recursive

# 必要ならstash復元
cd path/to/submodule
git stash pop

# 変更を完全に破棄して更新
git submodule update --recursive --force

警告:--forcesubmodule内の未コミット変更を破棄します。復元不可能なので、重要な作業があれば必ずstashやブランチ化で保全してから使ってください。

原因⑦:認証・権限不足でfetchできない

submoduleがプライベートリポジトリの場合、親は取得できてもsubmoduleだけfetch失敗するケース。親とsubmoduleでURL方式(SSH/HTTPS)を統一し、認証情報を揃えます。

認証関連の対処
# URLをSSHに揃える
git config -f .gitmodules submodule.path/to/sub.url \
  git@github.com:org/sub.git
git submodule sync --recursive

# HTTPSでトークン認証
git config -f .gitmodules submodule.path/to/sub.url \
  https://TOKEN@github.com/org/sub.git

# insteadOf で無理やり書き換え(CI環境で便利)
git config --global url."git@github.com:".insteadOf "https://github.com/"

# SSH接続テスト
ssh -T git@github.com

Permission deniedエラーの詳細は「Permission denied (publickey)」エラーの原因と解決方法を参照してください。

原因⑧:入れ子submoduleで再帰不足

submodule内にさらにsubmoduleがある場合、--recursiveを付けないと深い階層が古いまま残ります。

再帰オプション必須
# --recursive が常に必要
git submodule update --init --recursive
git submodule update --recursive
git submodule sync --recursive
git submodule status --recursive

# 自動で再帰になるよう設定
git config --global submodule.recurse true

原因⑨:浅いcloneで指すSHAが取得できない

CI環境などで--depth=1cloneすると、親が指すsubmoduleの特定SHAが浅い履歴に含まれていない場合があり、チェックアウト失敗します。submodule側を深掘りする必要があります。

浅いcloneの対処
# submoduleに降りて履歴を完全化
cd path/to/submodule
git fetch --unshallow
# あるいは
git fetch --depth=2147483647

# 親が指すSHAにcheckout
cd ../..
git submodule update --recursive

# CI環境なら最初から深くcloneする
# GitHub Actions: checkout@v4 の fetch-depth: 0

GitHub Actions での注意

actions/checkout@v4は既定でfetch-depth: 1(浅いclone)のため、submoduleのSHAがリモートhead付近になければエラーになります。submodules: recursivefetch-depth: 0を指定するとほぼ回避可能。

重要:最後に親のポインタ更新をcommit&push

submodule内で更新して満足せず、親側でポインタ変化をcommitしないとチームには伝わりません。「自分のローカルは反映されたのに、同僚に反映されない」事故の原因ナンバーワンです。

親でのcommit手順
# submoduleのポインタが変わっていることを確認
git status
#   modified:   path/to/submodule (new commits)

# ポインタ変更をadd
git add path/to/submodule

# 何のために更新したかを明記してcommit
git commit -m "chore: submodule path/to/submodule を v1.3.0 に更新"

# push
git push

ポイント:submoduleのcommitメッセージには更新後のバージョン/SHA/主要な変更を書くのが流儀。バージョンアップ理由が後から追えます。

submodule運用を楽にする設定

推奨Git設定
# submoduleコマンドを自動再帰に
git config --global submodule.recurse true

# clone時に自動recurse
git config --global clone.recurseSubmodules true

# 参照掃除
git config --global fetch.prune true

# fetch時にsubmoduleも取得
git config --global fetch.recurseSubmodules on-demand

# statusでsubmoduleの要約表示
git config --global status.submoduleSummary true

# diffにsubmoduleの変更内容を表示
git config --global diff.submodule log

運用ルール

  • 親pull後は必ずsubmodule update --recursive(または設定で自動化)
  • submodule更新は親ポインタcommitまでやりきる
  • URL変更は必ずsubmodule syncとセット
  • 入れ子ならすべて--recursive
  • submodule運用の難しさを知り、代替検討(subtree・monorepo・パッケージ)も視野に

submoduleの運用全般はsubmoduleの使い方と管理のベストプラクティスも参照してください。

困ったときの総合リカバリ手順

safety branch+クリーン再構築
# 親で退避ブランチ
git switch -c safety/submodule-$(date +%Y%m%d-%H%M)

# 参照同期
git fetch --prune
git submodule sync --recursive

# 強制再取得(ローカル変更は破棄されるので注意)
git submodule update --init --recursive --force

# それでもダメなら submodule を一度消して再初期化
# .gitmodulesの該当submoduleだけ削除
# git submodule deinit -f path/to/submodule
# rm -rf .git/modules/path/to/submodule
# git rm -f path/to/submodule
# その後、.gitmodulesに再追加して submodule update --init

警告:submodule deinit.git/modules/削除はsubmodule内の未push作業を失います。必ずバックアップしてから実行してください。

実践シナリオ

シナリオ① cloneしたが submodule フォルダが空

初期化
git submodule update --init --recursive

シナリオ② pullしたのに submodule が古い

ポインタ適用
git submodule update --recursive
# 今後自動化したいなら
git config --global submodule.recurse true

シナリオ③ submoduleを特定ブランチの最新に追従させたい

–remote で追従
# .gitmodulesにbranch=main を追加
git submodule update --remote --recursive
# 親にcommit
git add path/to/submodule
git commit -m "chore: submodule追従更新"
git push

シナリオ④ submoduleのURL変更

sync 必須
# .gitmodulesでURL変更
git submodule sync --recursive
git submodule update --init --recursive

やってはいけない落とし穴

submodule内でdetached HEADのままcommit

ブランチを切らずにcommitすると、そのcommitはどのブランチにも属さず迷子になります。親でポインタcommitすれば参照されるので消えはしませんが、submodule側のリモートにpushできない問題が残ります。submodule内で作業するときは必ずブランチを切ってから。

親のポインタcommitを忘れる

submodule側でpushしても、親リポジトリでポインタ更新をcommitしないと他メンバーには古いままです。最後のcommit+pushまでセットで忘れずに。

–remoteを使ったらポインタが勝手に進むと勘違い

submodule update --remoteはローカルのsubmoduleを追従させるだけで、親のポインタは自動更新されませんgit addcommitが必要。CIで自動化する場合もこのcommitステップが必須です。

submoduleの未コミットをそのまま force update で破棄

--forceでsubmodule内の作業を失う事故が多発します。必ず事前にgit statusでsubmodule側の状態確認、必要ならstashで退避してから実行しましょう。

submoduleが複雑すぎる場合に無理に使い続ける

submoduleは強力ですが、運用コストも高い仕組みです。「チームで常に迷う」「CIで問題頻発」なら、git subtreemonorepoパッケージマネージャ(npm・pip・Cargo等)への切替も検討価値あり。技術選択を見直すのも正しい判断です。

よくある質問

Qcloneでsubmoduleを忘れた
Agit submodule update --init --recursiveで初期化できます。今後はgit clone --recurse-submodulesgit config --global clone.recurseSubmodules trueで自動化。
Qpullでsubmoduleが自動更新されない
Asubmodule.recurse=true設定で自動化できます。未設定なら都度git submodule update --recursiveを。
Qsubmodule内でpushできない
Adetached HEAD状態かつ認証が通っていない可能性。git switch mainでブランチを切り替え、SSH/HTTPSで認証を通しましょう。
Q親に戻るとsubmodule内の変更が消える
Agit submodule updateがポインタの状態に作業ツリーを戻したためです。submodule側でpushして親でポインタcommitするまでやりきれば、戻されません。
QURLを変えたのにoldのままfetchしようとする
Agit submodule sync --recursiveを実行していない可能性が大。.gitmodulesの変更は.git/configに自動反映されないので明示的にsyncが必要です。
QCIでsubmodule関連が毎回エラー
AGitHub Actionsならcheckout@v4submodules: recursivefetch-depth: 0を指定。認証はSSH deploy keypersonal access tokeninsteadOfで書き換えが定番。
Qsubmoduleは辞めてもっと簡単な仕組みにしたい
Agit subtree(親履歴に取り込み)、monorepo(1リポで管理)、パッケージ化(npm/pip等)が代替案。規模・依存関係・リリースサイクルに応じて選定すると良いでしょう。

関連記事

まとめ

  • 親が保持するのは「submoduleのコミットSHA固定ポインタ
  • 9原因:init未実施/pull後update忘れ/detached/–remote未使用/sync未/ローカル変更/認証/入れ子/shallow
  • 診断はgit submodule statusの記号(-/+/U)で開始
  • clone後は--init --recursive、pull後は--recursive、URL変更はsync
  • submodule内作業はブランチを切り、最後に親でポインタcommitまで
  • 自動化:submodule.recurse=trueclone.recurseSubmodules=true
  • 複雑すぎるなら代替(subtree/monorepo/パッケージ)を検討

submoduleの「反映されない」は、Gitの概念モデル(親=SHA固定ポインタ/submodule=独立リポジトリ)を理解すれば大半が解決します。診断にはgit submodule statusの記号を読み、9原因パターンに当てはめて適切なコマンドを選びましょう。日常運用はsubmodule.recurse=true親ポインタcommitまでやりきる習慣でほとんどの事故を予防できます。チームで運用ルールを共有し、必要ならsubtreeやmonorepoへの切替も検討を。