Git submoduleの押さえておきたい理解ポイントのまとめ

みなさんgitのsubmoduleって理解して使ってますか？
親プロジェクトをpullしたら、submoduleがmodifiedになって混乱してgit addして...あばばばば。みたいな事ないですか？
私はsubmoduleがなかなか理解できずに結構苦労しました。^^;

ブランチ単位で管理する通常のリポジトリと違い、submoduleは<b>CommitID単位で管理する</b>というのが一番理解しにくい部分だと思います。

今回は、プロジェクトにsubmoduleを追加、更新、削除の動きを更新を掛ける側のプロジェクトと更新を受け入れる側のプロジェクトの2つの視点から追いながら、CommitIDで管理するとはどういう事なのかを解説していきます。
(結論だけ見たい人は末尾のまとめへ)

# 準備
「<b>submoduleを開発する役割のプロジェクト test_app_A</b>」と「<b>submoduleを取り入れる役割のプロジェクト test_app_B</b>」を用意し、更新する側と更新を受け取る側の動きを見て行きます。


* submoduleを開発する役割のプロジェクト test_app_A

```bash
git clone git@github.com:hogehoge/test_app.git test_app_A
```

* submoduleを取り入れる役割のプロジェクト test_app_B

```bash
git clone git@github.com:hogehoge/test_app.git test_app_B
```

# test_app_Aの準備

## git submodule add で submodule を取り込む

まず、```git submodule add```コマンドでtest_app_submoduleを追加します

```console
test_app_A $ git submodule add git@github.com:hogehoge/test_app_submodule.git 
submodule/test_app_submodule
Cloning into 'submodule/test_app_submodule'...
Warning: Permanently added 'github.com,192.30.252.131' (RSA) to the list of known hosts.
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.
Checking connectivity... done.
```

## submodule がプロジェクトに追加された事を確認する

test_app_A の.gitmodulesが追加されているのと、新しく test_app_submodule ディレクトリが出来ているのが確認できます。

```console
test_app_A $ git status
On branch develop
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitmodules
    new file:   submodule/test_app_submodule
```

## git status で test_app_A プロジェクトの変更内容の確認する

更新の内容を確認すると .gitmodules に test_app_submoduleの情報が書かれている事がわかります。

```diff
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..a255f9f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "submodule/test_app_submodule"]
+       path = submodule/test_app_submodule
+       url = git@github.com:hogehoge/test_app_submodule.git
diff --git a/submodule/test_app_submodule b/submodule/test_app_submodule
new file mode 160000
index 0000000..2406d35
--- /dev/null
+++ b/submodule/test_app_submodule
@@ -0,0 +1 @@
+Subproject commit 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff
```

## .gitmodules を確認する

.gitmodulesの中身はこんな感じです。

```ini
[submodule "submodule/test_app_submodule"]
       path = submodule/test_app_submodule ← 親プロジェクト内でのsubmoduleのpath
       url = git@github.com:hogehoge/test_app_submodule.git ← submodleのgit URL
```

## .gitmodules について

この .gitmodules の更新情報をpushすることで、別の人が test_app の最新版を落としてきた時に .gitmodules 内の情報を元に各 submodule のリモートリポジトリに接続しにいくわけですね。

## submodule は commitID で管理されている

さて、注目したいのが git status で確認した 「```new file: submodule/test_app_submodule```」 の部分。

更新内容を見てみると、

```diff
+Subproject commit 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff 
```
となっています。

これは test_app_A が現在参照している test_app_submodule の CommitID になります。
先ほどディレクトリが追加されていると言いましたが、 <b>イメージとしてはシンボリックリンクの様なイメージ</b> を持ってもらうと分かりやすいかと思います。

## submodule は CommitID を参照するものである

これが submodule の特徴で、test_app_A の<b>.gitmodules 内の情報を元に submodule の接続先を確認</b>し、test_app_A に保存されている submodule の <b>CommitID の情報をもとに、正しいバージョンのソースコードを持ってくる</b>わけです。

この時点で、通常のリポジトリと違い追跡する対象がブランチではなく、 CommitID である事がわかります。

<img width="1003" alt="スクリーンショット_2016-05-04_11_30_06.png" src="https://qiita-image-store.s3.amazonaws.com/0/93763/808cdfaf-ba02-8135-01d6-2f4304b22f32.png">


