Mercurial MQ でバイナリファイルを扱う場合はご用心

MQ でなんの気なしにバイナリファイルを扱うと,バイナリファイル自体を lost します。これはこわい。というか実際にはまりました。

現象

まずバイナリファイルを追加。

$ hg init

$ perl -e 'print "\x00"' > bin

$ ls
bin

$ hg addremove
adding bin

$ hg ci -m "binary file added"

$ hg log
changeset:   0:90e1a39f0fe7
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon Jun 08 11:31:12 2009 +0900
summary:     binary file added

バイナリファイルといっても NUL バイトいっこのファイルだけど。

この changeset を qimport する。

$ hg qimport -r tip

$ hg qseries -s
0.diff: binary file added

んで qpop

$ hg qpop -a
patch queue now empty

$ ls
# なにもない(問題ない)

qpop したから bin ファイルが消えるのは想定どおり。


ここで qpush すると bin ファイルが復活するはずだけど……

$ hg qpush -a
applying 0.diff
patch 0.diff is empty
now at: 0.diff

$ ls
# ファイルが追加されてない!

復活してない!

なぜ?

パッチファイルを見てみると

$ cat .hg/patches/0.diff
# HG changeset patch
# User dayflower <dayflower@example.com>
# Date 1244428471 -32400
# Node ID cabcbf570d050415463d8541b1045ed8ae497981
# Parent  0000000000000000000000000000000000000000
binary file added

diff -r 000000000000 -r cabcbf570d05 bin
Binary file bin has changed

「Binary file bin has changed」としか内容がない(バイナリ自体のデータが記載されていない)。

diff の出力をそのままパッチにしたからこうなったんですね。だから,戻しようがない。

対処するには --git オプションをつける

qimport からやりなおします。このとき --git オプションをつけると,パッチ(diff)が GIT 形式になる(後述)。

$ hg qimport -r tip --git

$ hg qseries -s
0.diff: binary file added

$ hg qpop -a
patch queue now empty

$ ls
# なにもない(問題ない)

さて,qpush すると……

$ hg qpush -a
applying 0.diff
now at: 0.diff

$ ls
bin     # ちゃんと復活した!

おお,今回はちゃんと復活しましたよ!


パッチファイルを覗いてみると,

$ cat .hg/patches/0.diff
# HG changeset patch
# User dayflower <dayflower@example.com>
# Date 1244428314 -32400
# Node ID d016fd57d57065401ac2b7d732adbc36ba3089a9
# Parent  0000000000000000000000000000000000000000
binary file added

diff --git a/bin b/bin
new file mode 100644
index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d
GIT binary patch
literal 1
Ic${MZ000310RR91

今回は 1 バイト の NUL 文字のファイルだからよくわかんないけど,もっとサイズが大きい場合は,base64 みたいな(どんな形式かは忘れた)テキストによるバイナリエンコーディングの表現が載ります。なので,きちんと復活できるわけです。

バイナリファイルじゃなくても属性まわりでも同じことが

こんどは,ファイルの実行属性をいじって,それを changeset / MQ patch 化してみます。

$ ls -F
test.cgi

$ hg qnew -m 'added eXecutable bit to test.cgi' add_x

$ chmod a+x sample.cgi

$ ls -F
sample.cgi*     # 実行属性を付与した

$ hg status
M sample.cgi

$ hg qrefresh

$ hg qpop -a
patch queue now empty

$ ls -F
sample.cgi      # 実行属性が消えた(想定どおり)

これで qpush すると実行属性が再び付与されるはずだけど……

$ hg qpush -a
applying add_x
patch add_x is empty
now at: add_x

$ ls -F
sample.cgi      # 実行属性が戻らない!

やっぱりだめです。


こんどは同じように --git オプションを使います。qimport に対してじゃなくて qrefresh に対してだけど。

$ chmod a+x sample.cgi

$ hg status
M sample.cgi

$ hg qrefresh --git

$ ls -F
sample.cgi*     # 実行属性を付与した

$ hg qpop -a
patch queue now empty

$ ls -F
sample.cgi      # 実行属性が消えた(想定どおり)

$ hg qpush -a
applying add_x
now at: add_x

$ ls -F
sample.cgi*     # 実行属性がきちんと戻った!

今回はきちんと実行属性についてもハンドリングできました。


なお qnew などにも --git オプションをつけることができるけど,これはあくまで一番最初のパッチを GIT 形式にするという意味しかなくて,qrefresh の際につけわすれると,結局バイナリファイル・属性値の変更履歴は lost します。

まとめ

qimportqrefresh するときは忘れずに --git オプションをつけよう。絶対。

おわりに

いちおう下記の issue があがってます。

たしかに無言でファイルが消えたり属性が消えたりするのは怖い。--git をデフォルトにするか,GIT 形式じゃなくてもバイナリなどの変更履歴を保存するようにしてほしいです。