【Git】サブモジュールとは何か?メリットや注意点、git submoduleの使い方やオプションを実例で解説|プルリクや共同開発時の注意点

github-prograshi(プロぐらし)-kv git/github
記事内に広告が含まれていることがあります。

サブモジュールとは何か?

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」オプションって何?という方は下記をご参考ください。

【Git】git diffの使い方を実例で解説


実例

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つあります。

サブモジュールの更新方法
  1. レポジトリ内のすべてのサブモジュールを更新する。(–remote)
  2. 指定したサブモジュールのみを更新する。
  3. git fetchでサブモジュールの情報を取得し、マージ or リベースする。


レポジトリ内のすべてのサブモジュールを更新する

リモートレポジトリに含まれている全てのサブモジュールのコミット履歴などの情報を更新するには「–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を行います。

point

サブモジュールのディレクトリに移動すれば、そのプロジェクトがメインのプロジェクトの位置づけになります。


リモート追跡ブランチとは何か?

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/<リモートレポジトリ名>/<リモートのブランチ名>
point

リモート追跡ブランチを指定するときは、冒頭の「remote/」を省略することができます。

git merge origin/main

 ↑↓ 同じ

git merge remotes/origin/main


実例

例えば、以下のようなブランチの状態だとします。

$ 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 mergeの処理内容や詳細、コンフリクトが発生した場合の対処法については下記をご参考ください。

【Git】git merge(マージ)とは何か?使い方を実例で解説

git rebaseする方法

git rebaseで現在作業中のブランチに、コミットを取得してきたリモート追跡ブランチの内容を取り込みたい場合は以下のようにします。

git rebase remote/<リモートレポジトリ名>/<リモートのブランチ名>
point

リモート追跡ブランチを指定するときは、冒頭の「remote/」を省略することができます。

git rebase origin/main

 ↑↓ 同じ

git rebase remotes/origin/main


実例

例えば、以下のようなブランチの状態だとします。

$ 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」ブランチのコミット履歴を取り込むことができます。

合わせて読みたい

git rebaseの処理内容や詳細、コンフリクトが発生した場合の対処法については下記をご参考ください。

【Git】git rebase(リベース)とは何か?使い方を実例で解説



サブモジュールのあるレポジトリのクローン

サブモジュールのあるレポジトリをクローンする場合は--recrusiveをつける必要があります。

これがないと、サブモジュールのフォルダは作成されるものの、中身が空の状態となってしまうので注意してください。

git clone --recursive <リモートレポジトリのURL>

なお、recrusiveは再起的の意味です。

このオプションをつけることで、大元のレポジトリのみではなく、その配下のサブモジュールの中身も取ってくる指示になります。


–recrusiveなしでクローンしてしまった場合の対処法

通常のcloneでフォルダの中身がコピーされなかった場合は以下手順で後から修正可能です。

(1)ローカルのsubmodule設定ファイルを初期化。(2)プロジェクトからデータを取得。(3)コミット。

  1. git submodule init
  2. git submodule update –remote
  3. 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)などにマージされた場合、コミット番号が変更され、結果として、リモートレポジトリが指すサブモジュールのコミット番号と、ローカルの大本のレポジトリが指すサブモジュールのコミット番号がズレるためです。

この状態で、大本のリモートレポジトリの内容をプッシュすると、他の人たちがその内容をマージしたときに、「指定したサブモジュールのコミットが無い」という状況が発生し、みんなが混乱に陥ります。

ここでは、なにやら複雑だがヤバそうというのがわかっていただければ十分です。

次の対処法に沿って処理をすればプルリクとサブモジュールのあるプロジェクトも怖くありません。


対処法

対処法は以下の手順になります。

コミットの流れ
  1. サブモジュールで変更が発生する。
  2. サブモジュールで変更内容をコミット。
  3. 変更内容をリモートレポジトリの自分のブランチにプッシュ。
  4. リモートレポジトリでサブモジュールの本流ブランチに対してプルリクを出す。
  5. プルリクが承認される。
  6. プルリクが承認されたらその情報をローカルのブランチに統合する
  7. 本流ブランチにサブモジュールの変更内容を取り込む。

ポイントは先にサブモジュールの中で変更やプルリクを簡潔させて、マージが終わったら、その最新のコミットをローカルに取り込むのが先ということです。

こうすれば、ローカルのサブモジュールの最新のコミットはマージ後のコミットになる。リモートレポジトリのサブモジュールにもそのコミットが含まれる。ローカルの本流にマージしたコミットも同じになります。

point

大本のローカルレポジトリが指すサブモジュールのコミット番号は、リモートレポジトリのサブモジュールのコミット番号と合わせる。

タイトルとURLをコピーしました