Git
Chapters ▾ 2nd Edition

7.11 Git のさまざまなツール - サブモジュール

サブモジュール

あるプロジェクトで作業をしているときに、プロジェクト内で別のプロジェクトを使わなければならなくなることがよくあります。 サードパーティが開発しているライブラリや、自身が別途開発していて複数の親プロジェクトから利用しているライブラリなどがそれにあたります。 こういったときに出てくるのが「ふたつのプロジェクトはそれぞれ別のものとして管理したい。だけど、一方を他方の一部としても使いたい」という問題です。

例を考えてみましょう。ウェブサイトを制作しているあなたは、Atom フィードを作成することになりました。 Atom 生成コードを自前で書くのではなく、ライブラリを使うことに決めました。 この場合、CPAN や gem などの共有ライブラリからコードをインクルードするか、ソースコードそのものをプロジェクトのツリーに取り込むかのいずれかが必要となります。 ライブラリをインクルードする方式の問題は、ライブラリのカスタマイズが困難であることと配布が面倒になるということです。すべてのクライアントにそのライブラリを導入させなければなりません。 コードをツリーに取り込む方式の問題は、手元でコードに手を加えてしまうと本家の更新に追従しにくくなるということです。

Git では、サブモジュールを使ってこの問題に対応します。 サブモジュールを使うと、ある Git リポジトリを別の Git リポジトリのサブディレクトリとして扱うことができるようになります。 これで、別のリポジトリをプロジェクト内にクローンしても自分のコミットは別管理とすることができるようになります。

サブモジュールの作り方

まずは単純な事例を見ていきましょう。大きな1プロジェクトを、メインの1プロジェクトとサブの複数プロジェクトに分割して開発していているとします。

開発を始めるにあたり、作業中のリポジトリのサブモジュールとして既存のリポジトリを追加します。サブモジュールを新たに追加するには git submodule add コマンドを実行します。追跡したいプロジェクトの URL (絶対・相対のいずれも可)を引数に指定してください。この例では、“DbConnector” というライブラリを追加してみます。

$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

デフォルトでは、このコマンドで指定したリポジトリと同名のディレクトリに、サブプロジェクトのデータが格納されます。他のディレクトリを使いたい場合は、コマンドの末尾にパスを追加してください。

ここで git status を実行してみましょう。いくつか気づくことがあるはずです。

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

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

	new file:   .gitmodules
	new file:   DbConnector

まず気づくのが、新たに追加された .gitmodules ファイルです。 この設定ファイルには、プロジェクトの URL とそれを取り込んだローカルサブディレクトリの対応が格納されています。

[submodule "DbConnector"]
	path = DbConnector
	url = https://github.com/chaconinc/DbConnector

複数のサブモジュールを追加した場合は、このファイルに複数のエントリが書き込まれます。 このファイルもまた他のファイルと同様にバージョン管理下に置かれることに注意しましょう。.gitignore ファイルと同じことです。 プロジェクトの他のファイルと同様、このファイルもプッシュやプルの対象となります。 プロジェクトをクローンした人は、このファイルを使ってサブモジュールの取得元を知ることになります。

注記

.gitmodules ファイルに記述された URL を他の利用者はまずクローン/フェッチしようとします。よって、可能であればそういった人たちもアクセスできる URL を使うようにしましょう。もし、自分がプッシュする URL と他の利用者がプルする URL が違う場合は、他の利用者もアクセスできる URL をここでは使ってください。そのうえで、git config submodule.DbConnector.url PRIVATE_URL コマンドを使って自分用の URL を手元の環境に設定するのがいいでしょう。 可能であれば、相対 URL にしておくと便利だと思います。

また、git status の出力にプロジェクトフォルダも含まれています。 これに対して git diff を実行すると、ちょっと興味深い結果が得られます。

$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

DbConnector は作業ディレクトリ内にあるサブディレクトリですが、Git はそれがサブモジュールであるとみなし、あなたがそのディレクトリにいない限りその中身を追跡することはありません。 そのかわりに、Git はこのサブディレクトリを元のプロジェクトの特定のコミットとして記録します。

差分表示をもうすこしちゃんとさせたいのなら、git diff コマンドの --submodule オプションを使いましょう。

$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+       path = DbConnector
+       url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

コミットすると、このようになります。

$ git commit -am 'added DbConnector module'
[master fb9093c] added DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

DbConnector エントリのモードが 160000 となったことに注目しましょう。 これは Git における特別なモードで、サブディレクトリやファイルではなくディレクトリエントリとしてこのコミットを記録したことを意味します。

