彷徨えるフジワラ

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

the inside of largefiles extension

最新の情報に関しては、オンラインヘルプ "hg help largefiles"(ウェブからも参照可能)を参照してください。large 扱いのファイルの取得方法/契機周りが色々改善されています。

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

Mercurial の 2.0 版から、largefiles エクステンション (※ 以下 "largefiles") が標準同梱されるようになりました。

このエントリでは、largefiles の概要と併せて、このエクステンションがどのような仕組みで対象ファイルの構成管理を行っているのか、ちょっとだけ内側の話をしてみたいと思います。

ちなみに、largefiles は名前こそ『large』を冠していますが、ファイルサイズの大小での使い分け以外にも、『バイナリファイルか否か』で使い分ける運用の検討をお勧めします。

これは、対象ファイルの内容管理方式として、通常用いられる『差分記録 (+ 圧縮)』では無く、『ファイル内容の直接記録』が使用されることによって、バイナリ差分の処理における効率低下を回避できるためです。

largefiles の有効化

何は無くても、まずは largefiles を有効化する必要が有ります。

${HOME}/.hgrc%USERPROFILE%\Mercurial.ini などのユーザ毎設定に以下のような記述を追加しましょう。

[extensions]
hgext.largefiles =

hg showconfig extensions の出力に hgext.largefiles 行が含まれていれば、設定は完了です。
前にもどこかで同じ事を書いた気がしますが(笑)、経験上、extensions 末尾と largefiles 末尾の "s" は、どちらも付け忘れミスが結構な頻度で発生するので、注意してください。

TortoiseHg の GUI 経由であれば、「TortoiseHg Settings」⇒「〜のユーザ設定」で「エクステンション」を選択して、「largefiles」のチェックボックスをチェックします。

なお、他のリポジトリとの連携を行う場合、連携先リポジトリでも largefiles が有効化されていることが必須となります。

共有サーバ等で古い版の Mercurial を使用しているような場合は、Mercurial 自身を 2.0 以降の版へ更新する事が必要となりますので注意してください。

但し、手元のリポジトリで largefiles が有効となっていても、largefiles によって管理されるファイルが無いリポジトリに関しては、連携先リポジトリにおいて largefiles が有効になっている必要はありません。

largefiles 管理の開始

largefiles が有効になっている場合、以下の条件に合致する新規追加ファイルは、largefiles による管理下に置かれます。

  • --large オプション付きで hg add されたファイル
  • 既に largefiles 管理下のファイルがあるリポジトリにおいて、[largefiles] patterns 設定で列挙されたパターンに合致するファイル
  • hg add--lfsize オプションで指定された値 (単位: MB) 以上のサイズのファイル
  • 既に largefiles 管理下のファイルがあるリポジトリにおいて、[largefiles] minsize 設定値 (単位: MB, デフォルト値: 10) 以上のサイズのファイル

既に largefiles 管理下のファイルがあるリポジトリでのみ有効、という微妙な縛りがあるため、少々わかりづらいかもしれません。これに関しては改善案を提案中です。

なお、既に履歴管理対象としてコミット済みのファイルは、largefiles 管理下に移行する事ができません。

hg lfconvert を使用することで、ファイルサイズ/ファイル名パターン条件に合致したファイルを largefiles 管理下に置くように、既存リポジトリを変換することが可能ですが、変換後のリポジトリは変換元リポジトリとは別物になってしまいます (各リビジョンのハッシュ ID が変わってしまうため)。

そのため、共有リポジトリを中心に複数のメンバー/作業領域が連携している場合は、これらを順次置き換えて行く必要が有ります。

また、同じ名前のファイルを別ブランチで、それぞれ通常ファイル/largefiles 管理ファイルとして追加した場合、これらをマージした際にはどちらを選ぶのかを選択する修正が 2.0.2 版から適用される予定です。

largefiles は修正/機能強化が活発に行われていますので、特にユーザが直接使用するものに関しては、極力最新版を使用することをお勧めします。

従来と異なる挙動

リポジトリ連携契機の違い

通常のファイルの場合、あるリポジトリにおいて構成管理対象として履歴が記録されているファイルの内容は、そのリポジトリに対する hg pull/clone 契機で、履歴情報と共に他のリポジトリへと伝播されます。大雑把には、『履歴情報』自体に『ファイルの (差分) 内容』が含まれるわけですから、『履歴情報と共に』と言うのも変なのですけどね(笑)。

