彷徨えるフジワラ

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

Mercurial と Subversion の連携

最近、twitter 上で subversion 連携に関する質問に答えるために、convert エクステンション(標準同梱)とか、hgsubversion エクステンションのソースを確認しまくったので、折角だから MercurialSubversion の連携に関してまとめてみた。

結論から先に書いておくと、これまで twitter 上では:

MercurialSubversion への成果反映が必要であれば、選択肢は hgsubversion エクステンションの一択

という回答をしていたのだけれど、色々考えてみるに:

『hg コマンド経由での svn リポジトリへの反映』に拘らなければ、実は convert エクステンションの方が便利じゃねぇ?

というのが現在の僕の印象。

但し、Mercurial の convert エクステンションは、認証処理周りの実装が無い。そのため、keyring系ツール等で認証情報を管理していて、HTTP/HTTPS アクセス時にそのような認証情報を使用するケースでは、認証処理を実装している hgsubversion でないとアクセスできないので注意が必要。

なお、僕自身は Subversion の運用経験が殆ど無いので、ブランチマージ周りを splicemap 利用で上手いこと取り回す運用とかに関しては、誰かが書いてくれると嬉しいなぁ(笑)。

subversion リポジトリからの変換

convert エクステンション、hgsubversion エクステンション、いずれの場合も、エクステンションを有効にした上で、subversion リポジトリを指定して、取り込みコマンドを実行すれば良い。

convert の場合は:

※ 変換先は SVN-URL のパスから自動生成:
$ hg convert SVN-URL

または

※ 既存リポジトリへの変換取り込み(変換先指定のドット(".")を忘れずに):
$ hg convert SVN-URL .

hgsubversion の場合は:

※ 変換先は SVN-URL のパスから自動生成:
$ hg clone SVN-URL

または

※ 既存リポジトリへの変換取り込み:
$ hg pull SVN-URL

# 他のパターンもあるけど、とりあえず代表的なものを掲載

どちらのエクステンションでも、subversion-python バインディング等が必要になるが、TortoiseHg には subvesion-python バインディングのライブラリが付属しているらしい。やるなぁ! > TortoiseHg

ちなみに、ウェブ上の情報では『要 svn コマンド』と書かれているものも見かけるけれど、最新版 (convert は 2.1.2 版同梱、hgsubversion は 8a226f0f99aa) だと、subversion-python バインディングライブラリがあれば、svn コマンド類は無くても大丈夫な模様。

少なくとも、cygwin 上で svn* コマンド群を一時的に全て改名した状態でも、取り込み機能が動作することを確認できた。

ちなみに、python-subversion (debian 系のパッケージ名) と subversion-python (cygwin でのパッケージ名) のどっちが正式名称なんだろう? > python バインディング

変換情報の管理

変換先の Mercurial リビジョンには、Subversion の変換元リビジョンの情報が埋め込まれており、テンプレート機能で以下のキーワードを利用して参照することができる (キーワードは両方式で共通):

  • svnrev: Subversion 側のリビジョン番号
  • svnpath: コミット時のベースモジュールパス
  • svnuuid: "svn info" で表示される Subversionリポジトリ識別子

継続的に Subversion 側からリビジョンを取り込むために、現時点での取り込み実施状況を保持していているのも、両方式で共通:

  • convert の場合は、対象リポジトリ.hg/shamap ファイルに格納
  • hgsubversion の場合は、.hg/svn/rev_map ファイルに格納

但し、hgsubversion の .hg/svn 配下が『変換時に変換先各リビジョンに埋め込んだメタデータ』(= テンプレート機能で使用)を元に再構築可能な、一種のキャッシュであるのに対して、convert の .hg/shamap は単に変換結果を追記してあるファイルに過ぎない。

そのため、別のリポジトリで継続的な取り込みを行う場合、hgsubversion は "hg svn rebuild SVN-URL" でメタデータの再構築後に取り込みを実施できるのに対して、convert は (1) 先述したテンプレート機能を使って、対応関係を再構築するか、(2) 管理用ブランチを作成するなどして、リポジトリの一部として変換結果ファイルを伝播させる必要がある。

