git submodule(サブモジュール)とは、あるGitリポジトリの中に、別のGitリポジトリを「特定のコミットを指す形」で組み込む仕組みです。 共通ライブラリや設定ファイルを複数のリポジトリで使い回すときに使われます。親リポジトリが覚えているのは中身ではなく「どのコミットを使うか」というコミットIDだけ、という点を押さえると、ほとんどのつまずきが理解できます。

この記事では、ローカルにbareリポジトリ(作業ツリーを持たない共有用リポジトリ)を作り、追加・clone・update・foreach・status・sync・set-url・set-branch・deinit・削除までを全部実行した出力をそのまま載せています。空ディレクトリ、detached HEAD、modified (new commits)、URL変更、削除後に再追加できない、CIでの取得といったよくあるトラブルも、実際に再現してから直した手順で解説します。


git submoduleコマンドの早見表

やりたいことから引けるようにまとめました。 <path> はサブモジュールを置くディレクトリ(例:libs/lib)です。

やりたいこと コマンド
サブモジュールを追加する git submodule add <URL> <path>
ブランチを指定して追加する git submodule add -b <branch> <URL> <path>
サブモジュールごとcloneする git clone --recurse-submodules <URL>
clone後に中身を取得する(空ディレクトリを直す) git submodule update --init --recursive
親が記録したコミットに合わせる git submodule update --recursive
サブモジュールを最新に更新する git submodule update --remote <path>
状態を確認する git submodule status --recursive
変更内容をコミット単位で見る git diff --submodule
全サブモジュールで同じコマンドを実行 git submodule foreach --recursive '<コマンド>'
追従するブランチを設定する git submodule set-branch --branch <branch> <path>
URLを変更する git submodule set-url <path> <新URL>
変更されたURLを手元に反映する git submodule sync --recursive
一時的に無効化する(中身を消す) git submodule deinit <path>
完全に削除する git rm <path> → rm -rf .git/modules/<path>
pull時に自動で更新する git config submodule.recurse true
未pushのサブモジュールがあればpushを止める git push --recurse-submodules=check
サブモジュールも一緒にpushする git push --recurse-submodules=on-demand

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「git submodule使い方|追加・更新・削除のコマンド一覧」


検証環境とGitのバージョン

Gitの最新版は 2.56.0(2026-09-28公開、git-scm.com で確認)です。 この記事の実行結果は、検証環境に入っている Git 2.43.0 で取得しました。この記事で使うサブコマンドはすべて 2.43.0 で動作しています。

項目 内容
Git 2.43.0(Linux)
リポジトリ ローカルの bare リポジトリ(remote/app.git・remote/lib.git・remote/util.git)
構成 app の中に libs/lib、その中に vendor/util(入れ子)
表示 出力中の作業ディレクトリは ~/work と短く置き換えています

lib は最初から vendor/util をサブモジュールとして持たせ、入れ子(再帰)の動きも確認できるようにしました。


仕組み:親リポジトリには「コミットID」だけが記録される

サブモジュールの情報は3か所に分かれて保存されます。 ここを理解すると、sync や削除の手順が必要な理由がわかります。

保存場所 中身 共有されるか
.gitmodules(親の作業ツリー) パス・URL・追従ブランチ コミットされて全員に共有
親のツリー(gitlink、モード 160000) 使うコミットID コミットされて全員に共有
.git/config(親) 手元で実際に使うURL・有効フラグ 自分の環境だけ
.git/modules/<path>/ サブモジュールのGitデータ本体 自分の環境だけ
<path>/.git(ファイル) gitdir: で上の場所を指す 自分の環境だけ
flowchart LR
    subgraph parent["親リポジトリ app"]
        GM[".gitmodules<br/>path / url / branch"]
        TREE["ツリー<br/>160000 libs/lib → 0257807"]
        CFG[".git/config<br/>submodule.libs/lib.url"]
        MOD[".git/modules/libs/lib<br/>(Gitデータ本体)"]
        WT["libs/lib/.git<br/>gitdir: ../../.git/modules/libs/lib"]
    end
    subgraph child["lib リポジトリ"]
        C1["コミット 0257807"]
    end
    GM -- init / sync でコピー --> CFG
    TREE -- 指しているのは --> C1
    WT --> MOD