サブモジュールを含むプロジェクトのクローン

ここでは、内部にサブモジュールを含むプロジェクトをクローンしてみます。 デフォルトでは、サブモジュールを含むディレクトリは取得できますがその中にはまだ何もファイルが入っていません。

$ git clone https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
$ cd MainProject
$ ls -la
total 16
drwxr-xr-x   9 schacon  staff  306 Sep 17 15:21 .
drwxr-xr-x   7 schacon  staff  238 Sep 17 15:21 ..
drwxr-xr-x  13 schacon  staff  442 Sep 17 15:21 .git
-rw-r--r--   1 schacon  staff   92 Sep 17 15:21 .gitmodules
drwxr-xr-x   2 schacon  staff   68 Sep 17 15:21 DbConnector
-rw-r--r--   1 schacon  staff  756 Sep 17 15:21 Makefile
drwxr-xr-x   3 schacon  staff  102 Sep 17 15:21 includes
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 scripts
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 src
$ cd DbConnector/
$ ls
$

DbConnector ディレクトリは存在しますが、中身が空っぽです。 ここで、ふたつのコマンドを実行しなければなりません。まず git submodule init でローカルの設定ファイルを初期化し、次に git submodule update でプロジェクトからのデータを取得し、親プロジェクトで指定されている適切なコミットをチェックアウトします。

$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

これで、サブディレクトリ DbConnector の中身が先ほどコミットしたときとまったく同じ状態になりました。

また、これをもうすこし簡単に済ませるには、git clone コマンドの --recursive オプションを使いましょう。そうすると、リポジトリ内のサブモジュールをすべて初期化し、データを取得してくれます。

$ git clone --recursive https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

サブモジュールを含むプロジェクトでの作業

さて、サブモジュールを含むプロジェクトのデータをコピーできましたので、メインとサブ、両方のプロジェクトでの共同作業をしてみましょう。

上流の変更の取り込み

まずはサブモジュールの使用例で一番シンプルなモデルを見ていきます。それは、サブプロジェクトをただ単に使うだけ、というモデルです。上流の更新はときどき取り込みたいけれど、チェックアウトした内容を変更したりはしない、という使い方になります。

サブモジュールが更新されているかどうかを調べるには、サブモジュールのディレクトリで git fetch を実行します。併せて git merge で上流のブランチをマージすれば、チェックアウトしてあるコードを更新できます。

$ git fetch
From https://github.com/chaconinc/DbConnector
   c3f01dc..d0354fc  master     -> origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
 scripts/connect.sh | 1 +
 src/db.c           | 1 +
 2 files changed, 2 insertions(+)

ここでメインプロジェクトのディレクトリに戻って git diff --submodule を実行してみてください。サブモジュールが更新されたこと、どのコミットがサブモジュールに追加されたかがわかるでしょう。なお、git diff--submodule オプションを省略したい場合は、設定項目 diff.submodule の値に “log” を指定してください。

$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

この状態でコミットしておけば、他の人がサブモジュールを更新したときに新しい内容が取り込まれるようになります。

サブモジュールのディレクトリでのフェッチとマージを手動で行いたくない人のために、もう少し簡単な方法も紹介しておきます。git submodule update --remote です。これを使えば、ディレクトリに入ってフェッチしてマージして、という作業がコマンドひとつで済みます。

$ git submodule update --remote DbConnector
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'

なお、このコマンドはデフォルトでは、サブモジュールのリポジトリの master ブランチの内容まで手元にチェックアウトした内容を更新する、という前提で動作します。ですが、そうならないよう設定することもできます。たとえば、DbConnector サブモジュールを “stable” ブランチに追従させたいとしましょう。その場合、.gitmodules ファイルに記述することもできますし(そうすれば、みんなが同じ設定を共有できます)、手元の .git/config ファイルに記述しても構いません。以下は .gitmodules に記述した場合の例です。

$ git config -f .gitmodules submodule.DbConnector.branch stable

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'

また、この設定コマンドから -f .gitmodules の部分を除くと、設定は手元の環境に対してのみ反映されます。ただ、この設定はリポジトリにコミットして追跡しておくほうがよいと思います。関係者全員が同じ設定を共有できるからです。

ここで git status を実行すると、「新しいコミット」(“new commits”)がサブモジュールに追加されたことがわかります。

$ 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:   .gitmodules
  modified:   DbConnector (new commits)

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