以下は、各リビジョンに格納されたメタデータを元に、再構築する場合のコマンド実行例:

$ hg log
     -r 0:tip # 一覧生成順序の調整
     --template 'svn:{svnuuid}{svnpath}@{svnrev} {node}\n'

# 対象外リビジョンの除外とかもあるので、もうちょっと工夫が必要だけど…

一見すると、convert の方が『面倒臭い』印象になるかもしれないが、実はこの『ゆるさ』が後々重要になって来る。

MercurialSubversion への成果反映

convert には『MercurialSubversion への成果反映』機能が無いが、hgsubversion は "hg push" の際に subversion リポジトリを指定することで、『MercurialSubversion への成果反映』を行うことができる。

$ hg push SVN-URL
pushing to SVN-URL
searching for changes
[r6] fujiwara: add e.txt on trunk branch
pulled 1 revisions
saved backup bundle to ......
$

SVN-URL から clone した場合には、URL 指定が不要になるのは、Mercurial リポジトリでの運用と同様。

当然、pull/incoming/outgoing 等の hg コマンドも同様に機能する。

『hg コマンドから透過的に Subversion リポジトリと連携』できるのは、一見すると便利なのだが、Subversion の履歴管理の概念は、基本的には CVS のそれと同等であるため、Mercurial における「名前無しブランチ」とか「複数ヘッド」とかに対応する概念は扱う事ができない。つまり:

Mercurial 側の履歴情報を、全て Subversion 側に反映できるわけではない

ということに。

そのため、rebase エクステンションを使用するなどして、履歴ツリーを線形に保つ必要が有る。

なお、マージで衝突が発生しないケースであれば、"hg push" の際に hgsubversion が自動で rebase してくれたりする。やるなぁ! > hgsubversion

しかしその一方で、マージで衝突が検出されるケースだと、エラー表示とかじゃなくて、いきなり subvesion-python バインディングからの例外スタックトレースが表示されたりして、精神衛生によろしくないので、極力自前で rebase した方が良さげ。

# あるいは MQ を併用して、常時適用先を移動できるようにしておくとか ....

convert エクステンション利用時の Subversion への成果反映

MercurialSubversion への成果反映』が必要なケースでは、convert ではなく hgsubversion を選択するのが一般的と思われるが、先述したように hgsubversion は『履歴の線形性』を維持する必要がある。

これは、単に『rebase が必須』であること以外にも、『Subversion に反映させる必要の無いリビジョンは作成できない』という制約でもあるので、例えば:

別途名前付きブランチで実装/試験等を繰り返し、Subversion へは最終成果だけを反映したい

みたいな運用をすることができない。っていうか、Mercurial の旨みが相当に目減りする感じ。

先述したように、hgsubversion の取り込み実施状況の管理は相当にカッチリしている+あくまでキャッシュ扱いであることから、基本的には利用者側で改変することは出来ないと思って良い。

その一方で、convert が取り込み実施状況の管理に使用する .hg/shamap ファイルは、自由に改変でき、且つ変更内容で変換挙動を制御することが可能なので、Subversion への成果反映は、hg コマンドで実施したい』という制約を外すと:

という運用が可能になる。

なお、ここでの『svn コマンドを直接使用して成果反映』は、拙著『入門TortoiseHg+Mercurial』で述べた『ハイブリッドリポジトリ運用』を前提としている。『ハイブリッドリポジトリ』とは以下の状態のこと:

同一のディスク領域を、MercurialSubversion の両方の管理下にある状態にする (= いわゆる「working copy」領域を共有する) ことで、両方のリポジトリに対する操作/参照ができる状況を作る

# "hg archive" の出力で Subversion のワーキングコピーを上書き、といった方法でも良いけどね

変換における対応関係の整合性を検証しない convert の『ゆるさ』が、逆に運用の柔軟性に繋がるわけだ。

