TT Lab
はじめる
学ぶ 学習パス コース

Git実戦

消したからといって消えてはいない

TT Labで続きを見る

一言でいうと

ファイルを削除してコミットすることは、今後そのファイルがないという意味にすぎず、履歴に残ったオブジェクトはそのままです。実際になくすには、履歴を書き換え(filter-branch)、バックアップ参照を取り除き、reflogを空にし、gc --prune=nowまでする必要があります。それでも、すでに広まったコピーは残ります。

なぜ必要なのか

2つの事故が、同じ形をしています。

1つは、機密情報です。本番のトークンが入った.envを誤ってコミットしてしまい、気づいて削除しました。リポジトリを開いてみると、そのファイルはありません。ところが、git log -- conf/prod.envは今でもコミットを2つ出し、git cat-file -p <커밋>:conf/prod.envは、トークンの原文をそのまま表示してくれます(プレースホルダーはコミットです)。gitは内容アドレス方式のストアなので、一度入ったblobは、それを指すコミットが残っている限り、消えません。

もう1つは、サイズです。数百MBのデータファイルやビルドのアーティファクトを一度コミットすると、あとで削除しても、cloneする人のたびに、そのオブジェクトをダウンロードし続けます。「なぜ自分たちのリポジトリはcloneに10分かかるのか」の答えは、たいていここにあります。

どちらも、今後ではなく過去の履歴を直さないと解決しません。

どう動くのか

git filter-branchは、指定したrefのコミットを1つずつ作り直しながら、コミットごとにフィルターを適用します。ファイルを取り除くときは、インデックスだけを直す--index-filterが最も速いです。ワーキングツリーを展開しないからです。

FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --index-filter \
  'git rm -r --cached --ignore-unmatch conf/prod.env assets/blob.bin' \
  --prune-empty --tag-name-filter cat -- --all

オプション1つ1つに、理由があります。

オプション なぜ必要か
--ignore-unmatch そのファイルがないコミットでgit rmが失敗して、全体が止まらないように
--prune-empty そのファイルだけを追加していたコミットが、空のコミットとして残らないように
--tag-name-filter cat タグが古いコミットを指したまま残らないように
-- --all ブランチ1つではなく、すべてのrefを書き換えるため
FILTER_BRANCH_SQUELCH_WARNING gitが表示する10秒の警告を飛ばすために

ここで、ほとんどの人が止まります。コマンドが終わってgit logを見るとファイルはないのに、git rev-list --objects --allには、そのファイルが今でも出てきます。理由は、refs/original/です。filter-branchは、元に戻せるように、元のrefをすべてその下にバックアップしておきますが、それもrefなので、古いコミットと古いblobを掴んでいます。

そのため、順序があります。

  1. refs/original/を削除します。git for-each-ref --format='%(refname)' refs/originalで一覧を取り出し、git update-ref -dを実行します。
  2. reflogを空にします。git reflog expire --expire=now --expire-unreachable=now --allを使います。reflogも参照です。HEADが通ったすべての場所を記憶しています。
  3. git gc --prune=nowで、到達不能なオブジェクトを削除します。--prune=nowがないと、デフォルトの猶予期間(2週間)が過ぎるまで削除されません。

git count-objects -vHで前後を測ると、この過程が実際に何をしたのかが、数字で見えます。

現場での姿

書き換えが終わると、すべてのコミットのハッシュが変わります。コミットのハッシュは、親とツリーを含めて計算されるため、履歴の一番最初で何か1つを直すと、その後がすべて変わります。そのため、次のことが伴います。

そして、最も重要なこと。すでにcloneを持っていった人のコピーには、そのオブジェクトがそのまま残ります。同僚のノートパソコン、CIのキャッシュ、フォークしたリポジトリ、バックアップに、すべて残っています。そのため、機密情報が履歴に入ったときに最初にすべきことは、履歴の掃除ではなく、その機密情報を取り消して、新しく発行することです。履歴の掃除は、2つ目です。順序を逆にすると、掃除をしている数日の間、生きている認証情報が外に出たままになります。

大きなリポジトリでは、git filter-branchは遅いです。公式ドキュメントも、git filter-repoやBFGのようなツールを勧めています。ただし、それらのツールは別にインストールする必要があり、行うことと後処理の順序は、まったく同じです。書き換え、バックアップ参照の削除、reflogの期限切れ、gcです。

次のラボですること

トークンが入った設定ファイルと、900KBの塊が混ざった履歴を作り、ワーキングツリーから削除してもgit cat-fileで原文が出ることを、まず確認します。そのあと、書き換えを実行し、まだ残っている理由(refs/original)を自分で見つけて取り除き、サイズが実際に減ることをgit count-objects -vHで測ります。最後に、あらかじめ取っておいた同僚のコピーに、その機密情報がそのまま残っているのを示し、そのため何を先にすべきかで締めくくります。

公式ドキュメントは、git-filter-branch、git-reflog、git-gcです。