Git サブモジュール

# はじめに

サブモジュールを理解する上でわかりづらいのは、

**親リポジトリでブランチ移動しても、サブモジュールのブランチは追従してこない** 

こと。サブモジュールを追従させるには `$ git submodule update` を実行する必要がある。

# サブモジュールとは

複数のプロジェクトを「別々に管理したい」が、「それぞれのプロジェクトがお互いのバージョンに依存しているため、紐付けは行いたい」という場合に使用される。つまり、サブモジュールを利用することで、利用しているリポジトリの外部のリポジトリに対する依存を簡単に管理できるようになる。

サブリポジトリはそれ自身が通常のリポジトリであるため、サブモジュール内に入り、自身のリポジトリに対してGit操作を行う際は、通常のリポジトリでの操作と何ら変わりがない。そのため、以下の説明では「リポジトリA」内に「リポジトリB」をサブモジュールとして追加した場合、「リポジトリA」を**親リポジトリ**、「リポジトリB」をサブリポジトリと呼ぶことにする。

サブモジュールは親リポジトリと紐づいているが、親リポジトリから独立したサブモジュール自身のコミット履歴を保持している。

# 親リポジトリの生成

`parent`ディレクトリを生成する。

```console:
$ mkdir parent
```

`parent`ディレクトリにリポジトリを生成する。

```console
$ cd parent
parent/$ git init
```

リポジトリが生成された（`.git`ファイルの存在）ことを確認する。

```console
parent/$ ls -a
.   ..  .git
```

`index.html`ファイルを生成して、`main`ブランチとしてコミットする。

```console
parent/$ git branch
* master

parent/$ git branch 'main'
parent/$ git branch
  main
* master

parent/$ git switch main
parent/$ touch index.html
parent/$ ls
index.html

parent/$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	index.html

nothing added to commit but untracked files present (use "git add" to track)

parent/$ git add .
parent/$ git commit -m 'first commit'
parent/$ git status
On branch main
nothing to commit, working tree clean
```

# サブモジュールにするリポジトリの生成

GitHubを利用して、親リポジトリ`parent`のサブモジュールにする予定のリモートリポジトリ `sub_module` を生成する。

具体的には、まずローカルリポジトリとして `sub_module` を生成する。

続いて `sub_main` ブランチ上で `index.html` を作成した状態を **sub first commit** としてコミットし、そのコミットをリモートリポジトリへプッシュする。

```console
parent/$ mkdir sub_module
parent/$ cd sub_module
parent/sub_module/$ git init
parent/sub_module/$ touch index.html
parent/sub_module/$ git add .
parent/sub_module/$ git commit 'sub first commit'
parent/sub_module/$ git branch sub_main
parent/sub_module/$ git switch sub_main
parent/sub_module/$ git branch
  master
* sub_main

parent/sub_module/$ git remote add origin https://github.com/<GitHubのユーザ名>/sub_module.git
parent/sub_module/$ git push origin sub_main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 282 bytes | 282.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/<GitHubのユーザ名>/sub_module.git
   a33abc6..f733b3a  sub_main -> sub_main
```

この時点で以下の状態になっている。

![Screenshot 2024-09-09 at 6.51.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/1a913edb-caac-d4f9-a95f-8d23cecf5938.png)


```console:parent
(repository: parent)
(branch: main)

parent
  ├ index.html
  ├ sub_module/
```

```console:sub_module
(repository: sub_module)
(branch: sub_main)

sub_module
  ├ index.html
```



# サブモジュールの生成 / `$ git submodule add <URL>`

生成したリモートリポジトリ `sub_module` を、ローカルリポジトリの `parent` のサブモジュールとして登録する。

```console
parent/$ git submodule add https://github.com/<GitHubのユーザ名>/sub_module.git
Cloning into '<ローカルPCのパス>/parent/sub_module'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (3/3), done.
```

リポジトリ `parent` 内に、サブモジュール `sub_module` が生成されていることを確認。

![Screenshot 2024-09-08 at 18.19.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/4cc7655b-9e11-7863-bf60-b74e8bb07e55.png)

```console:parent
parent/$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   .gitmodules
	new file:   sub_module
```