これなら、Subversion 側に「見せたいリビジョン」と「見せたくないリビジョン」を任意に選択することができるし、Subversion からの継続的な取り込みも自動化できるから、Subversion 側での履歴をブラウズするのも楽チン。

オープンソース/ウェブ系開発のワークフローだと、『MercurialSubversion のリビジョンを1対1対応させたい』というニーズが多い (or そういった発言をする人の声が大きい) ようだけれど:

などのような上流側の都合で Subversion 連携が強制されるケースでは、必ずしも『MercurialSubversion のリビジョンの1対1対応』は必須では無いか、あるいはむしろ『1対1対応を見せたくない (= ひとまとまりの作業の最終成果だけを見せたい)』ケースの方が多いと思われる。

# あくまで僕個人の印象だけど…

そういったケースでは、本エントリで述べた convert + svn コマンド併用の運用は、検討する価値があると思われる。Mercurial の旨みも失わないしね。

なお、継続的な運用を行う場合、ハイブリッドリポジトリと、変更作業用リポジトリは、作業領域を分離して運用した方が良い。

Mercurial側の変更作業とSubversionとの同期を、頑張って同一リポジトリ上でやっても良いけど、Mercurial/Subversion双方の変更がごちゃごちゃになって混乱しかねねないので、共有はあまりお勧めできない。

連携運用の詳細は、『入門TortoiseHg+Mercurial』で説明しているので、よろしければ是非ご購入ください(笑)。

その他の変換に関する話

● ブランチマップによる変換

両方式とも、branchmap ファイルを指定することで、Subversion 側でのブランチ名を、別な名前に変換して Mercurial 側に取り込む事ができる。

但し、この機能で "trunk" ブランチを改名しようとしても、無視されるので注意が必要。

両方式とも、 Subversion の "trunk" ブランチは、 Mercurial の "default" に変換される。

● エクステンションの有効化手段

convert エクステンションを使用する場合、コマンドラインでの "--config extensions.hgext.convert=" 指定で一時的にエクステンションを有効にしただけでは、変換に失敗するので注意が必要。

これは、変換処理を行うために裏で立ち上げたプロセスにおいて、"hg debugsvnlog" という一種の裏コマンドが実行されるのだけれど、コマンドラインでのエクステンション有効化だと、この裏プロセス側の hg コマンドでは convert エクステンションが有効にならないのだ。

なので、ユーザ毎設定ファイル ("${HOME}/.hgrc") やリポジトリ毎設定ファイル (".hg/hgrc") で、あらかじめ convert エクステンションを有効にしておく必要がある。

hgsubversion の場合は、この手の問題は発生しない模様。

● 言語設定

出力メッセージの言語設定次第では、Subversion 側処理の出力が文字化けする可能性がある。

変換処理の際は、一時的に言語設定は非日本語化しておいた方がよさげ。

hgsubversion を使用する場合は、push/pull でも Subversion 側処理が実行されるので、こちらも要注意。

● hgsubversion によるリビジョン改変

hgsubversion 有効時の "hg help subversion" 曰く:

Currently, pushing to Subversion destroys the original changesets and replaces them with new ones converted from the resulting commits.
Due to the intricacies of Subversion semantics, these converted changesets may differ in subtle ways from the original Mercurial changesets.
For example, the commit date almost always changes.

つまり、Subversion リポジトリへの成果反映前に、Mercurial の共有リポジトリに push とかしてしまうと、同一の変更を行う複数のリビジョンが存在するなど、色々と面倒なことに。

● 変換先のルート

convert も hgsubversion も、変換結果は必ず null リビジョンの子リビジョンとして生成される。

そのため、事前に何らかのリビジョンが存在しているリポジトリに対して、Subversion からの取り込みを実施した場合、複数のルートリビジョンが存在することになる。

     :
     :
Subversion から取り込んだ最初のリビジョン
changeset:   1:f9d48820eba3
parent:      -1:000000000000
summary:     add initial contensts

※ 事前に存在していたリビジョン
changeset:   0:4c9d57046126
                            ※ 暗黙に 000000000000 が親
summary:     Added tag hogehoge for changeset 000000000000