章 ▾ 第2版

7.11 Gitツール - サブモジュール

サブモジュール

あるプロジェクトで作業しているときに、その内部で別のプロジェクトを使用する必要が生じることがよくあります。それはサードパーティが開発したライブラリかもしれませんし、あなたが個別に開発して複数の親プロジェクトで使用しているものかもしれません。これらのシナリオでは共通の問題が発生します。それは、2つのプロジェクトを別々に扱いながらも、一方をもう一方の内部から使用できるようにしたいということです。

例を挙げましょう。あなたがウェブサイトを開発し、Atomフィードを作成しているとします。独自のAtom生成コードを書く代わりに、ライブラリを使用することにしました。このコードは、CPANインストールやRuby gemのような共有ライブラリから含めるか、ソースコードを自分のプロジェクトツリーにコピーするかのどちらかになるでしょう。ライブラリを含めることの問題は、ライブラリを何らかの方法でカスタマイズするのが難しく、すべてのクライアントがそのライブラリを利用できるようにする必要があるため、デプロイがより困難になることが多いことです。コードを自分のプロジェクトにコピーすることの問題は、アップストリームの変更が利用可能になったときに、あなたが行ったカスタム変更をマージするのが難しいことです。

Gitはサブモジュールを使用してこの問題に対処します。サブモジュールを使用すると、Gitリポジトリを別のGitリポジトリのサブディレクトリとして保持できます。これにより、別のリポジトリをプロジェクトにクローンし、コミットを個別に管理することができます。

サブモジュールを使い始める

メインプロジェクトといくつかのサブプロジェクトに分割された簡単なプロジェクトを開発する手順を説明します。

まず、既存のGitリポジトリを、現在作業中のリポジトリのサブモジュールとして追加してみましょう。新しいサブモジュールを追加するには、トラッキングを開始したいプロジェクトの絶対URLまたは相対URLを指定してgit submodule addコマンドを使用します。この例では、「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.

デフォルトでは、サブモジュールはサブプロジェクトをリポジトリと同じ名前のディレクトリに、この場合は「DbConnector」に、追加します。別の場所に配置したい場合は、コマンドの最後に異なるパスを追加できます。

この時点で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も役立ちます。

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 'Add DbConnector module'
[master fb9093c] Add DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

DbConnectorエントリの160000モードに注目してください。これはGitの特殊なモードで、基本的にサブディレクトリやファイルではなく、コミットをディレクトリエントリとして記録していることを意味します。

最後に、これらの変更をプッシュします。

$ git push origin master

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

ここでは、サブモジュールを含むプロジェクトをクローンします。このようなプロジェクトをクローンすると、デフォルトではサブモジュールを含むディレクトリは取得されますが、その内部のファイルはまだ取得されません。

$ 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ディレクトリはありますが、空です。メインプロジェクトから2つのコマンドを実行する必要があります。ローカル設定ファイルを初期化するための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コマンドに--recurse-submodulesを渡すと、リポジトリ内の各サブモジュールが自動的に初期化および更新されます。これには、リポジトリ内のサブモジュール自体がサブモジュールを持っている場合、ネストされたサブモジュールも含まれます。

$ git clone --recurse-submodules 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'

すでにプロジェクトをクローンしていて--recurse-submodulesを忘れてしまった場合は、git submodule update --initを実行することでgit submodule initgit submodule updateのステップを組み合わせることができます。ネストされたサブモジュールも初期化、フェッチ、チェックアウトするには、確実なgit submodule update --init --recursiveを使用できます。

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

これで、サブモジュールを含むプロジェクトのコピーが手元にあり、メインプロジェクトとサブモジュールプロジェクトの両方でチームメイトと共同作業を行います。

サブモジュールリモートからのアップストリーム変更のプル

プロジェクトでサブモジュールを使用する最もシンプルなモデルは、サブプロジェクトを単に消費し、時折そこから更新を取得したいが、チェックアウトで実際に何も変更しない場合です。簡単な例を見てみましょう。

サブモジュールに新しい変更があるかどうかを確認したい場合は、ディレクトリに入り、git fetchgit 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がサブモジュールに入り、自動的にフェッチと更新を行います。

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

