Git submodule の基礎

この記事は [Git Advent Calendar](http://qiita.com/advent-calendar/git) 6日目の記事です！
Git submodule って最初わかりにくいと思うので、基本的な説明をしようと思います。

## git submodule とは

`git submodule` は、外部の git リポジトリを、自分の git リポジトリのサブディレクトリとして登録し、特定の commit を参照する仕組みです。
Subversion でいうところの、external と似ています。


さて、解説のため、手元に、リポジトリA (/path/to/a) とAの submodule として、よく使う例として  Bootstrap (元Twitter Bootstrap) を登録してみます。

git submodule を理解するうえで重要なのは、

* リポジトリAが指し示すsubmoduleとしてのBootstrapのcommit
* 現在のBootstrapのcommit

です。

## 編集補足

* 2022年現在も Git Submodule の仕様としては参考になる記事ですが、Bootstrap のリポジトリ URL が変わっていたりなどしていて、コピペでは動作しなくなっています (どうでも良いけどもう10年も前の記事なんだなぁ😇)
* 記事中の Bootstrap は「依存する先の何かしらの外部のGitリポジトリ」であれば基本的な動作は同じなので、自分の学習用 Git リポジトリの中に適当な外部の Git リポジトリを指定して試してみてください。


## git submodule add してみる

では、リポジトリA内、submodule add してみます。

```bash:
$ git submodule add https://github.com/twbs/bootstrap.git bootstrap
Cloning into 'bootstrap'...
remote: Counting objects: 73135, done.
remote: Compressing objects: 100% (15/15), done.
remote: Total 73135 (delta 6), reused 0 (delta 0), pack-reused 73120
Receiving objects: 100% (73135/73135), 85.84 MiB | 3.91 MiB/s, done.
Resolving deltas: 100% (44541/44541), done.
Checking connectivity... done.
```

これで 手元の `bootstrap` ディレクトリに、GitHub にある Bootstrap が submodule として登録されました。
git status をみてみると .gitmodules というファイルと、submodule の入れものである `bootstrap` というディレクトリが新しく登録されています。

```bash:
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       new file:   .gitmodules
#       new file:   bootstrap
#
```

diff も確認します。


```diff:
$ git diff --cached
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2fdbccb
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "bootstrap"]
+       path = bootstrap
+       url = https://github.com/twbs/bootstrap.git
diff --git a/bootstrap b/bootstrap
new file mode 160000
index 0000000..5c02844
--- /dev/null
+++ b/bootstrap
@@ -0,0 +1 @@
+Subproject commit 5c028448c1fc171cf4a0fd99d3a672b649bef2aa
```

注目すべきは、 `+Subproject commit 5c028448c1fc171cf4a0fd99d3a672b649bef2aa` の diff で、これは、

* Bootstrap の、 `5c028448c1fc171cf4a0fd99d3a672b649bef2aa` コミットを、
* `bootstrap` ディレクトリに Submodule として登録した

という意味になります。
これをコミットしておきます。

```bash:
$ git commit -m "Add Twitter Bootstrap as a submodule"
```

では、ためしに、bootstrapディレクトリに移動して、`git show` してみると、

```bash:
$ cd bootstrap
$ git show
commit 5c028448c1fc171cf4a0fd99d3a672b649bef2aa
Merge: 5e7c5a6 0c4c1e4
Author: Chris Rebert <github@rebertia.com>
Date:   Wed Jul 29 15:00:54 2015 -0700

    Merge pull request #16908 from twbs/crbug-309483

    Remove http://crbug.com/309483 from the Wall of Browser Bugs
```

たしかに、現在、`5c028448c1fc171cf4a0fd99d3a672b649bef2aa` にいることがわかります。


## Submodule を更新する

いま追加した Bootstrap は、master ブランチの最新のコミットでした。
プロジェクトでは、別のブランチの Bootstrap が使いたくなったとします。たとえば、現在進行中の `14226-rebased` ブランチを使いたいとしましょう。 (このブランチ名等は、タイミングや、Submodule に追加するプロジェクトによって変わりますので、適宜読み替えてください)

このときの手順は、

* submodule のディレクトリに入り、対象のブランチやコミットをチェックアウトする
* submodule の外側に戻り、その submodule の現在のコミットを記録する (指し示す先を変更する)

という手順になります。

```bash:
$ cd bootstrap
$ git branch -a
* master
  remotes/origin/14226-rebased
  remotes/origin/HEAD -> origin/master
  remotes/origin/bhamodi-update-dependencies
  remotes/origin/bundler
  remotes/origin/derp
  remotes/origin/fix-15534
  remotes/origin/fix-popover-setContent
  remotes/origin/gh-pages
  remotes/origin/master
  remotes/origin/travis2

$ git checkout 14226-rebased
Branch 14226-rebased set up to track remote branch 14226-rebased from origin.
Switched to a new branch '14226-rebased'
```

この状態で外に戻ると、

```bash:
$ cd ..
$ git status
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:   bootstrap (new commits)

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

bootstrap ディレクトリに diff が出た状態となっています。

```diff:
$ git diff
diff --git a/bootstrap b/bootstrap
index 5c02844..88697c0 160000
--- a/bootstrap
+++ b/bootstrap
@@ -1 +1 @@
-Subproject commit 5c028448c1fc171cf4a0fd99d3a672b649bef2aa
+Subproject commit 88697c03a9315b5f1944fc82534a824aafebda0e
```

これが、submodule がコミットを記録する仕組みで、つまり、

* リポジトリAの参照する Bootstrap のコミットが、`5c028448c1fc171cf4a0fd99d3a672b649bef2aa` から `88697c03a9315b5f1944fc82534a824aafebda0e` に変更されたという意味になります。

これを add してコミットしておきます

```bash:
$ git add bootstrap
$ git commit -m "Update submodule: Bootstrap"
```


## リポジトリAで別のコミットをチェックアウトする & git submodule update

submodule のわかりづらい点としては、おそらく、**リポジトリA (submodule の外側) と submodule の中が連動しない** という点ではないでしょうか。

たとえば、今の状態で、リポジトリAでひとつ前のコミットをチェックアウトしてみるとします。(今回はミニマムなgitリポジトリなので直前のコミットをチェックアウトしますが、実際いろいろな作業では、他のブランチをチェックアウトしたり、そういう操作の例として、です)

まずは、チェックアウトするため、ログを確認。

```bash:
$ git log --oneline
284ffed Update submodule: Bootstrap
45db3c1 Add Twitter Bootstrap as a submodule
30ef9ed initial commit
```

一つ前なので、 `45db3c1` をチェックアウトします。

```bash:
$ git checkout 45db3c1
...
HEAD is now at 45db3c1... Add Twitter Bootstrap as a submodule
```

この状態で status を見てみると、

```bash:
$ git status
HEAD detached at 45db3c1
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:   bootstrap (new commits)

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

あれ！bootstrap ディレクトリに diff がでてしまいました。
これが、**連動しない、というわかりにくい点** と言った点です。

* 外側のリポジトリで前のコミットをチェックアウトしたため、そのコミット時点での submodule としての bootstrap は `5c028448c1fc171cf4a0fd99d3a672b649bef2aa` が記録されている
* しかし、bootstrap の内側は、先程操作したため、`88697c03a9315b5f1944fc82534a824aafebda0e` を指し示したまま

という状態になっています。リポジトリA上で色々なブランチやコミットを移動しても、submodule 内は自動的にチェックアウトされたりはしないんですよね。
そこで、

* boostrapディレクトリ内を、コミット `88697c0` が指し示している bootstrap のコミット `5c02844` にする

という操作が、

```bash:
$ git submodule update
```

というコマンドになります。submodule update というコマンド名は、「submoduleを更新する」という意味なので、考えてみればそのままの意味なのですが、わかってないと混乱してしまいますね。

これで、

```bash:
$ git status
```

が clean な状態となり、 bootstrap ディレクトリの中に入って確認してみると、`5c02844` がチェックアウトされた状態になっています。


## まとめとその他のこと

時間がなくなってきたので強引にまとめに入ると、

* submodule として追加したディレクトリは、ただの外部リポジトリへ参照を記録した箱である
* submodule の外側は、その箱のどのコミットを参照しているのかを記録している
* submodule の内側は、基本的には、自分が操作したときにしか更新されない

よくある現象集:

* git pull したら submodule に `(new commits)` とか出た
    * これは、自分以外の誰かが submodule を更新した (指し示すコミットを変更した) 場合です
    * さっきの、`0a30745` をチェックアウトしたときと同じ
    * `git submodule update` すれば、その指し示すコミットがチェックアウトされる
* git status したら submodule に `(modified content)` とか出てる
    * これは、submodule 内に diff がある状態です。今回の例で言えば、bootstrapディレクトリ内のなにかのファイルを変更したりすると、リポジトリAではこういうふうにいわれます
    * もとに戻したいなら、bootstrapディレクトリ内をcleanな状態にします。`git checkout`とかで変更ファイルをなくしたり。
    * 中でコミットなどをすると、 `(new commits)` になります。
        * submodule の中身を変更して、そのコミットをリポジトリAから指し示したい場合、さらに `git add bootstrap` して `git commit` します。内側でコミット、外側でもそれをコミット、という形

あとなんだっけ、もっと書きたいことがあったけど忘れたのでまた思い出したら書きます。