マージ後のreset HEAD^は危険だった

直前のマージを取り消す場合は、

× git reset --hard HEAD^

ではなく、

○ git reset --hard ORIG_HEAD

としないと危ない、という話。

「マージ後にgit reset --hard HEAD^で取り消し」は去年の日記でもけっこう使ってるけど、たまたま上手くいっていたからよかったが、ORIG_HEADが正しい指定方法だった。場合によってはちょっと危ない。


マージコミットは複数のparentが記録されるが、mergeコマンドによって先端を移動するブランチ(=カレントのブランチ)を1番目の親としてマージコミットが作成される。

例えば topicブランチで git merge master とした場合に作成されるコミットオブジェクトは、1番目のparentはtopicブランチのハッシュ値で、2番目はmasterブランチのハッシュ値となる。
なので、その後topicブランチで git reset --hard HEAD^ とするとマージをする前に戻れる(HEAD^はHEADの1番目の親を指す)。

しかし、これは危険だった。
上記マージ後すぐにmasterブランチに移動してtopicブランチをマージすると、さっきマージしたtopicブランチの先頭に Fast-forwardとなる。新しいマージコミットは作成されず、topicブランチの先頭のマージコミットがそのまま使用されるので、マージ後のHEADの1番目のparentはtopicブランチの以前の先端であり、さっきと同じように git reset --hard HEAD^ とやると、topicブランチのマージ前の状態にresetしてしまう。

実験

masterブランチとtopicブランチで枝分かれしている状態をつくる

$ git init
$ echo hehehe > hehehe.txt
$ git add hehehe.txt
$ git commit -m 'hehehe追加'
[master (root-commit) 41cd63b] hehehe追加
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 hehehe.txt
$ git checkout -b topic
Switched to a new branch 'topic'
$ echo fuhihi > fuhihi.txt
$ git add fuhihi.txt
$ git commit -m 'fuhihi追加'
[topic 9f74cf3] fuhihi追加
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 fuhihi.txt
$ git log --oneline
9f74cf3 fuhihi追加
41cd63b hehehe追加
$ git checkout master
$ echo hoge > hoge.txt
$ git add hoge.txt
$ git commit -m 'hoge追加'
[master 4b20f85] hoge追加
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 hoge.txt
$ git log --oneline
4b20f85 hoge追加
41cd63b hehehe追加

とりあえず準備完了

$ git show-branch
* [master] hoge追加
 ! [topic] fuhihi追加
--
*  [master] hoge追加
 + [topic] fuhihi追加
*+ [master^] hehehe追加

topicブランチでmasterブランチをマージしてみる

$ git checkout topic
Switched to branch 'topic'
$ git log --oneline
9f74cf3 fuhihi追加
41cd63b hehehe追加
$ git merge master
Merge made by recursive.
 hoge.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 hoge.txt

作成されたマージコミットの1番目の親が以前の先端「9f74cf3」であることが分かる。

$ git log --oneline --parents
c93f737 9f74cf3 4b20f85 Merge branch 'master' into topic
4b20f85 41cd63b hoge追加
9f74cf3 41cd63b fuhihi追加
41cd63b hehehe追加

よって、以下の方法でマージ前の状態に戻れる

$ git reset --hard HEAD^
HEAD is now at 9f74cf3 fuhihi追加

このパターンではHEAD^にresetしても大丈夫だった。


もう一度topicブランチでmasterブランチをマージ

$ git merge master
Merge made by recursive.
 hoge.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 hoge.txt
$ git log --oneline --parents
acc49c4 9f74cf3 4b20f85 Merge branch 'master' into topic
4b20f85 41cd63b hoge追加
9f74cf3 41cd63b fuhihi追加
41cd63b hehehe追加

次に、masterブランチでtopicブランチをマージしてみる

$ git checkout master
Switched to branch 'master'
$ git log --oneline --parents
4b20f85 41cd63b hoge追加
41cd63b hehehe追加
$ git merge topic
Updating 4b20f85..acc49c4
Fast-forward
 fuhihi.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 fuhihi.txt

Fast-forward になった。

$ git log --oneline --parents master
acc49c4 9f74cf3 4b20f85 Merge branch 'master' into topic
4b20f85 41cd63b hoge追加
9f74cf3 41cd63b fuhihi追加
41cd63b hehehe追加

topicブランチに Fast-forward したということは、、、

$ git log --oneline --parents topic
acc49c4 9f74cf3 4b20f85 Merge branch 'master' into topic
4b20f85 41cd63b hoge追加
9f74cf3 41cd63b fuhihi追加
41cd63b hehehe追加

両ブランチとも全く同じ状態になったことが分かる。

もしここで直前のマージを取り消そうとして

git reset --hard HEAD^

とした場合、どうなるか。

$ git log --oneline HEAD^
9f74cf3 fuhihi追加
41cd63b hehehe追加

topicブランチのマージ前の状態になってしまう。

$ git log --oneline ORIG_HEAD
4b20f85 hoge追加
41cd63b hehehe追加

ORIG_HEADなら大丈夫。