破損したGitレポジトリからファイルを救出してみた
はじめに
こんにちは、SHIFTのDAAE部に所属しているKyselovです。
先日の事象
先日VSCodeで開発していたところ、突然自動再接続のメッセージが出てきました。
またWSLが落ちてしまったのかと思い、WSLを再起動しました。
VSCodeで再読込みをした後、プロジェクトをビルドしようと思ったら次のエラーメッセージが出てきました。
$ pnpm install
ERR_PNPM_JSON_PARSE Unexpected end of JSON input while parsing empty string in /package.json
あれ?と思って、package.jsonの中身を確認したらなんと空でした!
他のファイルを確認しても、最近編集したものであれば全部空でした。
しかし、ソース管理タブでの変更一覧に何も記載されていないのはおかしいですね。
ログを確認しようと思ったらようやく問題に気付きました。
$ git log
error: object file .git/objects/7c/43f833d4b0805e98de038d00c8f68d08266ee6 is empty
fatal: loose object 7c43f833d4b0805e98de038d00c8f68d08266ee6 (stored in .git/objects/7c/43f833d4b0805e98de038d00c8f68d08266ee6) is corrupt
WSLが落ちた時にGitレポジトリの一部が破損したようです。
ほとんどのgitコマンドが同じエラーになり、git fsckというエラーチェックコマンドも例外ではありません。
このとき、「新しいタスクに着手してからのブランチをまだプッシュしていない状態」でした。
こういう場合に、ファイルを救出する方法がないかを確認していきます!
救出の試行錯誤
まずはレポジトリ本体である.gitフォルダをバックアップします。
$ cp -r .git .git.bak
次に、エラーメッセージに書かれている.git/objects/7c/43f833d4b0805e98de038d00c8f68d08266ee6ファイルの中身を確認します。
そちらも空ですし、他にもいくつかの空ファイルがあるみたいです。
$ find .git/objects/ -type f -empty
.git/objects/7c/43f833d4b0805e98de038d00c8f68d08266ee6 .git/objects/f2/e9075a6f4dbf4107c0ca1374ef927fe4dc7eac .git/objects/aa/6c05e219a0788a0920326941c52d2cf2ed20ff .git/objects/a6/88376fa9f756be6ee133b540f89bd8d4651c18
バックアップがあるので空ファイルを削除してみます。
$ find .git/objects/ -type f -empty -delete
それから削除した部分をリモートから再度fetchできないか試しますが、結果は失敗でした。
$ git fetch
From https://github.com/project/api
fatal: bad object refs/heads/feat/branchA
error: https://github.com/project/api.git did not send all necessary objects
これはbranchAをまだプッシュしていなかったことが原因です。
ここで足りないオブジェクトを全てリモートから取得できれば、問題は無事に解決できたでしょう。しかし、残念ながら私の場合はそうはいきませんでした。
最初にエラーになっていたエラーチェックをもう一度実行してみます。
fsck(ファイルシステム一貫性チェック)のコマンドを使うとGitのデータベースに問題がないか確認できます。
$ git fsck
Checking object directories: 100% (256/256), done.
Checking objects: 100% (94641/94641), done.
error: refs/heads/feat/branchA: invalid sha1 pointer 7c43f833d4b0805e98de038d00c8f68d08266ee6
error: HEAD: invalid sha1 pointer 7c43f833d4b0805e98de038d00c8f68d08266ee6
error: HEAD: invalid reflog entry f2e9075a6f4dbf4107c0ca1374ef927fe4dc7eac
error: HEAD: invalid reflog entry 7c43f833d4b0805e98de038d00c8f68d08266ee6
error: refs/heads/feat/branchA: invalid reflog entry f2e9075a6f4dbf4107c0ca1374ef927fe4dc7eac
error: refs/heads/feat/branchA: invalid reflog entry 7c43f833d4b0805e98de038d00c8f68d08266ee6
error: a688376fa9f756be6ee133b540f89bd8d4651c18: invalid sha1 pointer in cache-tree
dangling commit 7a013c0792c7c9b1cd84ae76df49cd76ada38c78
dangling commit d30f0cc189f4c5f301fd2a0ad855372d309762a9
(...)
今度のエラーチェックは無事に実行できましたが、エラーの数は約300個です。
ざっくり解説すると
refs/heads/feat/branchA: invalid sha1 pointer 7c43f833: branchAはWSLが落ちるまでに作業していたブランチ名で、救出したいブランチです。そのブランチの先端を示すコミットが失われたみたいです。
HEAD: invalid sha1 pointer 7c43f833: こちらも同じ状態のHEADです。HEADというのはチェックアウト中の最新データへのポインタです。急にファイル内容が空になった原因はHEADが失われたからです。
error: HEAD: invalid reflog entry 7c43f833: reflogも当たり前ながら同じエラーです。reflogというのは動作履歴のようなもので、HEADの移動を記録します。git reflogコマンドを使うと、自分が行ったcheckoutやcommitなどを確認できます。
dangling commit 7a013c07: こちらは特にエラーではないdanglingコミットです。dangling(宙ぶらりん状態の)オブジェクトはその内容へのポインタが存在しないデータです。例えば、新しいコミットBをした後、その前のコミットAにgit resetするとコミットBがdanglingコミットになります*
*正確にはコミットAがとある期間reflogだけに残ります。reflogの保存期間が過ぎたら、そのコミットはdangling状態になり、いずれ他のdanglingオブジェクトと同じくGC(ガベージコレクタ)に要らないものとして回収されます。
エラーログに書かれているブランチやハッシュを直接checkoutしようとしても、もちろん無意味です。
fatal: bad object 7c43f833d4b0805e98de038d00c8f68d08266ee6
danglingオブジェクトだけでも救出しましょう。ブランチの先端コミットが失われたら親コミットへのポインタも消えて、そのコミットがdangling状態になります。
そういうコミットはいずれGCに削除されるので、そうなる前にfsckの--lost-foundパラメータでポインタを付けましょう。
$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
Checking objects: 100% (94641/94641), done.
error: refs/heads/feat/branchA: invalid sha1 pointer 7c43f833d4b0805e98de038d00c8f68d08266ee6
dangling commit 7a013c0792c7c9b1cd84ae76df49cd76ada38c78
(...)
dangling commit 14f9db222d58c8c4209391b176a40452a48d4b9e
Verifying commits in commit graph: 100% (9698/9698), done.
実行後、danglingオブジェクトへのポインタは.git/lost-foundフォルダー内で確認できます。
danglingオブジェクトの検査
特に気になるのはコミットの方です。
$ ls -1q .git/lost-found/commit/* | wc -l
358
358個のコミットとは、数が多いですね…1個1個確認したら日が暮れそうですね。
列挙しながらコミットlogに救出したいbranchAの記載がないか確認してみましょう。
$ for f in $(find .git/lost-found/commit -maxdepth 1 -type f); do git log --grep="branchA" $(basename $f); done;
commit 8b664e171cdca944a14e488d5edad2942f7e8e5e
Merge: 1dfeff142 d95409a75
Author: user <109655030+user@users.noreply.github.com>
Date: Thu Jul 13 15:59:32 2023 +0900
WIP on branchA: 1dfeff142 feat: temp
1件だけですが、何もないよりもマシですね。logを改めて確認します。
$ git log 8b664e171cdca944a14e488d5edad2942f7e8e5e
commit 8b664e171cdca944a14e488d5edad2942f7e8e5e
Merge: 1dfeff142 d95409a75
Author: user <109655030+user@users.noreply.github.com>
Date: Thu Jul 13 15:59:32 2023 +0900
WIP on branchA: 1dfeff142 feat: temp
commit d95409a7547179024465f7f29c6ad35affea0066
Author: user <109655030+user@users.noreply.github.com>
Date: Thu Jul 13 15:59:32 2023 +0900
index on branchA: 1dfeff142 feat: temp
commit 1dfeff14292f92dda65ee38b386d396203e25a2d
Author: user <109655030+user@users.noreply.github.com>
Date: Thu Jul 13 15:59:27 2023 +0900
feat: temp
branchA上でのスタッシュだったのですね。
1dfeff14292f92dda65ee38b386d396203e25a2dは先日、別のブランチをcheckoutした時の一時的なコミットでした。
HEADは現在不明な状態なので、-f付きでcheckoutします。
$ git checkout -f 1dfeff14292f92dda65ee38b386d396203e25a2d
少し前の状態ですが、救出は成功しました。
クリーニング
救出が終わり、必要なものを無事にプッシュできたら、danglingオブジェクトへのポインタを消します。
$ rm -rf .git/lost-found/
その後、エラーチェックの際に動作履歴であるreflogのエラーも検知されたので、reflog expireコマンドで現在時刻までの全ての履歴を削除します。
$ git reflog expire --expire=now --all
次は普段定期的に実行されるガベージコレクタを手動で実行して、現在時刻までのdanglingオブジェクトをレポジトリのデータベースから掃除してもらいます。
$ git gc --prune=now
Enumerating objects: 87254, done.
Counting objects: 100% (87254/87254), done.
Delta compression using up to 8 threads
Compressing objects: 100% (21923/21923), done.
Writing objects: 100% (87254/87254), done.
Total 87254 (delta 61654), reused 87032 (delta 61468), pack-reused 0
ちなみに、破損したreflogを消さないと以下のワーニングになります。
warning: reflog of 'HEAD' references pruned commits
最後はもう一度エラーチェックを行って、そっちも問題なければ掃除終了です。
$ git fsck
Checking object directories: 100% (256/256), done.
Checking objects: 100% (87254/87254), done.
Verifying commits in commit graph: 100% (10173/10173), done.
今日学んだこと
1.ファイル破損はいつでも起こり得るものです。
2.以下の動作は失われたはずのファイルの救出につながる可能性があります。
コミットをすること
ブランチを切ること(変更の検索が楽になるため)
スタッシュすることさえも
3.そして、一番重要なのは、このような試行錯誤に頼らずにレポジトリを復元できるための定期的なプッシュです。
\もっと身近にもっとリアルに!DAAE公式Twitter/
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/