彷徨えるフジワラ

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

bundle マニアックス

このエントリは、Mercurial Advent Calendar 2013 の23日目です。

本エントリでは、rebase や MQ、同梱版の shelve 等を使わない場合は、普段あまり目にする機会がない、日陰者の bundle ファイルに関して、徹底的に説明しようと思います。

hg diffhg export 等が出力する差分形式のような可読性が無いこともあり、一般的な利用者が明示的に bundle ファイルを生成する機会は少ないとは思いますが、知っておくと何かと便利です(特に、履歴改変等でやらかしてしまった場合の復旧とか)。

リポジトリ」としての bundle ファイル指定

Mercurial では、hg incominghg pull 等における連携先リポジトリとして、bundle 形式ファイルを指定することができます。読み出し専用になりますので、hg push での連携対象にはなりません。

bundle 形式ファイル bundlefileリポジトリとして指定する場合の記述形式は、以下のような仕様になっています。

[bundle:[mainreporoot+]]bundlefile

完全な表記 bundle:mainreporoot+bundlefile でのリポジトリ指定は、以下の2つの履歴を併せ持つ「擬似的なリポジトリ」とみなされます。

  • ローカルリポジトリ mainreporoot 中の履歴
  • bundle ファイル bundlefile 中の履歴

mainreporoot 部分を省略した bundle:bundlefile 表記の場合、mainreporoot は以下の優先順位で決定されます。

  1. -R オプションで明示的に指定されたローカルリポジトリ
  2. コマンド実行時のカレントディレクトリが含まれるローカルリポジトリ
  3. 一時領域に作成した仮のローカルリポジトリ(=空の履歴)

操作対象リポジトリを指定する -R オプションに対して、mainreporoot 部分を省略した bundle: 形式を指定した場合は、(2) 以降が評価されます。

リポジトリとして bundle ファイルを指定する場合は、スキーマ指定部分の bundle: も省略可能です。これは、スキーマ指定が無いリポジトリ URL が指定された場合に、下記の要領でリポジトリ形式を判断しているためです。

  • 指定されたパスがファイルの場合は bundle: 形式とみなす
  • それ以外の場合は、ローカルリポジトリfile: 形式)とみなす

ちなみに、union リポジトリを併用することで、複数の bundle ファイルの履歴情報を重ね合わせることもできる……と言いたいところですが、union リポジトリで結合可能なリポジトリは、共にローカルリポジトリでなければなりません。残念ながら、union:bundle1+bundle2 のような指定はできないのです。

まぁ、bundle: 形式自体が「ローカルリポジトリと bundle ファイルの union」と考えれば、これ以上屋上屋を重ねられないのも止む無しといったところでしょうか。

キャッシュとしての bundle ファイル

例えば、他のリポジトリから履歴の取り込みを行う際に、実際に hg pull で履歴を取り込む前に、取り込み対象のリビジョンを hg incoming で確認したいかもしれません。

※ 取り込み対象確認のために、連携先リポジトリへの
   問い合わせい(ネットワーク経由の通信等)が発生
$ hg incoming 連携先URL

※ 再度連携先への問い合わせが発生
$ hg pull 連携先URL

連携先リポジトリへのアクセスが、http/httpsssh を経由する場合、上記のようなコマンド実行では、hg incominghg pull の両方で、ネットワーク経由での通信が発生してしまいます。

高速通信を湯水のように使える環境であれば問題ないのですが、連携先リポジトリへの通信帯域が広くない場合、このようなコマンド実行の応答性はあまりよろしくありません。モバイル回線のように、従量制課金や上限制約付きな通信環境などでは、そもそも通信自体を回避したいこともあるでしょう。

このような場合は、--bundle オプションを使って、hg incoming に「取り込み対象リビジョン」の情報を格納した bundle ファイルの生成を指示しましょう。

※ 連携先リポジトリへの問い合わせが発生
$ hg incoming --bundle incoming.hg 連携先URL

※ ローカルファイル incoming.hg へのアクセスのみ
$ hg pull incoming.hg

hg pull での履歴取り込みの際に、連携先URLの代わりに、取り込み対象リビジョンを格納した bundle ファイル(上記例では incoming.hg)を使うことで、連携先リポジトリへの問い合わせを抑止できます。

なお、bundle ファイルからの取り込みは hg unbundle でも可能ですが、hg unbundle が常に bundle ファイル中の全ての履歴を取り込むのに対して、hg pull-r 等のオプション指定による選択的な取り込みが可能です。

さて、hg incoming コマンドが --bundle オプションを持つ一方で、hg outgoing コマンドには --bundle オプションがありません。その代わり、hg bundle コマンドに連携先URLを指定することで、連携先リポジトリに存在しないリビジョンを bundle ファイルに保存することができます。

$ hg bundle outgoing.hg 連携先URL

hg unbundle による bundle ファイルからの履歴取り込みが、全リビジョンの一括取り込みのみであるのに対して、hg bundle による bundle ファイル書き出しでは、-r 等のオプション指定により、書き出し対象リビジョンを指定できます。

