「submoduleのリモートは更新されているのに、親リポジトリ側のファイルが古いまま」「pullしたのにsubmoduleの中身が変わらない」——submodule運用でもっとも多い困りごとです。原因は単純なpull忘れに見えて、実は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の本質:親は「コミット固定ポインタ」を持つ
- 「反映されない」9つの典型原因
- STEP 0:git submodule statusで状況を把握する
- 原因①:新規clone後に初期化未実施
- 原因②:親pull後にポインタ適用を忘れている
- 原因③:submodule内がdetached HEADで手動pullしている
- 原因④:ブランチ追従は--remoteで更新する
- 原因⑤:URL変更後にsubmodule syncしていない
- 原因⑥:submodule内にローカル変更が残っている
- 原因⑦:認証・権限不足でfetchできない
- 原因⑧:入れ子submoduleで再帰不足
- 原因⑨:浅いcloneで指すSHAが取得できない
- 重要:最後に親のポインタ更新をcommit&push
- submodule運用を楽にする設定
- 困ったときの総合リカバリ手順
- 実践シナリオ
- やってはいけない落とし穴
- よくある質問
- 関連記事
- まとめ
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つの典型原因
ポイント:①②③で全体の7割を占めます。「clone直後ならinit、pullしたらupdate」「submodule内作業はブランチ経由で、親にcommitまで」の2点を徹底すれば多くの問題は予防できます。
STEP 0:git 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 add→git 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 git pull # submoduleのポインタを作業ツリーに適用 git submodule update --recursive # これで親が指すSHAにsubmoduleの作業ツリーが揃う # 確認 git submodule status # 記号が出ていなければOK
# 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内に入る 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で設定を同期する必要があります。
# .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
警告:--forceはsubmodule内の未コミット変更を破棄します。復元不可能なので、重要な作業があれば必ず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側を深掘りする必要があります。
# 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: recursive+fetch-depth: 0を指定するとほぼ回避可能。
重要:最後に親のポインタ更新をcommit&push
submodule内で更新して満足せず、親側でポインタ変化を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運用を楽にする設定
# 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の使い方と管理のベストプラクティスも参照してください。
困ったときの総合リカバリ手順
# 親で退避ブランチ 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を特定ブランチの最新に追従させたい
# .gitmodulesにbranch=main を追加 git submodule update --remote --recursive # 親にcommit git add path/to/submodule git commit -m "chore: submodule追従更新" git push
シナリオ④ submoduleのURL変更
# .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 add+commitが必要。CIで自動化する場合もこのcommitステップが必須です。
submoduleの未コミットをそのまま force update で破棄
--forceでsubmodule内の作業を失う事故が多発します。必ず事前にgit statusでsubmodule側の状態確認、必要ならstashで退避してから実行しましょう。
submoduleが複雑すぎる場合に無理に使い続ける
submoduleは強力ですが、運用コストも高い仕組みです。「チームで常に迷う」「CIで問題頻発」なら、git subtree/monorepo/パッケージマネージャ(npm・pip・Cargo等)への切替も検討価値あり。技術選択を見直すのも正しい判断です。
よくある質問
git submodule update --init --recursiveで初期化できます。今後はgit clone --recurse-submodulesかgit config --global clone.recurseSubmodules trueで自動化。submodule.recurse=true設定で自動化できます。未設定なら都度git submodule update --recursiveを。git switch mainでブランチを切り替え、SSH/HTTPSで認証を通しましょう。git submodule updateがポインタの状態に作業ツリーを戻したためです。submodule側でpushして親でポインタcommitするまでやりきれば、戻されません。git submodule sync --recursiveを実行していない可能性が大。.gitmodulesの変更は.git/configに自動反映されないので明示的にsyncが必要です。checkout@v4でsubmodules: recursiveとfetch-depth: 0を指定。認証はSSH deploy keyやpersonal access tokenをinsteadOfで書き換えが定番。関連記事
- 【Git】submoduleの使い方と管理のベストプラクティス — submodule運用の全体像
- 【Git】detached HEAD状態から元の作業ブランチに戻る方法 — submodule内detachedの対処
- 【Git】「Permission denied (publickey)」エラーの原因と解決方法 — submodule認証失敗
- 【Git】「refusing to merge unrelated histories」エラーの対処法 — 履歴整合性の別問題
- 【Git】pushを取り消す方法 — submoduleポインタのpush取消
- 【Git】管理からファイルを外す方法 — submoduleの削除
- 【Git】「Pulling is not possible because you have unmerged files」の解決方法 — submodule内でのmerge途中
- 【Git】よく使うgitコマンドまとめ — 日常コマンドの早見表
まとめ
- 親が保持するのは「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=true+clone.recurseSubmodules=true - 複雑すぎるなら代替(subtree/monorepo/パッケージ)を検討
submoduleの「反映されない」は、Gitの概念モデル(親=SHA固定ポインタ/submodule=独立リポジトリ)を理解すれば大半が解決します。診断にはgit submodule statusの記号を読み、9原因パターンに当てはめて適切なコマンドを選びましょう。日常運用はsubmodule.recurse=true+親ポインタcommitまでやりきる習慣でほとんどの事故を予防できます。チームで運用ルールを共有し、必要ならsubtreeやmonorepoへの切替も検討を。

