2014年5月10日土曜日

Gitでsubtree mergeによるライブラリ管理

※ これから新規に外部ライブラリの管理をする場合は、 Git のバージョンが1.7.11以降である必要がありますが、 Git subtree のほうがお勧めです。

背景


Git では外部ライブラリ(これまた Git で管理されている)を取り込む仕組みとして、 submodule が用意されています。たんなるソースのスナップショットをコピーして置いておくのに比べると、外部ライブラリのバージョンアップに追随できるという利点があります。しかし、 submodule は運用上の問題をいくつか抱えています。

1. git clone した直後に git submodule update をしなくてはならない
 リポジトリをクローンしてすぐにコードをいじり始めたくても、余計なひと手間が必要になります。
 GUI クライアントの中には、この作業を忘れないように通知してくれるものもあります。

2. 外部ライブラリの URL が固定化してしまう
 .gitmodules には submodule の URL が埋め込まれており、ライブラリ側の URL の変更があると追随できなくなります。(正確には、追随するために一つコミットが必要になります。) Web に公開されているオープンソースライブラリの多くは URL を頻繁に変えたりはしませんが、内作のライブラリでは構成変更での取回しの悪さは意外と煩わしいものです。

3. とにかく、なんか仕組みがややこしい
 submodule の目的は外部ライブラリの更新をトレースすることであって、プロジェクト本体のソースをいじる人の多くにとっては関心のないものです。このためにユーザー全員に新しい仕組みの使い方を意識してもらうのはオーバーヘッドが大きすぎる気がします。


ちなみに、 Subversion にも svn:externals プロパティという仕組みがありましたが、 Git submoduleと同じような問題があり、使うのがためらわれました。

他の分散型 VCS もいろいろ探索しましたが、結局のところ、 Git の subtree merge ほど素直にやりたいことができる仕組みはありませんでした。




そもそも Git のマージとは


Git は個々のコミットのスナップショットだけを記憶し、コミット間の差分に関する情報は一切記憶していません( pack ファイルの差分コーディングは別として)。マージコミットというのは、親コミットが2つ以上あるコミットというだけで、それ以上の特別な情報は何も持っていません。

つまり、任意の2コミットをマージして、任意の内容に書き換えたマージコミットは、データ構造的に何の問題もなく作れるわけです。

これを利用したものの一つが subtree merge といわれるマージです。


Subtree merge

Subtree merge とはマージの strategy (戦略)の一つで、ディレクトリ構成が相対的に異なるブランチをマージすることができます。

たとえば、下記のようなディレクトリ構成の master ブランチがあったとして

bin/
data/
lib/include/
lib/src/

それとは別に library ブランチが下記のような構成であるとすると

include/
src/

master ブランチにて git merge -s subtree library とコマンドを打つと、 library ブランチの include/ と src/ の変更内容が、 master ブランチの lib/include/ と lib/src/ にマージされます。このディレクトリの相対関係は Git がヒューリスティックによって推測するため、コマンドで与えてやる必要はありません。

これで、外部ライブラリをリモートとして登録しておけば、 fetch してから subtree merge という2コマンドでとても直観的にライブラリ側のバージョンアップに追随できるわけです。

また、外部ライブラリのリモートは、普通のリモートと変わらないので、 URL の変更も自由にできます。最初は公開リポジトリを http で取得していたが、ライブラリの内容にも手を加えたいのでローカルにコピーして相対パスでのアクセスに変更するといったことも簡単にできます。

さらに、 subtree merge は単なるマージなので、何も知らずにクローンした人が submodule update などしなくてもすぐに使い始められます。


Subtree merge のための初期化


Subtree merge はヒューリスティックにより相対パスを推測するため、初めてライブラリをプロジェクトに取り込むときには、どこのサブフォルダに置くのか指示できません。ここでは、まず最初の取り込みをどうするか、上記の lib サブディレクトリの例で説明します。

Git 1.7.11 以降であれば、下記 1 行で可能です。ここで、 lib-url はリモートの名前です。最後の master は、自分自身のリポジトリのブランチ名ではなく、 lib-url から取り込みたいブランチの名前です。

git subtree add --prefix=lib/ lib-url master

それより古いバージョンであれば、以下の手順に従う必要があります。

master が自分のプロジェクト、 library が外部ライブラリの最新ブランチとして進めます。

まず、強制的にマージコミットを作ってしまいます。 -s ours オプションにより、 library 側の変更は取り込まず、自分だけのファイル構成で作ります。

git merge -s ours library

次に、ライブラリのディレクトリツリーを lib サブディレクトリに取り込んだ形にステージします。

git read-tree --prefix=lib/ -u library

最後に、先ほど作ったマージコミットを改訂してサブツリーに取り込んだ形のマージコミットにします。

git commit --am

これで、以降の subtree merge が正しくヒューリステックを働かせるようになります。


その他


実は、初期化のところでなにげなく実行したマージは共通祖先を持たないマージです。歴史上、何の交わりもないブランチがマージできてしまうのも、 Git の柔軟性を示しているといえるでしょう。(実は Mercurial でもできます。)

余談ですが、共通祖先を持たないマージによる外部プロジェクトの取り込みは、 Git 自身のリポジトリでも使われた実績があります。 gitk というリビジョングラフ表示プログラムと、 git-gui というコミット画面用の GUI がそれです。これは外部ライブラリとして取り込んだわけではありませんが、履歴を失わずにプロジェクトの取り込みを行う Git の適応力を、 Git 開発チームも余すところなく使っているということになります。


0 件のコメント:

コメントを投稿