git submodule add 直後の実際の出力で確認します。

git submodule add ../lib.git libs/lib
cat .gitmodules
git ls-files -s libs/lib
cat libs/lib/.git
git config --get-regexp '^submodule\.'
Cloning into '~/work/app/libs/lib'...
done.

[submodule "libs/lib"]
    path = libs/lib
    url = ../lib.git

160000 0257807a6f247a874aa755cb9a4a36c4510bcaf6 0   libs/lib

gitdir: ../../.git/modules/libs/lib

submodule.libs/lib.url ~/work/remote/lib.git
submodule.libs/lib.active true
  • 160000 はgitlink(サブモジュール)を表すモードで、その後ろの 0257807... が親に記録されたコミットIDです。
  • .gitmodules の ../lib.git は相対URLです。親のリモートURLを基準に解決され、.git/config には絶対パスで書き込まれています。
  • libs/lib/.git はディレクトリではなくファイルで、本体は親の .git/modules/ にあります。

サブモジュールを追加する(git submodule add)

ローカルパスで試すと「transport 'file' not allowed」になる

Git 2.38.1 以降、ローカルのファイルパスからのサブモジュール取得は既定で許可されていません(セキュリティ対策)。手元のbareリポジトリで練習すると、最初に次のエラーになります。

$ git submodule add ../lib.git libs/lib
Cloning into '~/work/app/libs/lib'...
fatal: transport 'file' not allowed
fatal: clone of '~/work/remote/lib.git' into submodule path '~/work/app/libs/lib' failed

練習用の環境だけで、次の設定を入れると通ります。 GitHubなどHTTPS・SSHのURLを使う通常の運用では、この設定は必要ありません。

# ローカルパスで練習するときだけ
git config --global protocol.file.allow always

追加してコミットする

git submodule add ../lib.git libs/lib
git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   .gitmodules
    new file:   libs/lib

.gitmodules と libs/lib(コミットID)の2つがステージされます。 両方をコミットしてpushすれば追加は完了です。

git commit -m 'add lib submodule'
git push origin main

入れ子のサブモジュールは、追加しただけでは取得されません。 status --recursive で - が付いているのが未取得の印です。

$ git submodule status --recursive
 0257807a6f247a874aa755cb9a4a36c4510bcaf6 libs/lib (heads/main)
-6aa5a56fe42a9e482e6b85d5a4fdddba8a10db85 libs/lib/vendor/util

$ git submodule update --init --recursive
Submodule 'vendor/util' (~/work/remote/util.git) registered for path 'libs/lib/vendor/util'
Cloning into '~/work/app/libs/lib/vendor/util'...
done.
Submodule path 'libs/lib/vendor/util': checked out '6aa5a56fe42a9e482e6b85d5a4fdddba8a10db85'

サブモジュールを含むリポジトリをcloneする

普通にcloneすると空のディレクトリになる

git clone だけでは、サブモジュールのディレクトリは空のままです。

$ git clone -q remote/app.git plain
$ ls -A plain/libs/lib
(何も表示されない)

$ git submodule status
-0257807a6f247a874aa755cb9a4a36c4510bcaf6 libs/lib

後から update --init --recursive を実行すれば取得できます。

$ git submodule update --init --recursive
Submodule 'libs/lib' (~/work/remote/lib.git) registered for path 'libs/lib'
Cloning into '~/work/plain/libs/lib'...
done.
Submodule path 'libs/lib': checked out '0257807a6f247a874aa755cb9a4a36c4510bcaf6'
Submodule 'vendor/util' (~/work/remote/util.git) registered for path 'libs/lib/vendor/util'
Cloning into '~/work/plain/libs/lib/vendor/util'...
done.
Submodule path 'libs/lib/vendor/util': checked out '6aa5a56fe42a9e482e6b85d5a4fdddba8a10db85'
オプション 役割
--init .gitmodules の内容を .git/config に登録する(初回だけ必要)
--recursive 入れ子のサブモジュールもたどる

