Mercurial でマージをなかったことにする


2011年 08月 30日

本件はMercurialでアレを元に戻す108の方法に含まれるような内容ではあるのだが、非常に長くなるので独立した記事にしてみたい。QA形式に倣うのならこんな感じだろうか。

問題:誤ってブランチをマージしてしまった。しかしマージは公開され、それぞれのブランチには新しい修正も加えられている。それでもマージをなかったことにしたい。

ちょっと長くなる。サンプルのリポジトリを用意しつつ実際に実行できるようにしておいたので、読むだけでなくぜひ手元で実行してみてほしい。そうそう、途中 log -G と strip コマンドを使用しているので、graphlog と mq の extension は ON にしておいてほしい。具体的には hgrc に次の行を書いておく。

[extensions]
graphlog =
mq =

あと、各コマンドの結果は煩雑なので、blog に記載するにあたっていろいろとそぎ落としてある(特に log -G)。実際の実行結果は自分で確かめて欲しい。

サンプルリポジトリ

具体的なリポジトリがないととても分かりづらいので、実際にサンプルのリポジトリを操作しながら見ていく。

ひとまず、下記のように default と branch という2本のブランチで作業をしていたリポジトリがあるとする。

% hg init;          touch root; hg ci -Am root
% hg branch branch; touch B1; hg ci -Am B1
% hg up default;    touch D1; hg ci -Am D1
% hg up branch;     touch B2; hg ci -Am B2
% hg up default;    touch D2; hg ci -Am D2
% hg log -G
@  4 (2) [default] D2
|
| o  3 (1) [branch] B2
| |
o |  2 (0) [default] D1
| |
| o  1 [branch] B1
|/
o  0 [default] root

さて、ここで重大なミスマージを行ってしまった。まだ未完成の branch を、default に取り込んでしまったのだ。

% hg merge branch; hg ci -m"merge branch"
% hg log -G
@    5 (4,3) [default] merge branch
|\
| o  4 (2) [default] D2
| |
o |  3 (1) [branch] B2
| |
| o  2 (0) [default] D1
| |
o |  1 [branch] B1
|/
o  0 [default] root

この時点で気づいていれば、hg rollback で救出が可能だった。しかし、世界は残酷である。ミスマージのあと公開が行われ、たくさんの修正がそれぞれのブランチに加えられてしまった。

% hg up branch;  touch B3; hg ci -Am B3
% hg up default; touch D3; hg ci -Am D3
% hg up branch;  touch B4; hg ci -Am B4
% hg up default; touch D4; hg ci -Am D4
% hg log -G
@  9 (7) [default] D4
|
| o  8 (6) [branch] B4
| |
o |  7 (5) [default] D3
| |
| o  6 (3) [branch] B3
| |
o |  5 (4,3) [default] merge branch
|\|
o |  4 (2) [default] D2
| |
| o  3 (1) [branch] B2
| |
o |  2 (0) [default] D1
| |
| o  1 [branch] B1
|/
o  0 [default] root

rollback はもはや不可能であり、strip も困難を極める。どうにか、backout するしかない…。

単純な backout

default の状態をとにかく戻したい、というだけであれば、単純な backout がその仕事をしてくれる。マージリビジョンを backout するためには、「どちら側の」ブランチを backout するのか、–parent オプションで指定が必要だ。graph log の結果を良く見て考えよう。今回誤ってしまったマージ操作は r5、親は r4 で、r3 からの影響を取り除きたい。そのことを Mercurial に伝える。

% hg backout 5 --parent 4
% hg ci -m"backed out B1, B2"
% ls
D1  D2  D3  D4  root

よし、誤って取り込んでしまった B1, B2 を取り除くことに成功した。やったね。

しかしこの操作には問題がある。本当に branch を default に取り込む時がきたとき、B1, B2 が取り込まれないのだ。

% hg merge branch
% ls
B3  B4  D1  D2  D3  D4  root

ミスマージの後に branch で行われた B3, B4 は取り込まれているのに、何故?

いや、何故も何もない。だって、B1 と B2 はもう既に取り込んだじゃないか!

従って、この方法では不完全なことが分かる。運用として、B1, B2 を backout したリビジョンを記録しておき、本当のマージタイミングが来たときに backout の backout も合わせて行うようにする、という回避方法もなくはない。しかしまあ、そんなことを覚えておくのはリスキーだ。忘れて事故るのがオチである。どうにかしたい。

branch に default をマージしても良い場合 (片側 backout)

まず、ちょっとした前提をつける。branch に default をマージするのは自由、ということにしよう。つまり、default は公開ブランチなので余計な修正が混ざっては困るが、branch は所詮内部の開発ブランチなので、ちょっとくらい余計な修正が混じっていても我慢できる、ということだ。そこまで無茶な前提ではないはずだ。

まず、以下の段階に戻る。

