彷徨えるフジワラ

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

逆向きパッチの生成

※ "hg diff" の --reverse オプションに関する記述を追加@ 2012/02/08
Windowsコマンドプロンプトで revset 記述を使用する際の注意事項に関する追記あり@2012/02/06
TokyoMercurial #1 で話題になった件の詳細シリーズ - その2。

コミット済み履歴に対して、その『変更内容を打ち消す』=『変更を無かった事にする』(≠ 『履歴記録を無かった事にする』)場合、"hg backout" を使用するのが Mercurial でのお作法。"hg backout" 挙動の詳細は、1.7 版での仕様変更に関して書いた別エントリを参照のこと。

但し、"hg backout" の『打ち消し対象』には、マージ実施リビジョンを指定することができないので、マージ実施リビジョンを打ち消そうとするなら、自前で『マージの打ち消し』に相当する改変を準備する必要がある。

例えば、以下のような履歴ツリーの場合:

"hg diff -c M" によって得られるマージ実施リビジョン M における変更内容を、『逆向き』 (= M の内容を B に戻す) に適用することで、マージ実施を『打ち消す』事ができる。

# M における差分を、逆向きに適用
$ hg diff -c M | patch -R -p1
# 適用結果をコミット
$ hg commit -m 'backout merge at M
$

マージ直後であれば、"hg revert" で作業領域の状態をマージ前のリビジョン (この例だと B) に戻してコミットする方が簡単かもしれないけれど:

$ hg revert --all -r B
reverting 〜
....
$ hg commit -m 'backout merge at M
$

マージ実施後に幾つかのリビジョンが記録されている場合、この方法は使えない (※ マージ後の変更内容も破棄されてしまうため) し、そもそもマージ直後なら "hg rollback" した方が妥当。

逆向きパッチによる打消しの場合、マージ実施後にリビジョンが記録済みであっても、(1) "hg update M" で作業領域をマージ実施リビジョンに変更した上で、(2) 打ち消しリビジョン R をコミットし、(3) 最新のリビジョンとマージ (X で実施) することで、マージ実施以後の変更内容 (この例では E) を維持したまま、マージ実施を打ち消すことが可能。

っつーか、これは正に "hg backout --merge" がやっていることの手動実施。

さて、上記のような手順による逆向きパッチの適用は、外部コマンド patch を必要としている点で、あまり美しくない。まぁ、あくまで僕の主観的な基準での『美しさ』だけれど ....

そもそも外部コマンド patch を必要としているのは、"hg import" に『変更内容の逆向き適用』機能が無いためなのだけれど、だったら『逆向きパッチ』が生成できれば良くねぇ?という話。

"hg diff -c" には『逆向きパッチ生成』的なオプションが無いけれど (間違い。 --reverse で逆向きパッチが生成可能) 、そこで発想の転換を:

"hg diff -c M" は "hg diff -r B -r M" と等価であるので、リビジョン指定の順序を入れ替えることで、逆向きパッチを生成可能

つまり、冒頭での実行例にこの手法を適用すると:

$ hg diff -r M -r B |
  hg import -m 'backout merge at M' -
applying patch from stdin
$

上記のように、外部コマンドを用いなくても逆向きパッチの適用が可能になる。

# 『標準入力からの読み込み』を指示する引数 "-" を忘れないように > "hg import"

※ 2012/02/08 追記 >>>> ここから

M から、Mの第1親の B への逆向きパッチの生成は、-c と --reverse の組み合わせで生成可能。

$ hg diff -c M --reverse |
  hg import -m 'backout merge at M' -

"hg diff" の -c 指定は、第1親との差分に固定されてしまうので、第2親 (この場合は "D") との差分を取りたい場合は、この方法は使用できないので注意。

以下、revset 表記で『-r X -r "X^1"』となっている箇所は、全て『-c X --reverse』で代替可能。

※ 2012/02/08 追記 <<<< ここまで

ちなみに、revset 機能を用いると、"-r M -r B" を "-r M -r M^1" と記述できるので、revset 表記の使用が断然お勧め。

※ 2011/02/06 追記 >>>> ここから

Windowsコマンドプロンプトの場合、文字『^』が特殊文字扱いになるため、ダブルクォートで囲むことが必須。シングルクォートは機能しないので、必ずダブルクォートを用いること!

※ 2011/02/06 追記 <<<< ここまで

"hg import" での変更適用先が現行の作業領域(の親リビジョン)になるので、手順を汎用化するなら、以下のような感じかな?:

$ hg update M
$ hg diff -r . -r ".^1" |
  hg import -m "backout merge at M" -

適用済み MQ パッチに対して逆向きパッチが欲しい場合、例えば "patch1" という名前のパッチに対する逆向きパッチが欲しい場合は:

$ hg diff -r patch1 -r "patch1^1" |
  hg qimport --name "rev_patch1" -

また、"hg backout" での打ち消し実施は個別のリビジョン毎になるため:

一連のリビジョン群における変更内容を、一括して打ち消したい

というようなケースでは、対象リビジョン群前後の差分を逆向き適用する方が手間が掛からない。