最初から --recurse-submodules でcloneする

clone時に --recurse-submodules を付けると、入れ子まで1回で取得できます。

$ git clone --recurse-submodules remote/app.git full
Cloning into 'full'...
done.
Submodule 'libs/lib' (~/work/remote/lib.git) registered for path 'libs/lib'
...
Submodule path 'libs/lib/vendor/util': checked out '6aa5a56fe42a9e482e6b85d5a4fdddba8a10db85'

状態を確認する(git submodule status)

先頭の1文字で状態がわかります。

先頭 意味 次にやること
(空白) 親の記録どおりのコミット なし
- まだ取得していない(未初期化) git submodule update --init
+ 親の記録と違うコミットになっている 合わせるなら update、採用するなら git add してコミット
U マージでコンフリクトしている コミットIDを選んで git add

末尾のかっこ(heads/main など)は、そのコミットを指している名前です。


サブモジュールを更新する(git submodule update)

update には「親の記録に合わせる」と「リモートの最新を取ってくる」の2種類があります。 ここを混同すると、思った版にならない原因になります。

コマンド 取得するコミット 親の記録
git submodule update 親に記録されたコミット 変わらない
git submodule update --remote 追従ブランチの最新コミット 変わる(コミットが必要)

--remote で最新にして、親にコミットする

lib 側に「lib v2」をpushした後、親で --remote を実行しました。

$ git submodule update --remote libs/lib
From ~/work/remote/lib
   0257807..e236931  main       -> origin/main
Submodule path 'libs/lib': checked out 'e236931b58a4444a8889c40e1567436997bd2419'

$ git submodule status
+e236931b58a4444a8889c40e1567436997bd2419 libs/lib (remotes/origin/HEAD)

$ git status
...
    modified:   libs/lib (new commits)

$ git diff --submodule
Submodule libs/lib 0257807..e236931:
  > lib v2

+ と modified: libs/lib (new commits) は「親の記録とサブモジュールの位置がずれている」という意味です。 新しい版を使うと決めたら、親でコミットします。

git add libs/lib
git commit -m 'bump lib to v2'
git push origin main

他の人がpullした後の「modified (new commits)」

親だけをpullすると、記録は新しくなりますが、サブモジュールの中身は古いままです。 別のclone(plain)でpullした結果です。

$ git pull -q
$ git status
...
    modified:   libs/lib (new commits)

$ git submodule status
+0257807a6f247a874aa755cb9a4a36c4510bcaf6 libs/lib (heads/main)

ここで git add すると、古い版に戻すコミットになってしまいます。 親の記録に合わせるのが正しい対応です。

$ git submodule update --init --recursive
Submodule path 'libs/lib': checked out 'e236931b58a4444a8889c40e1567436997bd2419'

$ git status -s
(変更なし)

毎回の手間を減らすなら git config submodule.recurse true を設定します。 pull や switch のときにサブモジュールも合わせて更新されます(git pull --recurse-submodules を毎回付けるのと同じ効果)。


detached HEAD になる理由と作業の始め方

update で取得したサブモジュールは、ブランチではなくコミットを直接指す「detached HEAD」になります。 親が記録しているのはコミットIDなので、これが通常の状態です。

$ cd libs/lib
$ git status
HEAD detached at 0257807
nothing to commit, working tree clean

サブモジュールの中で修正してコミットしたいときは、先にブランチに切り替えます。

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git status -sb
## main...origin/main
やりたいこと 状態
使うだけ(読むだけ) detached HEAD のままでよい
サブモジュールを修正する git switch <branch> してから作業
修正を親に反映する サブモジュールでpush → 親で git add <path> してコミット