さらに、設定項目 status.submodulesummary を指定しておけば、リポジトリ内のサブモジュールの変更点の要約も確認できます。

$ git config status.submodulesummary 1

$ 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:   .gitmodules
	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c3f01dc...c87d55d (4):
  > catch non-null terminated lines

この段階で`git diff` を実行すると、.gitmodules ファイルが変更されていることがわかります。 また、サブモジュールについては、上流からコミットがすでにいくつも取得されていて、手元のリポジトリでコミット待ちの状態になっていることがわかります。

$ git diff
diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
 Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

手元のサブモジュールにこれから何をコミットしようとしているのかがわかるので、これはとても便利です。また、実際にコミットしたあとでも、git log -p を使えばこの情報は確認できます。

$ git log -p --submodule
commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Sep 17 16:37:02 2014 +0200

    updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

なお、git submodule update --remote を実行すると、デフォルトではすべてのサブモジュールの更新が行われます。よって、サブモジュールが多い場合は更新したいものだけを指定するとよいでしょう。

サブモジュールでの作業

サブモジュールを使う動機を考えてみましょう。その多くは、メインプロジェクトで(あるいは複数のサブモジュールに渡って)作業をしつつ、サブモジュールのコードも変更したいから、だと思います。というのも、そうでなければ Maven や Rubygems のようなシンプルな依存関係管理の仕組みを使っているはずだからです。

ということでここでは、メインプロジェクトとサブモジュールを行ったり来たりしながら変更を加えていく方法を見ていきましょう。併せて、それらを同時にコミット/公開する方法も紹介します。

これまでの例では、git submodule update コマンドを実行してサブモジュールのリモートリポジトリの変更内容を取得すると、サブモジュール用ディレクトリ内のファイルは更新されますが、手元のサブモジュール用リポジトリの状態は「切り離された HEAD (detached HEAD)」になってしまっていました。つまり、作業中のブランチ(“master” など)は存在せず、変更も追跡されない、ということです。 このままでは、たとえサブモジュールになにかコミットを追加したとしても、`git submodule update`を実行したタイミングで追加した内容はなくなってしまうことになります。そういった事態を避け、サブモジュールに追加した内容をちゃんと記録するには、事前準備が必要なのです。

では、どうすればサブモジュールをハックしやすくなるでしょうか。やるべきことは2つです。まず、サブモジュール用のディレクトリで、作業用のブランチをチェックアウトしましょう。次に、何らかの変更をサブモジュールに加えたあとに git submodule update --remote を実行して上流から変更をプルした場合の挙動を設定します。手元の変更内容に上流の変更をマージするか、手元の変更内容を上流の変更にリベースするかのいずれかを選択することになります。

実際にやってみましょう。まず、サブモジュール用のディレクトリに入って、作業用のブランチをチェックアウトします。

$ git checkout stable
Switched to branch 'stable'

次の手順ですが、ここでは「マージ」することにします.実施のたびに指定するのであれば、update コマンド実行時に --merge オプションを使います。以下の例では、サーバーにあるサブモジュールのデータは変更されていて、それがマージされていることがわかります。

$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable     -> origin/stable
Updating c87d55d..92c7337
Fast-forward
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'

DbConnector ディレクトリを見ると、上流の変更が手元の stable ブランチに取り込み済みであるとわかります。では次に、手元のファイルに変更を加えている間に、別の変更が上流にプッシュされたらどうなるかを説明しましょう。

$ cd DbConnector/
$ vim src/db.c
$ git commit -am 'unicode support'
[stable f906e16] unicode support
 1 file changed, 1 insertion(+)

この段階でサブモジュールを更新してみましょう。手元のファイルは変更済みで、上流にある別の変更も取り込む必要がある場合、何が起こるかがわかるはずです。

$ git submodule update --remote --rebase
First, rewinding head to replay your work on top of it...
Applying: unicode support
Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

--rebase--merge オプションを付け忘れると、サブモジュールはサーバー上の状態で上書きされ、「切り離された HEAD」状態になります。

$ git submodule update --remote
Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

ただ、こうなってしまっても慌てる必要はありません。サブモジュールのディレクトリに戻れば、変更を追加したブランチをチェックアウトできます。そのうえで、origin/stable (などの必要なリモートブランチ)を手動でマージなりリベースなりすればよいのです。

また、手元で加えた変更をコミットしていない状態でサブモジュールを更新したとしましょう。これは問題になりそうですが、実際はそうなりません。リモートの変更だけが取得され、サブモジュール用ディレクトリに加えた変更でコミットしていないものはそのまま残ります。

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable     -> origin/stable
error: Your local changes to the following files would be overwritten by checkout:
	scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

