巨大な Git submodule のバージョンを一瞬で更新する方法

容量が大きいリポジトリをサブモジュールに登録していると、サブモジュールの更新には結構な時間が掛かります。

なぜ時間がかかるかと言うと `git submodule update --remote` をすると新しい内容のダウンロード（とそのチェックアウト）が発生するためです。小さいリポジトリであれば何も問題ありませんが、巨大なリポジトリになると無視できない時間が掛かりますし、ディスク容量も消費します。（私の見たことのあるプロジェクトでは一回の更新に10分以上かかってました😫）

このダウンロードを避け、手早くサブモジュールのバージョンだけを更新したいという話です。

尚、今回の方法は事前に更新後の Commit ID (ハッシュ値) を知っている必要があります。事前に更新後の Commit ID が分かっている状況であればダウンロードを省略して更新できるよ！という趣旨です。


## サブモジュールのダウンロードを省略した更新コマンド

先に結論を書くと、`git submodule update` の代わりに `git update-index` コマンドを使うと参照するコミットを強制的に書き換えられます。このコマンドは一瞬で終わります。（ここで指定する Commit ID は外部リポジトリのものです。）

```sh
$ git update-index --cacheinfo "160000" "更新後のCommitID" "サブディレクトリのパス"
```

実際にプッシュまで行うときの流れは以下のようになるでしょう。

```sh
$ git update-index --cacheinfo "160000" "d732318e5286f6e06beb5d1c958a9ecaee4fe1b5" "vendor/laradock"
$ git commit -m "Update submodule"
$ git push origin
```

これでなぜ更新できるのか、仕組みを知りたい人は以下も読んでみてください。

## そもそも Git submodule とはどんな仕組み？

`git submodule` は、他のリポジトリの内容を、自分のリポジトリにサブモジュール（サブディレクトリ）として登録し、参照するための仕組みです。基本的な仕組みについては [Git submodule の基礎](https://qiita.com/sotarok/items/0d525e568a6088f6f6bb) などが参考になると思います。

今回の話で重要になるのが、サブモジュールを登録する側の参照元リポジトリでは、**外部リポジトリの特定コミットへの参照だけを登録する** 点です。参照先にあるファイルの実体については登録しません。

たとえ巨大なリポジトリをサブモジュールとして登録したとしても Git のデータベース容量はほとんど増えません。参照先の実体の内容を `git submodule update --init` などでダウンロードするときに初めてローカルのディスク容量を消費します。

## Git のデータベースにはどのように登録される？

Git のデータベースに参照だけが登録されることを実際に確認してみます。

```sh
# テスト用の参照元リポジトリを適当に作る
$ mkdir parent-repo
$ cd parent-repo
$ git init

# 外部リポジトリをサブモジュールとして追加する
$ git submodule add https://github.com/laradock/laradock vendor/laradock
Cloning into '/tmp/parent-repo/vendor/laradock'...
Resolving deltas: 100% (5379/5379), done.

# 追加後に Git にステージされているファイルの一覧を出力する
$ git ls-files --stage
100644 c0d8e4208598468cf860099446f32ba7127b6759 0 .gitmodules
160000 8203e5da2992db2e242a6dd9733f12f5c664842e 0 vendor/laradock
```

上記の結果を見ると `git submodule add` により2つのファイルが Git に追加されています。サブモジュールの一覧を持つメタファイル `.gitmodules` と、サブモジュールの `vendor/laradock` です。

注目すべきは出力された1列目の数字です。この数字はモードビットと呼ばれ、ビット列の前半部分がファイル種別を、後半部分がパーミッションを表します。

**主なモードビットの一覧:**

- 100644 (`1000000110100100`): 実行不可能な通常ファイル
- 100755 (`1000000111101101`): 実行可能な通常ファイル
- 040000 (`0100000000000000`): ディレクトリ
- 120000 (`1010000000000000`): Symbolic link
- 160000 (`1110000000000000`): Gitlink (Submodule)

これと照らし合わせると、先ほどの `.gitmodules` の `100644` は「実行不可能な通常ファイル」として登録されています。一方で `vendor/laradock` は「Gitlink」として、つまり **外部リポジトリの特定コミットへの参照として登録されています。** 

実際に参照している外部リポジトリの Commit ID は以下のコマンドで確認できます。

```sh
$ git submodule status
8203e5da2992db2e242a6dd9733f12f5c664842e vendor/laradock (v9.3-22-g8203e5d)
```

## Git submodule の参照先を更新するには？

サブモジュールのリモート上の変更を取り込みたい場合は通常 `git submodule update --remote` のコマンドを実行します。このコマンドはサブモジュールのサブディレクトリ上で `git fetch` してから `git merge origin/master` を行うのと同じことです。このときの `git fetch` で実体のダウンロードとチェックアウトが発生します。

このダウンロードを回避し Git データベース上の参照ポインターだけを書き換えるコマンドが、冒頭に書いた `git update-index` になります。

```sh
$ git update-index --cacheinfo "160000" "d732318e5286f6e06beb5d1c958a9ecaee4fe1b5" "vendor/laradock"
```

ここで引数に `160000` という見覚えのある数字が出てきました。これは先ほどのモードビットの Gitlink を意味します。そして次の引数で Gitlink の参照先の Commit ID を指定します。

Commit ID には実際に存在しないものも指定できてしまうので、間違えないように注意が必要です。

## 参考

- [Git \- git\-submodule Documentation](https://git-scm.com/docs/git-submodule)
- [Git \- git\-update\-index Documentation](https://git-scm.com/docs/git-update-index)
- [git \- How can I set a submodule to point to a specific commit without fetching it? \- Stack Overflow](https://stackoverflow.com/questions/33514642/how-can-i-set-a-submodule-to-point-to-a-specific-commit-without-fetching-it)