♻️

git-submodule と git-subtree

2020/09/26に公開

高知工科大 Advent Calendar 2016
4日目の\ヒッカリ~ン/です
ネタ無いな~と思ってたんですが、最近gitのライブラリ管理で困ってる人を見かけたのでしょうがないからsubmoduleとsubtreeのまとめ記事を書こうと思います。

とか偉そうなこと言ってますが全部合ってる自信ないので間違ってたらマサカリコメント入れてください。(できれば枕投げコメント程度にやさしくお願いします)

git-submodule

git submoduleは外部のgitリポジトリを自分のgitリポジトリのサブディレクトリ取り込み、その特定のcommitを参照するものです。
とりあえずやってみましょう。

submoduleの追加

例えば、現在fooというリポジトリのルートにいるとして、lib/barフォルダにbarというリポジトリを取り込みたいとします。

$ git submodule add https://github.com/s-nlf-fh/bar.git lib/bar

これでlib/barフォルダにbarリポジトリのmasterが取り込まれました。
.gitmodulesというsubmodule管理用のファイルも作成されてその中身は

.gitmodules
[submodule "lib\\bar"]
	path = lib\\bar
	url = https://github.com/s-nlf-fh/bar.git

となっており、ディレクトリのパスとURLを保存しています。
次にgit diffを実行してみます。

$ git diff --cached lib/bar
diff --git a/lib/bar b/lib/bar
new file mode 160000
index 0000000..b44a41b
--- /dev/null
+++ b/lib/bar
@@ -0,0 +1 @@
+Subproject commit b44a41b85eba12390534152c5a9e97793ed0b768

ここでbarリポジトリのcommit hashが出てきました。つまりsubmoduleはmasterとかdevelopとかのブランチではなく特定のコミットを参照するようになっていることがわかります。

submoduleを含むプロジェクトのclone

barを含んだfooプロジェクトをcloneするには以下のコマンドを打ちます。

$ git clone https://github.com/s-nlf-fh/foo.git
$ git submodule init
$ git submodule update

cloneしただけではlib/barフォルダは空っぽなのでsubmoduleの初期化とチェックアウトが必要です。
3行もあってめんどくさいですが実は以下の1行で同じことができます。

$ git clone --recursive https://github.com/s-nlf-fh/foo.git

これでlib/barフォルダは特定のコミットにチェックアウトされた状態のbarリポジトリになります。

参照するコミットの変更

barモジュールのmasterが更新されました。現在barモジュールは以前のコミットを参照するようになっていますが、最新のmasterを使いたいとします。その時はlib/barフォルダに移動してmasterをpullすればいいだけです。

$ cd lib/bar
$ git pull
$ cd ../../
$ git diff lib/bar
$ git add lib/bar
$ git commit -m "[barモジュール] 最新のmasterを使うようにする"

これでdiffで出力されるcommit hashが最新のmasterのものになります。
最新のものではなく特定のcommitを使いたいときはgit pullgit checkout `commit hash` に変えればいいです。
また今回のように最新のmasterに追従するだけとかdevelopブランチに追従するだけとかだとgit submodule update --remoteの一行でもできるのですがそれは公式ドキュメントに丸投げします。git-submodule

submoduleの編集

さてここからが本番です。
submoduleでbarを追加しましたがbarを編集したくなりました。なのでlib/barのフォルダ内を編集するのですがその前にすることがあります。
まず1つ目。barを編集するということはbarリポジトリにpushする権限がないといけません。なので他人のGitHubリポジトリ等をsubmoduleにする場合はまずforkしてそれをsubmoduleにしましょう。途中から変更も出来ます。
2つ目。必ずbarに新しいブランチを作ってから作業しましょう。

$ cd lib/bar
$ git checkout -b feature/foo

すでに作ってあるときは普通にチェックアウト

$ cd lib/bar
$ git checkout feature/foo

