git submoduleはコミットへのシンボリックリンクである説

個人的にgitで思った通りの挙動をしないコマンドナンバー2、それがsubmodule。ちなナンバー1はrebase。

submodule自体、使い込むとたいへん便利な機能なのは間違いないものの、誤解をされやすい挙動をするので扱いにくい感じがある。特にチームで使うとなると全員がsubmoduleを理解していないとハマる人が続出しがち。
なので最近はあれはコミットへのシンボリックリンクだ、と説明してごまかしていたりする。

例えばひとつのリポジトリからライブラリ的なものだけを切り出したり、アセット的なものだけを切り出して別途管理したくなることがよくありまして。そういうときにsubmoduleを使って複数のリポジトリに分割すればスッキリするんじゃね、って考えるわけです。
で、いざsubmoduleを使って分割してみたら submodule 配下がなぜか空だったり、なぜか更新されてなかったり、なぜか pull できなかったりする問題が起こったりする、と。そのうち、散々悩んだものの原因がよくわからないので「submoduleは意味不明な挙動をするから使うべからず」みたいなルールが確立したりする。
こうして世界の平和は守られた。めでたしめでたし。

まとめ。

ちゃんとした挙動の説明についてはこのへん( Git - サブモジュール )を読むのが確実。

と書くと、何も書くことがなくなるのでここではsubmoduleがコミットへのシンボリックリンクだと解釈するとすっきり理解できるよね、って話をする。

あるある1: git clone 直後にsubmoduleが空になってる

~/git$ git clone git@github.com:hell0again/get-test-parent-repo.git parent_clone
fatal: Not a git repository (or any of the parent directories): .git
Cloning into parent_clone...
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 7 (delta 0), reused 4 (delta 0)
Receiving objects: 100% (7/7), done.

~/git$ ls -a parent_clone/submodules/sub_repo/
./	../

submoduleにあるはずのファイルが無いので分かりにくいが実際には.gitmodulesを見るとsubmoduleはしっかり登録されている。これはsymlinkに例えるとリンクはある(= .gitmodulesに書いてある)が、リンク先がない(= submodule init --updateしていない)状態と考えると分かりやすい。

もちろん空になってるのをなんとかするには submodule init --updateを打つかgit clone に --recursiveオプションをつけるかすればOKだが、いずれにしてもclone側がsubmoduleを意識しないといけないのがイマイチ。

あるある2: 親リポジトリでgit submodule updateしてもsubmoduleの修正が反映されない

リポジトリが記録しているsubmoduleのコミット情報が更新されていないときに起こる。
リポジトリがsubmoduleのどのコミットを参照しているかはgit submoduleコマンドで確認できる。

~/git/parent_clone (master)$ git submodule 
 d1c01fecf8b31819cdbf5a4ce3cb5bb223119c65 submodules/sub_repo (heads/master)

ところがこのコマンド、下手にブランチ名を表示しているので一見するとgit submodule updateとかでsubmoduleのmasterをpullってくれそうに見える。しかし実際はsubmoduleはブランチをtrackしているのではなく、コミットに対する参照でしかないのでupdateしても何も起きない。なのでsubmoduleが更新されたらちゃんと親リポジトリ側でsubmoduleのコミット参照を更新してあげる必要がある。

これもsubmoduleはコミットへのシンボリックリンクだと考えると参照先がブランチではなくコミットなのでupdateしても何も起きないのはイメージしやすいはず。

ちなみにgithubでのsubmoduleの表示は単にリポジトリとコミットハッシュしかないの。そのため、submoduleがコミットへの参照であることに気がつきやすいといえる( https://github.com/hell0again/get-test-parent-repo/tree/master/submodules )。

git1.8.2の場合

なお、実はgit1.8.2からはsubmoduleでブランチをtrackできるようになった模様。
http://stackoverflow.com/questions/1777854/git-submodules-specify-a-branch-tag
詳しい挙動は調べてないものの、おそらくclone側がgit1.8.2にしないといけないはず。そうなると実際に導入するのはやや微妙か。

あるある3: bash-completionでブランチ名を表示していると submodule のブランチ名が空になってる

要するにアレ。

~/git$ git clone --recursive git@github.com:hell0again/get-test-parent-repo.git parent_clone
Cloning into parent_clone...
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 7 (delta 0), reused 4 (delta 0)
Receiving objects: 100% (7/7), done.
Submodule 'submodules/sub_repo' (https://github.com/hell0again/git-test-sub-repo) registered for path 'submodules/sub_repo'
Cloning into submodules/sub_repo...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Submodule path 'submodules/sub_repo': checked out 'd1c01fecf8b31819cdbf5a4ce3cb5bb223119c65'

~/git$ cd parent_clone

~/git/parent_clone (master)$ cd submodules/sub_repo/

~/git/parent_clone/submodules/sub_repo ()$  ## コレ

ブランチ名が表示されない状態がどうしても気持ち悪くて、いちいちsubmoduleでブランチをcheckoutしてたりしません? なぜブランチ名が表示されないのかが長い間謎だったものの、おそらくsubmoduleの参照先はブランチではなく、あくまでコミットなのでブランチ名はあえて表示しないのです、ということなのではないでしょうか(適当)。
バグなのか、あえてそうなってるのかは知らないものの、どのみちsubmoduleは参照先がどのコミットなのかを把握しないといけないのでブランチを知ったところで実はあまり意味がない。そう考えるとブランチ名は空のままでいいや、とも思えてくる。

よくよく考えるとみっつ目の話はシンボリックリンクどうのとは関係ない気がしなくもない。