サブモジュールの変更をpushし忘れない

親だけpushして、サブモジュール側のコミットをpushし忘れると、他の人がcloneしたときにそのコミットを取得できません。 --recurse-submodules=check を付けると、pushの前に止めてくれます。

$ git push --recurse-submodules=check origin main
The following submodule paths contain changes that can
not be found on any remote:
  libs/lib

Please try

    git push --recurse-submodules=on-demand

or cd to the path and use

    git push

to push them to a remote.

fatal: Aborting.
fatal: the remote end hung up unexpectedly

on-demand にすると、サブモジュール → 親の順にまとめてpushされます。

$ git push --recurse-submodules=on-demand origin main
Pushing submodule 'libs/lib'
To ~/work/remote/lib-new.git
   ac959c4..b86a18d  main -> main
To ~/work/remote/app.git
   b4a6f92..71689d3  main -> main

常に確認したい場合は git config push.recurseSubmodules check を設定しておくと便利です。


全サブモジュールでコマンドを実行する(foreach)

foreach は各サブモジュールのディレクトリでコマンドを実行します。 --recursive で入れ子にも入ります。

$ git submodule foreach --recursive 'git log --oneline -1'
Entering 'libs/lib'
e236931 lib v2
Entering 'libs/lib/vendor/util'
6aa5a56 util v1

コマンドの中では $name・$sm_path・$sha1 などの変数が使えます。

$ git submodule foreach 'echo $name $sm_path $sha1'
Entering 'libs/lib'
libs/lib libs/lib e236931b58a4444a8889c40e1567436997bd2419

追従するブランチを変える(set-branch)

--remote で取ってくるブランチは、set-branch で指定できます。 指定しないときはリモートの既定ブランチ(HEAD)です。

$ git submodule set-branch --branch develop libs/lib
$ cat .gitmodules
[submodule "libs/lib"]
    path = libs/lib
    url = ../lib.git
    branch = develop

$ git submodule update --remote libs/lib
From ~/work/remote/lib
 * [new branch]      develop    -> origin/develop
Submodule path 'libs/lib': checked out '687f24f74b78d30214156cf0d64586152c02e41b'

$ git -C libs/lib log --oneline -1
687f24f lib develop

元に戻すときは --default です。 branch = の行が消え、既定ブランチの最新に戻りました。

$ git submodule set-branch --default libs/lib
$ git submodule update --remote libs/lib
Submodule path 'libs/lib': checked out 'e236931b58a4444a8889c40e1567436997bd2419'

URLを変更する(set-url と sync)

リポジトリの移転などでURLが変わったときは、変更する人は set-url、受け取る人は sync を使います。

変更する側:set-url

$ git submodule set-url libs/lib ../lib-new.git
Synchronizing submodule url for 'libs/lib'

$ cat .gitmodules
[submodule "libs/lib"]
    path = libs/lib
    url = ../lib-new.git

$ git config --get submodule.libs/lib.url
~/work/remote/lib-new.git

$ git -C libs/lib remote get-url origin
~/work/remote/lib-new.git

set-url は .gitmodules・.git/config・サブモジュールの origin の3つをまとめて更新します。 .gitmodules をコミットしてpushします。

受け取る側:pull が失敗したときの直し方

URL変更と新しいコミットが同時に届くと、pull がサブモジュールの取得で失敗します。 pull は親の取得時にサブモジュールも取得しようとしますが、手元の .git/config には古いURLが残っているためです。

$ git pull -q
fatal: '~/work/remote/lib.git' does not appear to be a git repository
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
...
Errors during submodule fetch:
    libs/lib
    libs/lib

$ git log --oneline -1
c56b746 bump lib to v2

最新のコミット(lib moved, bump to v3)が取り込まれておらず、親の更新も止まっています。

直し方は「親だけpull → sync → update」の3手順です。

$ git pull -q --no-recurse-submodules
$ git log --oneline -1
b4a6f92 lib moved, bump to v3

