Git
GitDay 24

submoduleとsubtree-mergingの使い分け

More than 5 years have passed since last update.

gitで外部リポジトリを取り込んで利用するには、submodule(サブモジュール)

subtree merging(サブツリーマージ)の2手法があります。

私の観測範囲内では、サブモジュールはよく利用されていますが、サブツリーマージは目にしません。

そこで、サブツリーマージを試してみての使い分けや思ったことをつらつらと。


大雑把な要点を最初に書いておく


  • 外部リポジトリを外のものとして取り込むならサブモジュール

  • 外部リポジトリを取り込みつつ、中のものとして手を加えていくならサブツリーマージ

  • git初心者にはサブモジュール(異論ありそう)


サブツリーマージ使ってみた

github.com/marutanm/dotfiles

中身としては、よくある環境設定ファイル一覧です。

前提として、

というファイル群があり、これをサブツリーマージしてdotfilesというひとつのリポジトリにまとめました。

さらに、git設定を別ブランチで作成し、サブツリーマージでmasterにとりこみました。このgit設定はあくまでもブランチであり、独立したリポジトリはありません。


手順

省略。主として以下を参考にしました。


サブモジュールとサブツリーマージの違い

サブモジュールだと、外部リポジトリの特定のリビジョンに対しての参照をもっています。あくまでも参照なので外部リポジトリのファイルはコミットされておらず、git cloneしただけではファイルおちてきません。

サブツリーマージでは、外部リポジトリは通常リモートリポジトリとして追加されます。

つまりは別ブランチです。取り込むときにはread-treeを使い、さらにはsubtreeストラテジを指定してマージすることも可能です。(前者はあくまでファイルをとりこむだけ、後者によりコミットツリーがひとつになる、と私は認識しています。)

前述のリポジトリのネットワークグラフを見ればわかりますが、複数ブランチがそれぞれのコミット履歴をもっています。これがそれぞれ外部リポジトリと対応しており、マージコミットでmasterブランチに統合されています。

サブモジュールを使うと、外部リポジトリのコミット履歴はここには表示されません。

要は、サブモジュールは外部リポジトリを含めたひとつのブランチとして扱う、サブツリーマージは外部リポジトリそれぞれをブランチとして複数ブランチを扱うということです。


サブモジュールの使いどころ

外部リポジトリを取り込んで使いたいが、外のものとして使いたい場合。もう少し踏み込んでいうと、


  • (外部リポジトリに)手を加えることはない

  • (外部リポジトリの)コミット履歴は気にしない

ような場合は、サブモジュールがよさそうです。サブモジュールのコミット履歴を見るときや手をくわえるときは、そのリポジトリに対して別途gitを操作する必要があります。サブモジュールはあくまでも別リポジトリなのです。


サブツリーマージの使いどころ

サブツリーマージにおける外部リポジトリは、単にリモートリポジトリ。つまるところ別ブランチなわけです。

ネットワークグラフも併せて一覧できますし、diff-treeでdiffも見られる。ブランチ間の操作もsubtreeストラテジを指定することにより、mergeだろうがpullだろうがcherry-pickだろうがrebaseだろうが可能です。サブディレクトリに移動せずとも、外部リポジトリに対する任意のgit操作ができます。もちろんコミットしたものをpushしてもとの外部リポジトリに反映することも容易です。

つまり、外部リポジトリを取り込みつつ、中のものとして手を加えたい場合にはサブツリーマージを選択するのが良さそうです。


サブモジュールやめてサブツリーマージ使おうかな?

ケースバイケース。

試してみたところ、サブツリーマージしようとする外部リポジトリが、さらにサブモジュールをもっていると破滅を導きます。


git clone && git submodule update --initで済むのは存外に容易かもしれない

サブモジュールを使うとgit cloneにくわえてgit submodule update --initをする必要があります。これを煩雑と捉えるかどうか。それさえしてしまえば作業コピーの状態は同じになります。

サブツリーマージを使うと、git cloneのみでファイルが落ちてくるので作業コピーはおおよそ同じになりますが、サブツリーのリモートリポジトリ等の実際に開発をはじめられるまでの作業環境の復元にはもう一手間かかります。

また、サブツリーマージでは外部リポジトリをブランチとして扱います。つまり必然的に複数ブランチを育てていくことになります。コミットがどのブランチに属するべきかを考え、それぞれのコミット履歴を整然と保つためにコミットを分割し、統合し、移動する必要があります。

そういった意味で、冒頭のgit初心者にはサブモジュールにつながります。サブツリーマージと比較してかんたんに作業可能な状態になりますし、その後の運用もかんたんな気がします。(git初心者の定義にもよりますが、たとえばGUIツールではサブツリーマージできないのでは?)

特にチームでgitリポジトリとして運用していくとき、全メンバがgitに十分に習熟していない限りは、サブモジュールを選択した方が無難でしょう。


じゃあサブツリーマージのメリットは?

サブツリーマージがもたらしてくれるのは、より柔軟なプロジェクト運用だと思います。たとえば、ひとつのプロジェクトで複数のモジュールを開発していくときなど。それぞれのモジュールをメインリポジトリ内の独立したブランチあるいは別のリポジトリとしておいて、メインリポジトリのメインブランチでサブツリーマージでとりこみます。

# メインブランチのイメージ

% tree
/main-repository/
|-- api-server # リモートリポジトリserverに対応
`-- client-app # リモートリポジトリclientに対応

% git remote
server
client

% git checkout server
# メインブランチでいうapi-server以下が展開される

% git checkout client
# メインブランチでいうclient-app以下が展開される

メインブランチはなくても良さそうですが、モジュール間の同期をとる場合に役立ちます。具体的には、APIサーバのモジュールとクライアントアプリのモジュールを同時開発していく場合など、基本的にはそれぞれ独立して開発していけますが、API仕様変更などで同期をとらざるを得ないタイミングが少なからず訪れるでしょう。

通常は各モジュールのブランチに対してコミットしていき、それを任意のタイミングでサブツリーマージでメインブランチに取り込みます。また、モジュール間で同期する必要がある場合はメインブランチにコミットして各モジュールに対してサブツリーマージで反映するという手も考えられます。

モジュールをリポジトリorブランチで独立させておくことによるメリットは、デプロイ時などに各モジュールのみを取得可能となるところです。gitではリポジトリのサブディレクトリのみを取得することができません。そこでリポジトリorブランチを独立させておけば、それぞれのモジュールのみを取得できます。APIサーバのコード群のみを取得してデプロイ、あるいはクライアントアプリのコード群のみを取得してビルドといったことが可能となります。

柔軟な運用が可能になる反面、複数ブランチとその関係性を管理していく煩雑さは無視できません。ブランチ間のサブツリーマージはgit-hookである程度自動的に処理できるよう構築するとしても、サブモジュールと比較して導入の敷居は高いように感じます。

プロジェクトの要件と、それに対してどういったリポジトリ運用が理想的か。サブモジュールだけではなく、サブツリーマージもひとつの案として検討してみる価値はあるかもしれません。