Git Submodule の代替: Git Subtree

インターネットには、Git submodule を使っては いけない という記事が飛び交っています。私はこれらの記事が言うほどひどいものとは思っていませんが、そういった主張が大方正しいことは認めます。以前の投稿でも説明しましたが、submodule は利用価値のあるユースケースは少なく、逆にいくつもの欠点があります。

では、これに代わるものはあるのでしょうか? 答えは「ある」です。Git の利用は続けつつ、プロジェクトにおけるソフトウェアの依存関係を追跡することができるツールが (少なくとも) 二つあります :

この記事では、git subtree に注目し、完全とまではいえないもののそれが git submodule の問題を解決するものであることを説明しようと思います。

実例としていつもの私のユースケースを取り上げます。自分の dotfiles において使用する vim プラグインを保存し、かつ常に最新のものに更新するために私がとっている方法を説明しましょう。

submodule ではなく subtree を使う理由

subtree の方が使いやすいと誰もが思う理由がいくつかあります:

  • ワークフローがシンプルであり、管理が容易である。
  • git の古いバージョンもサポートしている (v1.5.2 より古いバージョンにも対応している)。
  • 親プロジェクトを clone した直後からサブプロジェクトのコードが利用可能である。
  • subtree の場合、依存関係を管理するために subtree を利用していることをそのリポジトリのユーザーが関知する必要はなく、ユーザーに余計なタスクを発生させない。
  • submodules の場合と異なり、subtree ではメタデータファイル (即ち、.gitmodule) を新に作成することはない。
  • 依存関係にあるモジュールの内容を変更する場合でも、そのリポジトリのコピーを別に持つ必要がない。

これには次のような欠点もあるが、それらは受け入れ可能だと思います:

  • 利用者には、新たなマージ戦略 (即ち、subtree) に関する知識が必要である。
  • サブプロジェクトの upstream にコントリビューションを戻す手順はやや複雑になる。
  • コミットの際に親プロジェクトとサブプロジェクトのコードの入り混じりを防止する責任は自分自身にある。

git subtree の使用方法

git subtree は、2012年5月以降にリリースされた git (1.7.11 以降) において利用可能です。OSX 上で homebrew を使用してインストールした場合は subtree も適切に組み込まれているが、プラットフォームによってはインストールガイドに従った作業が必要です。

典型的な使用例として、git subtree を利用して vim プラグインのトラッキングを行う場合を説明しましょう。

リモートトラッキング不要の簡便な方法

ワンライナーを二・三行カット・ペーストする程度の作業の場合、この節の内容が参考になると思われます。

最初に、指定した prefix フォルダにおいて subtree を定義します:


1
git subtree add --prefix .vim/bundle/tpope-vim-surround https://bitbucket.org/vim-plugins-mirror/vim-surround.git master --squash

(通常の慣行ではサブプロジェクトの履歴のすべてを親リポジトリに保存することはないが、保存したい場合は –squash フラグを削除すればよいでしょう)。

上のコマンドを実行すると次のような出力が得られます:


1
2
3
4
5
6
7
8
9
10
git fetch https://bitbucket.org/vim-plugins-mirror/vim-surround.git master
warning: no common commits
remote: Counting objects: 338, done.
remote: Compressing objects: 100% (145/145), done.
remote: Total 338 (delta 101), reused 323 (delta 89)
Receiving objects: 100% (338/338), 71.46 KiB, done.
Resolving deltas: 100% (101/101), done.
From https://bitbucket.org/vim-plugins-mirror/vim-surround.git
* branch            master     -} FETCH_HEAD
Added dir '.vim/bundle/tpope-vim-surround'

見ると分かるように、ここでは vim-surround の全履歴をひとつにまとめた merge commit の記録を行っています:


1
2
1bda0bd [3 minutes ago] (HEAD, stree) Merge commit 'ca1f4da9f0b93346bba9a430c889a95f75dc0a83' as '.vim/bundle/tpope-vim-surround' [Nicola Paolucci] ca1f4da [3 minutes ago] Squashed '.vim/bundle/tpope-vim-surround/' content from commit 02199ea [Nicola Paolucci]