## submodule が参照している CommitID を確認
test_app_A が参照している submodule の CommitID は下記の様に```git submodule```コマンドで確認できます。

```console
test_app_A $ git submodule
 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff submodule/test_app_submodule (heads/master)
```


## test_app_A ディレクトリ構成の確認
test_app_submodule を追加した、test_app_A のディレクトリ構成はこんな感じになっています。

```console
test_app_A $ tree
.
├── README.md
└── submodule
    └── test_app_submodule
        └── README.md
```

上記の更新をpushすれば、他の人にもsubmoduleの追加が反映されるようになります。
では、その他の人の動きを見る為にtest_app_B側はどうなるのか見てみましょう。

# test_app_Bの準備
では次にtest_app_Bを見てみます。
## test_app_B プロジェクトを clone してくる

test_app_B を clone しただけでは subumodule 内部が反映されていない事がわかります。(README.mdが無いですね)

```console
test_app_B $ tree
.
├── README.md
└── submodule
    └── test_app_submodule
```

## submodule の参照先を確認

参照先の CommitID を見てみると「-」(マイナス)とついています。
これは、CommitIDの参照先の情報はあるけど、実際にソースコードを持って来ていない場合に表示されます。
シンボリックリンクがつながっていないイメージですかね。

```console
test_app_B $ git submodule
-2406d35dbc1581e56f94ad122d7a9f7ff248a6ff submodule/test_app_submodule
```

## submodule を取り込む

では実際にソースコードを持ってきます。
```git submodule update -i```で submodule を取り込みます。
-i オプションを付ける事で init と update を行ってくれます。

```console
test_app_B $ git submodule update -i
Submodule 'submodule/test_app_submodule' (git@github.com:hogehoge/test_app_submodule.git) registered for path 'submodule/test_app_submodule'
Cloning into 'submodule/test_app_submodule'...
Warning: Permanently added 'github.com,192.30.252.131' (RSA) to the list of known hosts.
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 5 (delta 0), pack-reused 0
Receiving objects: 100% (5/5), done.
Checking connectivity... done.
Submodule path 'submodule/test_app_submodule': checked out '2406d35dbc1581e56f94ad122d7a9f7ff248a6ff'
```

## 取り込めた事を確認

確認すると「-」が取れて、「(heads/master)」というCommitIDの位置情報も表示され、test_app_submoduleが反映されている事が分かります。

```console
test_app_B $ git submodule
 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff submodule/test_app_submodule (heads/master)
```

```console
test_app_B $ tree
.
├── README.md
└── submodule
    └── test_app_submodule
        └── README.md
```

これでtest_app_Aとtest_app_Bの準備が整いました。

# submodule を更新する
では、次に実際に submodule に更新をかけた時の test_app_A (更新元) と test_app_B  (取り込み先) の動きを見てみます。

## test_app_A の submodule のブランチを変更する
submodule のブランチを masterブランチ から testブランチ 変更します。

```console
test_app_A/submodule/test_app_submodule $ git checkout -b test
Switched to a new branch 'test'
test_app_A/submodule/test_app_submodule $ git branch
  master
* test
```

## test_app_A の submodule 更新し push する
試しに test_app_submodule の testブランチ に test.txt ファイル追加を commit して、push します。

```console
test_app_A/submodule/test_app_submodule $ git status
On branch test
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   test.txt
```
<img width="980" alt="スクリーンショット_2016-05-04_19_42_59.png" src="https://qiita-image-store.s3.amazonaws.com/0/93763/3fa61867-66be-fb9e-e3d2-87106b7921d4.png">

これで test_app_submodule (testブランチ) は test.txt を追加した更新が反映されました


## test_app_A プロジェクトの更新を確認
test_app_submodule の更新が出来たら、親の test_app_A の状態を確認してみます。
すると、```submodule/test_app_submodule (new commits)```という風にsubmoduleの更新が反映されています。

