git submodule update [--init|--remote] を勉強し直す

## 概要

submodule について理解した気になっていましたが、色々とあやふやだったためきちんとまとめる事にしました。自分用の備忘録です。

## git submodule とは

git リポジトリの中に別の git リポジトリを管理対象として含め、参照できるようにする機能。

便宜上、submodule を追加するリポジトリを親、submodule 自体を子と呼称します。

## git submodule の登録

今回 submodule として子に登録する `Child` リポジトリは次のようなコミット履歴を持っています。

```bash
$ git log --oneline
b995cb4 add commitA
```

親に上記の子を submodule として登録します。

```bash
# Childリポジトリをsubmoduleとして登録する（指定したリポジトリがcloneされる）
$ git submodule add <登録対象のリポジトリURL>

# 登録したsubmoduleを確認する（submoduleの指しているコミットが表示され、Childリポジトリの最新コミットを指している
$ git submodule status
 b995cb430d4c2f8de807b89ab6b8cc5f962fb670 Child (heads/main)

# 追加されるファイルの確認（コミットして管理対象にしておく）
$ git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   .gitmodules
        new file:   Child
```

### .gitmodules

submodule として登録したリポジトリの URL やローカルに存在するディレクトリとの対応関係を記録している。今回の例だと`Child`というディレクトリが作成され、それが submodule の中身になる。

```bash
[submodule "Child"]
path = Child
url = <登録対象のリポジトリURL>
```

## git submodule update

[git submodule の登録](#git-submodule-の登録)で確認した通り、親は登録した submodule がどのコミットを指しているかを管理しています。

`git submodule update`は submodule が指しているコミットの状態に「更新」するコマンドです。

これだけだとよくわからないので子側にもう 1 つコミットを積んで動作確認してみます。

```bash
# submoduleのChild内に入り、pull
$ cd Child
$ git pull

# 子に新しいコミットが積まれていることを確認
$ git log --oneline
84bb88c (HEAD -> main, origin/main, origin/HEAD) add commitB
b995cb4 add commitA

# この時、親側で差分を確認すると
cd ..
$ git status
...略...
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:   Child (new commits)

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

$ git diff
diff --git a/Child b/Child
index b995cb4..84bb88c 160000
--- a/Child
+++ b/Child
@@ -1 +1 @@
-Subproject commit b995cb430d4c2f8de807b89ab6b8cc5f962fb670
+Subproject commit 84bb88c6c278698c6b8df6b38b75234f4d35757e
```

親は submodule を登録した際、子がどのコミットを指しているかを管理するようになります。それは子側で pull しても自動的に更新はされず、影響を受けません。

そのため、子が実際に指しているコミットと親から見た子が指しているコミットに差分が出ている状態です。

この状態で`git submodule update`を実行すると、「親から見た submodule のコミット」へリポジトリを「更新」します。

```bash
# 親から見たsubmoduleのコミットと子が実際に指しているコミットに差分があることを確認
$ git diff
diff --git a/Child b/Child
index b995cb4..84bb88c 160000
--- a/Child
+++ b/Child
@@ -1 +1 @@
-Subproject commit b995cb430d4c2f8de807b89ab6b8cc5f962fb670
+Subproject commit 84bb88c6c278698c6b8df6b38b75234f4d35757e

# 親側で実行
$ git submodule update
Submodule path 'Child': checked out 'b995cb430d4c2f8de807b89ab6b8cc5f962fb670'

$ git diff
<差分がなくなる>

# 子側のコミットを確認(親が管理しているsubmoduleのコミットに戻されたことを確認)
$ cd Child
$ git log --oneline
b995cb4 (HEAD) add commitA
```

子が実際に指しているコミットは関係なく、「親から見た submodule のコミット」へ子を更新します。

## git submodule update --init

submodule を登録したリポジトリを clone した直後は submodule の中身が空っぽになっています。`--init`オプションをつけることで登録した submodule を clone することができます。

この時、clone されてくるのは「親から見た子が指しているコミット」です。親が submodule を登録した時点より子がコミットを積んでいたとしてもそれは引っ張ってきません。

```bash
# submoduleを登録した親をcloneする
$ git clone <リポジトリURL>

# 親の中に入り、中身を確認する
$ ls -l
total 8
drwxr-xr-x 2 develop develop 4096 Oct  6 11:02 Child/
-rw-r--r-- 1 develop develop   61 Oct  6 11:02 README.md

# 子の中身が空っぽであることを確認する
$ ls Child
total 0

# この状態でgit submodule update --initを実行すると、空っぽだった子(Child)の中身がcloneされる
$ git submodule update --init

# 子の中に入り、ブランチと中身を確認してみる
$ ls -l Child
total 0
-rw-r--r-- 1 develop develop 0 Oct  6 11:04 commitA

# 親から見た子が指していた当時のコミット(HEADは最新のコミットを指さない)
$ git branch
* (HEAD detached from b995cb4)
  main
```

HEAD や detached from などの解説はこちらがわかりやすかったです。

https://qiita.com/ymzkjpx/items/00ff664da60c37458aaa

## git submodule update --remote

子が最新のコミットを指すようにするには、`--init`オプションで submodule 自体を clone 後に`--remote`オプションを付けて実行します。

```bash
# 親から見た子が指していた当時のコミット(最新のHEADは指さない)
$ cd Child
$ git branch
* (HEAD detached from b995cb4)
  main

# 子が最新のコミットを指すようにする
$ cd ..
$ git submodule update --remote
Submodule path 'Child': checked out '84bb88c6c278698c6b8df6b38b75234f4d35757e'

# 子のブランチを確認してみる
$ cd Child
$ git branch
* (HEAD detached at 84bb88c)
  main
```

`--remote`を付けて実行すると、commitB コミットを追加した最新の状態に更新された。

## おわりに

あまり深追いするとボリュームが重いのでこの辺りまで。