例えば、以下のようなコミット履歴があるとします。
A---B---C---D master
ここで git rebase -i HEAD~3 をして、 コミットB を E に書き換えたくなったとします。このとき、rebase -i によって履歴を書き換えてしまうと、以下のようにリポジトリからB〜Dのコミットは消滅してしまうと思っている人も居るのではないでしょうか。
A---E---C'---D' master
確かに、Gitがこのような動作をするのであれば、rebase後に元の状態へ戻すことは到底困難であるように見えます。しかし、正確に書けば、実際のレポジトリの状態は以下のようになります。
E---C'---D' master / A---B---C---D
実はコミットA〜Dの一連のコミットは手つかずで残っており、「master」というラベル*1が新たな枝に付け替えられただけなのです。よって、この「master」というラベルをコミットDに付け替えるだけで、簡単に元の状態に戻せるということになります。
ただし、ラベルをコミットDへ付け替えるためには、コミットDの参照の仕方を知らなければなりません。git log をするだけだと、コミット A〜D' が表示されるだけで、コミットDを見つけることができません。これが、mergeやrebase後にはコミットA〜Dが消えてしまうと誤解させる一因だと思えます。
それでは、コミットDを見つけるのにはどうすればよいのか。簡単な方法を2つ紹介します。
1. ORIG_HEAD
mergeやreset、rebaseを行うと、ORIG_HEADに元々ラベルが貼られていたコミットが記録されます。ここに、元のコミットDが格納されていることが多いです。なので、 git reset --hard ORIG_HEAD とすれば元の状態に戻れます。
ただし、後述するように ORIG_HEAD の内容が上書きされて意図しないコミットを指している可能性もありますので、 reset する前には git log ORIG_HEAD などとして、本当にこれが戻したい状態かをチェックすべきです。*2。
2. reflog
ORIG_HEADはお手軽なのですが、履歴が1回分しか保存できないため、実行する操作によっては古い内容が上書きされていて使えないことがあります。そこで、 git reflog を使います。git reflogを使えば、今までラベルがついていたコミットの履歴を見ることができます。
例えば、今回の例でmasterブランチの履歴を見ると、以下のようになります。しっかりコミットDが見えていますので、これを元にgit reset --hard master@{1} とすることで元の状態に戻れます。
% git reflog show master 25a53f0 master@{0}: rebase -i (finish): refs/heads/master onto 1476f9f) c5f85d7 master@{1}: commit: D 4db8fb8 master@{2}: commit: C 04841ba master@{3}: commit: B 1476f9f master@{4}: commit (initial): A
ちなみに、今回の例ではORIG_HEADは使えませんでした。git rebase -i の終了直後、各コミットについているラベルは以下のようになっていました。
E---C'---D' master / A ORIG_HEAD \ B---C---D master@{1}
まとめ
git resetやmerge、rebase で履歴が消えることは決してありません。探しにくくなるだけでリポジトリには全ての状態が存在しています*3。git logで見えなくなった履歴を探す手段として、ORIG_HEADを参照したり、git reflog コマンドを使ったりすることができます。