手元で加えた変更が上流の変更とコンフリクトする場合は、サブモジュール更新を実施したときにわかるようになっています。 If you made changes that conflict with something changed upstream, Git will let you know when you run the update.

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLICT (content): Merge conflict in scripts/setup.sh
Recorded preimage for 'scripts/setup.sh'
Automatic merge failed; fix conflicts and then commit the result.
Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

そうなったら、サブモジュール用ディレクトリのファイルを編集しましょう。いつものようにコンフリクトを解消できます。

サブモジュールに加えた変更の公開

これまでの作業で、サブモジュール用ディレクトリの内容は変更されています。上流の変更を取り込みましたし、手元でも変更を加えました。そして、後者の存在は誰もまだ知りません。プッシュされていないからです。

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > updated setup script
  > unicode support
  > remove unnecessary method
  > add new option for conn pooling

メインプロジェクトに変更をコミットしてプッシュしたけれど、サブモジュールの変更はプッシュしていないとします。その場合、プッシュされたリポジトリをチェックアウトしようとしてもうまくいかないでしょう。メインプロジェクトの変更が依存しているサブモジュールの変更を、取得する手段がないからです。必要とされる変更内容は、手元の環境にしかありません。

こういった状態にならないよう、サブモジュールの変更がプッシュ済みかどうかを事前に確認する方法があります。メインプロジェクトをプッシュするときに使う git push コマンドの、 --recurse-submodules オプションです。 これを “check” か “on-demand” のいずれかに設定します。“check” に設定すれば、サブモジュールの変更でプッシュされていないものがある場合、メインプロジェクトのプッシュは失敗するようになります。

$ git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
  DbConnector

Please try

	git push --recurse-submodules=on-demand

or cd to the path and use

	git push

to push them to a remote.

ご覧のとおり、事態を解決する方法もいくつか提示されます。そのなかで一番単純なのは、全サブモジュールを個別にプッシュしてまわる方法です。サブモジュールの変更が公開された状態になれば、メインプロジェクトのプッシュもうまくいくでしょう。

他にも、このオプションを “on-demand” に設定する方法があります。そうすると、さきほど「単純」といった手順をすべて実行してくれます。

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/MainProject
   3d6d338..9a377d1  master -> master

そうです、DbConnector モジュールがプッシュされたあと、メインプロジェクトがプッシュされています。もしサブモジュールのプッシュが何らかの理由で失敗すれば、メインプロジェクトのプッシュも失敗するようになっています。

変更されたサブモジュールのマージ

サブモジュールの参照を他の人と同じタイミングで変更してしまうと、問題になる場合があります。つまり、サブモジュールの歴史が分岐してしまい、その状態が両者の手元にあるメインプロジェクトにコミットされ、ブランチも分岐した状態になってしまいます。これを解消するのは厄介です。

この場合でも、一方のコミットがもう一方のコミットの直系の先祖である場合、新しいほうのコミットがマージされます(fast-forward なマージ)。何も問題にはなりません。

ただし、“trivial” なマージすら行われないケースがあります。具体的には、サブモジュールのコミットが分岐してマージする必要があるようなケースです。その場合、以下のような状態になります。

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

何が起こったのでしょうか。まず、サブモジュールの歴史の分岐点になっているブランチが2つあって、マージする必要があることがわかります。次に、“merge following commits not found” であることもわかります。え、何がわかったの?と思った方、ご安心ください。もう少し先で説明します。

この問題を解決するには、サブモジュールがどういった状態にあるべきかを把握しなければなりません。ですが、いつもとは違い、上記の Git コマンド出力からは有用な情報は得られません。分岐してしまった歴史で問題となっているコミット SHA-1 すら表示されません。ただ、ありがたいことに、それらは簡単に確認できます。git diff を実行してみましょう。マージしようとしていた両ブランチのコミット SHA-1 が表示されます。

$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

この例では、コミット eb41d76手元 のサブモジュールに追加されていたもので、コミット c771610 は上流にあったものであることがわかります。さきほどのマージでは処理が行えなかったので、サブモジュール用ディレクトリの最新コミットは eb41d76 のはずです。何らかの理由で仮にそうなっていなければ、そのコミットが最新になっているブランチを作成し、チェックアウトすればよいでしょう。

