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

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

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

準備

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

  • submoduleを開発する役割のプロジェクト test_app_A
git clone git@github.com:yusukeyamatani/test_app.git test_app_A
  • submoduleを取り入れる役割のプロジェクト test_app_B
git clone git@github.com:yusukeyamatani/test_app.git test_app_B

test_app_Aの準備

git submodule add で submodule を取り込む

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

test_app_A $ git submodule add git@github.com:yusukeyamatani/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 ディレクトリが出来ているのが確認できます。

test_app_A $ git stash
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 --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:yusukeyamatani/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の中身はこんな感じです。

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

.gitmodules について

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

submodule は commitID で管理されている

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

更新内容を見てみると、

+Subproject commit 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff 

となっています。

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

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

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

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

スクリーンショット_2016-05-04_11_30_06.png

submodule が参照している CommitID を確認

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

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

test_app_A ディレクトリ構成の確認

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

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が無いですね)

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

submodule の参照先を確認

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

test_app_B $ git submodule
-2406d35dbc1581e56f94ad122d7a9f7ff248a6ff submodule/test_app_submodule

submodule を取り込む

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

test_app_B $ git submodule update -i
Submodule 'submodule/test_app_submodule' (git@github.com:yusukeyamatani/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が反映されている事が分かります。

test_app_B $ git submodule
 2406d35dbc1581e56f94ad122d7a9f7ff248a6ff submodule/test_app_submodule (heads/master)
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ブランチ 変更します。

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 します。

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

    new file:   test.txt

スクリーンショット_2016-05-04_19_42_59.png

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

test_app_A プロジェクトの更新を確認

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

test_app_A $ git stash
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 に変更されています。

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 プロジェクトに反映するために親プロジェクトの更新を取り入れます。

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:yusukeyamatani/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 となっている状態です。

test_app_B $ git stash
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は無視します。

ポイント

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

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

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

↓test_app_A の時

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)

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ブランチ になるはずですね。

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 してるのが分かります。

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

もう一度 submodule の CommitID 参照先を確認

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

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

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

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

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

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

test_app_B $ git stash
On branch develop
nothing to commit, working directory clean

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

submoduleの削除する

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

参照先とのリンクを切る

まずはgit submodule deinit コマンドでコミットの参照を削除します。

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

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

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

test_app_A $ git submodule
-6514fb8a80aa6606e60bd00e09bd5d9ff5f924fd submodule/test_app_submodule
test_app_A $ tree
.
├── README.md
└── submodule
    └── test_app_submodule

.gitmodules からも情報を削除

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

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

変更内容の確認

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

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

    modified:   .gitmodules
    deleted:    submodule/test_app_submodule
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:yusukeyamatani/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ディレクトリ以下が空なのが分かります。

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

submoduleはupdateコマンドで更新しなければならない。

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

submodule削除するときはファイルを消すだけではダメ

  • git submodule deinitコマンドとgit rmコマンドの2段階で削除する。
  • 削除の場合は親プロジェクトをpullしただけで、削除が反映される。
git submodule deinit submodule/test_app_submodule  
git rm submodule/test_app_submodule

導入メリット

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

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