彷徨えるフジワラ

年がら年中さまよってます

Mercurial でのサブリポジトリの利用

ここ暫く、#TokyoMercurial (+ 懇親会)の席や twitter上などで、サブリポジトリ(subrepo)の利用に関する質問等が多かったのですが、口頭説明や、twitter の文字制限内では、なかなか正確に伝えるのが難しかったので、ブログエントリとしてまとめてみました。

っつーか、ある程度書きあがってから、僕の勘違いに突っ込みが入って、慌てて加筆修正しました(笑)。指摘感謝です! > @yujauja 氏

サブリポジトリの利用開始

Mercurial のサブリポジトリ機能とは:

Mercurial リポジトリを親に、外部のリポジトリやプロジェクトを入れ子にし、 コマンドの実行の際に、 それら一連のリポジトリに対して処理を行えるようにします。
〜 "hg help subrepos" より

というものです。

サブリポジトリの追加手順は、hg help subrepos (日本語訳の表示には言語設定が必要!) を見て頂ければ事足りると思いますが、一応簡単な手順を例示します。

● サブリポジトリの作成

例えば『自前のプロダクトに Mercurial 自体を組み込みたい』と考えたとしましょう。

自前ソースツリーの external/hg 配下に Mercurial のソースを配置しする場合は:

$ mkdir external
$ cd external
$ hg clone -U https://www.mercurial-scm.org/repo/hg/
複製先ディレクトリ: hg
全リビジョンを取得中
リビジョンを追加中
マニフェストを追加中
ファイルの変更を追加中
16779 個のリビジョン(32739 の変更を 2118 ファイルに適用)を追加
$

次に external/hg 配下の作業領域を、適切な版の状態に更新します。例えば、自前プロダクトの現行版が Mercurial の 2.1 に対応している場合は、上記に引き続き以下の操作を行います:

$ cd hg
$ hg update 2.1
ファイルの更新数 962、 マージ数 0、 削除数 0、 衝突未解消数 0
$
● サブリポジトリの追加

リポジトリの作業領域配下に、別なリポジトリの複製を配置しただけでは、単なる「入れ子リポジトリ」に過ぎません。

どの入れ子リポジトリをサブリポジトリとみなすのかを、親リポジトリに対して知らせる必要があります。

リポジトリの作業領域ルート直下に、以下の様な内容を持つ .hgsub ファイルを作成してください。

external/hg = https://www.mercurial-scm.org/repo/hg/

"external/hg" は作業領域ルートに対する対象サブリポジトリ相対パス、URL はサブリポジトリの履歴取得における連携先 URL です。

連携先 URL の記述形式の選択に関しては、別途詳細を後述します。

● 関連付けの記録

最後に、親リポジトリの内容が、各サブリポジトリの内容と整合性が取れている (例:ビルド/テストが通る) ことを確認したならば、親リポジトリ側で commit を実施します。

サブリポジトリを使用した commit 後に、親リポジトリ.hgsubstate (自動的に作成/更新されます)は以下のようになっている筈です。

$ cat .hgsubstate
2aa5b51f310fb3befd26bed99c02267f5c12c734 external/hg
$

2aa5b51f310fb3befd26bed99c02267f5c12c734 は、Mercurial 2.1 版 (= 親リポジトリでの commit 時における external/hg の作業領域のリビジョン) に相当するリビジョンの完全なハッシュ値です。

これを見れば、hg help subrepos の訳注で:

サブリポジトリに関する「構成管理」は、あくまで「親リポジトリの各リビジョンが、サブリポジトリのどのリビジョンと、どう対応するのか?」 という対応付け情報のみです。

と書いた理由がわかってもらえるのではないでしょうか?

これ以後の、親リポジトリでの各種操作における、管理下のサブリポジトリへの影響に関しては、hg help subrepos の『Mercurial コマンドとの連携』に記載がありますので、そちらを参照してください。

サブリポジトリの clone 契機

