Git Submoduleについてまとめてみる

# はじめに

Git Submoduleについて自分で分かった範囲をまとめてみます。
同じような記事が沢山ありますが、バージョンが違うからか微妙に動作が異なっていたり、自分の思うようにいかない部分があったために記事にしました。

Gitのバージョンは、
Git Shell git version 2.11.0.windows.3
(GitHub for Windwos 3.3.4.0)
になります。

# 要件

-   親プロジェクトで、子プロジェクト(サブモジュール)を使いたい。
-   親プロジェクトでは、常に最新版の子プロジェクトを使う。
-   親プロジェクト側からも子プロジェクトを更新したい。
-   親プロジェクトは、公開する。
-   子プロジェクトは、公開しなくても良い
-   子プロジェクトは、それ単体でバージョン管理している。
-   子プロジェクトをサブモジュールとして扱う。

3つ目の条件は、あまりよくないかもしれませんが個人のプロジェクトなので利便性を考えてこうしています。

# 構成

![gitsubmodule Object diagram.png](https://qiita-image-store.s3.amazonaws.com/0/194349/cce21b43-38dd-b72d-2e1b-371061de26f6.png)

親プロジェクトをParentとし、子プロジェクトをChildとします。
リポジトリの構成は、特に変哲もない図の上のようにしました。
子プロジェクトは、公開しなくても良いので図の下にして試してみましたが、ファイルの整合性がおかしくなるのであきらめました。
詳しくは、後述します。

それぞれのディレクトリとファイルの構成は、

```text
Child
  child.txt

Parent
  parent.txt
```

のように、単純にディレクトリに1つのテキストファイルが存在するだけです。

child.txtは、

```text
child
```

parent.txtは、

```text
parent
```

と書いてあるだけです

# リポジトリを作る

Childディレクトリに移動して、

```text
# Child
git init
git add .
git commit -m "first"
git remote add origin <ChildRemoteURL>
git push -u origin master
```

を実行し、Parentディレクトリに移動して、

```text
# Parent
git init
git submodule add <ChildRemoteURL>
git add .
git commit -m "first"
git remote add origin <ParentRemoteURL>
git push -u origin master
```

を実行します。
こうするとParentは、

```text
Parent
  .gitmodules
  parent.txt
  Child
    child.txt
```

になります。
既存の記事には、

```text
git submodule init
git submodule update
```

が必要と書いてありますが、現状では必要ないようです。

# 子プロジェクトの更新を取り込む

次に、子プロジェクトの更新を取り込む場合の操作を試してみます。
下準備として、Childリポジトリで、child.txtを

```text
child
1 on child
```

と更新し、以下を実行します。

```text
# Child
git add .
git commit -m "1 on child"
git push
```

Parentリポジトリに移り、以下を実行します。

```text
# Parent
git submodule foreach git fetch
git submodule foreach git merge origin/master
```

これでParent配下のchild.txtが最新版に更新されます。
ここで上記のコマンドの代わりに、

```text
# Parent
git submodule update --remote --merge
```

でも可能です。
また、更新するだけでならば以下のコマンドでも可能です。

```text
# Parent
git submodule update --remote
```

`git submodule update --remote`でもchild.txtの最新版を取り込むことが出来ます。
ただし、このコマンドの場合は、Parent配下のChildが`Detached HEAD`になってしまいます。
Parent側からサブモジュールを更新する場合は、`Detached HEAD`になるとひと手間かかります。
サブモジュールを参照するだけの場合は、このコマンドで十分です。

ここまでの操作で、ローカルのみ更新しました。
リモートも更新するために以下を実行します。

```text
# Parent
git add .
git commit -m "update child1"
git push --recurse-submodules=check
```

# 親プロジェクト側から子プロジェクトを更新する

Parentリポジトリ配下のchild.txtを以下のように編集します。

```text
child
1 on child
2 on parent
```

Parentリポジトリで以下を実行します。

```text
# Parent
git submodule foreach git add .
git submodule foreach git commit -m "2 on parent"
git submodule foreach git push
```

この時点でリモートのChildが更新されます。
Parentのリモートも更新するために以下を実行します。

```text
# Parent
git add .
git commit -m "update child 2"
git push --recurse-submodules=check
```

最後のChildのローカルを更新するために、Childリポジトリで以下を実行します。

```text
# Child
git fetch
git merge origin/master
```

## `Detached HEAD`でのプッシュ

`git submodule update --remote` を使ってサブモジュールを更新していた場合、Parent配下のChildが`Detached HEAD`になっています。
この状態で、child.txtを更新してしまった場合、ParentからのChildへのプッシュが拒否されます。
その場合は、以下のコマンドなどでプッシュすることが出来ます。

```text
# Parent
git submodule foreach git status #HEADのリビジョン名(SHA-1)を調べる
git submodule foreach git checkout master
git submodule foreach git merge <HEADのSHA-1> #rebse
git submodule foreach git push
```

# 最後に

以上が、サブモジュールの使い方の概要だと思います。
この記事では、全てリポジトリのルートディレクトリで操作するために`git submodule foreach`を使っています。
サブモジュールのルートディレクトリ(上記の記事の場合はParent/Child)に移動すれば、`submodule foreach`なしのコマンドが使えます。
複数のサブモジュールを使っている場合などは、こちらで操作しなければいけないこともあるでしょう。

# コマンド

-   `git push --recurse-submodules=check` 配下のサブモジュールがプッシュされていないとメッセージを表示しプッシュが中断されるようになる。
-   `git submodule foreach` 配下の全てのサブモジュールに対して、指定したコマンドを実行する
-   `git submodule update` サブモジュールを、登録されているコミットIDで更新する
-   `git submodule update --remote` サブモジュールを、サブモジュールのリモート追跡ブランチ (remote-tracking branch)で更新する
-   `git submodule update --remote --rebase` サブモジュールの現在のブランチを、リモート追跡ブランチにリベースする
-   `git submodule update --remote --merge` サブモジュールの現在のブランチとリモート追跡ブランチをマージする

* * *

# non-bareリポジトリにプッシュ

サブモジュールには直接関係ないのですが、図に示した下の構成をしないほうが良い理由なので記述します。

デフォルトの設定では、non-bareリポジトリにプッシュすると、全て拒否されます。
そこでプッシュを受け入れるリポジトリに、`git config receive.denyCurrentBranch ignore`を設定しなければいけません。
設定すればプッシュを受け入れて履歴が更新されますが、ファイルの内容が更新されません。(されていないように見えます。)
何故更新されないかと言うと、

1.  プッシュされた状態にファイルを更新。
2.  今までの状態を新しい変更と解釈して更新。つまり1を削除したものになる。
    の2つの更新がかかるからです

プッシュ側のファイルを

```text
 a
 b
```

受け取り側のファイルを

```text
 a
```

としてプッシュすると

```text
 a
 b
```

に更新した後

```text
 a
```

を新しい更新として上書きする事になります。
結果、ファイルの内容は更新以前のままになります。