% hg up -C
% hg strip 10
% hg log -G
@  9 (7) [default] D4
|
| o  8 (6) [branch] B4
| |
o |  7 (5) [default] D3
| |
| o  6 (3) [branch] B3
| |
o |  5 (4,3) [default] merge branch
|\|
o |  4 (2) [default] D2
| |
| o  3 (1) [branch] B2
| |
o |  2 (0) [default] D1
| |
| o  1 [branch] B1
|/
o  0 [default] root

さて、merge をしよう。

…OK、backout ではない。merge である。傷口を広げるように見えるかもしれないが、落ち着いて欲しい。まずやることは、branch へ default のミスマージを取り込むことだ。ただし、ミスマージの親リビジョンに対してである。今回の場合は、r3 だ。

% hg up 3
% hg branch
branch
% hg merge 5; hg ci -m"merge 5 (for missmerge recovery)"
% hg log -G
@    10 (3,5) [branch] merge 5 (for missmerge recovery)
|\
| | o  9 (7) [default] D4
| | |
| | | o  8 (6) [branch] B4
| | | |
| | o |  7 (5) [default] D3
| |/ /
+---o  6 (3) [branch] B3
| |
| o  5 (4,3) [default] merge branch
|/|
| o  4 (2) [default] D2
| |
o |  3 (1) [branch] B2
| |
| o  2 (0) [default] D1
| |
o |  1 [branch] B1
|/
o  0 [default] root

グラフが厄介になってきた。まあそれはそれとして、ここでようやく backout の発動だ。なにをしたかったのかというと、r5 でのマージ操作について、r3 からの影響を取り除きたいのだった。先もやったことだが、今度は「branch で」実行する。先に merge した理由がこれだ。backout 操作ができるのは、自分の祖先だけだからだ。

% hg backout 5 --parent 4
% hg ci -m"backed out B1, B2"
% ls
D1  D2  root

誤って取り込んでしまった B1, B2 を取り除くことに成功した。

しかし、これはあくまで branch から B1, B2 を取り除いたのであり、default からではない。B1, B2 は本来 branch には含まれているべきで、今この状態は正しいわけではない。これから「正しい」状態に持っていくためには、次のことをすればいい。

  1. default に対して branch での backout リビジョンのマージ
  2. branch での backout リビジョンの再 backout
  3. (もしあれば)branch での双頭解消

順にやる。まず、default に対して branch で行った backout リビジョンのマージ。

% hg up default
% hg merge branch; hg ci -m"backed out B1, B2"
% ls
D1  D2  D3  D4  root

B1, B2の除去に成功した。しかし、まだ branch からも B1, B2 が削られてしまっている。branch には含まれているべき変更なので、再 backout を行う。

% hg log -b branch -k "backed out B1, B2"
11 [branch] backed out B1, B2
% hg up branch
% hg backout 11
% hg ci -m"reapply B1, B2"
% ls
B1  B2  D1  D2  root

B1, B2が復活した。余分に default の D1, D2 も取り込まれているが、今回はこれは問題ないという前提を切ってある。

で、マージ直後での操作であればここで完了だが、今回は更に変更が加えられてしまっていることを想定していた。一時的にマージリビジョンまで戻っていたので、branch は「双頭」状態である。解消しよう。

% hg merge; hg ci -m"merge heads"
% ls
B1  B2  B3  B4  D1  D2  root

ミスマージの後に行われた B3, B4 の変更も取り込んだ。これでミスマージの修正は完了だ。

% hg log -G
@    14 (13,8) [branch] merge heads
|\
| o  13 (11) [branch] reapply B1, B2
| |
| | o  12 (9,11) [default] backed out B1, B2
| |/|
| o |  11 [branch] backed out B1, B2
| | |
| o |    10 (3,5) [branch] merge 5 (for missmerge recovery)
| |\ \
| | | o  9 (7) [default] D4
| | | |
o | | |  8 (6) [branch] B4
| | | |
| | | o  7 (5) [default] D3
| | |/
o | |  6 (3) [branch] B3
|/ /
| o  5 (4,3) [default] merge branch
|/|
| o  4 (2) [default] D2
| |
o |  3 (1) [branch] B2
| |
| o  2 (0) [default] D1
| |
o |  1 [branch] B1
|/
o  0 [default] root

default で、branch が正しくマージできるか、念のため確認しておこう。

% hg up default
% ls
D1  D2  D3  D4  root
% hg merge branch
% ls
B1  B2  B3  B4  D1  D2  D3  D4  root

backout していた B1, B2 も、新しく追加していた B3, B4 も、全部正しく取り込めている。

良かった良かった。

だが、このやり方には前提があったことを忘れてはいけない。これでは終われない。

branch 側も綺麗にしたい場合 (両側 backout)

先の方法は前提を切っていた。一般的な運用ではそこまで無理な前提ではない、とは思っているが、この前提を常に切れるとは限らない。

先の前提も外そう。両ブランチに対して、backout と merge のみでミスマージを「なかったこと」にする。基本的には、同じことを両ブランチに適用すればよい。

説明のために、サンプルリポジトリを切り落として戻す。

