エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

git submodule はトモダチ!怖くないよ! (チートシート付き)

f:id:doloopwhile:20200226161647p:plain
この画像は本文とは関係ありません。

こんにちは、エムスリー・エンジニアリングG・基盤開発チーム小本です。

みなさん、git submodule コマンドは好きですか?git submodule は特定の状況下では便利なコマンドです。

f:id:doloopwhile:20200219195042p:plain
社内アンケートでも25%が怖いという結果に

しかし、なぜか世間にはgit submodule が怖いという人が相当数いるようです。推測ですが、git submodule は動作モデルや使用手順が誤解されがちなところがあり、それで「怖い」と思われているのないでしょうか。git 本体でも昔そんなことがありましたよね。

この記事では git submodule の誤解を解き、適切な使い方を解説します。また、記事の最後にチートシートをつけます。

git submoduleはトモダチ!怖くないよ!

git submodule って何?

Git の標準機能の一つで、追加されたのは2007年05月26日です。意外と古いのです。

別レポジトリのコミットを参照し、git clone 時に同時にチェックアウトしたりできます。

「git clone したら、hogelib も deps/hogelib に git clone してください」的な手順を自動化・標準化できるのが特長です。

公式ページはこちらです:

誤解1 「プロジェクトが大きくなったら git submodule で分割するものだ」

Gitレポジトリのファイル数が多くなると、なぜか「大きくなったから分割しよう」「アプリとAPIとAPIクライアントに分割しよう」「ようし git submodule 使っちゃうぞ!」と言い出す人が現れます。

リリースサイクルが大きく異なるとか開発チームが別ということであれば分割する手もあるのですが、 常に同時に手を入れる必要があり同じチームが開発しているようなレポジトリを分割してもメリットはありません。手間が増えるだけです。

参考までにGoogleやFacebookは単一レポジトリで開発しているそうです:

git clone に時間がかかる?それは多分、画像ファイルとかをコミットしているからです。Git LFS などを使いましょう。

誤解2 「サブモジュールのmasterブランチのHEADを自動追尾する」

git submodule が「サブモジュールのmasterブランチのHEADを自動追尾する機能」だと思っていませんか? それは誤りです。

f:id:doloopwhile:20200225165415p:plain
誤ったイメージ

git submodule は、あくまでコミットを参照するものです。サブ側の master ブランチに更新があっても、親側で明示的に操作をしないと反映されません (git ではどの変更も大体明示的操作が必要です)。

また、参照先を master ブランチ外のコミットに変えることだってできます。

f:id:doloopwhile:20200225165451p:plain
正しいイメージ

実際、GitLabやGitHub上ではサブモジュールにはブランチではなくコミットが表示されます。

f:id:doloopwhile:20200225170837p:plain

誤解3:「.gitmodulesを変更したのに反映されない」

これは誤解というか git submodule の設計上の問題点なのですが、サブモジュールの情報が複数の場所に書かれており、分かりにくくなっています。

  • .gitmodules
  • .git/config
  • gitのインデックスとワークツリー

とりあえず、後述のチートシートに従って作業してください。その範囲では問題は起きないはずです。

その上で

  • サブモジュールのリビジョンを変更する際には .gitmodules.git/config は変更しない
  • サブモジュールを追加削除したりURLを変更するには .gitmodules を変更し、git submodule sync.git/configに反映させる

という手順を覚えておいてください。詳しくは公式ドキュメントを参照。

git submodule の使い所

では git submodule はどのようなタイミングで使えば良いのでしょうか?

実は git submodule を使うべきシーンはそんなに多くありません。単にレポジトリを分割したり別レポジトリの内容を取り込んだりしたいだけなら、プログラミング言語が提供するパッケージ配布の仕組み(RubyのGem、PythonのWheel、TerraformのTerraform Registryなど)を利用すれば良いからです。

使い所1:パッケージ配布の仕組みがそもそも無い場合

gRPCOpenAPIの定義ファイルなど配布の仕組みがそもそも無い場合は git submodule が選択肢の一つになります。

ファイルをコピペしたり git clone で取得したりするのに比べて、

  • 手順を標準化できる(チートシート参照)
  • バージョンを指定できる

といったメリットがあります。

使い所2:プライベートサーバーが大げさな場合