注目すべきは上流のコミット SHA-1 です。マージしてコンフリクトを解消しなければなりません。SHA-1 を直接指定してマージしてみてもよいですし、該当のコミットを指定して作ったブランチをマージしても構いません。どちらかと言えば後者がオススメです(マージコミットのメッセージがわかりやすくなるくらいのメリットしかありませんが)。

では実際にやってみましょう。サブモジュール用ディレクトリで該当のコミット(さきほどの git diff の2番目の SHA-1)を指定してブランチを作り、手動でマージしてみます。

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610
(DbConnector) $ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

そうすると、実際にどこがコンフリクトしているかがわかります。それを解決してコミットすれば、その結果をもとにメインプロジェクトがアップデートできる、というわけです。

$ vim src/main.c (1)
$ git add src/main.c
$ git commit -am 'merged our changes'
Recorded resolution for 'src/main.c'.
[master 9fd905e] merged our changes

$ cd .. (2)
$ git diff (3)
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit eb41d764bccf88be77aced643c13a7fa86714135
 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d
++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a
$ git add DbConnector (4)

$ git commit -m "Merge Tom's Changes" (5)
[master 10d2c60] Merge Tom's Changes
  1. まずはコンフリクトを解決します

  2. 次にメインプロジェクトのディレクトリに戻ります

  3. SHA を改めて確認します

  4. コンフリクトしていたサブモジュールの登録を解決します

  5. マージした内容をコミットします

少しややこしいかもしれませんが、そう難しくはないはずです。

また、こういったときに別の方法で処理されることもあります。 サブモジュール用ディレクトリの歴史にマージコミットがあって、上述した 両方 のコミットがすでにマージされている場合です。それを用いてもコンフリクトを解消できます。サブモジュールの歴史を確認した Git からすれば、「該当のコミットふたつが含まれたブランチを、誰かがすでにマージしてるよ。それでいいんじゃない?」というわけです。

これは、さきほど説明を省略したエラーメッセージ “merge following commits not found” の原因でもあります。1つめの例、このエラーメッセージを初めて紹介したときは この方法 は使えなかったからです。わかりにくいのも当然で、誰もそんなことが 行われようとしてる なんて思わないですよね。

この方法で処理するのに使えそうなマージコミットが見つかると、以下のようなメッセージが表示されます。

$ git merge origin/master
warning: Failed to merge submodule DbConnector (not fast-forward)
Found a possible merge resolution for the submodule:
 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes
If this is correct simply add it to the index for example
by using:

  git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"

which will accept this suggestion.
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

インデックスを更新してコミットしましょう、ということのようです。git add コマンドを実行してコミットを解消するのと同じですね。ただ、素直にそうするのはやめておいたほうがよさそうです。その代わり、サブモジュール用ディレクトリの差分を確認し、指示されたコミットまで fast-forward すればいいでしょう。そうすれば、きちんとテストしてからコミットできます。

$ cd DbConnector/
$ git merge 9fd905e
Updating eb41d76..9fd905e
Fast-forward

$ cd ..
$ git add DbConnector
$ git commit -am 'Fast forwarded to a common submodule child'

この方法でも処理結果は代わりません。そのうえ、きちんと動作するか確認できますし、作業が終わった後にもサブモジュール用ディレクトリにはコードが残ることになります。

サブモジュールのヒント

サブモジュールを使った作業の難しさを和らげてくれるヒントをいくつか紹介します。

Submodule Foreach

submodule foreach コマンドを使うと、サブモジュールごとに任意のコードを実行してくれます。たくさんのサブモジュールをプロジェクトで使っていれば、便利だと思います。

例えば、新機能の開発やバグ修正を着手したいとします。ただし、使っているサブモジュールに加えた変更がまだコミットされていません。この場合、そのコミットされていない状態は簡単に隠しておけます。

$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

うまく隠せたら、全サブモジュールで新しいブランチを作ってチェックアウトします。

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

どうでしょう、簡単だと思いませんか。他にも、メインプロジェクトとサブプロジェクトの変更内容の差分をユニファイド形式でとることも可能です。これもとても便利です。

$ git diff; git submodule foreach 'git diff'
Submodule DbConnector contains modified content
diff --git a/src/main.c b/src/main.c
index 210f1ae..1f0acdc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -245,6 +245,8 @@ static int handle_alias(int *argcp, const char ***argv)

      commit_pager_choice();

+     url = url_decode(url_orig);
+
      /* build alias_argv */
      alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1));
      alias_argv[0] = alias_string + 1;
