削除したGitコミットとブランチをreflogで復旧する

読了 6分

git reset --hardを実行してから、対象のコミットを間違えたことに気づきます。あるいはgit branch -Dで削除したブランチに、まだマージしていないコミットが残っていたことを後から思い出します。git logをいくら探しても、消えたコミットは見つかりません。この状況で最初に知っておくべき結論を先に置きます。一度でもコミットした内容なら、ほとんどの場合復旧できます。コミットはすぐには削除されず、Gitが残している移動の記録をたどり直せるからです。その記録がreflogです。

reflog — HEADの移動記録 #

git logがコミットの親の連なりを見せるのに対して、reflogはまったく別の軸の記録です。自分のリポジトリでHEADと各ブランチがどこを指していたか、その移動の全過程を時系列で残します。コミット、ブランチの切り替え、merge、rebase、resetまで、参照を動かすすべてのコマンドが1行ずつ記録されます。

reflogの確認
$ git reflog
2f8a1c3 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
9d4b7e5 HEAD@{1}: commit: feat: 注文キャンセルAPIを追加
6c1f0a2 HEAD@{2}: commit: feat: 注文照会APIを追加
2f8a1c3 HEAD@{3}: commit: feat: 注文モデルを追加

HEAD@{N}という表記は「HEADがN回移動する前の地点」という意味です。HEAD@{0}が現在で、数字が大きくなるほど過去です。git logから消えたコミットも、このリストにはハッシュとともにそのまま残っています。

回数の代わりに時間で照会することもできます。事故の時刻をおおよそ覚えているなら、こちらのほうが早いです。

時間基準でのreflog照会
git reflog --date=iso                       # 各項目に時刻を表示
git log -g --oneline 'HEAD@{1 hour ago}'    # 1時間前のHEAD基準で照会

git log -gはreflogの項目をlog形式で表示するオプションで、コミットメッセージの全文を確認しながら探すときに便利です。

2つの性質を覚えておくと判断が早くなります。

  • reflogは自分のリポジトリ専用です。コミットのようにpushされたり、cloneについて行ったりはしません。復旧は事故が起きたそのコンピュータでのみ可能です。
  • 保存期間があります。デフォルトでは、現在の履歴から到達可能な項目は90日、到達不能になった項目は30日で整理の対象になります。昨日の事故はほぼ確実に復旧できますが、数か月前の事故は保証できません。

シナリオ1 — reset –hardで消したコミットの復旧 #

直前にコミット2つをgit reset --hard HEAD~2で消してしまった状況です。復旧は2段階です。reflogで失ったコミットのハッシュを見つけ、そのハッシュへブランチを戻します。

ステップ1 — 失ったコミットを探す
$ git reflog
2f8a1c3 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
9d4b7e5 HEAD@{1}: commit: feat: 注文キャンセルAPIを追加
6c1f0a2 HEAD@{2}: commit: feat: 注文照会APIを追加

reset直前にHEADがあった場所、つまりHEAD@{1}9d4b7e5が、失った最後のコミットです。復旧方法は2つから選びます。

ステップ2 — 戻す
# 方法A — 安全: 失ったコミットにまずブランチを付けて確認
git branch rescue 9d4b7e5
git log rescue --oneline        # 内容を確認してからmergeなどで回収

# 方法B — 直行: mainをその地点へ移動し直す
git reset --hard 9d4b7e5

内容を確認しないまま、もう一度reset --hardを実行するのが不安なら、方法Aがよいです。ブランチはコミットを指すポインタにすぎないので、rescueブランチを付けた瞬間にそのコミットは再び到達可能になり、保存期間の心配からも外れます。

シナリオ2 — 削除したブランチの復旧 #

マージしていないブランチをgit branch -Dで削除した場合です。ブランチは消えましたが、その先端にあったコミットは残っています。

削除直後なら、最も早い手がかりは削除コマンドの出力そのものです。

削除出力に残ったハッシュ
$ git branch -D feature-coupon
Deleted branch feature-coupon (was 4e7a9b1).

括弧の中の4e7a9b1が、ブランチが指していた最後のコミットです。このハッシュからブランチを作り直せば復旧は終わりです。

ブランチの再作成
git branch feature-coupon 4e7a9b1

ターミナルをすでに閉じて出力がない場合は、reflogから探します。削除したブランチで最後に作業したコミットメッセージや、ブランチ切り替えの記録を手がかりにします。

reflogから手がかりを探す
$ git reflog | grep -n "feature-coupon\|クーポン"
7: 4e7a9b1 HEAD@{6}: commit: feat: クーポン有効期限の検証を追加
9: c3d5f80 HEAD@{8}: checkout: moving from main to feature-coupon

ブランチ別のreflogが残っている間は、git reflog show feature-couponでそのブランチの移動記録だけを見ることもできます。

reflogでも復旧できないもの #

reflogは万能ではありません。次の3つはreflogの範囲外です。

  • コミットしたことのない編集git restoreで捨てた作業ディレクトリの編集は、Gitのどこにも記録されたことがないため復旧できません。取り消しコマンドの危険範囲はGit基礎 #5 取り消し — restore・reset・revertの使い分けで整理しました。
  • 保存期間が過ぎたコミット — 到達不能のまま30日が経過し、ガベージコレクションが整理したコミットは取り戻せません。
  • 別のコンピュータのコミット — reflogはローカルの記録なので、同僚のリポジトリやサーバーで起きた移動は自分のreflogにはありません。
注記
git stash dropで消したstashはreflogに残りませんが、復旧の可能性はあります。git fsck --unreachable | grep commitでどのブランチにも属さないコミットの一覧を出すと、dropしたstashがコミットの形で現れることが多いです。ハッシュが見つかったらgit stash apply <ハッシュ>で復活させます。

まとめ #

この記事の手順を1行に要約するとこうです。reflogで失った地点のハッシュを見つけ、そのハッシュにブランチを付けるかresetで戻ります。コミットとブランチ削除の事故の大半は、この2段階で終わります。

そしてこの復旧手段の前提はひとつだけです。コミットが存在することです。コミットしていない編集にはreflogも役に立たないため、中途半端な時点でもこまめにコミットを作っておく習慣が、どんな復旧テクニックよりも確実な予防になります。作業単位が曖昧になっても、後で整理すれば済みます。記録されなかったものだけが本当に消えます。

X