コンテンツにスキップ

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つのことを同時にやる。

  1. indexをHEADに戻すgit add した内容がステージから消える
  2. 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 する習慣が最大の保険になる。