```console
test_app_A $ git status
On branch develop
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   submodule/test_app_submodule (new commits)

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

更新内容を見てみると、test_app_submodule を更新した事により、参照先の CommitID が先ほどの test.txt ファイルを追加したコミットの CommitID に変更されています。

```diff
test_app_A $ git diff submodule/test_app_submodule
diff --git a/submodule/test_app_submodule b/submodule/test_app_submodule
index 2406d35..6514fb8 160000
--- a/submodule/test_app_submodule
+++ b/submodule/test_app_submodule
@@ -1 +1 @@
-Subproject commit 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff
+Subproject commit 6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd
```

では、test_app_A プロジェクトで、この修正内容を commit して push します。

## test_app_B で submodule 更新を受け入れる
先ほどの test_app_A プロジェクトの更新 を test_app_B プロジェクトに反映するために親プロジェクトの更新を取り入れます。

```console
test_app_B $ git pull origin develop
Warning: Permanently added 'github.com,192.30.252.128' (RSA) to the list of known hosts.
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:hogehoge/test_app
 * branch            develop    -> FETCH_HEAD
   9287540..29dfec3  develop    -> origin/develop
Updating 9287540..29dfec3
Fast-forward
 submodule/test_app_submodule | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
```

すると先ほど test_app_A で submodule を更新した時と同じ様に、
```modified:   submodule/test_app_submodule (new commits)```
と test_app_submodule が更新されている状態になります。

これは、pullし た事により test_app_A で更新された test_app_submodule 最新の参照先の CommitID が反映されたのですが、test_app_B の test_app_submodule が現在参照している CommitID は古いままなので、ズレが生じている事で modified となっている状態です。

```console
test_app_B $ git status
On branch develop
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   submodule/test_app_submodule (new commits)

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

当然ですが、submoduleを更新してズレがなくればmodifiedは消えるので、ここでのmodifiedは無視します。

## ポイント
ここがイメージしにくい部分なのですが、
<b>submodule はあくまで別リポジトリなので、親のリポジトリの更新を反映しても submodule 自体は勝手に更新してくれない</b>のです。

## 既存の CommitID 参照先が上書きされる形になる？

変更内容も確認すると CommitID が test_app_A の時と逆ですね。
先ほど説明した通り、pull したことで参照先が新しい CommitID になっているのですが、この test_app_B の test_app_submodule 自体はまだ参照先が古いままなので、古い参照先の CommitID が上書きした事になっているようです。

↓test_app_A の時

```diff
test_app_A $ git diff submodule/test_app_submodule
diff --git a/submodule/test_app_submodule b/submodule/test_app_submodule
index 2406d35..6514fb8 160000
--- a/submodule/test_app_submodule
+++ b/submodule/test_app_submodule
@@ -1 +1 @@
-Subproject commit 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff 
+Subproject commit 6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd
```

↓今回 (test_app_B)

```diff
test_app_B $ git diff submodule/test_app_submodule
diff --git a/submodule/test_app_submodule b/submodule/test_app_submodule
index 6514fb8..2406d35 160000
--- a/submodule/test_app_submodule
+++ b/submodule/test_app_submodule
@@ -1 +1 @@
-Subproject commit 6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd #test_app_A更新の新しい参照先
+Subproject commit 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff #test_app_B古い参照先が上書きした形になる
```

## submodule の CommitID 参照先を確認

念のため、test_app_B の submodule の参照先を確認してみます。
古いCommitIDの「2406d35...(heads/master)」になっていますね。

よく見ると、CommitID の位置情報のブランチも masterブランチ になっています。
先ほど、test_app_A で submodule を更新する際に testブランチ に変更したので、最新の参照先は testブランチ になるはずですね。

```console
test_app_B $ git submodule
+2406d35dbc1581e56f94ad122d7a9f7ff248a6ff submodule/test_app_submodule (heads/master)
```

## submodule を update する

test_app_B でも ```git submodule update``` コマンドで submodule の更新を取り入れます。
最新の CommitID の「6514fb8...」に checked out してるのが分かります。

```console
test_app_B $ git submodule update
Submodule path 'submodule/test_app_submodule': checked out '6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd'
```

## もう一度 submodule の CommitID 参照先を確認
改めて test_app_B での参照先を確認すると、ちゃんと参照先が変わっています。
CommitID の位置情報のブランチもtestブランチになってます。

```console
test_app_B $ git submodule
 6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd submodule/test_app_submodule (remotes/origin/test)
```

## 更新が反映されたかファイルの有無を確認

更新が反映されたので、treeコマンドで test.txt が追加されている事が確認できました。
これで、submodule も test_app_A と同じ状態になりました。

```console
test_app_B $ tree
.
├── README.md
└── submodule
    └── test_app_submodule
        ├── README.md
        └── test.txt
```

もちろんsubmoduleを更新した事により、親プロジェクトの差分がなくなったので、親プロジェクトの modified も無くなっています。