このコマンドはデフォルトで、リモートサブモジュールリポジトリのデフォルトブランチ(リモートのHEADが指すブランチ)にチェックアウトを更新したいと仮定します。ただし、必要に応じてこれを別のものに設定することもできます。たとえば、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を実行すると、Gitはサブモジュールに「新しいコミット」があることを示します。

$ 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はサブモジュールの変更の短いサマリーも表示します。

$ 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を実行すると、Gitはデフォルトで**すべての**サブモジュールを更新しようとします。多数のサブモジュールがある場合は、更新したいサブモジュールの名前だけを渡すことを検討してください。

プロジェクトリモートからのアップストリーム変更のプル

次に、MainProjectリポジトリの独自のローカルクローンを持っているコラボレーターの立場になってみましょう。コミットしたばかりの変更を取得するためにgit pullを実行するだけでは不十分です。

$ git pull
From https://github.com/chaconinc/MainProject
   fb9093c..0a24cfc  master     -> origin/master
Fetching submodule DbConnector
From https://github.com/chaconinc/DbConnector
   c3f01dc..c87d55d  stable     -> origin/stable
Updating fb9093c..0a24cfc
Fast-forward
 .gitmodules         | 2 +-
 DbConnector         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

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

Submodules changed but not updated:

* DbConnector c87d55d...c3f01dc (4):
  < catch non-null terminated lines
  < more robust error handling
  < more efficient db routine
  < better connection routine

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

デフォルトでは、git pullコマンドはサブモジュールの変更を再帰的にフェッチします(上記最初のコマンドの出力で確認できます)。ただし、サブモジュールを**更新**しません。これはgit statusコマンドの出力で示されており、サブモジュールが「modified」(変更済み)であり、「new commits」(新しいコミット)があると表示されます。さらに、新しいコミットを示す括弧が左向き(<)になっており、これらのコミットがMainProjectに記録されているものの、ローカルのDbConnectorチェックアウトには存在しないことを示しています。更新を完了するには、git submodule updateを実行する必要があります。

$ git submodule update --init --recursive
Submodule path 'vendor/plugins/demo': checked out '48679c6302815f6c76f1fe30625d795d9e55fc56'

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

念のため、プルしたばかりのMainProjectのコミットによって新しいサブモジュールが追加された場合に備えて--initフラグを付けてgit submodule updateを実行し、また、いずれかのサブモジュールがネストされたサブモジュールを持っている場合は--recursiveフラグを付けて実行する必要があることに注意してください。

このプロセスを自動化したい場合は、git pullコマンドに--recurse-submodulesフラグを追加できます(Git 2.14以降)。これにより、プル直後にGitがgit submodule updateを実行し、サブモジュールを正しい状態にします。さらに、Gitに常に--recurse-submodulesを使用してプルさせたい場合は、設定オプションsubmodule.recursetrueに設定できます(これはGit 2.15以降のgit pullで機能します)。このオプションを設定すると、Gitはサポートするすべてのコマンド(cloneを除く)で--recurse-submodulesフラグを使用するようになります。

スーパープロジェクトの更新をプルする際に特別な状況が発生することがあります。それは、プルしたコミットのいずれかで、アップストリームリポジトリが.gitmodulesファイル内のサブモジュールのURLを変更している可能性がある場合です。これは、例えばサブモジュールプロジェクトがホスティングプラットフォームを変更した場合などに起こりえます。そのような場合、スーパープロジェクトが、あなたのリポジトリでローカルに設定されたサブモジュールリモートに見つからないサブモジュールのコミットを参照していると、git pull --recurse-submodulesまたはgit submodule updateが失敗する可能性があります。この状況を改善するためには、git submodule syncコマンドが必要です。

# copy the new URL to your local config
$ git submodule sync --recursive
# update the submodule from the new URL
$ git submodule update --init --recursive

サブモジュールでの作業

サブモジュールを使用している場合、それはメインプロジェクトのコード(または複数のサブモジュールにまたがるコード)と同時にサブモジュールのコードで作業したいと考えているためである可能性が高いです。そうでなければ、おそらくもっとシンプルな依存関係管理システム(MavenやRubygemsなど)を使用しているでしょう。

それでは、メインプロジェクトと同時にサブモジュールに変更を加え、それらの変更を同時にコミットおよび公開する例を見ていきましょう。

