12k
All articles

Git Reflog で失われたコミットを復元する

git reflogで失ったGitコミットを復元。孤立したコミットの探し方、hard resetやrebaseの取り消し、ブランチ復元まで解説。

OpenReplay Team
OpenReplay Team
Git Reflog で失われたコミットを復元する

失われたコミットを復元するには、git reflog を実行し、目的のコミットのハッシュをコピーして、git checkout -b recovered <hash> で復元します。

HEAD~1 と入力するつもりで HEAD~2 と打ってしまい、3 時間分の作業が git log から消えていくのを目の当たりにする。Enter を押してから reflog の存在を思い出すまでのわずか数秒は、Git を使っていて最悪の瞬間です。git reset --hard を実行したとき、rebase を失敗したとき、あるいはブランチを削除したとき、コミットが本当に破棄されることはほとんどありません。単に orphan(孤立)状態になっただけで、そのハッシュは今も reflog に記録されています。本記事では、パニックを引き起こす 3 つのシナリオに対する高速な復旧手順を示し、続いて二度と作業を失わないために知っておくべき厳しい制約を解説します。以下のコマンドはすべてコピー&ペーストしてそのまま使えます。

重要なポイント

  • git reflog はローカルリポジトリにおける HEAD のあらゆる移動(commit、checkout、reset、rebase、merge)を記録しているため、「失われた」コミットは通常 git reset --hard <hash> 一発で戻せます。
  • 最も安全な復旧方法は git checkout -b recovered <hash> です。新しいブランチ上でコミットを再構築し、実際のブランチに手を加える前に内容を確認できます。
  • reflog はコミットされていない作業ディレクトリの変更を復元できません。Git がそもそもそれらを ref として記録していないためです。
  • Git はデフォルトで、到達可能な reflog エントリを 90 日間、到達不能なものを 30 日間保持します。したがって速やかに復旧し、コミットを取り戻すまでは git gc を実行しないようにしてください。
  • reflog は完全にローカルなものであり、push されることはありません。そのため自分自身が失った作業は救えますが、チームメイトの未 push のコミットは救えません。

Git reflog はどのように動作するのか?

Git の reflog は、ブランチの先端(tip)やその他の ref が保持してきたすべての位置を記録するローカルなログです。commit、checkout、reset、rebase、merge のいずれも HEAD を移動させ、その都度ログに記録されます。したがってコミットが「失われた」状態(どのブランチやタグからも参照されなくなった状態)になっても、それは orphan(孤立) になっただけで削除されたわけではなく、そのハッシュは再び参照されるのを待って reflog に残っています。

引数なしでコマンドを実行すると、HEAD の履歴を確認できます:

git reflog
b58145c HEAD@{0}: reset: moving to HEAD~2
dd7c37e HEAD@{1}: commit: one more commit
b5b3286 HEAD@{2}: checkout: moving from main to feature
b58145c HEAD@{3}: commit: very important commit

各行は次のように読みます: 短縮ハッシュHEAD@{n} インデックス(そのエントリが何回前の移動に相当するか)、そして操作の説明です。上の出力では、HEAD@{0} が今まさに実行された破壊的な reset であり、HEAD@{1}(dd7c37e)がそこから移動する前のコミット、つまり取り戻したいコミットです。内部的には、git reflog showgit log -g --abbrev-commit --pretty=oneline と同じ処理を実行しており、任意の ref を受け取れるため、git reflog show main とすれば特定のブランチの履歴を確認できます。

誤った hard reset から復旧する

git reset --hard はブランチポインタを移動させますが、古いコミットはオブジェクトストアにそのまま残ります。reflog でそれを見つけ、再度そこを指すようにします。reset 直前のコミットが失ったコミットであり、通常は HEAD@{1} です。

git reflog
# HEAD@{0}: reset: moving to HEAD~2
# HEAD@{1}: commit: one more commit   <-- this hash

git checkout -b recovered dd7c37e

最も安全な復旧方法は、上書きする前に内容を確認することです。git checkout -b recovered <hash> を実行して新しいブランチ上にコミットを再構築し、git log で検証してから、実際のブランチを動かすかどうかを判断します。確認が取れたら、ブランチを直接移動させることもできます:

git reset --hard dd7c37e     # or: git reset --hard HEAD@{1}

二重の危険に関する警告: git reset --hard は作業ツリーにある現在のコミットされていない変更をすべて破棄します。作業ツリーが dirty な状態なら、まず git stash するか、新しいブランチを作る手順を使ってください。そうしないと復旧コマンド自体が 2 度目の損失を引き起こします。reset の直後であれば git reset --hard ORIG_HEAD も使えます。git reset は移動する前にブランチの以前の先端を ORIG_HEAD に記録するためです。

失敗した rebase を取り消す

rebase は履歴を書き換えるため、コンフリクトの解決を誤るとコミットが黙って失われることがあります。reflog には rebase 前の先端が保持されています。rebase に失敗した後は、git reset --hard ORIG_HEAD でブランチを開始時点に戻せます。ただし ORIG_HEAD は次の reset、rebase、merge によって上書きされるため、その後にそうしたコマンドを実行してしまった場合は、代わりに git reflog から rebase 前の先端を探します。

git reflog
# HEAD@{0}: rebase (finish): returning to refs/heads/feature
# HEAD@{1}: rebase (pick): one more commit
# HEAD@{2}: rebase (start): checkout main
# HEAD@{3}: checkout: moving from main to feature  <-- pre-rebase tip

git reset --hard HEAD@{3}

rebase (start) のエントリを探してください。そのすぐ前の行が、rebase 前のブランチの状態です。全体を取り消すのではなく、rebase で失われた特定のコミットだけが必要な場合は、reflog からそのハッシュを取得して再適用します:

git cherry-pick <hash>

削除したブランチを復元する

ブランチを削除しても消えるのは ref であり、コミットではありません。そのブランチの最後のコミットは(最後に checkout したときの記録として)HEAD の reflog に残っているため、そのハッシュを見つけてブランチをその位置に再作成します。

git reflog
# HEAD@{0}: checkout: moving from example-branch to main
# HEAD@{1}: commit: very important commit   <-- last commit on deleted branch

git checkout -b example-branch b5b3286    # or HEAD@{1}

git checkout -b <branch> <hash> は、復元したコミットを指すブランチを、履歴をそのまま保った状態で再作成します。コミットメッセージの一部は覚えているがハッシュがわからない場合は、git log --oneline -g --grep='<fragment>' で reflog を検索できます。(git switch -ccheckout -b の現代的な等価コマンドです。どちらも使用可能で、checkout は非推奨ではありません。)

Git reflog で復元できないものは?

reflog は HEAD やブランチの先端を移動させたもの(コミット、reset、rebase、削除したブランチ)なら何でも復元できますが、コミットされていない作業ディレクトリの変更は復元できません。Git がそもそもそれらを ref として記録していないためです。一度もコミットされていない変更には、reflog が指し戻せる ref の更新が存在しないため、reflog を検索しても何も見つかりません。

reflog で復元できるものreflog で復元できないもの
reset --hard で孤立したコミット作業ツリー内の未コミットの編集
失敗した rebase で失われたコミットステージ済みだが未コミットの変更
削除したローカルブランチ(最近のもの)チームメイトの未 push のコミット
force-push されたリモートのローカルビューすでに期限切れになり gc されたエントリ

復旧を左右する制約はさらに 3 つあります:

  • ローカルかつ一時的である。 reflog はあなたの .git ディレクトリ内にのみ存在し、push されることはありません。そのため自分の作業は救えますが、チームメイトの未 push のコミットは救えません。
  • エントリは期限切れになる。 Git はデフォルトで、gc.reflogExpiregc.reflogExpireUnreachable により、到達可能な reflog エントリを 90 日間、到達不能なものを 30 日間保持します。孤立したコミットは期限切れとガベージコレクションの実行までは残るため、速やかに復旧し、作業が完了するまでは git gc を実行しないようにしてください。
  • まず新しいブランチに復元する。 git checkout -b recovered <hash> をデフォルトの手順にすれば、共有ブランチを変更する前に検証できます。また、わかりやすいコミットメッセージを書きましょう。reflog にはメッセージが表示されるため、「Fix login bug」のような具体的なものは曖昧なものよりずっと見つけやすくなります。

おまけ(force-push 後): リモートサーバー側には読める reflog はありませんが、ローカルの追跡 ref にはあります。force-push によってリモートから消されたコミットを復元するには、git reflog show origin/<branch> でローカルの記録を確認してください。force-push の直前のエントリが古い先端を保持しています。

まとめ

reflog は Git のローカルなセーフティネットであり、「作業を失った」というほぼすべての事態を 2 つのコマンドで解決できる問題に変えてくれます。ログを読み、ハッシュを指し直すだけです。唯一救えないのは一度もコミットしていない作業なので、変わらない教訓は「早く、頻繁にコミットする」ことです。そうすれば見つけられる reflog エントリが必ず存在します。次にターミナルの表示で背筋が凍ったときは、何よりも先に git reflog を実行してください。

FAQ

git reflog と git log の違いは何ですか?

git log はブランチの先端から親ポインタを辿って到達できるコミット履歴を表示するため、孤立したコミットがそこに現れることはありません。一方 git reflog は、ローカルリポジトリで HEAD や ref が占めてきたすべての位置(コミット、checkout、reset、rebase、merge)を表示し、もはやどのブランチからも参照されていないコミットも含まれます。これが、hard reset の後に git log では見つけられないコミットを reflog なら見つけられる理由です。コミット自体は孤立していても、その ref の更新はログに残っているからです。

リポジトリをクローンした後でも git reflog は機能しますか?

いいえ。reflog はローカルの .git ディレクトリにのみ保存され、push も fetch もされないため、新しくクローンしたリポジトリの reflog は空の状態から始まり、クローン以降に行った操作のみが記録されます。クローン前の HEAD の移動を表示することはできず、チームメイトのローカル履歴を明らかにすることもできません。reflog は自分のリポジトリで自分が失った作業を救うものであり、共有されたりリモートに保存されたりする復旧ツールではありません。

git reflog のエントリが期限切れになった後でもコミットを復元できますか?

reflog 自体では復元できません。デフォルトでは到達可能なエントリは 90 日後、到達不能なものは 30 日後に期限切れとなり、ガベージコレクションが孤立オブジェクトを削除してしまえば本当に失われます。それ以前であれば、git fsck --lost-found でオブジェクトストア内の dangling コミットが一覧表示されるため、ハッシュを見つけられる場合があります。確実な対応は、速やかに復旧し、コミットを取り戻すまで git gc を実行しないことです。

ハッシュがわからない場合、失われたコミットはどうやって見つければよいですか?

reflog を直接検索します。git log --oneline -g --grep='fragment' を実行してコミットメッセージの一部で reflog エントリを絞り込むか、git reflog --date=relative で時刻順にエントリを確認し、取り消したい操作を特定します。reflog エントリが存在しない孤立コミットについては、git fsck --lost-found で dangling コミットが一覧表示されるので、git show で内容を確認したうえで git checkout -b recovered <hash> により新しいブランチへ復元できます。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.