$ git submodule sync --recursive
Synchronizing submodule url for 'libs/lib'
Synchronizing submodule url for 'libs/lib/vendor/util'

$ git config --get submodule.libs/lib.url
~/work/remote/lib-new.git

$ git submodule update --init --recursive
From ~/work/remote/lib-new
 * [new branch]      develop    -> origin/develop
   e236931..ac959c4  main       -> origin/main
Submodule path 'libs/lib': checked out 'ac959c43c67b3b0a0ce99a45bac4546409852470'

sync をせずに update すると、古いURLで取得を試みて Direct fetching of that commit failed. で止まることも確認しました。


一時的に外す(deinit)

deinit は、サブモジュールの登録を外して作業ツリーを空にします。 .gitmodules や親の記録はそのままなので、update --init でいつでも戻せます。

$ git submodule deinit libs/lib
Cleared directory 'libs/lib'
Submodule 'libs/lib' (../lib-new.git) unregistered for path 'libs/lib'

$ git submodule status
-b86a18dfc9f34e240c345023abce8b74792b27c7 libs/lib

$ git submodule update --init --recursive
Submodule 'libs/lib' (~/work/remote/lib-new.git) registered for path 'libs/lib'
Submodule path 'libs/lib': checked out 'b86a18dfc9f34e240c345023abce8b74792b27c7'
Submodule path 'libs/lib/vendor/util': checked out '6aa5a56fe42a9e482e6b85d5a4fdddba8a10db85'

サブモジュール内に未コミットの変更があると止まります。 消してよい場合は deinit -f です。


サブモジュールを削除する

現在のGitでは git rm <path> で、.gitmodules の該当部分と親の記録がまとめて消えます。

$ git rm libs/lib
rm 'libs/lib'

$ cat .gitmodules
(空になった)

$ git status
...
    modified:   .gitmodules
    deleted:    libs/lib

$ git commit -m 'remove lib submodule'

ただし .git/modules/libs/lib は残ります。

$ ls .git/modules/libs
lib

削除後に同じパスで再追加できないとき

残った .git/modules/<path> があると、同じパスへの再追加が止まります。

$ git submodule add ../lib-new.git libs/lib
fatal: A git directory for 'libs/lib' is found locally with remote(s):
  origin    ~/work/remote/lib-new.git
If you want to reuse this local git directory instead of cloning again from
  ~/work/remote/lib-new.git
use the '--force' option. If the local git directory is not the correct repo
or you are unsure what this means choose another name with the '--name' option.

別のリポジトリを入れるなら .git/modules/<path> を削除してから追加します。 同じリポジトリを入れ直すだけなら --force で再利用もできます。

$ rm -rf .git/modules/libs/lib
$ git submodule add ../lib-new.git libs/lib
Cloning into '~/work/app/libs/lib'...
done.

削除手順のまとめ

手順 コマンド 消えるもの
1 git submodule deinit -f <path> .git/config の登録、作業ツリー(省略可)
2 git rm <path> .gitmodules の該当部分、親の記録
3 git commit -m "remove ..." 削除を共有
4 rm -rf .git/modules/<path> 手元のGitデータ(再追加に備えて)

トラブル逆引き表

この記事で再現したものには「再現済み」と書いています。

症状 原因 直し方 確認
clone後にディレクトリが空 --recurse-submodules なしでclone git submodule update --init --recursive 再現済み
HEAD detached at ... updateはコミットを直接チェックアウトする 作業するなら git switch <branch> 再現済み
pull後に modified: <path> (new commits) 親だけ更新され中身が古い git submodule update --recursive 再現済み
--remote 後に modified ... (new commits) 新しい版を取ってきたがまだ記録していない git add <path> してコミット 再現済み
does not appear to be a git repository URL変更が手元に反映されていない pull --no-recurse-submodules → sync --recursive → update 再現済み
A git directory for '<path>' is found locally 削除後に .git/modules が残っている rm -rf .git/modules/<path> または --force 再現済み
transport 'file' not allowed ローカルパスの取得は既定で不可 練習時のみ protocol.file.allow always 再現済み
他の人のcloneでコミットが見つからない サブモジュール側のpush忘れ push --recurse-submodules=on-demand 再現済み
入れ子が空 --recursive なし update --init --recursive 再現済み

