「やらかした」と気づく瞬間は、背筋が凍るものです。
ふとローカルのファイル一覧を眺めていたとき、token.json.bak という見慣れないファイルがGitの管理下にあることに気づきました。
正規の token.json は .gitignore に入れて厳重に管理していたものの、一時的なバックアップとして作った .bak ファイルが、監視の目をすり抜けてコミットされていたのです。
最新のコミットで慌てて git rm しても意味がありません。Gitはすべてを覚えているからです。履歴を遡れば、そのファイルはそこにいます。
これは、過去の履歴そのものを書き換える外科手術が必要です。
選択肢:歴史改変のツール
Gitの歴史を書き換えるツールとしては、最近では Python製の git-filter-repo が推奨されています。速度も速く、安全性も高い。
しかし、今この場ですぐに解決したい。追加のツールをインストールする手間さえ惜しい緊急事態(あるいは単なる横着)において、古来から伝わる標準コマンド git filter-branch は未だに強力な武器です。
今回は、標準の filter-branch を使って、この異物を歴史の闇に葬り去ることにしました。
実行したコマンド
実行したコマンドは以下の通りです。この一行の呪文が、リポジトリの全歴史を走査し、指定したファイルを「最初から存在しなかったこと」にします。
git filter-branch --force --index-filter "git rm --cached --ignore-unmatch token.json.bak" --prune-empty --tag-name-filter cat -- --all
何が起きているのか、分解して見てみましょう。
-
--index-filter "git rm --cached ..."これが手術の執刀部分です。すべてのコミットにおいて、インデックスからtoken.json.bakを削除しようと試みます。--cachedをつけているのは、実ファイルが手元に残っていても構わないから(Git管理から消えれば良い)。--ignore-unmatchは重要です。「そのファイルが存在しない時代のコミット」でエラー停止させないために必要です。 -
--prune-emptyファイルを消した結果、「変更点が何もなくなったコミット」が生まれた場合、そのコミット自体を削除します。空っぽのコミットログを残さないための配慮です。 -
--tag-name-filter catコミットハッシュが変わるとタグが迷子になってしまいます。これを指定することで、タグも新しい歴史に合わせて付け直します。 -
-- --allすべてのブランチとタグを対象にします。
仕上げの強制プッシュ
ローカルでの手術が成功しても、GitHub上のリモートリポジトリにはまだ古い歴史が残っています。
これを上書きするには、禁断の --force オプションを使います。
git push origin --force --all
これで、GitHub上からもファイルは消滅しました。 コミットハッシュが全て変わってしまいましたが、個人開発プロジェクトなので誰にも迷惑はかかりません。もしチーム開発でこれをやるなら、Slackで土下座してから行う必要があります。
教訓と規律
今回の事故は、単純な .gitignore の設定漏れが原因です。
しかし、ミスは起こるもの。重要なのは、ミスが起きたときに「削除コミットで誤魔化す」のではなく、「リスクを完全に絶つために歴史を書き換える」 という判断が正しくできるかどうかです。
エンジニアリングには、時に大胆な外科手術も必要です。ただし、執刀の前には必ずバックアップ(あるいは覚悟)を忘れずに。