`parent` リポジトリに生成したサブモジュールに対する変更が差分として扱われており（`git add` された状態だった）、`sub_module` ディレクトリには `.gitmodules` ファイルが生成されていることがわかる。

`.gitmodules` ファイルの中身を見てみると、「サブモジュールとして指定したリポジトリのURL」と「それを取り込んだディレクトリ名」が記載されていた。

```console:
parent/$ cat .gitmodules
[submodule "sub_module"]
	path = sub_module
	url = https://github.com/<GitHubのユーザ名>/sub_module.git
```

![Screenshot 2024-09-08 at 18.20.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/18acdeed-0877-8732-76ef-80eb583f6b2a.png)

（以降 `$ cd` によるディレクトリの移動を省略しています）

```console:sub_module
parent/sub_module/$ git status
On branch sub_main
Your branch is up to date with 'origin/sub_main'.
```

サブモジュール（`sub_module`）に対しては変更を加えていないため、`sub_module`リポジトリにおいては、ブランチが最新のコミットを参照していることがわかる。また、親リポジトリに対して独立してリポジトリ（`sub_module`）が存在していることもうかがえる。

親リポジトリの変更分を **second commit** としてコミットする。

```console:
parent/$ git commit -m 'second commit'
parent/$ git status
On branch main
nothing to commit, working tree clean
```

# サブモジュールの情報を確認する / `$ git submodule` / `$ git submodule status` 

親リポジトリがサブモジュールのどのブランチを参照しているかなどを知ることができる。

`$ git submodule` コマンドは、リポジトリ内にサブモジュールが複数ある場合のサブモジュールの一覧や状態に関する情報を表示することができる。

```console:$ git submodule
parent/$ git submodule
a33abc6f5aff689c0914cb1b5d77739312d162ff sub_module (heads/sub_main)
```

`$ git submodule status` コマンドは、サブモジュールが参照しているコミットIDと、そのコミットがどのブランチに属しているかを表示することができる。

```console:$ git submodule status
parent/$ git submodule status
a33abc6f5aff689c0914cb1b5d77739312d162ff sub_module (heads/sub_main)
```

# 親リポジトリで起きた変更とサブモジュール（サブモジュールのブランチに変更がない場合）

ここで、親リポジトリの `main` ブランチを **third commit** コミットまで進めてみる。`index.html` を編集する。

![Screenshot 2024-09-08 at 18.14.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/9e9f576a-711f-5d31-fde3-be6a86183177.png)

```console:
parent/$ git status
On branch main
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)
	modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")

parent/$ git add .
parent/$ git commit -m 'third commit'
parent/$ git status
On branch main
nothing to commit, working tree clean
```

# サブモジュールで起きた変更と親リポジトリ

今度は、サブモジュールの `sub_main` ブランチを **sub second commit** コミットまで進めてみる。サブモジュールの `index.html` を編集する。

![Screenshot 2024-09-08 at 19.45.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/12daed9f-7309-b11c-9f4c-85a2547ea65e.png)


```console:
parent/sub_module/$ git status
On branch sub_main
Your branch is up to date with 'origin/sub_main'.

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)
	modified:   index.html

parent/sub_module/$ git add .
parent/sub_module/$ git commit -m 'sub second commit'
parent/sub_module/$ git push origin sub_main
```

```console:
parent/sub_module/$ git log --oneline
f733b3a (HEAD -> sub_main, origin/sub_main) sub second commit
a33abc6 sub first commit
```


この時、親リポジトリからはどのように見えるのかを確認してみる。


```console:
parent/$ git status
On branch main
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)
	modified:   sub_module (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

parent/$ git submodule status
+f733b3a79c7b27452c3772a969358b68f22531d9 sub_module (heads/sub_main)
```

サブモジュールの変更前は以下のコミットIDを参照していたので、サブモジュールの変更が差分として表示されていることになる。

> parent/$ git submodule status
a33abc6f5aff689c0914cb1b5d77739312d162ff sub_module (heads/sub_main)

つまり親リポジトリとサブモジュールの紐づけ情報とは、ローカルの状態そのもののことを指していることがわかる。

これをコミットする。

```console
parent/$ git add .
parent/$ git commit -m 'ref sub_module_second'
```