CIでサブモジュールを取得する

CIでは「浅いclone+並列取得」にすると速くなります。 ローカルで試すときは、--depth を効かせるために file:// 形式のURLを使います。

$ git clone -q --depth 1 --recurse-submodules --shallow-submodules -j 4 file://~/work/remote/app.git ci
$ git -C ci rev-list --count HEAD
1
$ git -C ci/libs/lib rev-list --count HEAD
1
オプション 役割
--depth 1 親を最新コミットだけ取得
--shallow-submodules サブモジュールも最新だけ取得
-j 4 4つまで並列で取得

主なCIサービスの設定例です(公式ドキュメントに沿った書き方で、この環境では未実行)。

# GitHub Actions
- uses: actions/checkout@v4
  with:
    submodules: recursive
# GitLab CI
variables:
  GIT_SUBMODULE_STRATEGY: recursive

.gitmodules のURLを相対URL(../lib.git など)にしておくと、HTTPSとSSHのどちらでcloneしても同じ方式で取得されます。 非公開リポジトリをサブモジュールにする場合は、CIのトークンにそのリポジトリの読み取り権限が必要です。


submodule・subtree・パッケージマネージャの使い分け

方法 親に入るもの 向いている場面 気をつける点
git submodule コミットID 共通コードを複数リポジトリで同じ版に固定したい clone・pull時に追加の操作が必要
git subtree ファイルそのもの 利用者に特別な操作をさせたくない 取り込むと親の履歴が大きくなる
パッケージマネージャ(npm・pip・Maven など) 依存の宣言(バージョン) 公開・配布できるライブラリ パッケージとして公開する手間がある
モノレポ(1つのリポジトリ) すべてのコード 同時に変更することが多い リポジトリが大きくなる

よくある質問

Q. git submodule update と git submodule update --remote の違いは? A. update は親に記録されたコミットに合わせ、--remote はリモートの追従ブランチの最新を取ってきます。 チームで同じ版を使うには update、版を上げるときだけ --remote を使い、その後親でコミットします。

Q. サブモジュールの中で変更してもいいですか? A. できます。 先に git switch <branch> でブランチに切り替えてからコミットし、サブモジュール側をpushしてから親で git add <path> してコミットします。push --recurse-submodules=check を使うとpush忘れを防げます。

Q. .gitmodules を手で編集してもいいですか? A. 編集はできますが、手元の .git/config には反映されません。 編集後は git submodule sync --recursive を実行しましょう。URL変更なら set-url、ブランチ変更なら set-branch を使うと、まとめて更新されます。

Q. サブモジュールを完全に消すには? A. git rm <path> でコミットし、残った .git/modules/<path> を削除します。 先に git submodule deinit -f <path> を実行しておくと、.git/config の登録も消えます。


まとめ

  • 親リポジトリに記録されるのは、サブモジュールの「コミットID」だけです(モード 160000 のgitlink)。
  • cloneは --recurse-submodules、後からなら update --init --recursive で中身を取得します。
  • update は親の記録に合わせる、--remote は最新を取ってくるという違いがあります。
  • modified (new commits) は記録と中身のずれです。pull後なら update、版を上げたなら git add してコミットします。
  • URL変更は、変更する人が set-url、受け取る人が sync → update で反映します。
  • 削除は git rm → コミット → .git/modules/<path> の削除の順です。
  • push忘れは --recurse-submodules=check / on-demand で防げます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「git submodule使い方|追加・更新・削除のコマンド一覧」

株式会社RJCでは、git submoduleのようなリポジトリ管理の知識を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?