とにかく編集する前になんらかのブランチにチェックアウトすることが重要です!!!
理由はググればいっぱい出てきます。コミットが消えるとかなんとか。
実はというかsubmoduleは特定のコミットを追跡するものなので、親リポジトリ側でgit submodule updateするとサブリポジトリ内でgit checkout `commit hash` したのと同じ状態になります。つまり**git submodule updateしたらsubmodule内で行った変更は無くなるし、HEADも'detached HEAD'になります**。
これがsubmoduleのデメリットとして書かれてたりしますが、そもそもgit checkout `commit hash` したら変更が消えるのは当然だし、'detached HEAD'の状態でcommitして、ましてやその後またgit checkout `commit hash` して名無しの枝分かれを作るなんてこと普通はしませんよね。
git submodule updategit checkout `commit hash` と同だとわかってたらそんなことにはなりませんし、正しい使い方をすればいいだけの話です。
"学習コストがどうの"とか"submoduleやめてsubtree使うべき"とか書かれてたりもしますが、subtreeだって正しい使い方を勉強しないとまともに使えなしデメリットもあるんだから適材適所で使っていったらいいと思います。(マジレス)

話が逸れましたが、チェックアウトすればもう安心。barを編集してcommitしても大丈夫です。もうお分かりかと思いますがlib/barフォルダ内はbarリポジトリそのものになっています。lib/barに移動すればfooリポジトリのサブディレクトリとしてではなくbarリポジトリとしてgitの操作ができますので作ったブランチ(feature/foo)をdevelopやmasterにmergeしたりしてbarの開発をしてください。

最後にもう1つ注意点。親リポジトリ側(foo)でsubmoduleの参照を更新するコミットをする場合は必ずsubmodule側(bar)でpushして変更をサーバーに上げること。submoduleは特定のコミットを参照するものなのでローカルにしか存在しないコミットを参照されても困ります。

あと親リポジトリ側でmergeするときにコンフリクトしちゃうとめんどくさいとかありますけど、普通のコンフリクト解消に毛が生えた程度の対応でなんとかなると思います。(私が複雑なコンフリクトを経験してないだけかもですが)
対処法はまた公式ドキュメントに投げます。git-submodule

もうちょっと注意点あったと思いますが、とりあえずこのへんを覚えていれば何とかなると思います。

submoduleまとめ

  • submoduleは特定のcommit hashだけを追跡する
  • 親プロジェクトとライブラリプロジェクトの開発を完全に分離できる
  • ライブラリプロジェクトの更新を追跡するだけ(編集はしない)の用途には向いてる (npm的な使い方)
  • ライブラリの編集はするけどライブラリとしての変更(後でmasterやdevelopにmergeする)ならあり
  • ライブラリを特定のアプリ用にポーティングしたい場合とかには向かないと思う (理由はライブラリ側のブランチがポーティングした数だけ増え続けて爆発するから)

git-subtree

git-subtreeは外部のgitリポジトリを自分のgitリポジトリのブランチとして取り込み、そのブランチを丸ごとサブディレクトリに配置するものです。

ブランチをサブディレクトリに配置とは一体ってなると思いますがgit-subtreeの前にまずsubtree-mergeというものを紹介したいと思います。

subtree-merge

subtree-mergeとはマージ戦略のことです。マージ戦略とはgit mergeするときのマージのしかた、つまりアルゴリズムのことです。gitのmergeコマンドはマージの仕方をオプションでいろいろ選択出来るようになっていてそのうちの一つにsubtreeというものがあります。
merge-strategies

このsubtree-mergeを使うとサブディレクトリにライブラリを取り込んで管理することが出来ます。
とりあえずやってみましょう。
submoduleの時と同じでfooプロジェクトのlib/barディレクトリにbarライブラリを入れることにします。

ライブラリのリモートの追加

まずはfooにbarのリモート参照を追加してbarのmasterブランチにチェックアウトします。