この挙動は、リポジトリにおける『履歴情報の完結性 (complete-ness)』という点では望ましいのですが、画像/動画といった、通常のテキスト/ソースファイルと比較して大きなサイズのファイルを大量に抱え込んでいるようなリポジトリの場合、初期 clone や更新取り込みの pull の際に、膨大な時間を要してしまいます。

そういったファイルが頻繁に更新されている場合、最新版しか参照しないとしても、全ての『履歴情報』が転送されるわけですから、ネットワーク帯域/時間の無駄と感じても不思議ではありません。

一方で、largefiles 管理下にあるファイルの場合、以下のような非対称な挙動となります。

  • hg push で転送されるのは、『履歴属性情報』と『ファイル内容』
  • hg pull/clone で転送されるのは、『履歴属性情報』のみ

『履歴属性情報』と言うのは説明便宜上の私の造語ですが、通常の『履歴情報』からファイルの内容/差分を除いたもの、と考えてください。『hg log -v で表示される範囲』と言えばイメージし易いかもしれませんね。

largefiles 管理対象ファイルを追加したリビジョンが、共有リポジトリhg push された場合、hg pull/clone しただけではローカルリポジトリへの当該ファイルの取り込みは行われないわけです。

ではどの契機で当該ファイルが取り込まれるのかと言うと、大方の予想通り、そのファイルを必要とするリビジョンへの hg update 契機で実施されます。

# 厳密には archive/revert/merge 等々も含まれますが、とりあえず置いておきます

つまり、実際にそのファイルの内容を参照する必要が生じる時点になって、はじめてファイルの転送を行うわけです。

この挙動は、push/pull/clone 以外の契機でのリポジトリ間連携が生じることを意味しますので、従来の『リポジトリの完結性』の原則からは逸脱してしまいます。そのため、『オフライン環境での履歴参照』が運用上極めて重要である場合などは、largefiles の併用には注意してください。

但し、この挙動による弊害を低減する機能 (= 対象ファイルの事前取得機能) は現在提案中ですので、上手く事が運べば、2〜3ヶ月中には何とかなるかもしれません。

# 他の修正に手が取られて、こちらの作業が止まってしまっています ... orz

リビジョン情報表示の違い

largefiles 利用時には hg log 等の出力にも注意が必要です。

例えば、a/b/c という通常ファイルが追加されたリビジョンを hg log 等で参照した場合:

changeset:   0:f424da3a2025
user:        flying-foozy
date:        Mon Dec 19 14:48:04 2011 +0900
files:       a/b/c
description:
add a/b/c as normal file

上記のような出力となりますが、a/b/c が largefiles 管理下にある場合、出力は以下のように変化します。

changeset:   0:c8e65fc37b61
user:        flying-foozy
date:        Mon Dec 19 14:48:48 2011 +0900
files:       .hglf/a/b/c
description:
add a/b/c as large file

.hglf (hg large files) ディレクトリ配下の詳細は後述しますが、これは『a/b/c ファイルが largefiles 管理下にある』ことを意味します。

コミット前の hg status 出力では、a/b/c として表示されていたものが、コミット後のログ表示でいきなり .hglf/a/b/c となってしまうため、驚かれるかもしれませんが、まずは『そういうものだ』と思ってください。

largefiles 管理下にあるファイルは、hg locatehg manifest の出力においても .hglf/ 配下のパスとして出力されます。ファイル一覧出力を元に、何らかの処理を行っているような場合は、注意してください。

対象ファイルの管理方式

※ 連携先リポジトリとオンライン状態での作業が主となる場合、ここで述べる内容は特に必要とはなりません

largefiles 管理下のファイル a/b/c を含むリビジョンへの hg update 過程の詳細を見ることで、largefiles による対象ファイルの管理方式を説明しようと思います。

hg update の実施を契機に、まずは .hglf/a/b/c ファイルが最新の内容に更新されます。

ユーザから見た a/b/c ファイルが largefiles 管理下にある場合、直接的な構成管理対象ファイルは .hglf/a/b/c となるためです。このファイルは、ユーザから見えるファイルに対する『standin』(替え玉) ファイルと呼ばれるものです。

この standin ファイルには、それぞれに対応する本来の構成管理対象ファイル (.hglf/a/b/c の場合は a/b/c) の内容に SHA1 を適用することで得られたハッシュ値が格納されているのです。ハッシュ値は文字列として格納されていますから、直接 standin ファイル内容を見ることで、ハッシュ値の格納を確認することが可能です。