これまで、git submodule updateコマンドを実行してサブモジュールリポジトリから変更をフェッチすると、Gitは変更を取得してサブディレクトリ内のファイルを更新しますが、サブモジュールリポジトリは「detached HEAD」状態と呼ばれる状態のままになります。これは、変更を追跡するローカル作業ブランチ(例えばmasterなど)がないことを意味します。作業ブランチが変更を追跡していないということは、サブモジュールに変更をコミットしても、次にgit submodule updateを実行したときにその変更が失われる可能性が非常に高いということです。サブモジュールの変更を追跡したい場合は、いくつかの追加の手順を実行する必要があります。

サブモジュールをより簡単に変更できるように設定するには、2つのことを行う必要があります。各サブモジュールに入り、作業するブランチをチェックアウトする必要があります。次に、変更を加えて後でgit submodule update --remoteがアップストリームから新しい作業をプルした場合にGitに何をすべきかを指示する必要があります。オプションとして、ローカルの作業にそれらをマージするか、新しい変更の上にローカルの作業をリベースしようとすることができます。

まず、サブモジュールディレクトリに入り、ブランチをチェックアウトしましょう。

$ cd DbConnector/
$ git checkout stable
Switched to branch 'stable'

「merge」オプションでサブモジュールを更新してみましょう。手動で指定するには、update呼び出しに--mergeオプションを追加するだけです。ここでは、このサブモジュールにサーバー上で変更があり、それがマージされることがわかります。

$ cd ..
$ 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(+)

ここでサブモジュールを更新すると、ローカルに変更を加えており、アップストリームにも取り込む必要がある変更がある場合に何が起こるかを確認できます。

$ cd ..
$ 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を忘れると、Gitはサブモジュールをサーバー上のものに更新し、プロジェクトをdetached HEAD状態にリセットするだけです。

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

これが起こっても心配いりません。単にディレクトリに戻って再度自分のブランチをチェックアウトし(作業はそのまま残っています)、origin/stable(または任意の他のリモートブランチ)を手動でマージまたはリベースするだけで済みます。

サブモジュールで変更をコミットしておらず、問題を引き起こす可能性のあるsubmodule updateを実行した場合、Gitは変更をフェッチしますが、サブモジュールディレクトリ内の未保存の作業を上書きすることはありません。

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

アップストリームで変更されたものと競合する変更を行った場合、更新を実行したときにGitが通知します。

$ 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
  > Update setup script
  > Unicode support
  > Remove unnecessary method
  > Add new option for conn pooling

メインプロジェクトでコミットして、サブモジュールの変更も一緒にプッシュせずにアップロードした場合、私たちの変更をチェックアウトしようとする他の人々は、依存しているサブモジュールの変更を取得する方法がないため、困ることになります。それらの変更は、私たちのローカルコピーにのみ存在することになります。

これが起こらないようにするには、メインプロジェクトをプッシュする前に、すべてのサブモジュールが適切にプッシュされているかGitに確認させることができます。git pushコマンドは、--recurse-submodules引数を取り、これは「check」または「on-demand」に設定できます。「check」オプションは、コミットされたサブモジュールの変更がプッシュされていない場合、pushを単純に失敗させます。

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

ご覧のとおり、次に行うべきことについていくつかの役立つアドバイスも提供されます。簡単なオプションは、各サブモジュールに入り、手動でリモートにプッシュして外部から利用可能であることを確認し、その後、このプッシュを再度試すことです。すべてのプッシュで「check」動作をさせたい場合は、git config push.recurseSubmodules checkを実行することでこの動作をデフォルトにできます。

もう1つのオプションは、「on-demand」値を使用することです。これにより、Gitがこれを自動的に試みます。

$ 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

ご覧のとおり、GitはDbConnectorモジュールに入り、メインプロジェクトをプッシュする前にそれをプッシュしました。もし何らかの理由でそのサブモジュールのプッシュが失敗した場合、メインプロジェクトのプッシュも失敗します。この動作をデフォルトにしたい場合は、git config push.recurseSubmodules on-demandを実行することで設定できます。

サブモジュールの変更のマージ

もしあなたが他の誰かと同時にサブモジュールの参照を変更した場合、いくつかの問題に遭遇するかもしれません。つまり、サブモジュールの履歴が分岐し、スーパープロジェクトの異なるブランチにコミットされている場合、修正にはある程度の作業が必要になることがあります。

もしコミットのいずれかがもう一方の直接の祖先である場合(ファストフォワードマージ)、Gitは単純にマージのために後者を選択するため、問題なく機能します。

しかし、Gitは単純なマージであっても試みません。サブモジュールのコミットが分岐しており、マージする必要がある場合、次のようなものが表示されます。

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

つまり、ここで何が起こったかというと、Gitが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に基づいて「try-merge」という名前のブランチを作成し、手動でマージします。

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610

$ 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-1を確認できます。

  4. 競合しているサブモジュールエントリを解決します。

  5. マージをコミットします。

少し混乱するかもしれませんが、実際にはそれほど難しくありません。

興味深いことに、Gitが扱う別のケースがあります。もしサブモジュールディレクトリに、履歴に**両方の**コミットを含むマージコミットが存在する場合、Gitはそれを可能な解決策として提案します。サブモジュールプロジェクトのある時点で、誰かがこれら2つのコミットを含むブランチをマージしたことをGitが認識するため、そのマージを望むかもしれません。

これが、以前のエラーメッセージが「merge following commits not found」だった理由です。Gitが**これを**実行できなかったからです。誰がGitに**これ**を実行しようとすることを期待するでしょうか?混乱を招きます。

もし単一の許容できるマージコミットが見つかった場合、次のようなものが表示されます。

$ 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が提供する推奨コマンドは、git addを実行したかのようにインデックスを更新し(これにより競合が解消されます)、その後コミットします。しかし、おそらくこれをすべきではありません。サブモジュールディレクトリに入り、何が違うのかを確認し、このコミットにファストフォワードし、適切にテストしてからコミットする方が簡単です。

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

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

これは同じことを達成しますが、少なくともこの方法なら、機能することを検証でき、完了時にサブモジュールディレクトリにコードが残ります。

サブモジュールのヒント

サブモジュールでの作業を少し簡単にするためにできることがいくつかあります。

サブモジュールforeach

各サブモジュールで任意のコマンドを実行する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'

お分かりいただけたでしょう。非常に便利なことの1つは、メインプロジェクトとすべてのサブプロジェクトで何が変更されたかの統一された差分をきれいに生成できることです。

$ 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 2.13より古いバージョンでは難しい場合があります。新しいブランチを作成し、そこにサブモジュールを追加した後、そのサブモジュールがないブランチに切り替えると、サブモジュールディレクトリが未追跡のディレクトリとして残ります。

$ git --version
git version 2.12.2

$ 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 'Add crypto library'
[add-crypto 4445836] Add 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 -ffdx
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バージョン(Git 2.13以降)では、git checkoutコマンドに--recurse-submodulesフラグが追加され、これらすべてが簡素化されました。これにより、サブモジュールを切り替えるブランチの正しい状態に配置する処理が行われます。

$ git --version
git version 2.13.3

$ 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 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout --recurse-submodules master
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'.

nothing to commit, working tree clean

git checkout--recurse-submodulesフラグを使用することは、スーパープロジェクト内で複数のブランチを扱い、それぞれが異なるコミットを指すサブモジュールを持っている場合にも役立ちます。実際、異なるコミットでサブモジュールを記録しているブランチ間を切り替えると、git statusを実行した際にサブモジュールが「modified」(変更済み)と表示され、「new commits」(新しいコミット)を示すことがあります。これは、ブランチを切り替える際にサブモジュールの状態がデフォルトで引き継がれないためです。

これは非常に混乱を招く可能性があるため、プロジェクトにサブモジュールがある場合は常にgit checkout --recurse-submodulesを使用することをお勧めします。--recurse-submodulesフラグがない古いGitバージョンでは、チェックアウト後にgit submodule update --init --recursiveを使用してサブモジュールを正しい状態にすることができます。

幸いなことに、Git(2.14以降)では、設定オプションsubmodule.recursetrueに設定することで、常に--recurse-submodulesフラグを使用するようにGitに指示できます(git config submodule.recurse true)。上記で述べたように、これによりGitは、--recurse-submodulesオプションを持つすべてのコマンド(git cloneを除く)でサブモジュールに再帰的に処理を行うようになります。

サブディレクトリからサブモジュールへの切り替え

多くの人が遭遇するもう一つの主な注意点は、サブディレクトリからサブモジュールへの切り替えに関することです。プロジェクト内でファイルを追跡しており、それらをサブモジュールに移動したい場合、注意しないと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