Git入門:バージョン管理のきほん

消したはずの作業を取り戻す(reflog)

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部
やらかしからの戻し方

このレッスンで分かること

  • reset --hard で消えたように見えたコミットが、まだ残っている理由
  • git reflog の出力の読み方と HEAD@{n} の指定方法
  • reflog が自分の手元にしか無いという性質と、その意味

消したつもりのコミットは消えていない

コミットを取り消す(reset の 3 モード)で、git reset --hard はコミットも作業ディレクトリの変更もまとめて捨てる操作だと学びました。強力なぶん、指定を間違えると半日の作業がまるごと消えたように見えます。

ターミナル

$ git log --oneline 9a1f4c7 検索フォームのテストを追加 3e8b2d5 検索フォームを実装 8f3c1a2 ログイン機能を追加 $ git reset --hard HEAD~2 HEAD is now at 8f3c1a2 ログイン機能を追加 $ git log --oneline 8f3c1a2 ログイン機能を追加

9a1f4c73e8b2d5 が履歴から消えました。エディタを見ても、書いたはずのファイルが元に戻っています。

ここで手を止めてください。この 2 つのコミットは、まだリポジトリの中に残っています。消えたのは「そこへたどり着くための道」だけです。

コミットは、いったん作られると .git ディレクトリの中に保存されます。git log が表示するのは、ブランチや HEAD という目印からたどれるコミットだけです。HEAD は「今いる場所」を指す目印のことです。reset は目印を動かす操作なので、動かした先からたどれなくなったコミットが、見えなくなるだけなのです。

その「見えなくなったコミット」への道を教えてくれるのが git reflog です。

git reflog を読む

reflog は reference log の略で、HEAD やブランチがどこを指していたかの移動履歴です。コミットだけでなく、switchmergeresetcommit --amend など、目印が動いたすべての操作が記録されています。

さきほどの状態で実行してみます。

ターミナル

$ git reflog 8f3c1a2 HEAD@{0}: reset: moving to HEAD~2 9a1f4c7 HEAD@{1}: commit: 検索フォームのテストを追加 3e8b2d5 HEAD@{2}: commit: 検索フォームを実装 8f3c1a2 HEAD@{3}: commit: ログイン機能を追加 1b7d0a4 HEAD@{4}: checkout: moving from main to feature/login

git log では見えなかった 9a1f4c73e8b2d5 が、ちゃんと並んでいます。

各行の読み方は次の通りです。

部分意味
8f3c1a2その時点で HEAD が指していたコミットのハッシュ値
HEAD@{0}何手前の状態か。{0} が今、数字が大きいほど過去
reset: moving to HEAD~2そのとき実行した操作

HEAD@{0} が現在地です。HEAD@{1} は「1 つ前の状態」、HEAD@{2} は「2 つ前の状態」を意味します。上の出力では、HEAD@{1} が事故の直前、つまり 9a1f4c7 にいた状態です。

操作の種類は、行の後半を見れば分かります。よく出るものは次の通りです。

表示何をしたときの記録か
commit:普通にコミットした
commit (amend):git commit --amend でコミットを作り直した
reset: moving to ...git reset で目印を動かした
checkout: moving from A to Bブランチ A から B へ移動した
merge <ブランチ名>:マージした
rebase (finish):rebase が完了した

つまり reflog は「事故が起きる直前に自分が何をしたか」の記録でもあります。原因が分からないまま履歴が変になったときは、まずここを上から読んでください。どの操作で状態が変わったのかが特定できます。

reflog の並び順は「操作の順番」であって、コミットの親子関係ではありません。git log はコミットのつながりをさかのぼりますが、reflog は自分がやった操作を時系列で並べているだけです。この違いを押さえておくと、出力に同じハッシュ値が何度も出てきても混乱しません。

戻す前に中身を確かめる

いきなり reset を打たず、まず目当てのコミットで合っているかを確認します。

ターミナル

$ git show HEAD@{1} --stat commit 9a1f4c7e2b8d5a3f01c6e4d7b9a2f5308c1e6b4d Author: Taro Yamada <taro@example.com> Date: Fri Jul 31 14:22:10 2026 +0900 検索フォームのテストを追加 tests/search.test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+)

今の状態との差分を見るなら git diff です。

ターミナル

$ git diff HEAD HEAD@{1} --stat src/search.js | 48 ++++++++++++++++++++++++++++++++++++++++++ tests/search.test.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+)

消えた 2 コミットぶんの中身が、そのまま残っていることが確認できました。

reset --hard HEAD@{2} で取り戻す

ここでは事故の直後なので、戻したい状態は HEAD@{1} です。ただし、確認のために何か操作をすると reflog に行が増えて番号がずれるので、実務では HEAD@{2}HEAD@{3} を指定する場面の方が多くなります。番号を打つ前に必ず git reflog を出し直してください。

たとえば、事故に気づいてから一度別のブランチに移ってしまった場合はこうなります。

ターミナル

$ git reflog 8f3c1a2 HEAD@{0}: checkout: moving from main to feature/login 1b7d0a4 HEAD@{1}: checkout: moving from feature/login to main 8f3c1a2 HEAD@{2}: reset: moving to HEAD~2 9a1f4c7 HEAD@{3}: commit: 検索フォームのテストを追加

この場合、戻したいのは HEAD@{3} です。指定を確定したら実行します。

ターミナル

$ git reset --hard HEAD@{3} HEAD is now at 9a1f4c7 検索フォームのテストを追加 $ git log --oneline 9a1f4c7 検索フォームのテストを追加 3e8b2d5 検索フォームを実装 8f3c1a2 ログイン機能を追加

