意外と知られていないGitのsubmoduleとsubtreeの使い方

Gitのsubmoduleとsubtreeの使い方


Git SubmoduleとGit Subtreeは、どちらもGitリポジトリ内に他のリポジトリを組み込むための機能です。

どちらも使い方や管理方法に大きな違いがあるのですが、違いをよく理解せずに、使い勝手の悪いGit Submoduleが使用されているケースがたまに見られます。

以下でそれぞれの特徴と、違いを詳しく説明します。

Git Submoduleの説明と使い方

Git Submoduleはリポジトリに別のGitリポジトリを参照(リンク)として追加する機能です。

あるプロジェクトの中にライブラリや他のプロジェクトを参照として組み込む場合に使われます。

例えば、以下のようなmain1とsubというリポジトリがあって、どちらもREADME.mdがある状態だとします。

https://github.com/iwbjp/main1.git
https://github.com/iwbjp/sub.git

main1をgit cloneして、cd main1でmain1に移動したら、git submodule add
<リポジトリのURL> <保存先のディレクトリ名> でsubmoduleが作成できます。

git clone https://github.com/iwbjp/main1.git
cd main1
git submodule add https://github.com/iwbjp/sub.git libs

git statusを実行すると、new fileとして.gitmodulesとlibsが追加されています。

git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   .gitmodules
	new file:   libs

.gitmodulesの中身は以下のようになっており、submoduleのpathとurlが記載されています。

[submodule "libs"]
	path = libs
	url = https://github.com/iwbjp/sub.git

ここまで確認したら、いったんコミットしてプッシュしてください。

git push origin main
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 375 bytes | 375.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/iwbjp/main1.git
   ce09177..5d0cf83  main -> main

GitHubで確認すると、

main1リポジトリ Git Submodule

.gitmodulesと「libs @ 98bd505」というのが追加されていますね。

「libs @ 98bd505」はリンクなので、クリックするとsubリポジトリに遷移します。

subリポジトリ Git Submodule

ローカルのlibsはリンクではなく、中にはsubリポジトリのREADME.mdが入っています。

mainのlibs内のREADME.mdを修正してコミット&プッシュすれば、subリポジトリのREADME.mdに反映されます。(add, commit, pushなどはcd libsで移動してから行います)

$ cd libs
$ git add README.md

On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   README.md

$ git commit "update sub repo README.md"
[main 57cc42a] update sub repo README.md
 1 file changed, 3 insertions(+), 1 deletion(-)
$ git push origin main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 262 bytes | 262.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/iwbjp/sub.git
   98bd505..57cc42a  main -> main

次にsubリポジトリをgit cloneしてfoo.txtというファイルを追加してプッシュしてください。

$ git clone https://github.com/iwbjp/sub.git
$ cd sub
$ touch foo.txt
$ git add foo.txt
$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   foo.txt

$ git commit -m "add foo.txt"
[main 7c4674e] add foo.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 foo.txt
$ git push origin main
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 266 bytes | 266.00 KiB/s, done.
Total 3 (delta 0), reused 1 (delta 0), pack-reused 0 (from 0)
To https://github.com/iwbjp/sub.git
   57cc42a..7c4674e  main -> main

これをmain1リポジトリに反映するには、ルートディレクトリで実行する場合は、git pull origin mainではなく、git submodule foreach git pull origin mainを使います。

実行するとmain1のlibs/foo.txtが反映されていることが確認できます。

ちなみに他の開発者がリポジトリをクローンするとき、サブモジュールも含めて取得するには、
git clone --recurse-submodulesコマンドを使う必要があります。

Git Subtreeの説明と使い方

Git Subtreeはサブリポジトリの内容を親リポジトリの特定ディレクトリに直接含めます。

サブツリーとして追加されたリポジトリは、親リポジトリの一部として扱われますが、必要に応じて独立したサブリポジトリとしての操作も可能です。

説明の前に、先ほど作成したGit Submoduleの.gitmodulesとlibsを削除してください。

$ git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    .gitmodules
	deleted:    libs

$ git commit -m "delete .gitmodules and libs"
$ git push origin main

Git Subtreeの追加はGit Submoduleと似ており、まずmainリポジトリで以下のコマンドを実行してGit Subtreeを追加します。

git subtree add --prefix=libs https://github.com/iwbjp/sub.git main --squash

成功するとlibsというディレクトリができていて、中にはsubリポジトリのfoo.txtとREADME.mdがあります。

Git Submoduleの.gitmodulesのような余計なファイルは作成されません。

確認したら、いったんプッシュしてください。

次にlibs/foo.txtに「Hello」と入力して、プッシュしてみてください。

$ git add foo.txt
$ git commit -m "add foo.txt Hello"
$ git push origin main
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 355 bytes | 355.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/iwbjp/main1.git
   865a94d..0595777  main -> main

これだとmain1リポジトリには反映されますが、subリポジトリには反映されません。

subリポジトリにプッシュして反映させるには以下のコマンドを実行します。

git subtree push --prefix=libs https://github.com/iwbjp/sub.git main

pullのときはこうなります。

git subtree pull --prefix=libs https://github.com/iwbjp/sub.git main --squash

最後にsubリポジトリのfoo.txtの中身をHelloからByeに書き換えてプッシュしてください。

main1リポジトリでgit pullしてもByeの書き換えた内容は反映されませんが、git subtree pullだと反映されます。

Gitのsubmoduleとsubtreeの違い

Gitのsubmoduleとsubtreeには以下のような違いがあります。

項目 Git Subtree Git Submodule
基本的な仕組み サブプロジェクトを親リポジトリの一部として統合し、依存関係を持たない形で管理する 親リポジトリから参照される独立した外部リポジトリをリンクとして管理する
バージョン管理 サブプロジェクトの変更が親リポジトリの履歴に直接反映される サブモジュールの変更は独立して管理され、親リポジトリには参照のみが保存される
セットアップの簡便さ クローン後に追加の操作なしでサブプロジェクトが利用可能 クローン後に git submodule initgit submodule update が必要
ファイルの配置 サブプロジェクトのファイルが親リポジトリの指定したディレクトリに直接配置される サブモジュールはリンクとして指定ディレクトリに配置され、実体は独立したリポジトリとして扱われる
使用シナリオ 小規模なサブプロジェクトを親リポジトリに直接取り込みたい場合に適している 大規模な外部リポジトリを依存関係として組み込みたい場合に適している
リモートとの同期 親リポジトリから直接プッシュ・プルでサブプロジェクトを更新できる サブモジュールは独自のリモート設定を持つため、親リポジトリからのプッシュ・プルが分離される
登場時期 Git 1.5.3 (2007年) Git 1.7.11 (2012年)

Gitのsubmoduleとsubtreeの違いを見るとsubtreeのほうが使い勝手が良いです。

それもそのはず、Git Submoduleが登場したのはGit Subtreeの5年もあとのことで、Git Submoduleの使い勝手が悪かったので新しく作られたという側面があります。

実際Git Subtreeのほうが操作が簡単で、クローン時の初期化が不要など、どちらも機能は似ていますが、Git Submoduleよりも使いやすくなっています。

特に理由がなければGit Subtreeのほうを使用することをオススメします。

カテゴリーgit