git reflog: 失われたコミットとブランチを復元する方法
git reflog は、HEAD とブランチの移動を記録するローカルのログです。コミット、reset、checkout、rebase、merge などの操作がすべて記録されます。エラーの前の記録(HEAD@{2} など)を git reflog で見つけ、git reset --hard HEAD@{2} で状態を戻すか、git branch rescue HEAD@{2} でブランチとして保存します。
git reflog は参照のログです。Git は HEAD とブランチの先端が動くたびに、その記録を残します。コミットしたとき、別のブランチに切り替えたとき、reset、rebase、merge を実行したときなど、どの操作の後も、そのときに HEAD が指していたコミットのハッシュが1行追加されます。そのため、どのブランチからも参照されなくなったコミットでも、見つけて戻すことができます。
覚えておくべきことが1つあります。reflog はローカルのものです。自分のクローンの中にだけあり、サーバーには送られず、git clone でも引き継がれません。しかし自分のリポジトリでは、履歴に関するほとんどのミスから救ってくれます。
git reflog の出力の読み方
git reflog
3c4d5e6 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
9a8b7c6 HEAD@{1}: commit: Добавить валидацию e-mail
1f2e3d4 HEAD@{2}: commit: Вынести конфиг в отдельный файл
3c4d5e6 (HEAD -> main) HEAD@{3}: checkout: moving from feature to main
7b6a5c4 (feature) HEAD@{4}: commit: Черновик формы входа
各行は HEAD の1つの状態を表し、新しいものが上に来ます。
- ハッシュ: 操作の後に HEAD が指していたコミット。
- HEAD@{n}: 「HEAD が n 回前の移動で指していた場所」。これは本物の参照で、どのコマンドにも渡せます。
- 説明: 操作の種類(commit、reset、checkout、rebase、merge、pull、cherry-pick、commit (amend))と詳細。
この例では、「Вынести конфиг」と「Добавить валидацию」の2つのコミットが作られた後、reset HEAD~2 がブランチを戻しています。git log にはもう表示されていませんが、reflog には残っています。
ブランチごとの reflog
ブランチごとに独立した記録があります。ブランチを頻繁に切り替える場合は、こちらの方が短く、役に立ちます。
git reflog show feature
git reflog show origin/main # fetch によってリモートのブランチがどう変わったか
時刻による参照
番号の代わりに時刻を指定すると、その時点で参照がどこを指していたかを Git が探してくれます。
git show main@{yesterday}
git diff main@{"2 hours ago"} main
git log -g --since="1 day ago" --oneline
git log の -g(--walk-reflogs)は、親のつながりではなく reflog をたどります。これにより、通常の書式やフィルターを使えます。日付を表示するには git reflog --date=iso が便利です。
git reset --hard の後でコミットを戻す
最もよくある状況は、git reset --hard を誤ったコミットに対して実行し、作業が「消えた」ように見える場合です。実際にはコミットは消えておらず、どのブランチからも見えなくなっているだけです。
- ログを開く:
git reflog。 reset: moving to ...の直前の行を探す。これがミスの前の状態です。上の例では、ハッシュ9a8b7c6のHEAD@{1}です。- 本当にそれかを確認する:
git log --oneline -5 HEAD@{1}。 - ブランチを戻す:
git reset --hard HEAD@{1}。
git reset --hard HEAD@{1}
HEAD is now at 9a8b7c6 Добавить валидацию e-mail
注意してください。操作が行われるたびに番号がずれます。reset 後には、元の HEAD@{1} は HEAD@{2} になります。そのため、番号ではなくハッシュをコピーする方が確実です。
重要な制限があります。reflog に残るのはコミットされたものだけです。reset --hard や git checkout -- ファイル で消えた未コミットの変更は、reflog からは戻せません。ただし、ファイルが一度インデックスに追加されていた場合は、まだ望みがあります(後述)。
削除したブランチを復元する
git branch -D feature でブランチを削除したが、そこにはまだ送っていないコミットがあった、という場合です。ブランチ自身の記録は一緒に消えますが、HEAD の記録は残ります。あなたがそのブランチ上で作業したとき、HEAD はそのコミットを指していたからです。
git reflog | grep feature
7b6a5c4 HEAD@{4}: commit: Черновик формы входа
3c4d5e6 HEAD@{3}: checkout: moving from feature to main
# ブランチの最後のコミットは、そこから離れたときのもの
git branch feature 7b6a5c4
checkout: moving from feature to ... の行を探してください。その直後(下方向)の記録のハッシュが、ブランチの最後の状態です。ちなみに、ブランチを削除すると Git は Deleted branch feature (was 7b6a5c4). と表示します。ターミナルがまだ開いていれば、そのハッシュを使えます。
失敗した rebase を取り消す
rebase はコミットを書き換えるため、途中でコンフリクトの解決を誤ると、元より悪い結果になることがあります。reflog には、rebase を始める前の状態が残っています。
git reflog
e1f2a3b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e1f2a3b HEAD@{1}: rebase (pick): Черновик формы входа
c4d5e6f HEAD@{2}: rebase (start): checkout main
7b6a5c4 HEAD@{3}: commit: Черновик формы входа
git reset --hard HEAD@{3}
必要なのは rebase (start) の前の記録です。もっと手早い方法もあります。reset、rebase、merge などの危険な操作の前に、Git は以前の位置を ORIG_HEAD に保存するので、rebase の直後であれば git reset --hard ORIG_HEAD で戻せます。ただし ORIG_HEAD は次の同様の操作で上書きされます。一方 reflog は履歴全体を保持します。
git commit --amend の前のコミットを戻す
--amend はコミットを変更するのではなく、その場所に新しいコミットを作ります。古いコミットは reflog に残ります。
git reflog
5a6b7c8 HEAD@{0}: commit (amend): Добавить валидацию e-mail
9a8b7c6 HEAD@{1}: commit: Добавить валидацию e-mail
git diff HEAD@{1} HEAD # amend が何を変えたか
git reset --soft HEAD@{1} # 古いコミットに戻す。amend の変更はインデックスに残る
安全な復元: reset の代わりにブランチを作る
どのコミットが必要か確信が持てない場合は、reset --hard を盲目的に使わないでください。救助用のブランチを作るのが安全です。何も壊さず、見つけたコミットを削除から守ります。
git branch rescue HEAD@{5}
git log --oneline rescue
git switch rescue
その後、rescue のコミットを cherry-pick やマージで移し、すべてが元どおりになったら、このブランチを削除します。
reflog はどれくらい残るか
記録は永久には残りません。Git が定期的に実行する git gc が古い記録を整理します。保存期間は設定で決まります。
| 設定 | 既定値 | 意味 |
|---|---|---|
gc.reflogExpire | 90 日 | 現在のブランチから到達できるコミットの記録の期限 |
gc.reflogExpireUnreachable | 30 日 | どのブランチからも到達できないコミット、つまり「失われた」コミットの記録の期限 |
gc.pruneExpire | 2 週間 | 何からも参照されなくなった到達不能なオブジェクトが削除されるまでの期間 |
実践的な結論は、失われたコミットはおよそ1か月間 reflog で確実に見つけられるということです。復元をためらったり、必要なものを戻す前に git gc --prune=now や git reflog expire --expire=now --all を手動で実行したりしないでください。
reflog で見つからない場合: git fsck
reflog にコミットがないこともあります。たとえば、git stash が作ったコミットを git stash drop で削除した場合や、変更がインデックスにしかなかった場合です。その場合は「宙に浮いた」オブジェクトを探します。
git fsck --lost-found
dangling commit 2b3c4d5e6f...
dangling blob 8e9f0a1b2c...
git show 2b3c4d5e6f # 内容を確認
git branch recovered 2b3c4d5e6f
削除された stash の宙に浮いたコミットには「WIP on ...」というメッセージが付いています。宙に浮いた blob は、かつて git add されたもののコミットされなかったファイルの内容です。ファイル名は残っていないため、内容から判断する必要がありますが、reset --hard の後で作業を救えることもあります。--lost-found を付けると、Git はこれらを .git/lost-found/ にもコピーします。
reflog にできないこと
- 別のマシンでは使えない。 同僚が強制 push でサーバー上のあなたのコミットを上書きしてしまった場合、そのコミットを持っていた人の reflog を探します。たとえば、上書きより前に fetch していた人の
git reflog show origin/mainです。 - 未コミットの変更は見えない。 危険な操作の前の最も確実な保険は、コミットするか、
git stashすることです。 - 永久には保存されない。 期限は上のとおりです。また、新しく clone したリポジトリの記録は空から始まります。
実践: 危険な操作の前の保険
reflog は救ってくれますが、正しい記録を複数の似たものの中から見つけるには注意深さが必要です。もっと簡単なのは、あらかじめ目印を付けておくことです。rebase、大きな reset、履歴の実験の前には、一時的なブランチかタグを作ります。
git branch backup/before-rebase
git rebase -i main
# 何かがうまくいかなかった場合
git reset --hard backup/before-rebase
# すべてうまくいった場合
git branch -D backup/before-rebase
これは1つのコマンドで済み、ハッシュを探す手間が省けます。reflog は、保険を思い出すのが遅すぎた場合の予備です。
正しい記録を素早く見つける
長いログでは絞り込みが役立ちます。操作の種類やコミットメッセージで探し、不確かな場合は内容を見ます。
git reflog | grep -E "reset|rebase \(start\)"
git log -g --grep="валидац" --oneline
git show --stat HEAD@{7}
git log -g に --grep を付けると、reflog に記録されたコミットのメッセージで検索できます。git show --stat は変更されたファイルを表示するので、メッセージが「wip」のように分かりにくくても、どのコミットかを見分けられます。
まとめ
git reflog は Git の安全網です。過去数週間にリポジトリでコミットしたものは、ほぼすべて見つけて戻せます。手順はいつも同じです。ログを開き、ミスの前の記録を見つけ、git log や git show で確かめてから、ブランチを作るか reset --hard を実行します。restore、reset、revert、そして失敗からの復旧の扱い方は、Gitコースで実践的に解説しています。
よくある質問
git reflog と git log の違いは何ですか?
git log は親のつながりをたどってブランチの履歴を表示します。git reflog は、リポジトリ内で HEAD とブランチが移動した場所を表示します。ブランチの履歴にもう含まれないコミットも含まれます。
reflog は GitHub や GitLab にも送られますか?
いいえ。reflog はローカルの .git/logs フォルダにだけあり、push や clone では送られません。
reflog を消すことはできますか?
はい。git reflog expire --expire=now --all を実行し、その後 git gc --prune=now を実行します。ただし、その後は失われたコミットを復元できなくなります。
reflog はどこにファイルとして保存されていますか?
テキストファイルの .git/logs/HEAD と、.git/logs/refs/heads/ブランチ名 に、移動1回につき1行ずつ記録されています。
bare リポジトリ(サーバー)でも reflog は使えますか?
既定では bare リポジトリの reflog は無効です(core.logAllRefUpdates)。そのため、サーバー側で reflog に頼ることはできません。 </content>