消えた 2 コミットが履歴に戻りました。作業ディレクトリのファイルも、そのときの状態に復元されています。

ハッシュ値を直接指定してもかまいません。むしろ、番号のずれを気にしなくて済むぶん確実です。

ターミナル

$ git reset --hard 9a1f4c7

reset --hard は、今の作業ディレクトリの変更も捨てます。救出しようとして、別の書きかけを巻き添えにしては本末転倒です。事故に気づいた時点で書きかけがあるなら、先に git stash -u で退避してから救出してください。退避のやり方は作業を一時退避する(stash)にあります。

上書きせずに救出する方法

現在のブランチを動かしたくない場合は、消えたコミットから新しいブランチを作る方が安全です。

ターミナル

$ git branch rescue 9a1f4c7 $ git switch rescue Switched to branch 'rescue'

これなら今のブランチはそのままで、救出した状態を別の場所に確保できます。中身を確認してから、必要なぶんだけ取り込めば済みます。取り返しの付かない操作を減らせるので、慣れないうちはこちらを勧めます。

ブランチごと消してしまった場合も同じ手が使えます。git branch -D で削除したときの出力には、消えた時点のハッシュ値が残っています。

ターミナル

$ git branch -D feature/search Deleted branch feature/search (was 9a1f4c7).

これを控えていなくても、reflog にはブランチ上で作ったコミットが記録されています。ブランチ単位の記録だけを絞って見たいときは、ブランチ名を付けて実行してください。

ターミナル

$ git reflog show feature/search

なお、reflog は HEAD の移動だけでなく、各ブランチの移動も別々に記録しています。.git/logs/HEAD が全体の記録、.git/logs/refs/heads/mainmain の記録です。普段は git reflog(実体は git reflog show HEAD)だけ覚えておけば足ります。

reflog は自分の手元にしか無い

ここが最も重要な性質です。reflog はリモートに送られません。

reflog は、自分のパソコンの .git/logs/ の中に記録されています。「自分がこのリポジトリで何をしたか」の記録なので、git push しても GitHub には上がりませんし、git clone しても相手の reflog は付いてきません。同じリポジトリでも、人によって reflog の中身はまったく違います。

ここから、実務上の結論が 2 つ出てきます。

  1. 事故のあとにリポジトリを消して clone し直してはいけない。 reflog ごと消えるので、救出できたはずのコミットが本当に失われます。「おかしくなったから入れ直そう」がいちばん危険な判断です
  2. 別のパソコンからは救出できない。 会社のパソコンで消したコミットを、自宅のパソコンの reflog からは取り出せません

もうひとつ、reflog には保存期限があります。既定では、どこからも到達できないコミットは 30 日程度、たどれるものは 90 日程度で整理の対象になります。事故に気づいたら、その日のうちに救出してください。

また、reflog はリポジトリごとに独立しています。同じ GitHub リポジトリを 2 か所にクローンしていれば、それぞれ別の reflog を持ちます。片方で消した作業を、もう片方から探すことはできません。

reflog にも救えないものがあります。一度も git add していない、コミットされていない変更です。reflog が記録しているのは目印の移動だけなので、コミットになっていない編集は対象外です。git reset --hard で消した「書きかけのファイル」が戻らないのはこのためです。だからこそ、区切りが付いたらこまめにコミットする価値があります。

reflog は最後の命綱であって、日常の道具ではありません。「あとで reflog から戻せるから」と reset --hard を雑に使うのは順番が逆です。危なそうな操作の前に git branch backup で目印を 1 本立てておけば、そもそも reflog を探す必要がなくなります。

よくある失敗と、そこからの戻し方

reflog を使い始めた人がやりがちなのが、HEAD@{2} の番号を古い出力のまま指定してしまうことです。

ターミナル

$ git reset --hard HEAD@{2} HEAD is now at 1b7d0a4 READMEを更新

まったく身に覚えのないコミットに移動してしまいました。reflog は操作するたびに行が増えるので、さきほどメモした番号は、もうずれています。

これも慌てる必要はありません。今の reset 自体が reflog に記録されているので、もう一度 git reflog を出して、正しい行のハッシュ値を直接指定し直すだけです。

ターミナル

$ git reflog -5 1b7d0a4 HEAD@{0}: reset: moving to HEAD@{2} 8f3c1a2 HEAD@{1}: checkout: moving from main to feature/login 1b7d0a4 HEAD@{2}: checkout: moving from feature/login to main 8f3c1a2 HEAD@{3}: reset: moving to HEAD~2 9a1f4c7 HEAD@{4}: commit: 検索フォームのテストを追加 $ git reset --hard 9a1f4c7 HEAD is now at 9a1f4c7 検索フォームのテストを追加

HEAD@{n} はその場かぎりの番号だと考え、確定したらハッシュ値で指定する。これを癖にしておけば、reflog はかなり安全に使えます。

なお Windows の Git Bash や PowerShell では、HEAD@{1} の波かっこがシェルに解釈されて動かないことがあります。そのときは "HEAD@{1}" のようにダブルクォートで囲んでください。Mac のターミナル(zsh)でも同様に囲むのが確実です。

この章のポイント
  • reset --hard で消えたコミットは、道が切れただけでリポジトリには残っている
  • git reflog は目印の移動履歴。ここから消えたコミットのハッシュ値を見つけられる
  • 戻すのは git reset --hard <ハッシュ値>。番号はずれるのでハッシュ値指定が確実
  • reflog は自分の手元にしか無い。事故のあとに clone し直すと本当に失われる