公開されているリポジトリが、配下にサブリポジトリを持つ (= .hgsub ファイルが履歴管理されている) 親リポジトリである場合、そこから単に履歴を取り込んだだけでは、管理下にあるサブリポジトリの履歴情報取り込みは行われません。サブリポジトリの履歴取り込みは、以下の契機で発生します。

  1. リポジトリの作業領域を update (※ clone 契機での update 含む)
  2. 当該リビジョンに .hgsub が無ければ終了
  3. 作業領域配下に、各サブリポジトリの作業領域を確保 (= ディレクトリ作成+初期化の保証)
  4. .hgsubstate の記録に従って、各サブリポジトリの作業領域を update
  5. 該当リビジョンが手元に無い場合は、連携先から履歴情報を取り込み

"-U" (※ 作業領域の update 抑止) 付きで親リポジトリを clone した場合、サブリポジトリに関する履歴が一切読み込まれない一方で、従来のリポジトリ構成では発生しなかった、hg update 契機でのリポジトリ連携が発生するわけです。

この辺は、largefiles エクステンション有効時の update 契機連携に似ていますね。

連携先 URL 指定のパス形式

.hgsub 中の連携先 URL としてのパス指定では、単純なパス表記が推奨されています。

child = child

このような単純表記では、例えば親リポジトリ parent-URL から clone した、cloned-parent-URL 配下で hg update を実施した場合、サブリポジトリ child の履歴情報取得は、親リポジトリの default 連携先 (= clone 元) である parent-URL に、.hgsub でのパス指定 child を足した、parent-URL/child との連携になります ("--config paths.default=連携先URL" 指定で変更可能)。

つまり、親リポジトリの clone 元である parent-URL の、作業領域配下にある child リポジトリをそのまま使用するわけです。

".." を使用した相対パスを記述することも可能で:

child = ../child

といった記述の場合は "parent-URL/../child" が child の連携先リポジトリになります。

このような ".." を使用した相対形式の場合:

中央に共有リポジトリがあって、全ての利用者は常にそこから直接 clone する

という「スター型」の履歴伝播経路での運用であれば、ほぼ問題なく運用できますが:

共有リポジトリ(parent-URL) ⇒ 作業リポジトリ A(A-parent-URL) ⇒ 作業リポジトリ B ⇒ ...

といった「ディジーチェーン型」の履歴伝播経路での運用の場合、作業リポジトリ B での update は、A-parent-URL/../child の存在を必要とします。

この条件を満たそうとすると、A-parent-URL においては、(1) A-parent-URL 配下の child (A-parent-URL/child) 以外に、(2) A-parent-URL/../child も用意しなくてはなりません。

(1) の child に関しては、A-parent-URL での update 契機で適宜 clone 元 (= parent-URL/child) から履歴が取り込まれますが、作業リポジトリ B での update に必要となる (2) の child は、別途手動での更新履歴取り込みが必要であることから、運用の手間を考えると、この形式でのパス記述にはあまりメリットが無いと言えます。

なお、通常は「スター型」で履歴情報を伝播していても、実験的な作業を行うためにリポジトリを複製する際に、(1) ある程度リポジトリサイズが大きいとか、(2) ネットワーク帯域が狭い場合などには、共有リポジトリからの直接 clone ではなく、手元のリポジトリ (= A-parent-URL) から複製する、といったケースが多々あります。

この場合も「ディジーチェーン型」の履歴伝播になりますから、「ディジーチェーン型」伝播は決して珍しい形態ではありません

この辺が、本家の Wiki で "will not generally allow clones to be cloned" といわれる所以ですね。

連携先指定に 'tirvial' subrepo paths が possible でないケース

さて、先に参照した本家 Wiki でのサブリポジトリ使用における推奨では、『Use 'trivial' subrepo paths where possible』(可能な限り"普通の"パスを記述せよ)とあるのですが、.hgsub ファイルの運用方針を決める際には、"普通の"パスだとまずいケースに関して理解しておく必要があるでしょう。