Entering 'DbConnector'
diff --git a/src/db.c b/src/db.c
index 1aaefb6..5297645 100644
--- a/src/db.c
+++ b/src/db.c
@@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len)
        return url_decode_internal(&url, len, NULL, &out, 0);
 }

+char *url_decode(const char *url)
+{
+       return url_decode_mem(url, strlen(url));
+}
+
 char *url_decode_parameter_name(const char **query)
 {
        struct strbuf out = STRBUF_INIT;

この例では、サブモジュールで関数が定義され、メインプロジェクトでそれを呼び出していることがわかります。簡易な例ではありますが、どんなふうに便利なのかわかったかと思います。

便利なエイリアス

紹介してきたコマンドの一部には、エイリアスを設定しておくとよいかもしれません。長いものが多いですし、紹介した挙動がデフォルトになるようには設定できないものが大半だからです。Git でエイリアスを設定する方法は Git エイリアス で触れましたが、ここでも設定例を紹介しておきます。Git のサブモジュール機能を多用する場合は、参考にしてみてください。

$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

このように設定しておくと、git supdate コマンドを実行すればサブモジュールが更新されるようになります。同様に、git spush コマンドであれば、サブモジュールの依存関係をチェックしたあとでプッシュするようになります。

サブモジュール使用時に気をつけるべきこと

しかし、サブモジュールを使っているとなにかしらちょっとした問題が出てくるものです。

例えば、サブモジュールを含むブランチを切り替えるのは、これまた用心が必要です。 新しいブランチを作成してそこにサブモジュールを追加し、サブモジュールを含まないブランチに戻ったとしましょう。そこには、サブモジュールのディレクトリが「追跡されていないディレクトリ」として残ったままになります。

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'adding crypto library'
[add-crypto 4445836] adding crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

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

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

残ったディレクトリを削除するのは大変ではありませんが、そもそもそこにディレクトリが残ってしまうのはややこしい感じがします。実際に削除したあとに元のブランチをチェックアウトすると、モジュールを再追加するために submodule update --init コマンドを実行しなければなりません。

$ git clean -fdx
Removing CryptoLibrary/

$ git checkout add-crypto
Switched to branch 'add-crypto'

$ ls CryptoLibrary/

$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'

$ ls CryptoLibrary/
Makefile	includes	scripts		src

繰り返しになりますが、大変ではないけれどややこしい感じがしてしまいます。

次にもうひとつ、多くの人がハマるであろう点を指摘しておきましょう。これは、サブディレクトリからサブモジュールへ切り替えるときに起こることです。 プロジェクト内で追跡しているファイルをサブモジュール内に移動したくなったとしましょう。よっぽど注意しないと、Git に怒られてしまいます。 ファイルをプロジェクト内のサブディレクトリで管理しており、それをサブモジュールに切り替えたくなったとしましょう。 サブディレクトリをいったん削除してから submodule add と実行すると、Git に怒鳴りつけられてしまいます。

$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index

まず最初に CryptoLibrary ディレクトリをアンステージしなければなりません。 それからだと、サブモジュールを追加することができます。

$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

これをどこかのブランチで行ったとしましょう。 そこから、(まだサブモジュールへの切り替えがすんでおらず実際のツリーがある状態の) 別のブランチに切り替えようとすると、このようなエラーになります。

$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
  CryptoLibrary/Makefile
  CryptoLibrary/includes/crypto.h
  ...
Please move or remove them before you can switch branches.
Aborting

checkout -f を使えば、強引に切り替えられます。ただし、そうしてしまうと未保存の状態はすべて上書きされてしまいます。強引に切り替えるのであれば、すべて保存済みであることをよく確認してから実行してください。

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'

さて、戻ってきたら、なぜか CryptoLibrary ディレクトリは空っぽです。しかも、ここで git submodule update を実行しても状況は変わらないかもしれません。そんな場合は、サブモジュール用のディレクトリで git checkout . を実行してください。ファイルが元通りになっているはずです。サブモジュールが複数ある場合は、submodule foreach スクリプトを使ったこの方法を全サブモジュールに対して実行するとよいでしょう。

最後にひとつ、大事なことを説明しておきます。相当古いバージョンの Git でなければ、サブモジュール関連の Git データはメインプロジェクトの .git ディレクトリに保存されます。古いバージョンを使っていなければ、サブモジュール用ディレクトリを削除してもコミットやブランチのデータは残ったままです。

この節で説明したツールを使ってみてください。依存関係にある複数プロジェクトを、サブモジュールを使ってわかりやすく効率的に開発できるはずです。

scroll-to-top