読者です 読者をやめる 読者になる 読者になる

北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

git rebaseとgit mergeはともだち こわくないよ

例えば、以下のようなコミット履歴があるとします。

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 コマンドを使ったりすることができます。

注意

このエントリで消えないと言った物は「すでにコミットされた履歴」です。逆にインデックスや作業ディレクトリの内容は、操作次第で簡単に消えてしまいます*4。インデックスや作業ディレクトリの更新に関しては、誤って消さないように常に神経を尖らせておくべきです。

履歴の操作をする前には、 git stash で編集中の更新を一時退避するか、もしくは、思い切ってコミットしてしまうのも手です。自分は、 git checkout -b tmp/now-working → git commit -am "!修正中!" とすることも多いです。一度コミットしてしまえばgcされるまでは消えないので安心です。

*1:リファレンスとかrefsとか言われてるもの

*2:と言っても、git reset に関しても git rebase と同様に元のコミットが消滅したりはしないので、失敗してもそんなに焦ることはないです。

*3:ただし、到達不能になるとそのうちgcされます

*4:一度インデックスに入った物であれば、誤って削除しても git fsck --unreachable で探索は可能ですが、大変なのでお勧めしませんw