$ git remote add bar_remote https://github.com/s-nlf-fh/bar.git
$ git fetch bar_remote
$ git checkout -b bar/master bar_remote/master

これでfooプロジェクトにbar/masterブランチが追加されてその中身はbarのmasterと同じになっています。

サブディレクトリへの取り込み

次はfooのmasterにbar/masterブランチを取り込みましょう。

$ git checkout master
$ git read-tree --prefix=lib/bar -u bar/master
$ git commit -m "lib/barにbarのmasterを取り込む"

これでlib/barフォルダの中身がbarのmasterと同じになりました。

ライブラリの変更に追従する

barプロジェクトが更新されたらそれに追従しないといけません。なのでpullします。

$ git checkout bar/master
$ git pull

これだけ。普通にbar/masterブランチに行ってpullすればいいだけです。
あとはfooのmasterに取り込み

$ git checkout master
$ git merge --allow-unrelated-histories -s subtree bar/master

これでmasterのlib/barディレクトリがbarの最新のmasterと同じになりました。
注意点としてはgitの2.9 ? からマージするときに--allow-unrelated-historiesのオプションを付けないと共通の親を持ってないコミットをマージ出来ないようになってるので付けましょう。

ライブラリの編集

さてlib/barフォルダ内を編集しようと思います。submoduleと違って編集前にすることはありません。普通に編集してコミットしてしまいましょう。

次にmasterブランチで行ったlib/barフォルダ内の更新をbar/masterブランチに反映させましょう。
※これもpushの権限が必要です。がsubmoduleと違いmasterブランチにlib/barの変更コミットは存在するので無理にbarリポジトリにpushしなくても大丈夫です (ポーティングする時とか)

$ git checkout bar/master
$ git merge -s subtree master
$ git push

これでfooリポジトリのmasterで行った変更をbarリポジトリに取り込むことが出来ました。

まぁここに書いたことは全部公式ドキュメントにも書いてあります。
公式の方がわかりやすいのでこっちを読みましょうw

git-subtree

さて本題のgit-subtreeの話に入りたいと思いますが、まずgit-subtreeのソースコードをちらっと見てください。シェルスクリプトで書かれてます。
git-subtree.sh
見ましたか? "read-tree"とか"merge -s subtree"とかが書いてあるのを発見できましたか?
そうです。subtree-mergeを使ったライブラリ管理の手順を使いやすいよういい感じにまとめてくれてるのがgit-subtreeのコマンドなんです。(ここちょっとマサカリ飛んできそうw)

中身がわかったら安心ですね。"マージするときコンフリクトしたんやけど"とか"subtree push / subtree pull出来ひんくなった"とかのトラブルにも対処できると思います。頑張ってください。
ちなみにgit-subtree.shの中のgit merge -s subtreeしてる所で--allow-unrelated-historiesのオプションが付いてないせいでrefusing to merge unrelated historiesってエラーになったことがあります。まぁ使い方が悪かっただけかもしれませんが。

もうしんどくなってきたので使い方はググったりgit subtree --helpしたりしてください。

subtreeまとめ

  • git-subtreeの中身ではsubtree-mergeを使ってる
  • 親プロジェクトのローカルリポジトリ内には2つのプロジェクトが入ってる状態になってちょっと複雑になる
  • 親プロジェクトの編集のコミットとライブラリプロジェクトの編集のコミットをちゃんと分けないとぐちゃぐちゃになる (分けろよって話ですが)
  • ポーティングするときとかはsubmoduleよりこっち使った方がいいかも
  • ライブラリプロジェクトへpushする権限がなくても編集したりできる (submoduleみたいにforkしなくても大丈夫)

さいごにまじれす

もしかしてあなたこれ全部読んだんですか?
暇人ですね。そんな時間があるならさっさと公式ドキュメント読みながら自分でやってみようぜ。そっちの方が数百倍わかりやすいと思うよ。

Discussion