# 親リポジトリで起きた変更とサブモジュール（サブモジュールのブランチに変更がある場合）

ここで、親リポジトリとサブモジュールのブランチとコミットの関係を整理する。

()内はサブモジュールのコミット。

- **first commit**
    - 親リポジトリ: `index.html` 作成
    - サブモジュール: 存在していない
- **second commit**（**sub first commit**）
    - 親リポジトリ: 更新なし
    - サブモジュール: `index.html` 作成
- **third commit** (**sub first commit**)
    - 親リポジトリ: `index.html` に `hello world`
    - サブモジュール: 更新なし
- **ref sub second** (**sub second commit**)
    - 親リポジトリ: 更新なし
    - サブモジュール: `index.html` に `sub hello world`

![Screenshot 2024-09-08 at 22.02.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/eb0e3e66-6e48-ef67-b6d1-2dfe2a21c90a.png)



親リポジトリに `test` ブランチを作り、一つ前のコミットを参照させる。

【変更前】 **ref sub second** (**sub second commit**)
【変更後】 **third commit** (**sub first commit**)

![Screenshot 2024-09-08 at 22.02.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/7683be9b-068a-91c5-f347-faf5571dc0d2.png)



```console
parent/$ git branch test
parent/$ git switch test
parent/$ git branch
  main
  master
* test

parent/$ git log --oneline
c33be2f (HEAD -> test, main) ref sub_module_second
03622c2 third commit
8cd9d33 second commit
ebea95c first commit

parent/$ git reset --hard 8cd9d33
HEAD is now at 8cd9d33 second commit

parent/$ git log --oneline
8cd9d33 (HEAD -> test) second commit
ebea95c first commit
```

この時、親ブランチの `test` ブランチはサブモジュール **sub first commit** と紐づいているはずなので、サブモジュール内の `index.html` からは `sub hello world` の文字がなくなっていそうだが、実際にどうなっているかを見てみる。

```console
parent/$ git status
On branch test
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)
	modified:   sub_module (new commits)

no changes added to commit (use "git add" and/or "git commit -a")
```

![Screenshot 2024-09-08 at 19.45.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/7a555c39-e99a-c1dc-187e-182decbacfdf.png)

これらの結果からわかるのは、サブモジュールは **sub second commit** のままであり、自動的に親リポジトリの **second commit** と紐づく **sub first commit** の状態にはならなかったということである。

つまり **親リポジトリにおいてブランチを移動しても、サブモジュールのブランチは追従してこない**。このことからサブモジュールはあくまでも親リポジトリから独立して存在する別のリポジトリであることがわかる。

:::note info
サブモジュールはあくまでも親リポジトリから独立して存在する別のリポジトリ
:::

# サブモジュールの更新 / `$ git submodule update`

この状態から、サブモジュールを **sub first commit** の状態にするには `$ git submodule update` コマンドを実行する必要がある。

`$ git submodule update` コマンドには `--init` オプションがあり、ローカルリポジトリに初めてリモートリポジトリからサブモジュールをクローンする場合などは、リポジトリの初期化が必要なため `--init` オプションをつける。

また、サブモジュールがさらにサブモジュールを持つ場合などには `--recursive` オプションをつけることで再帰的に更新を行うことができる。

どちらも付けて不具合の原因になることはないため、毎回つけていても良い。

```console
parent/$ git submodule update --init --recursive
Submodule path 'sub_module': checked out 'a33abc6f5aff689c0914cb1b5d77739312d162ff'

parent/$ git status
On branch test
nothing to commit, working tree clean
```

![Screenshot 2024-09-08 at 21.52.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/cbde6b86-3b38-db97-bd0d-b42ce24a5a38.png)

これで親リポジトリ `test` ブランチが紐づくサブモジュールの **sub first commit** の状態になったことになる。

参考にしたサイトでも述べられていたが、ここがサブモジュールの挙動がわかりづらい原因と思われる。

# 参考
- [Git submodule の基礎](https://qiita.com/sotarok/items/0d525e568a6088f6f6bb)
- [GitSubmoduleの基本的な使い方](https://qiita.com/lvn-takeuchi/items/58e8c8dfc2e775ba32dc)
- [Git サブモジュール](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E3%82%B5%E3%83%96%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB)