Git
目次 ▾ 第2版

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

サブモジュール

あるプロジェクトで作業しているときに、そのプロジェクトの中から別のプロジェクトを使用する必要があることはよくあります。おそらく、サードパーティが開発したライブラリであったり、あなたが別々に開発し、複数の親プロジェクトで使用しているライブラリであったりするでしょう。このようなシナリオでは、一般的な問題が発生します。2つのプロジェクトを別々に扱いながら、一方から他方を使用できるようにしたいということです。

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

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

サブモジュールの開始

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

まず、既存のGitリポジトリを、作業中のリポジトリのサブモジュールとして追加してみましょう。新しいサブモジュールを追加するには、追跡を開始したいプロジェクトの絶対または相対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 の出力にあるもう 1 つのリストは、プロジェクト フォルダーのエントリです。これに対して 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 はそのリポジトリの特定のコミットと見なします。

少し見やすい diff 出力が欲しい場合は、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 ディレクトリはありますが、空です。ローカル構成ファイルを初期化するために 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 initgit submodule update の手順を git submodule update --init を実行することで組み合わせることができます。また、ネストされたサブモジュールを初期化、フェッチ、チェックアウトするには、間違いのない git submodule update --init --recursive を使用できます。

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

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

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

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

サブモジュールで新しい作業を確認したい場合は、そのディレクトリに移動して 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 はサブモジュールに移動して、フェッチと更新を行います。

$ 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 は、git submodule update --remote を実行すると、デフォルトですべてのサブモジュールを更新しようとします。多数のサブモジュールがある場合は、更新を試みたいサブモジュールの名前だけを渡すことをお勧めします。

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

次に、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 コマンドの出力に示されています。さらに、新しいコミットを示す括弧は左 (<) を指しており、これらのコミットは 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 フラグを付けて、サブモジュールにネストされたサブモジュールがある場合は --recursive フラグを付けて git submodule update を実行する必要があります。

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

スーパープロジェクトの更新をプルする際に発生する可能性がある特別な状況があります。それは、アップストリーム リポジトリがプルしたコミットの 1 つで .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 は変更を取得してサブディレクトリ内のファイルを更新しましたが、サブリポジトリは「デタッチされた HEAD」状態のままにします。これは、(たとえば master のような) ローカルの作業ブランチが変更を追跡していないことを意味します。作業ブランチが変更を追跡していないということは、サブモジュールへの変更をコミットした場合でも、次回 git submodule update を実行すると、それらの変更が失われる可能性が高いことを意味します。サブモジュールの変更を追跡したい場合は、追加の手順を実行する必要があります。

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

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

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

「マージ」オプションでサブモジュールを更新してみましょう。手動で指定するには、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 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 を実行します。

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

他の人と同時にサブモジュールの参照を変更すると、いくつかの問題が発生する可能性があります。つまり、サブモジュールの履歴が分岐し、スーパープロジェクトの分岐したブランチにコミットされている場合、修正に少し手間がかかる場合があります。

コミットの 1 つが他方の直接の先祖 (早送りマージ) である場合、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 つのブランチがサブモジュールの履歴における分岐したマージが必要な時点を記録していることを理解したということです。「マージに従うコミットが見つかりません」と説明されていますが、混乱を招くため、後でその理由を説明します。

問題を解決するには、サブモジュールがどの状態にあるべきかを把握する必要があります。奇妙なことに、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 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 サブモジュールコマンドがあります。これは、同じプロジェクトに多数のサブモジュールがある場合に非常に役立ちます。

たとえば、新しい機能を開始したり、バグ修正を行ったりするときに、複数のサブモジュールで作業を進めているとします。すべてのサブモジュールで、すべての作業を簡単に stash することができます。

$ 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 バージョンではトリッキーになる可能性があります。新しいブランチを作成し、そこにサブモジュールを追加し、そのサブモジュールなしでブランチに戻ると、サブモジュールディレクトリはまだ追跡されていないディレクトリとして残ります。

$ 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 を使用してサブモジュールを正しい状態にすることができます。

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

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

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