git addした変更が消えた?git fsckでステージ済みファイルを復元する方法¶
対象: git add した変更が突然消えて焦っている人
この記事のポイント¶
- なぜ消えるのか
git checkout HEAD -- <path>がindexとworking treeの両方を巻き戻す仕組み - なぜ復元できるのか
git add時点でblobオブジェクトが保存され、参照が切れても即座には消えない - 復元手順
git fsck --lost-found→ blob特定 → ファイル復元の3ステップ
事故の典型パターン¶
リファクタリング作業で15ファイルほど git add していた。別の一括置換を取り消すため、こう打った:
git checkout HEAD -- docs/
ステージ済みの変更が全て消えた。 git status はクリーン。git log にも残っていない。
結論: git add 済みなら復元できる。 git gc さえ走っていなければデータは残っている。急いでいるなら復元手順(Step 1)へ。
復元の前提条件
git gc が実行されていないこと。git gc は参照のないオブジェクトを削除する。事故に気づいたら絶対に git gc を実行しないこと。
なぜ消えて、なぜ残っているのか¶
git checkout HEAD -- <path> は2つのことを同時にやる。
- indexをHEADに戻す →
git addした内容がステージから消える - working treeをHEADに戻す → ファイルの中身も元に戻る
一方、Gitは git add した時点でファイル内容をblobオブジェクトとして .git/objects/ に保存している。indexから参照が外れても、blobオブジェクト自体はすぐには消えない。
どこからも参照されなくなったこのオブジェクトをdangling blobと呼ぶ。データは .git/objects/ の中に残っている。問題は「どのblobが自分のファイルか」を特定することだ。
復元手順¶
Step 1: dangling blobsを掘り出す¶
git fsck --lost-found
.git/lost-found/other/ にdangling blobsやdangling treesがファイルとして書き出される(dangling commitsは .git/lost-found/commit/ に分離される)。
$ ls .git/lost-found/other/ | wc -l
314
件数はリポジトリの履歴量に依存する。数十〜数百個が一般的だ。ここから目的のファイルを探す。
先に件数だけ確認したい場合
git fsck --dangling でファイル書き出しなしに件数と種類を確認できる。
git fsck --dangling 2>&1 | head -10
Step 2: blobを特定する¶
候補から目的のファイルを見つける方法は2つある。
- 方法A(キーワード検索) — ファイル内容に覚えている文字列があるとき向き
- 方法B(サイズ比較) — 内容に特徴がない、またはバイナリに近いファイル向き
両方を組み合わせると精度が上がる。
方法A: ファイル内容のキーワードで絞り込む¶
ファイルの中身で覚えている文字列を grep する。関数名、クラス名、変数名、コメント、見出しなど何でもいい。
# 覚えている文字列で検索
for blob in $(ls .git/lost-found/other/); do
if grep -ql 'handleSubmit' \
".git/lost-found/other/$blob" 2>/dev/null; then
echo "FOUND: $blob"
fi
done
複数候補が出た場合はサイズや先頭数行を見て絞り込む。
# 候補の先頭5行とサイズを確認
for blob in $(ls .git/lost-found/other/); do
if grep -ql 'handleSubmit' \
".git/lost-found/other/$blob" 2>/dev/null; then
size=$(wc -c < ".git/lost-found/other/$blob")
echo "=== $blob ($size bytes) ==="
head -5 ".git/lost-found/other/$blob"
echo
fi
done
方法B: HEADのblobサイズで候補を絞る¶
HEADのblobサイズを基準にして、サイズが近い候補を絞る。
# HEADのサイズを取得
head_size=$(git cat-file -s \
$(git rev-parse HEAD:"path/to/file.md"))
echo "HEAD: $head_size bytes"
# サイズが近いblobを探す
for blob in $(ls .git/lost-found/other/); do
size=$(wc -c < ".git/lost-found/other/$blob")
if [ "$size" -gt $((head_size - 2000)) ] \
&& [ "$size" -lt $((head_size + 5000)) ]; then
echo "$blob ($size bytes)"
fi
done
同じtitleで複数候補が見つかった場合、サイズが最大のものが最新版である可能性が高い。 ただしmerge conflictマーカー(<<<<<<<)が入っている版は避ける。
Step 3: 復元する¶
特定できたら cp するだけ。
cp .git/lost-found/other/<blob_hash> path/to/file.md
復元後、HEADとの差分が元の変更内容と一致するか確認する。
git diff path/to/file.md
復元できないケース¶
| 状況 | 復元可否 |
|---|---|
git checkout HEAD 直後 | 可能 |
git gc 実行後 | 不可 |
git gc --prune=now 実行後 | 不可 |
| 数週間放置(自動gcの可能性) | 要確認 |
git add していなかった変更 | 不可 |
最後の行が重要だ。git add していないworking treeのみの変更は、Gitのオブジェクトストアに保存されていないため、復元手段がない。
予防策: git stashで退避してから操作する¶
そもそも事故を起こさないのが最善。ファイルを巻き戻す前に1コマンド挟むだけでいい。
# 1. ステージ済み変更を退避
git stash --include-untracked -m "before bulk operation"
# 2. 安全に巻き戻す
git checkout HEAD -- docs/
# 3. 退避を戻す
git stash pop
git stash はindexとworking treeの両方を保存する。git stash pop で完全に元の状態に戻せる。
Gitオブジェクトモデルの理解が復元力になる¶
git add → blob保存 → index参照。この流れを知っていれば、「indexから外れてもblobは残る」と気づける。
Gitの内部構造を知る意味は、日常操作を速くすることではない。 事故が起きたときに冷静でいられることだ。
関連: 機密情報を誤ってpushした場合の対処
blobの残存は復元に使えるが、逆に削除したいものが残り続ける問題にもなる。 APIキーを誤ってcommitした場合の対処は GitHubに機密情報を誤って公開した時の完全対処ガイドを参照。
よくある質問¶
Q: git addした変更は復元できる?
git gc が走っていなければ復元できる。 git add した時点でGitはファイル内容をblobオブジェクトとして保存する。indexから参照が外れてもblob自体は残るため、git fsck --lost-found で掘り出せる。
Q: git gcした後でも復元できる?
できない。 git gc は参照のないオブジェクト(dangling blob)を削除する。デフォルトでは2週間以上前のオブジェクトが対象だが、git gc --prune=now を実行すると即座に全て消える。
Q: git addしていない変更は?
復元できない。 git add していないworking treeの変更はGitのオブジェクトストアに保存されないため、どの方法でも復元できない。こまめに git add する習慣が最大の保険になる。