Gitで消えたコミットはどこへ?reflogで探して救出用ブランチに復元
git reset --hard を打った直後に、さっきまであったはずのコミットが git log から消えている。あるいは、マージ済みだと思って git branch -D で消したブランチに、実は未マージの作業が残っていた。そんなときは、まず今の作業状態を確認しましょう。コミットと操作の記録が残っていれば、git reflog で場所を突き止め、救出用ブランチを切って取り戻せます。この記事では、今ある作業を守りながら、見失ったコミットを探して復元する手順を、練習用リポジトリで試せる形で説明します。
コミットが見えなくなったとき、まずgit statusで作業状態を確認する
最初にやるのは、復元コマンドを打つことではなく、現状を確認することです。焦って git reset や git checkout を重ねると、状況がかえって分かりにくくなります。
git status
git branch --show-current
git stash list
git status では、未コミットの変更(ステージ済み・未ステージ)が残っているか、いまどのブランチにいるかが分かります。git stash list も見ておくと、退避した変更が別にあるかどうかが確認できます。git branch --show-current でブランチ名が表示されず、git status に「HEAD detached at …」などと出ていたら、HEAD がブランチではなくコミットを直接指している状態です。
ここで未コミットの変更があるなら、先に守ります。たとえば、一時コミットを作るか、次の git stash で退避します。
git stash push -u -m "復元作業前の退避"
-u を付けると、まだ追跡されていない新規ファイルも退避されます。ただし、.gitignore などで無視されているファイルは対象外です。必要なものは別の場所にもコピーしておきます。対象範囲は Git公式のgit-stashドキュメントで確認できます。個人的には、調べものを始める前に「今の作業を一度どこかに固定する」癖をつけておくのが一番安全だと思います。調査自体は履歴を読むだけで壊れませんが、その後の復元操作でファイルが書き換わる場面があるためです。
git logとgit reflogの違い:コミットの履歴と手元の操作記録
引数なしの git log は、HEAD から親をたどった「コミットの系譜」を表示します。通常、HEAD はいまのブランチの先端を指しています。reset でブランチの指す位置を戻せば、戻した先より後のコミットは、この系譜から外れて見えなくなります。この時点では、通常はコミット自体が消えたわけではありません。
一方の git reflog は、HEAD やブランチの先端が過去にどこを指していたかの記録です。コミット、reset、checkout、merge、rebase などに伴う参照の更新が記録されます。すべてのコマンド実行を記録するものではありません。引数なしでは HEAD の記録が表示され、ブランチの切り替えも確認できます。Git公式のgit-reflogドキュメントでも、ローカルの参照の更新履歴を記録する仕組みと説明されています。
違いを短くまとめると、git log は「コミットの親子関係」、git reflog は「手元の参照がどこを指していたかの記録」です。
reflog は自分のリポジトリにだけ存在します。git clone で取得した直後の別の人のリポジトリには、あなたの操作の記録は入っていません。この点は最後のセクションでもう一度触れます。
練習用リポジトリでコミットを作り、reset後のreflogから探してみる
本番のリポジトリで試す前に、使い捨ての場所で一度通しておくと安心です。以下は、Git がインストールされ、コミット用の名前とメールアドレスが設定されている環境を前提にしています。reflog-practice というフォルダーがまだない場所で実行します。
mkdir reflog-practice && cd reflog-practice
git init
echo "one" > memo.txt
git add memo.txt
git commit -m "first: メモを追加"
echo "two" >> memo.txt
git commit -am "second: 2行目を追加"
echo "three" >> memo.txt
git commit -am "third: 3行目を追加"
ここで git log --oneline を見ると、3つのコミットが並びます。では、わざと2つ戻します。
git reset --hard HEAD~2
git log --oneline
first だけが残り、second と third が消えたように見えます。memo.txt も1行に戻ります。ここで reflog の出番です。
git reflog
出力はたとえば次のようになります(ハッシュは環境ごとに変わります)。
a1b2c3d HEAD@{0}: reset: moving to HEAD~2
f4e5d6c HEAD@{1}: commit: third: 3行目を追加
9c8b7a6 HEAD@{2}: commit: second: 2行目を追加
a1b2c3d HEAD@{3}: commit (initial): first: メモを追加
HEAD@{1} の third が、reset 直前に HEAD が指していた場所です。左端のハッシュが、見失ったコミットの住所になります。この例では、reset の行のすぐ下にある古い記録が探している地点です。以降のコマンドでは、例の f4e5d6c を自分の出力にあるハッシュに置き換えます。
ブランチごと消した場合は、git reflog の中から「削除したブランチから最後に切り替えたときの記録」を探します。checkout: moving from feature-x to main のような行の左端は、切り替え先の main のコミットです。切り替え直前の feature-x のコミットは、そのすぐ下の古い記録で確認します。ただし、その後に feature-x の先端が別の操作で更新されていれば、最後の先端とは限りません。
git showで内容を確かめ、救出用ブランチを作って変更を確保する
候補が見つかったら、すぐ戻さずに中身を確認します。
git show f4e5d6c
コミットメッセージ、作者、日時、差分が表示されます。探していたものか自信がなければ、git show f4e5d6c --stat で変更ファイルと変更量の概要を見る方法もあります。複数の候補がある場合は、日時とメッセージで絞り込みます。
確認できたら、そのコミットを指す新しいブランチを作ります。これが救出用ブランチです。
git branch rescue/third f4e5d6c
このコマンドは、今いるブランチも作業ツリーも書き換えません。ブランチ名というラベルを1枚貼るだけです。ラベルが付いたコミットは、通常のブランチ経由で到達できるようになるため、後述するガベージコレクションによる削除の対象から外れます。
中身を実際に触りたいなら、切り替えて確認します。
git switch rescue/third
cat memo.txt
3行そろっていれば成功です。元のブランチに取り込みたい場合は、戻ってから git merge rescue/third を使います。git cherry-pick f4e5d6c なら、そのコミットの差分だけを取り込めます。ただし、この例では third の差分だけでは second の変更を復元できず、競合する可能性もあります。
私の意見では、git reset --hard f4e5d6c で一気に戻すのは、現状を確認し終えるまで避けたほうがいいです。速いのは確かですが、追跡対象ファイルの未コミット変更を上書きします。まず git branch でラベルを貼り、それから落ち着いて合流させる。この順番なら、やり直しが利きます。
復元できる範囲を知る:未コミットの変更・記録の有効期限・チーム共有時の注意点
reflog を手掛かりに救えるのは、コミットや stash などとして Git に保存され、対応する記録とオブジェクトが残っている内容です。git add も git commit もしておらず、stash にも退避していない変更を git reset --hard や git checkout -- . で消した場合、その内容は reflog からは復元できません。ステージ済みなら、git fsck --lost-found で宙に浮いたオブジェクトが見つかる可能性はありますが、確実ではありません。未コミットの作業は、こまめに git commit するか git stash するのが現実的な防御です。
記録には有効期限があります。reflog エントリの期限は既定で90日、その参照の現在の先端から到達できないコミットに関するエントリは既定で30日です。設定項目は gc.reflogExpire と gc.reflogExpireUnreachable。期限を過ぎた記録は git gc などの処理で削除されます。コミットなどのオブジェクト自体の削除には、ほかの参照からも到達できないことや、別途設定された保持期間などの条件が関わります。既定の日数が復元を保証するわけではないため、気づいた時点で救出用ブランチを作るのが安全です。Git公式のgit-gcドキュメントに、期限と保持条件が説明されています。
チームで使うときの注意もあります。reflog はローカル専用で、git push しても共有されません。別の人のマシンでは、その人のリポジトリに対象のコミットや記録が残っていれば復元できる場合があります。また、リポジトリを clone し直しても、元の reflog は引き継がれません。復元できたら、救出用ブランチを git push -u origin rescue/third でリモートに上げておけば、バックアップにもなります。
共有ブランチで git reset した後に git push --force をすると、他のメンバーの履歴とも食い違う場合があります。実行前に、チームに状況を伝えてください。
練習では、使い捨てリポジトリで reset 前後の git log と git reflog を見比べておくと、探すべき記録が分かります。事故が起きる前に、コミットを確認して救出用ブランチを作るまでの流れを押さえておきましょう。