$ cat .hglf/a/b/c
3f786850e387550fdab836ed7e6dc881de23001b
$

作業領域が対象リビジョン時点の状態に更新されたなら、largefiles は standin ファイルの内容=本来のファイルのハッシュ値を読み出します。

これで、作業領域中のファイルの更新に必要とされるファイルのハッシュ値が得られました。このハッシュ値を以下 ${HASH} と表記しましょう。

次に largefiles は、管理領域中の .hg/largefiles/${HASH} ファイルの有無を確認します。

この時点でファイルが存在すれば、作業領域にファイルの内容を書き出し、このファイルに関する hg update 作業は完了です。

先述したように、largefiles 管理下のファイルの内容は、hg pull/clone 契機では転送されませんので、この時点ではファイルが存在しない事の方が一般的でしょう。

largefiles は次に、『ユーザーキャッシュ』(usercache)領域の確認に着手します。

『ユーザーキャッシュ領域』の場所は、以下の優先順位で確定します (対応する個々の環境変数が未定義の場合、そのディレクトリは対象から除外されます)。

  1. Mercurial[largefiles] usercache 設定値
  2. Windows NT 系 OS の場合:
    1. ${LOCALAPPDATA}/largefiles
    2. ${APPDATA}/largefiles
  3. Mac OS の場合、${HOME}/Library/Caches/largefiles
  4. その他の POSIX 系環境 (= Unix/Linux/Cygwin) の場合:
    1. ${XDG_CACHE_HOME}/largefiles
    2. ${HOME}/.cache/largefiles

この仕様は 2.0.1 時点のオンラインドキュメントでは明記されておらず、以後のリリースでも変更となる可能性がありますので注意してください。

ユーザキャッシュ領域を ${USERCACHE} と表記した場合、largefiles は ${USERCACHE}/${HASH} ファイルの有無を確認します。

ファイルが存在する場合は、${USERCACHE}/${HASH} から .hg/largefiles/${HASH} にコピーを行った上で、このファイルの内容を作業領域に書き出します。

ちなみに、ハードリンクが使用可能な環境であれば、${USERCACHE}/${HASH}.hg/largefiles/${HASH} が同じ実体を共有することで、ディスク消費量の増加を抑止しています。

# ローカルホスト上での hg clone における管理情報複製と同じ原理ですね

さて、ここまでの手順でも対象ファイルが見つからない場合、largefiles は default-push あるいは default 名義で定義されたリポジトリに対して、対象ファイルの転送要求を発行します(default/default-push の詳細は hg help config[paths] セクションを参照してください)。

hg update 契機でのファイル転送要求は、感覚的には hg pull と同じですから、連携先として default が選択されそうな気がしますが、ここで default-push の方が優先されるのは、以下のような理由からではないかと思います。

push 先 (= ファイル内容転送先) に指定されているリポジトリであれば、他のリポジトリからもファイルが転送されている筈なので、対象ファイルが格納されている可能性が高い

ファイル転送要求を受けた連携先リポジトリでも、リポジトリ固有の .hg/largefiles 配下と、ユーザーキャッシュ配下に対するファイル存在確認が実施されます。

連携先リポジトリに対象ファイルが存在すれば、転送されたファイル内容を用いて以下の作業を実施します。

  1. ファイル内容を .hg/largefiles/${HASH} に書き出し
  2. ${USERCACHE}/${HASH} にコピー
  3. 作業領域にファイル内容を書き出し

ここまでやっても対象ファイルが見つからない場合、largefiles は『ファイル取得に失敗』とみなし、作業領域中からファイルを削除して処理を終了します。この場合、hg status の出力は "!" (missing/不在) 扱いになります。

# fileset 指定では "deleted" に相当

largefiles エクステンションを利用するリポジトリで、[paths]defaultdefault-push が設定されていない (or 普段とは別なリポジトリからファイルを取り込みたい) 場合は、以下のようにして一時的に設定を上書きしてやる必要があります。

$ hg --config paths.default-push=REPO-URL update

まとめ

2.0 版で公式リリースされてから間も無いため、largefiles は修正/機能拡張共に、パッチの投函が活発に行われています。

その点では、万人にお勧めできる安定した機能というわけではありませんが、バイナリファイルの取り扱いに関して不満を持っている方は、本エントリを読まれた上で、試してみる価値はある機能だと思います。

使ってみて『おや?』と思った点などがあれば、どんどん質問/要望等をお知らせください!