```console
test_app_B $ git status
On branch develop
nothing to commit, working directory clean
```

これで、test_app_A も test_app_B も無事に最新の submodule を保持する事ができました。


#submoduleの削除する
最後に、submoduleを削除してみます。

## 参照先とのリンクを切る
まずは```git submodule deinit``` コマンドでコミットの参照を削除します。

```console
test_app_A $ git submodule deinit submodule/test_app_submodule
Cleared directory 'submodule/test_app_submodule'
Submodule 'submodule/test_app_submodule' (git@github.com:hogehoge/test_app_submodule.git) unregistered for path 'submodule/test_app_submodule'
```

この時点でsubmoduleの参照先とのリンクが切れる形になります。
一番初めにtest_app_Bで```git submodule update -i```を行う前と同じ状態になりますね。

この時点では、.gitmodules の中の情報は健在です。

```console
test_app_A $ git submodule
-6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd submodule/test_app_submodule
```

```console
test_app_A $ tree
.
├── README.md
└── submodule
    └── test_app_submodule
```

## .gitmodules からも情報を削除

次に```git rm```コマンドで.gitmodules内からも情報を削除します。

```console
test_app_A $ git rm submodule/test_app_submodule
rm 'submodule/test_app_submodule'
```

## 変更内容の確認

では、変更内容を確認してみましょう。
.gitmodules内の情報と、参照先のCommitIDが削除されている事がわかります。 

```console
$ git status
On branch develop
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

  modified:   .gitmodules
  deleted:    submodule/test_app_submodule
```

```diff
diff --git a/.gitmodules b/.gitmodules
index a255f9f..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "submodule/test_app_submodule"]
-       path = submodule/test_app_submodule
-       url = git@github.com:hogehoge/test_app_submodule.git
diff --git a/submodule/test_app_submodule b/submodule/test_app_submodule
deleted file mode 160000
index 6514fb8..0000000
--- a/submodule/test_app_submodule
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd
```

ファイルを確認しても、submoduleディレクトリ以下が空なのが分かります。

```console
test_app_A $ tree
.
├── README.md
└── submodule
```
あとは、この修正をコミットし、pushしたら削除完了です。
ちなみに、削除の場合はtest_app_Bがこのコミットpullするだけで、submoduleの削除は反映されます。

submoduleの追加、更新、削除までの基本的な使い方としては以上になります。

#まとめ
長々と説明してきましたが、最後にsubmoduleを覚える為のポイントをまとめます。

## submoduleは.gitmodulesで管理されている。
* submoduleは```git submodule add```コマンドで追加される。
* submoduleのリポジトリ先の情報等は.gitmodulesというファイルに記載される。
* .gitmodulesに記載されたリポジトリ情報と参照先のCommitID情報を元にsubmoduleと接続する。

## submoduleは親プロジェクトの特定のコミットとコミット単位で紐づく。
* submoduleを追跡する対象はブランチではなくコミットである。
* submoduleはCommitIDを参照する事で管理しています。
* ```git submodule```コマンドで現在の参照先のCommitIDが確認できます。
    
![submodule_commit.png](https://qiita-image-store.s3.amazonaws.com/0/93763/13f11972-2d66-3c5e-aea1-82248c2c7f96.png)

## submoduleはupdateコマンドで更新しなければならない。
* 親プロジェクトをpullで更新しても、submoduleは勝手に更新されません。
* 必ず```git submodule update```コマンドでの更新手続きが必要。
* 最悪の場合<b>「親プロジェクトをpullした時にsubmoduleがmodifiedになっていたらsubmodule updateする」</b>というおまじないを覚えておけばなんとかなる。笑

## submodule削除するときはファイルを消すだけではダメ
* ```git submodule deinit```コマンドと```git rm```コマンドの２段階で削除する。
* 削除の場合は親プロジェクトをpullしただけで、削除が反映される。

```bash
git submodule deinit submodule/test_app_submodule  
git rm submodule/test_app_submodule
```


## 導入メリット
* 親プロジェクトから全てのsubmoduleのバージョンを一括管理できる。
* submoduleはあくまで外部モジュールなので、親プロジェクトと別途開発ができる。
* コミットレベルで外部モジュール(submodule)のバージョンと紐づけるので、親プロジェクトのバージョンを変更した場合も安心。


とまぁ、こんな感じでしょうか^^;