パッケージ配布の仕組みが備わっている場合でも社内限定で配布する場合には git submodule が簡便です。

GemやWheelでもプライベートサーバーを用意すれば社内限定で配布することができますが、サーバーの構築・運用にそれなりのコストがかかります。 その点 git submodule はサーバーの構築が不要です。

使い所3:GitLab CIで他のレポジトリを参照する

弊社で使っている GitLab CI では、CIジョブ内で他の社内レポジトリを git clone することが原則としてできません(認証情報を追加して git clone することもできますが、認証情報を漏洩させるリスクがあったり、コードが冗長になってしまう問題があります)。

// terraform で社内の別レポジトリを参照する
// これを GitLab CI で実行すると `git clone`しようとして、認証エラーになる。
module "esc_ec2_cluster_template" {
  source = "ssh://gitlab.example.com/m3dev/m3-terraform-modules.git//ecs_ec2_cluster_template"
}

GitLab CIでは代わりに git submodule を使うことになっています^3

# 参照したいレポジトリをサブモジュールとして追加する
git submodule add "ssh://gitlab.example.com/m3dev/m3-terraform-modules.git
// git submodule でチェックアウトしたローカルマシン上のファイルを参照する
module "esc_ec2_cluster_template" {
  source = "m3-terraform-modules/ecs_ec2_cluster_template"
}

git submodule チートシート

以下はよくある操作の方法です。

サブモジュールを追加する

git submodule add で追加し git commit で保存しましょう。

git submodule add https://example.com/sub-modules # .gitmodules ファイルが更新される
git commit -a # 変更を保存(必須!)

サブモジュールを含んだGitレポジトリを git clone する

--recursive オプションをつけてください。

git clone https://example.com/repo-with-sub-modules --recursive

すると、サブモジュールのファイルも自動で取得されます。

--recursive をつけ忘れた / サブモジュールのファイルを取得したい

git clone--recursiveを付けないとサブモジュールのファイルが取得されませんが、 git submodule update --init で後からチェックアウトできます。

git submodule update --init

新しいバージョンを git checkout したら、他の人がサブモジュールを追加していた。

git submodule update --init でサブモジュールのファイルを取得しましょう。

過去のバージョンを git checkout したら、サブモジュールのファイルがそのまま残ってしまった!どうしよう!?

落ち着いて。サブモジュールを追加する以前のリビジョンを git checkout すると、サブモジュールのファイルは削除されず、 「Git管理されていないファイル」としてファイルシステム上に残ります。

それらは単なる「Git管理されていないファイル」なので削除しても問題ありません。

サブモジュールがどのバージョンを指しているか確認する

git submodule status で情報を表示できます。ただし、事前にgit fetchを実行しないとタグなどが反映されないことがあります。

git submodule foreach git fetch # サブモジュールの最新情報を取得
git submodule status # ステータスを表示

以下のように、ハッシュタグ・モジュール名・タグorブランチが表示されます。

 43e37830eb3bc2133c4f5f4aeb82bc938e0fde3c hoge (v1)
 76454850ef27be60bc9946d2b7183e66f6048232 fuga (heads/master)

サブモジュールを削除する

サブモジュールの情報が複数の場所に保存されているので多少複雑です。

git submodule deinit -f sub-module # 登録解除
git rm -f sub-module # ファイルを削除
git config -f .gitmodules --remove-section submodule.sub-module # 設定ファイルから削除
git commit -a # 変更を保存(必須!)

サブモジュールのリビジョンを変える

基本的には「サブモジュール側でリビジョンに変更→親側でコミット」

cd sub-module # サブモジュールのディレクトリに移動
git fetch && git reset --hard origin/master # 適当なコマンドで、必要なバージョンをチェックアウトする

cd ../ # 親側に移動
git commit -a # 親側でコミット

ちなみに git submodule update --remote を使えば「masterブランチのHEADをチェックアウト」することもできるんですが、 これが誤解2を招いている気がしてなりません。

もっと知りたい

ググるとサードパーティの解説記事もたくさん見つかりますが、まずは公式ページを当たってください:

We are hiring!

エムスリーでは一緒に開発に参加してくれる仲間を募集中です。 お気軽にお問い合わせください。

jobs.m3.com