この後で upstream リポジトリからプラグインのアップデートを行う場合は、単に subtree pull を行えばよいでしょう:


1
git subtree pull --prefix .vim/bundle/tpope-vim-surround https://bitbucket.org/vim-plugins-mirror/vim-surround.git master --squash

この方法は簡便ではあるが、ややコマンドが長く覚えにくいのが欠点です。サブプロジェクトをリモートとして作成することにより、コマンドを短くすることができます。

サブプロジェクトをリモートとして作成する

サブプロジェクトをリモートとして作成することにより、短い形式でそれを参照することができます:


1
git remote add -f tpope-vim-surround https://bitbucket.org/vim-plugins-mirror/vim-surround.git

前と同様にここでサブツリーを定義するが、リモートを参照するため短い形式が利用できます:


1
git subtree add --prefix .vim/bundle/tpope-vim-surround tpope-vim-surround master --squash

後にサブプロジェクトをアップデートする場合のコマンドは次のようになります:


1
2
git fetch tpope-vim-surround master
git subtree pull --prefix .vim/bundle/tpope-vim-surround tpope-vim-surround master --squash

コントリビューションを upstream に戻す

ローカルの作業ディレクトリ内ではサブプロジェクトの修正は自由です。

ローカルのコントリビューションを upstream プロジェクトに戻す場合は、そのプロジェクトをフォークして別のリモートとして作成する必要があります:


1
git remote add durdn-vim-surround ssh://git@bitbucket.org/durdn/vim-surround.git

ここで、次のように subtree push コマンドを実行することができます:


1
2
3
4
5
6
7
8
9
10
git subtree push --prefix=.vim/bundle/tpope-vim-surround/ durdn-vim-surround master

git push using:  durdn-vim-surround master
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 308 bytes, done.
Total 3 (delta 2), reused 0 (delta 0)
To ssh://git@bitbucket.org/durdn/vim-surround.git
  02199ea..dcacd4b  dcacd4b21fe51c9b5824370b3b224c440b3470cb -} master

これが完了すると、パッケージ開発全体のメインテナーに対して pull-request を送る準備が整います。

subtree コマンドを使用しない方法

git subtree は、 subtree マージ戦略とは異なるものです。何らかの理由で git subtree コマンドが利用できない場合であっても、マージ戦略を利用することは可能です。ここではその手順を説明します:

依存関係にあるプラグインを通常の git remote として作成します:


1
git remote add -f tpope-vim-surround https://bitbucket.org/vim-plugins-mirror/vim-surround.git

依存関係にあるプラグインの内容をリポジトリに読み取る前に、プラグインのその時点までの全履歴を追跡できるようにマージを行ってそれを記録しておくことが重要です:


1
git merge -s ours --no-commit tpope-vim-surround/master

これにより次のような出力が得られます:


1
Automatic merge went well; stopped before committing as requested

ここでプラグインリポジトリ内のツリーオブジェクトの最新の内容を作業ディレクトリに読み取ることが可能になり、それをコミットする準備が整います:


1
git read-tree --prefix=.vim/bundle/tpope-vim-surround/ -u tpope-vim-surround/master

ここでコミットを行います (このコミットは読み取るツリーの全履歴を保存可能なマージコミットとするべきです):


1
2
3
git ci -m"[subtree] adding tpope-vim-surround"

[stree 779b094] [subtree] adding tpope-vim-surround

プロジェクトのアップデートを行う場合は、subtree マージ戦略を使用して pull を実行すればよいでしょう:


1
git pull -s subtree tpope-vim-surround master

結論

私はしばらくの間 submodule も使ってみましたが、subtree では submodule で発生するほとんどの問題が解決されており、従って git subtree の方を格段に高く評価しています。いつものことですが、Git の機能はどれもそれを活用するにはラーニングカーブに沿った経験が必要なのです。

Git の素晴らしさをもっと理解したい方は、@durdn で私をフォローしたり、優秀な開発チームである @AtlDevtools をフォローしてください。

*本ブログは Atlassian Blogs の翻訳です。本文中の日時などは投稿当時のものですのでご了承ください。
*原文 : 2013 年 3 月 16 日 "Alternatives To Git Submodule: Git Subtree"