先述したように、child = child のような "普通の" パス記述の場合、親リポジトリの default 連携先が parent-URL であれば、サブリポジトリ child の連携先は parent-URL/child とみなされ、parent-URL の作業領域中の child をそのまま使用する形態となります。

このケースでは『parent-URL の作業領域には、必ず child が存在している』ことが想定されているのですが、そうなると、一般的な共有リポジトリの『履歴情報の格納のみで、作業領域は持たない』形式の場合、連携が上手く行かないことを意味します。

もう少し踏み込むと、親リポジトリ中の各リビジョンで、サブリポジトリの増減/移動が発生する場合、parent-URL の作業領域配下の構成には、相応の配慮を払う必要が出てきます: 増減はともかく、移動は出来れば勘弁して欲しいし、多分無いとは思いますけどね(笑) > サブリポジトリ

あるいは、サブリポジトリ child の元ネタが、既に運用が開始されている公開リポジトリである場合には、これを新規リポジトリ parent-URL の配下に移動するコストが高い (= 既存の child の複製先で連携先書き換えが必要) ので、改めて parent-URL/child 構成にするのは難しいと言えます。

parent-URL と child-URL が共に自前の共有リポジトリである場合は、child-URL から parent-URL 配下に clone して、以後の child-URL から parent-URL/child への更新履歴取り込みはフックで自動化、という手も使えますが、冒頭の例での『外部プロダクト(= Mercurial)のリポジトリをサブリポジトリ化』するようなケースも含めて、parent-URL/child 化が必ずしも可能とは限りません: child-URL からの取り込み頻度が低くても良いのであれば、parent-URL 側主導での定期取り込みでも許容範囲かな?

また、共有リポジトリの公開に bitbucket のようなホスティングサービスを使用する場合も、parent-URL/child のような形式は取れません。

このような、.hgsub での連携先指定において 'tirvial' subrepo paths が possible ではないケースでは、ssh/http(s) スキーマ形式等で、グローバルに特定可能な連携先を記述する必要があります。

あるいは『とにかく 'tirvial' subrepo paths で書いておいて、(後述する) [subpaths] 設定で書き換える』という手もありですが、どっちが運用上楽なんでしょうね? 多分この方法で、一番面倒なのは、ディジーチェーン型伝播で複製するケース (= 連携先 URL が多様化してしまう) なんだろうけど、『どうせ複製先の default パス設定は、共有リポジトリに向くように書き換えるんでしょ?』という前提なら、『とにかく 'tirvial' subrepo paths』もありなのかな?([subpaths] での書き換えルールが単純化できる)

※ 注意: 別エントリ「リポジトリホスティング使用時の .hgsub 記述Comments」も参照してみてください。

TortoiseHg みたいな、連携先リポジトリをメニューで選択する GUI 経由での使用では、『default パス設定を書き換える』という操作が馴染まない気もするけど、そういった利用形態では、そもそも『ディジーチェーン型伝播でリポジトリを複製』的なケースは少ないかも。

まぁ、この辺は、サブリポジトリの構成、利用インフラの形態、メンバーの習熟度、典型的な利用シーンを想定した上で、エイヤ!で決めてしまうしかないでしょうね。

自前のリポジトリに閉じている分には、'tirvial' subrepo paths は概ね possible と考えて良いと思います。

ちなみに、親リポジトリでの update の一環でサブリポジトリが履歴取り込みを行う場合、サブリポジトリ.hg/hgrc に記載された default 連携先 URL は (少なくとも 2.2.1 版では) 参照されません。この設定を有効にしたリポジトリ連携を行うには、当該サブリポジトリに移動して、そこで改めて連携コマンド(push/pull 等)を実施する必要があります。

この設定に効果があれば、後述する [subpaths] の必要性も幾分低下する気もするのですが、まぁ、一貫性とかを考えた場合は、却って面倒ごとが増えるのかもしれません > .hg/hgrc@サブリポジトリ

subpaths による連携先の書き換え