もしも、hg pullhg push 実施の際に:

  • 事前に hg incominghg outgoing で対象リビジョンを確認することが多い
  • 一度の pull/push に対して、incoming/outoging での確認を複数回実施してしまう

上記のような心配性なルーチンワークを多用している方も、対象リビジョンの bundle ファイルへの保存がお勧めです。

対象リビジョンを保存した bundle ファイルを使って確認作業を行うことで、以後の連携先リポジトリへの問い合わせが不要になりますから、確認操作でのコマンド応答性が向上します。

TortoiseHg は上記のようななコスト削減を積極的に利用していて、履歴取り込み/反映の際には、対象リビジョンの一時保存用に、常に bundle ファイルを生成しています。

bundle 形式リポジトリ固有の revsets 述語

操作対象リポジトリとして bundle 形式が指定された場合、revsets 記述において bundle() 述語が使用できます。

$ hg -R bundlefile log -r "bundle()"

bundle() は、bundlefile で指定した bundle ファイルに含まれる履歴を列挙します。

上記の実行例の -R オプションによる対象リポジトリ指定では、ファイル指定による bundle: の省略以外に、暗黙の「コマンド実行時のカレントディレクトリが含まれるリポジトリ」選択による mainreporoot 指定の省略を使用しています。

リポジトリの作業領域の外で実行した場合、暗黙の mainreporoot 指定が機能しませんので注意してください。

bundle() には、「bundle 形式リポジトリに対してしか使えない」という制限はあるものの、それ以外は他の revsets 述語と同様に扱えます。andor を使った他の述語との組み合わせも可能です。

$ hg -R  bundlefile log -r "bundle() and branch('default')"

上記の実行例では、bundlefile ファイル中の default ブランチの履歴のみを表示します。

bundle ファイルの内容は:

$ hg incoming bundlefile

上記の方法でも確認可能ですが、hg incoming の利用は、後述する secret フェーズに絡む問題がありますので、極力 hg log -r "bundle()" の使用を習慣付けることをお勧めします。

secret フェーズ併用時の問題

先日、以下のようなツイートを見つけました。

上記ツイートを契機に、動作確認等を行った結果、この問題は以下の状況で発生していることがわかりました。

  1. 3つのリビジョン A、B、C を想定(A 〜 C が、そのまま親子関係に相当)
  2. リビジョン C の破棄+bundle生成
    「履歴を修正」⇒「除外(strip)」(hg strip 相当)等による履歴破棄の延長で bundle ファイルが生成
  3. リビジョン B のフェーズを secret 化
    あるいは、あらかじめ B 〜 C を secret 化した状態で C を破棄。現状、bundle ファイルにはフェーズ情報が保持されないので、secret 化の実施タイミングは重要ではありません。
  4. bundle からのリビジョン C の取り込み
    ここで TortoiseHg は「取り込めるチェンジセットはありません」扱い

つまり、TortoiseHg経由では「bundle ファイルからは、secret なリビジョンの子孫を取り込むことができない」ということになります。

ちなみに、コマンドライン経由での、hg -R bundlefile log -r "bundle()" による対象リビジョン確認や、hg unbundle bundlefile での取り込みは可能です。

上記のような問題に遭遇した場合、当面はコマンドラインからの上記コマンドの実行で対処してください。

以下、問題の原因について、踏み込んだ話を少々。

この問題は、bundle ファイルからの取り込みに先だって、TortoiseHg が hg incomoing による確認処理を行い、対象リビジョンが検出されない場合は hg unbundle を実装しない、という実装であることに原因があります。

hg incoming bundlefilehg -R bundlefile log -r "bundle()" は、基本的には同じリビジョンを表示するのですが、ここに secret フェーズが絡んでくると、少々面倒な話になります。

hg -R bundlefile log 実行における主リポジトリは、「カレントディレクトリが含まれるローカルリポジトリ」と「bundlefile」の内容を足し合わせたものになります。

リポジトリ中のリビジョンはフェーズに関わらず可視(visible)なので、hg log は secret フェーズリビジョンとその子孫(= bundlefile 中のリビジョン)を表示することができます。

その一方で、hg incoming bundlefile 実行は:

という2つのリポジトリ間での連携になります。

hg incoming での両者は、それぞれ独立したリポジトリとして扱われますから:

という、secret フェーズの基本的な制約が有効になります。

結果として、連携先リポジトリ中の secret フェーズリビジョンとその子孫は、主リポジトリからは見えないもの(invisible)として扱われます。つまり hg incoming での表示対象からは除外されるわけですね。

一度は Mercurial 本体側に修正案を送ったのですが、secret フェーズ周りの挙動の統一性の点から、取り込んでもらえそうにはありません(色々整理してみると、確かに無理のある修正でもありますし……)。

一応、TortoiseHg の方にもバグ報告しておきましたが、コミッタの西原さんと直接やりとりした範囲では「現状の実装が結構入り組んでいるので、近々の対応は難しそう」とのことでした。

というわけで、運用の際に以下の操作が含まれる場合は、十分ご注意ください(特に TortoiseHg 利用者)。

  • secret フェーズ化
  • bundle ファイルからのリビジョン復旧