CUBE SUGAR CONTAINER

技術系のこと書きます。

Git のサブモジュールを使ってみる

今回は Git の中でもちょっととっつきにくいサブモジュール機能を使ってみることにする。 サブモジュール機能を使うと、外部のある Git リポジトリを別の Git リポジトリの中に取り込んで使うことができる。 これは例えばひとつの巨大なリポジトリを何らかのポリシーに基いて分割することに使える。

今回は Git を簡単に試すためにすべてのリポジトリをローカルホスト上に作ることにする。 環境には Ubuntu 14.04 LTS を使った。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.4 LTS
Release:    14.04
Codename:   trusty
$ uname -r
3.16.0-30-generic

下準備

まずは Git をインストールする。

$ sudo apt-get -y install git

最近の Git はうるさいのでユーザ名とメールアドレスを適当に登録しておく。

$ git config --global user.name "myname"
$ git config --global user.email "myemail@example.jp"

今回は Git の転送プロトコルに SSH を使うので OpenSSH をインストールしておく。

$ sudo apt-get -y install openssh-client

毎回パスワードを入力するのは大変なので公開鍵を作っておこう。 次のコマンドを入力したら何も入力せずにエンターキー押下し続けてパスワードなしの公開鍵を用意する。

$ ssh-keygen -t rsa -C default
Generating public/private rsa key pair.
Enter file in which to save the key (/home/vagrant/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
...(省略)

できた公開鍵をローカルホスト自身に登録しよう。 登録するときはパスワード認証なので聞かれたら今のユーザのパスワードを入力する

$ ssh-copy-id -i ~/.ssh/id_rsa.pub localhost
...(省略)...
vagrant@localhost's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'localhost'"
and check to make sure that only the key(s) you wanted were added.

これでローカルホストにパスワードなしでログインできるようになった。

$ ssh localhost
Welcome to Ubuntu 14.04.4 LTS (GNU/Linux 3.16.0-30-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
Last login: Sat Apr 16 12:23:31 2016 from 10.0.2.2

ここまでで下準備がおわった。

リポジトリを用意する

次に Git のリモートリポジトリをふたつ作っておく。 ひとつ目はサブモジュールを取り込む側で、もうひとつがサブモジュールとして取り込まれる側になる。

サブモジュールを取り込む側のリポジトリを main-repo という名前で /var/tmp ディレクトリに作る。

$ mkdir /var/tmp/main-repo
$ cd /var/tmp/main-repo
$ git init --bare --share
Initialized empty shared Git repository in /var/tmp/main-repo/

そしてサブモジュールとして取り込まれる側のリポジトリを sub-repo という名前で /var/tmp ディレクトリに作る。

$ mkdir /var/tmp/sub-repo
$ cd /var/tmp/sub-repo
$ git init --bare --share
Initialized empty shared Git repository in /var/tmp/sub-repo/

ローカルリポジトリは /tmp 以下に用意しよう。 まずはサブモジュールとして取り込まれる側の sub-repo をクローンする。

$ cd /tmp/
$ git clone localhost:/var/tmp/sub-repo

これで /var/tmp にある sub-repo のリモートリポジトリが /tmp 以下にクローンされた。

$ ls | grep sub-repo
sub-repo

サブモジュールとして取り込まれる側にファイルを用意する

サブモジュールとして取り込まれる側にファイルが何もないと分かりづらいので、まずは適当なものを用意しておこう。

$ cd sub-repo/
$ cat << 'EOF' > greet.txt
Hello, World!
EOF

作ったファイルをインデックスに追加して、コミットしたらリモートリポジトリにプッシュする。

$ git add -A
$ git commit -m "Initial commit for sub-repo"
$ git push origin master

これでサブモジュールとして取り込まれる側のリポジトリにファイルが追加された。

取り込む側のリポジトリにサブモジュールを追加する

次は先ほどファイルを追加した sub-repo を main-repo でサブモジュールとして取り込む。

まずは main-repo をクローンしてこよう。

$ cd /tmp/
$ git clone localhost:/var/tmp/main-repo

クローンできたらローカルリポジトリのディレクトリに移動する。

$ ls | grep main-repo
main-repo
$ cd main-repo/

そして git submodule add コマンドを使ってサブモジュールを追加する。 最初の引数がサブモジュールとして取り込むリポジトリで、その次が取り込むときのディレクトリの名前になる。

$ git submodule add localhost:/var/tmp/sub-repo sub
Cloning into 'sub'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.
Checking connectivity... done.

取り込んだサブモジュールのディレクトリには先ほど sub-repo に用意したファイルが追加されていることがわかる。

$ ls sub/
greet.txt
$ cat sub/greet.txt
Hello, World!

git status コマンドで状態を確認するとふたつのファイルがインデックスに追加されていることがわかる。

$ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   .gitmodules
    new file:   sub

.gitmodules のファイルにはサブモジュールとしてどこのリポジトリが何という名前のディレクトリとして取り込まれているかが書かれている。

$ cat .gitmodules
[submodule "sub"]
    path = sub
    url = localhost:/var/tmp/sub-repo

サブモジュールを追加した状態でコミットしてプッシュしておく。

$ git commit -m "Initial commit"
$ git push origin master

ここまでで Git リポジトリには別の Git リポジトリをサブモジュールとして取り込むことができることがわかった。

サブモジュール側のリポジトリを変更する

次はサブモジュール側のリポジトリが変更されたときの挙動を確認しておく。

sub-repo のローカルリポジトリに移動して最初の用意したファイルの内容を書き換えよう。

$ cd /tmp/sub-repo/
$ cat << 'EOF' > greet.txt
Hello, Git!
EOF

変更した内容をインデックスに追加したらコミットしてプッシュしておく。

$ git add -A
$ git commit -m "change message"
$ git push origin master

さて、それでは main-repo に戻ってどうなっているかを確認する。 ディレクトリの直下で git pull しても変更があるようには見えていない。

$ cd /tmp/main-repo
$ git pull
Already up-to-date.

もちろんファイルの内容も変更されていない。

$ cat sub/greet.txt
Hello, World!

では、どうやってサブモジュール側の更新を反映すれば良いのかという話になる。 これには、サブモジュールのディレクトリに移動した上で git pull コマンドを実行しなきゃいけないようだ。

$ cd sub/
$ git pull
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From localhost:/var/tmp/sub-repo
   2bb962e..e9e786e  master     -> origin/master
Updating 2bb962e..e9e786e
Fast-forward
 greet.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

これでやっと更新内容が反映された。

 $ cat greet.txt
 Hello, Git!

サブモジュールの原理

トップディレクトリに戻って git status コマンドを実行するとサブモジュールとして取り込んだ sub ディレクトリに変更があるように見えている。

$ cd ../
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

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:   sub (new commits)

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

ここで git diff コマンドを実行してみるとサブモジュール機能がどのように内容を管理しているかが分かる。 サブモジュールとして取り込まれたディレクトリの差分は、実際にはコミットの識別子としてしか表示されない。

$ git diff
diff --git a/sub b/sub
index e69271a..7717d35 160000
--- a/sub
+++ b/sub
@@ -1 +1 @@
-Subproject commit e69271a5d2f2d84a87c730ac79424049cd38d542
+Subproject commit 7717d35bad558cecc83507c7862aa4092d7e1730

つまりサブモジュール機能では、あるリポジトリのどの時点 (コミットの識別子) の状態を取り込む、という考え方で内容を管理しているようだ。

なので、サブモジュール側に変更があってそれを使いたいときは、明示的にそれに追随する必要がある。 手順としては、まずサブモジュールを取り込んでいる側のリポジトリで参照するコミットを更新 (git pull) する。 そして、その内容をインデックスに追加したらコミットしてプッシュすれば完了。

$ git add sub
$ git commit -m "update submodule"
$ git push origin master

ちなみに先ほどのコミットの識別子は sub-repo の方に記録されているそれを示している。

$ cd /tmp/sub-repo/
$ git log -n 1
commit 7717d35bad558cecc83507c7862aa4092d7e1730
Author: myname <myemail@example.jp>
Date:   Mon Apr 18 02:55:32 2016 -0400

    change message

取り込んでいる側からサブモジュールのリポジトリの内容を変更する

先ほどはサブモジュールとして取り込まれる側のリポジトリを変更してから、それを取り込んだ側で更新された内容に追従する方法を扱った。 今度は取り込んでいる側から直接サブモジュールのリポジトリを更新してみよう。

main-repo に移動したらサブモジュールのディレクトリにあるファイルの内容を変更する。

$ cd /tmp/main-repo/
$ cat << 'EOF' > sub/greet.txt
Hello, Submodule!
EOF

この状態でトップディレクトリで git status コマンドを実行するとサブモジュールのディレクトリに変更があるという表示になっている。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

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)
  (commit or discard the untracked or modified content in submodules)

    modified:   sub (modified content)

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

それではサブモジュールのディレクトリに加えた変更をリモートリポジトリに反映しよう。 それにはディレクトリに移動した上で変更をインデックスに追加してコミット、プッシュする。

$ cd sub/
$ git add -A
$ git commit -m "change message from main-repo"
$ git push origin master
Counting objects: 5, done.
Writing objects: 100% (3/3), 263 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To localhost:/var/tmp/sub-repo
   e9e786e..ec80564  master -> master

注意点としては、この時点ではまだサブモジュールを取り込んだ側のリポジトリが参照しているコミットは更新されていないこと。 main-repo のトップディレクトリで git status コマンドを実行すると、それがわかる。

$ cd ..
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

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:   sub (new commits)

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

なので、こちらでも更新されたコミットを参照するようにしてコミット、プッシュしよう。

$ git add sub/
$ git commit -m "update submodule"
$ git push origin master

サブモジュールとして取り込まれている側の sub-repo の方に移動して git pull してみよう。 すると main-repo の方で加えた変更がちゃんとリモートリポジトリに反映されていることがわかる。

$ cd /tmp/sub-repo/
$ git pull
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From localhost:/var/tmp/sub-repo
   e9e786e..ec80564  master     -> origin/master
Updating e9e786e..ec80564
Fast-forward
 greet.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ cat greet.txt 
Hello, Submodule!

新規にクローンするとき

ここまでは既存のリポジトリにサブモジュールを追加してから使い続ける流れを確認してきた。 今度は、既にサブモジュールが追加されているリポジトリをクローンして使いはじめるまでの流れについて書く。

まずは main-repo を main-repo2 という名前のローカルリポジトリとしてクローンしてくる。

$ cd /tmp/
$ git clone localhost:/var/tmp/main-repo main-repo2

移動して ls コマンドを実行するとサブモジュールのディレクトリはあることがわかる。

$ cd main-repo2/
$ ls
sub

ただしサブモジュールのディレクトリに ls コマンドを実行しても中身が空っぽになっている。

$ ls sub/

はて greet.txt はどこにいったんですかと思いながら sub ディレクトリの中で git pull しても何も起こらない。

$ cd sub/
$ git pull
Already up-to-date.

実はサブモジュールを含むリポジトリを新たに使いはじめるには一連の操作が必要になっている。 具体的には git submodule init コマンドと git submodule update コマンドを実行する。

$ git submodule init
Submodule 'sub' (localhost:/var/tmp/sub-repo) registered for path '../sub'
$ git submodule update
Cloning into 'sub'...
remote: Counting objects: 9, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 9 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (9/9), done.
Checking connectivity... done.
Submodule path '../sub': checked out '34682bf1f50234509b5c2b634a7cfb3f44fa4959'

これでサブモジュールにファイルが現れる。

$ ls
greet.txt
$ cat greet.txt 
Hello, Submodule!

これでサブモジュールをひと通り使う方法がわかった。

まとめ

  • Git にはサブモジュールという機能がある
  • サブモジュールを使うと別のリポジトリを取り込むことができる
  • サブモジュールでは外部リポジトリの特定のコミットを参照している
  • サブモジュールを含むリポジトリは使い始めに一連の手順が必要になる

めでたしめでたし。