例えば:

  • 『'tirvial' subrepo paths が possible ではない』ケースにおいて、'tirvial' subrepo paths で運用を開始してしまった
  • ".." を使用した相対パスで運用を開始してしまった

上記の様なケースで、サブリポジトリにおける履歴の取り込みが上手く行かなくなった場合、どうすれば良いでしょうか?

このようなケースに対応するのが、設定ファイル中での [subpaths] セクション記述です。

例えば、プロジェクトの共有リポジトリが作業領域を持たないため、サブリポジトリの連携先を parent-URL/child ではなく、全然別の child-URL にしたい、といったケースでは、親リポジトリ側の設定ファイル (.hg/hgrc) において以下の様な設定を記述します。

[subpaths]
parent-URL/child = child-URL

この記述により、parent-URL/child に対するサブリポジトリのアクセスは、全て child-URL へと振り替えられます。

この連携先書き換え機能は、上記以外にも、以下の様な状況で威力を発揮します。

1番目のケースは、.hgsub 中の連携先を直接書き換えれば対処可能な気がするかもしれませんが、書き換え実施以前のリビジョンの .hgsub には効果がありませんから、親リポジトリの複製において、古いリビジョンへの update を保証するためには、[subpaths] のような機能が必要なわけです。

サブリポジトリの応用

● 部分コピー (partial copy) の実現

以下の様な質問を時々耳にしますが:

特定のディレクトリ配下の履歴だけを clone できませんか?

基本的には Mercurial では部分コピーはできません。

しかし、非常に大きなリポジトリの場合、全体の clone が重い、という事情も理解できます。

もしも、このような状況が多々生じる可能性があるならば、サブリポジトリ機能を使用して、適切な粒度のリポジトリに分割することを考えてみるべきでしょう。

ひょっとしたら:

既にリポジトリの運用を開始しているので、サブリポジトリ化なんてできない!

と思われるかもしれません。

しかし、既に運用を開始しているリポジトリでも、以下の様な手順で分割することが可能です。

  1. 分割対象の範囲を決める
  2. 分割対象の範囲を include する設定を書いた filemap ファイルを作成
  3. filemap を使った hg convert で、分割対象範囲に限定した MercurialMercurial 変換を実施
  4. 分割対象がなくなるまで繰り返す
  5. リポジトリを作成
  6. 分割対象リポジトリ群を、サブリポジトリとして親リポジトリ配下に展開
  7. 適当なタグ単位で、親リポジトリとサブリポジトリの対応付けを(親リポジトリ側で)記録
  8. リポジトリ/サブリポジトリを共有リポジトリとして公開

このようにして分割されたリポジトリは、分割されたリポジトリ単位で clone 等を実施できるようになります。

広く世間に周知されたプロジェクトの場合、途中でリポジトリ分割して『親リポジトリ/サブリポジトリを共有リポジトリとして公開』するのは、ちょっと難儀ではありますが、絶対数で見れば、むしろそういったプロジェクトは少数派ではないかと思います。

Subversion との機能分担

MS-Office 系ファイルを履歴管理している(or 履歴管理したい)人などからは、以下のような質問を度々受けます。

DVCS はファイルのロックは取れないのですか?

答えは勿論 No なのですが、バイナリファイルの改変の場合、改変結果をマージするよりも、『ロック獲得 ⇒ 変更/記録 ⇒ ロック解放』の方が簡単、という心理も理解できます。

非同梱のエクステンションですが、LockExtension (hglock) を利用することで、ロック機能を実現することが可能な模様です。

hglock の用法/原理に関しては、こちらのエントリを参照してください。

Mercurial のサブリポジトリ機能は、連携先リポジトリSubversion 形式も利用できますすので:

といった運用も可能です。

先述したように、親リポジトリ側で記録されるのは「リポジトリ間でのリビジョンの関連付け」情報のみで、サブリポジトリ側の個々のファイル名やファイル内容は記録されません。

そのため、日本語ファイル名のファイルは UTF-8 ベースの Subversion サブリポジトリ側で管理、という手で日本語ファイル名の問題を回避する手もあります。