% hg up -C
% hg strip 10
% hg log -G
@  9 (7) [default] D4
|
| o  8 (6) [branch] B4
| |
o |  7 (5) [default] D3
| |
| o  6 (3) [branch] B3
| |
o |  5 (4,3) [default] merge branch
|\|
o |  4 (2) [default] D2
| |
| o  3 (1) [branch] B2
| |
o |  2 (0) [default] D1
| |
| o  1 [branch] B1
|/
o  0 [default] root

まずやることは同じ。branch へのミスマージの取り込みと、その backout だ。

% hg up 3
% hg branch
branch
% hg merge 5; hg ci -m"merge 5 (for missmerge recovery)"
% hg backout 5 --parent 4
% hg ci -m"backed out B1, B2"
% ls
D1  D2  root

続いて行うのは、default での backout だ。何を backout するのかって? それは、自分自身のミスマージまでの変更だ。この場合、r5 を backout するとき、親を branch 側にすれば良い。D3, D4 の影響を受けたくないので、親はミスマージ自身である r5 にして無名 head を分岐させておこう。

% hg up 5
% hg branch
default
% hg backout 5 --parent 3
% hg ci -m"backed out D1, D2"
% ls
B1  B2  root

これで二本のブランチは、「自分自身の過去」を、互いの分岐元まで打ち消した状態になっている。次に行うべきことは、自身の過去を失ったお互いを取り込むことだ。

% hg merge branch; hg ci -m"backed out B1, B2"
% ls
root
% hg up branch
% hg merge default; hg ci -m"backed out D1, D2"
% ls
root

これで、両ブランチはお互いに分岐する直前まで若返った。さて、ミスマージが行われる前まで互いのブランチを育てなおそう。それぞれのブランチは互いに自分の過去を打ち消していたのだった。それを戻すには、もう一度その打ち消したリビジョンを打ち消し直せばよい。

% hg log -G
@    14 (11,13) [branch] backed out D1, D2
|\
| o  13 (12,11) [default] backed out B1, B2
|/|
| o  12 (5) [default] backed out D1, D2
| |
o |  11 [branch] backed out B1, B2
| |
o |  10 (3,5) [branch] merge 5 (for missmerge recovery)
|\|
| | o  9 (7) [default] D4
| | |
| | | o  8 (6) [branch] B4
| | | |
| | o |  7 (5) [default] D3
| |/ /
+---o  6 (3) [branch] B3
| |
| o  5 (4,3) [default] merge branch
|/|
| o  4 (2) [default] D2
| |
o |  3 (1) [branch] B2
| |
| o  2 (0) [default] D1
| |
o |  1 [branch] B1
|/
o  0 [default] root

default では r12 が、branch では r11 が、それぞれ自分の過去を打ち消したリビジョンだ。

% hg backout 11; hg ci -m"reapply B1, B2"
% ls
B1  B2  root
% hg up default
% hg backout 12; hg ci -m"reapply D1, D2"
% ls
D1  D2  root

OK。互いにミスマージが行われる前まで戻ってきた。ミスマージ直後からの操作であればこれでおしまい。だが、今回は両枝共に更に成長している。双頭を解消して、もう一度枝を伸ばそう。

% hg merge; hg ci -m"merge heads"
% ls
D1  D2  D3  D4  root
% hg up branch
% hg merge; hg ci -m"merge heads"
% ls
B1  B2  B3  B4  root

以上で、ミスマージが「なかったこと」になった。お疲れ様。最終的にはこんなグラフとなる。

 % hg log -G
 @    18 (15,8) [branch] merge heads
 |\
 | | o    17 (16,9) [default] merge heads
 | | |\
 | | | o  16 (13) [default] reapply D1, D2
 | | | |
 | o | |  15 [branch] reapply B1, B2
 | | | |
 | o---+  14 (11,13) [branch] backed out D1, D2
 | | | |
 | +---o  13 (12,11) [default] backed out B1, B2
 | | | |
 | | | o  12 (5) [default] backed out D1, D2
 | | | |
 | o | |  11 [branch] backed out B1, B2
 | | | |
 | o---+  10 (3,5) [branch] merge 5 (for missmerge recovery)
 | | | |
 | | o |  9 (7) [default] D4
 | | | |
 o | | |  8 (6) [branch] B4
 | | | |
 | | o |  7 (5) [default] D3
 | | |/
 o | /  6 (3) [branch] B3
 |/ /
 | o  5 (4,3) [default] merge branch
 |/|
 | o  4 (2) [default] D2
 | |
 o |  3 (1) [branch] B2
 | |
 | o  2 (0) [default] D1
 | |
 o |  1 [branch] B1
 |/
 o  0 [default] root

さて、最後に、本当に互いに正しく merge ができるかどうか、念のため確認しておこう。

% hg up default
% ls
D1  D2  D3  D4  root
% hg merge branch
% ls
B1  B2  B3  B4  D1  D2  D3  D4  root

% hg up -C branch
% ls
B1  B2  B3  B4  root
% hg merge default
% ls
B1  B2  B3  B4  D1  D2  D3  D4  root

問題ないようだ。本当にお疲れさま!