git reset --hard で消えたコミットを reflog で取り戻す

reflog はローカルリポジトリでブランチや HEAD がどこを指していたかを記録します。消えたコミットを探してブランチとして復元する手順、既定の保持期間(90日・30日)、復元できないケースを公式ドキュメントに基づいて整理しました。

開発Gitバージョン管理

git reset --hard HEAD~2 を実行してから、その2つのコミットが必要だったと気づくことがあります。git log には表示されなくなりますが、多くの場合、コミット自体はまだリポジトリに残っています。その場所を教えてくれるのが reflog です。

reflog が記録するもの

Git のマニュアルは reflog を「ローカルリポジトリでブランチの先端やその他の参照がいつ更新されたか」を記録するものと説明しています。コミット、reset、rebase、ブランチの切り替えなどで参照が動くたびに1行ずつ追加されます。HEAD の reflog にはブランチの切り替えも記録されます。

git reflog は git reflog show と同じで、参照を指定しなければ HEAD の記録を表示します。各エントリは HEAD@{n} の形で指定できます。HEAD@{2} は「HEAD が2回前の移動で指していた位置」を意味し、main@{yesterday} のように時刻でも指定できます。

取り戻す手順

git reflog                       # reset 直前のエントリを探す
git branch rescue HEAD@{1}       # その位置に新しいブランチを作る

git branch <名前> <開始点> は、開始点にコミット ID や reflog の表現を受け取り、そのコミットを指すブランチを作ります。作業ツリーには触れないので、中身を確認してからマージするか、必要に応じてそのコミットへリセットできます。reset の直後なら git branch rescue ORIG_HEAD も使えます。git reset は実行前に現在のブランチの先端を ORIG_HEAD に保存するので、このブランチは reset 前の位置を指します。

いつまで残るか

reflog のエントリは永久には残りません。git gc が内部で git reflog expire を実行するときの既定値は次のとおりです。

  • その reflog を持つ参照の現在の先端から到達できるエントリ:gc.reflogExpire、既定 90 日
  • その先端から到達できないエントリ(reset や amend で捨てたコミットなど):gc.reflogExpireUnreachable、既定 30 日

reflog から参照されているオブジェクトは git gc が削除しません。ただし該当する reflog のエントリが期限切れになり、ほかの参照からも到達できなくなると、すでに gc.pruneExpire の基準(既定 2 週間)より古いオブジェクトは、同じ処理の中で削除されることがあります。この 2 週間はオブジェクト自体の経過時間で、reflog の期限切れ後に追加される猶予ではありません。期限や prune の設定を変えたり、関連するコマンドを手動で実行したりすると、さらに早く消えることもあります。コミットをなくしたと気づいたら、すぐに復元してください。

reflog でできないこと

  • コミットしていない変更: reflog は参照の移動を記録するだけです。reset --hard で上書きされた作業ツリーの未コミットの内容は reflog のエントリになりません。
  • 別のリポジトリの履歴: reflog はそれぞれのローカルリポジトリの記録です。日時の表現 main@{yesterday} も「ローカルの main が昨日どこを指していたか」を意味します。
  • bare リポジトリ: core.logAllRefUpdates は作業ディレクトリのあるリポジトリでは既定で true、bare リポジトリでは既定で false です。そのため bare リポジトリには既定で reflog が作られないことがあります。

既定値は設定で変えられるので、実際のリポジトリでは git config --get gc.reflogExpire などで確認してください。

ブログへ