サブモジュールとは何か?
Gitのサブモジュールとは、元となるレポジトリに他のレポジトリを追加したときの、追加したレポジトリの通称です。
具体的には、Githubのリモートレポジトリにおいて、以下のように青色で表示されるのがサブモジュールになります。
githubのサブモジュールの例
サブモジュールは青字で表示されます。下図だと、calculator, modal, textCounterがメインのレポジトリに対するサブモジュールです。
サブモジュールを使うメリット
サブモジュールはそれ自体が別のレポジトリとして存在しているため、大本のレポジトリからは独立したものとして扱うことができます。
このため、コードの管理やファイル群がごちゃ混ぜになったり煩雑にならずにすみ、管理が楽になります。
また、機能ごとに分割すれば、エンジニアをレポジトリ単位で開発に割り当てることができます。
サブモジュールを追加する方法
コードの内容
現在推進中のプロジェクトにサブモジュールを追加するのはとても簡単です。
追加したいレポジトリのURLを指定して、「git submodule add」を実行するだけです。
git submodule add <レポジトリのURL>
- リモートレポジトリのURLを指定
- レポジトリ名のディレクトリが作成される
- .gitmoduleというファイルも作成される
- git addされた状態になる(commitの手前)
実例
例えば、現在のプロジェクトにリモートレポジトリ(https://github.com/xxx/slideshow.git)を追加する場合は以下のようになります。
$ git submodule add https://github.com/xxx/slideshow.git
Cloning into '/Users/projects/test2/slideshow'...
remote: Enumerating objects: 82, done.
remote: Counting objects: 100% (82/82), done.
remote: Compressing objects: 100% (56/56), done.
remote: Total 82 (delta 27), reused 78 (delta 23), pack-reused 0
Unpacking objects: 100% (82/82), done.
git submodule addを実行した時点では、ステージの状態になっています。コミット履歴に反映するには、コミットすれば完了です。
追加したサブモジュールの確認(git status)
追加したサブモジュールの状態を確認するにはgit statusを実行します。
submodule add後(コミット前)の状態確認
git submodule addを実行した時点では以下のようになっています。
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: .gitmodules
new file: slideshow
新規ファイル(ディレクトリ)slideshowと.gitmodulesがステージに追加されていることがわかります。
.gitmodulesには以下のようにサブモジュールの情報が記載されています。
[submodule "slideshow"]
path = slideshow
url = https://github.com/xxx/slideshow.git
サブモジュールのパスとURL情報が保存されています。
またlsでディレクトリの状態を確認すると、git submodule addしたレポジトリのディレクトリが作成されているのがわかります。
$ ls
babel.config.js slideshow/ package.json package-lock.json public/ README.md src/
git diffで確認する
git diffでサブモジュールの変更内容だけを確認するには「–submodule」オプションを使います。
git submodule addを実行した時点では、ステージの状態になっいるため、ステージのファイルと比較を行う「–cached」もつける必要があります。
git diff --cached --submodule
実例
git diff --cached --submodule
を実行すると以下のように表示されます。
$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
index e94e555..714291b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
+[submodule "slideshow"]
+ path = slideshow
+ url = https://github.com/xxx/slideshow.git
Submodule slideshow 0000000...885fd4b (new submodule)
.gitmodulesにレポジトリが追加されていることがわかります。
サブモジュールの更新方法
リモートレポジトリにあるサブモジュールの最新の更新内容をとってくる方法は3つあります。
レポジトリ内のすべてのサブモジュールを更新する
リモートレポジトリに含まれている全てのサブモジュールのコミット履歴などの情報を更新するには「–remote」オプションを付けて、「submodule update」コマンドを実行します。
git submodule update --remote
指定したサブモジュールのみを更新する
全てのサブモジュールではなく、指定したサブモジュールの情報のみを更新したい場合は、引数で「サブモジュール名」を指定します。
git submodule update --remote <サブモジュール名>
git fetchでサブモジュールの情報を取得し、マージ or リベースする
git fetchの挙動
git fetchを実行すると指定したリモートレポジトリのサブモジュールのコミット履歴などの情報も全て更新されます。
git fetch <リモートレポジトリ名>
引数の無しのgit fetchは上流ブランチの設定状況やoriginの有無で挙動が変わるので注意してください。
詳細は下記をご参考ください。
【Git】git fetch(フェッチ)とは何か?使い方を実例で解説
サブモジュール更新の実例
サブモジュールのあるプロジェクトでgit fetchを実行すると以下のようになります。
$ git fetch
remote: Enumerating objects: 193, done.
remote: Counting objects: 100% (193/193), done.
remote: Compressing objects: 100% (79/79), done.
remote: Total 193 (delta 135), reused 156 (delta 114), pack-reused 0
Receiving objects: 100% (193/193), 82.78 KiB | 657.00 KiB/s, done.
Resolving deltas: 100% (135/135), completed with 48 local objects.
From github.com:xxxx/rails-pj
8fccfd033..2a06b79c8 test -> origin/test
* [new branch] apc -> origin/apc
888570c19..0ce7f2188 master -> origin/master
+ 98ab8117f...c3d2ff203 test_ga -> origin/test_ga (forced update)
Fetching submodule rails-user
From github.com:xxxx/rails-user
f5bd9fa..e0af519 master -> origin/master
6439adb..e63c502 test -> origin/test
Fetching submodule rails-api
From github.com:xxxx/rails-api
d2ab8a8..865f236 master -> origin/master
+ 302d6ae...8aa0c0e ft -> origin/ft (forced update)
* [new branch] pr -> origin/pr
実行したのはgit fetch
のみですが、メインのリモートレポジトリ「rails-pj」以外に、サブモジュール「rails-user」と「rails-api」の全てのブランチの最新のコミットも取得していることがわかります。
git fetchしたサブモジュールの更新内容を取り込む方法
git fetchしてサブモジュールの内容が更新されたら、その内容をgit mergeやgit rebaseしてローカルレポジトリのサブモジュールのコミット履歴に取り込みます。
最初に、サブモジュールのディレクトリに移動します。
cd <サブモジュール名>
この状態で、サブモジュールの中のリモート追跡ブランチに対してgit mergeやgit rebaseを行います。
リモート追跡ブランチとは何か?
Gitではローカルレポジトリのブランチとリモートレポジトリの同じ名前のブランチが直接連携しているわけではありません。
このため、例えば、ローカルレポジトリのmainブランチで作業しているときに、git fetchを行うと、コミット履歴が更新されるのはローカルレポジトリのmainブランチではありません。
リモートレポジトリの各ブランチと直接連動しているブランチは他にあり、そのブランチのことを「リモート追跡ブランチ」と呼びます。
リモート追跡ブランチは「remotes/リモートレポジトリ名/リモートブランチ名」という名前がついているブランチです。(例: remotes/origin/main)
リモート追跡ブランチは「git branch -a」で参照できます。-aはall(全てのブランチ)という意味です。
実例:リモート追跡ブランチの参照
$ git branch -a
* aa
main
vue-router
remotes/origin/HEAD -> origin/main
remotes/origin/aa
remotes/origin/main
上記の例だと、リモートレポジトリには「main」と「aa」の2つのブランチがあり、それぞれのリモート追跡ブランチは「remotes/origin/main」「remotes/origin/aa」となっていることがわかります。
なお、「remotes/origin/HEAD -> origin/main」は取得してきた時点で最新のリモートレポジトリのデフォルトブランチを指しています。
「remotes/origin/HEAD」を使うことはほぼないので、あまり気にする必要はありません。
「remotes/origin/ブランチ名」はよく使うので覚えておく必要があります。
git mergeする方法
git mergeで現在作業中のブランチに、コミットを取得してきたリモート追跡ブランチの内容を取り込みたい場合は以下のようにします。
git merge remote/<リモートレポジトリ名>/<リモートのブランチ名>
実例
例えば、以下のようなブランチの状態だとします。
$ git branch -a
aa
main
* test
remotes/origin/HEAD -> origin/main
remotes/origin/aa
remotes/origin/main
このときに、現在のtestブランチにおいて、リモート追跡ブランチ「remotes/origin/aa」の内容をマージで取り込みたい場合は以下のようにします。
$ git merge origin/aa
Updating a90d4ef..0701d9d
Fast-forward
docker-compose.yml | 2 ++
1 file changed, 2 insertions(+)
すると、git fetchしてきた最新のリモートレポジトリ「origin」の「aa」ブランチの情報をマージすることができます。
git rebaseする方法
git rebaseで現在作業中のブランチに、コミットを取得してきたリモート追跡ブランチの内容を取り込みたい場合は以下のようにします。
git rebase remote/<リモートレポジトリ名>/<リモートのブランチ名>
実例
例えば、以下のようなブランチの状態だとします。
$ git branch -a
aa
main
* test
remotes/origin/HEAD -> origin/main
remotes/origin/aa
remotes/origin/main
このときに、現在のtestブランチにおいて、リモート追跡ブランチ「remotes/origin/aa」の内容をリベースで取り込みたい場合は以下のようにします。
$ git rebase origin/aa
Successfully rebased and updated refs/heads/test.
すると、git fetchしてきた最新のリモートレポジトリ「origin」の「aa」ブランチのコミット履歴を取り込むことができます。
サブモジュールのあるレポジトリのクローン
サブモジュールのあるレポジトリをクローンする場合は--recrusive
をつける必要があります。
これがないと、サブモジュールのフォルダは作成されるものの、中身が空の状態となってしまうので注意してください。
git clone --recursive <リモートレポジトリのURL>
なお、recrusiveは再起的の意味です。
このオプションをつけることで、大元のレポジトリのみではなく、その配下のサブモジュールの中身も取ってくる指示になります。
–recrusiveなしでクローンしてしまった場合の対処法
通常のcloneでフォルダの中身がコピーされなかった場合は以下手順で後から修正可能です。
(1)ローカルのsubmodule設定ファイルを初期化。(2)プロジェクトからデータを取得。(3)コミット。
- git submodule init
- git submodule update –remote
- git commit -m “コメント”
以上で、サブモジュールの中身が入ります。
プルリクのあるプロジェクトでサブモジュールを使用するの注意点
注意点
Githubのリモートレポジトリなど複数人で開発を進め、プルリクをする場合は注意が必要です。(かなり重要です)
自分のローカルのサブモジュールのディレクトリで作業を行ったとします。するとサブモジュールの変更内容が大本のレポジトリに表示されます。
▼実例
$ git status
On branch test
Your branch is ahead of 'origin/test' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
(commit or discard the untracked or modified content in submodules)
modified: dySlideshow (untracked content)
no changes added to commit (use "git add" and/or "git commit -a")
以下の部分に注目してください。
(commit or discard the untracked or modified content in submodules)
modified: dySlideshow (untracked content)
submodulesの中で変更があったという内容が表示されていることがわかります。
この状態でサブモジュールをgit addして、git commitしてはいけません。
なぜなら、サブモジュールの中でプルリクを出して本流ブランチ(mainやmaster)などにマージされた場合、コミット番号が変更され、結果として、リモートレポジトリが指すサブモジュールのコミット番号と、ローカルの大本のレポジトリが指すサブモジュールのコミット番号がズレるためです。
この状態で、大本のリモートレポジトリの内容をプッシュすると、他の人たちがその内容をマージしたときに、「指定したサブモジュールのコミットが無い」という状況が発生し、みんなが混乱に陥ります。
ここでは、なにやら複雑だがヤバそうというのがわかっていただければ十分です。
次の対処法に沿って処理をすればプルリクとサブモジュールのあるプロジェクトも怖くありません。
対処法
対処法は以下の手順になります。
ポイントは先にサブモジュールの中で変更やプルリクを簡潔させて、マージが終わったら、その最新のコミットをローカルに取り込むのが先ということです。
こうすれば、ローカルのサブモジュールの最新のコミットはマージ後のコミットになる。リモートレポジトリのサブモジュールにもそのコミットが含まれる。ローカルの本流にマージしたコミットも同じになります。