Git
AdventCalendar
GitDay 6

Git submodule の基礎

More than 3 years have passed since last update.

この記事は Git Advent Calendar 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

です。

git submodule add してみる

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

$ 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 というディレクトリが新しく登録されています。

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

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 として登録した

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

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

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

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

このときの手順は、

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

という手順になります。

$ 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'

この状態で外に戻ると、

$ 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 が出た状態となっています。

$ 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 してコミットしておきます

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

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

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

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

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

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

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

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

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

$ 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 にする

という操作が、

$ git submodule update

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

これで、

$ 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 します。内側でコミット、外側でもそれをコミット、という形

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