Gitの歴史上から特定のファイルを削除したい

2022/01/28に公開

課題

あるファイルをGitの歴史上から削除したい。

経緯

Git管理すべきでないファイルをremoteのリポジトリにpushしてしまった。
はるか昔にpushしてしまったようだ。
はるか昔からのGitの歴史に残っている。
過去から現在に至るまでのGitの歴史上から、そのファイルの痕跡を抹消したい。

解決策

削除の為のコマンド

git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch 消したいファイルのパス' -- --all

コマンドを理解するための前提知識

indexとは

commit前に一時的にステージングする場所がindexです。
Gitを使っている人なら下図を見ればなんとなく分かるはず。
この記事の言葉が分かりにくい人は、下図を見ながら読んでみて下さい。

refs/original/とは

Gitはfilter-branchで歴史を書き換える際に、自動でバックアップを作成してくれます。
そのバックアップの保存先のディレクトリがrefs/original/です。
refs/original/には書き換える前のGitの歴史が保存されます。

コマンドの解説

git filter-branch

Gitの歴史をまとめて書き換えるコマンドです。

--force

filter-branchは下記の2つの条件下では実行できません。

  1. 実行するディレクトリがtemporary directoryである場合
  2. refs/original/に既にバックアップが存在する場合

--forceオプションを指定することで、上記のような場合でもfilter-branchを強制的に実行することが出来ます。

--index-filter

local repositoryに保存されたファイルをindex上で書き換えるオプションです。
index-filter後の''内のコマンドが、index上で実行される処理です。

--index-filter--tree-filterの違い

--index-filterとよく似たオプションに--tree-filterというものがあります。
--index-filterはlocal repositoryのファイルをindex上で書き換えます。
一方、--tree-filterはlocal repositoryのファイルをworking treeにcheckoutしてから書き換えます。
--index-filterの利点は、高速に実行されることです。
--tree-filterの利点は、index外のファイルまで書き換えられることです。例えば、.gitignoreなどでGit追跡から外しているファイルはindex上には現れません。--tree-filterでのみ書き換える事が出来ます。

git rm --cached --ignore-unmatch

git rmは、indexとwoking treeからファイルを削除するコマンドです。
--cachedを指定することで、indexのみから削除できます。今回は--index-filterを使用していて、working directoryは関係ないので--cachedを指定しています。
--ignore-unmatchは、ファイルが存在しなくてもそのまま実行するオプションです。今回削除したいファイルは一部のコミットにのみ存在するので、このオプションを指定しています。

消したいファイルのパス

消したいファイルのパスを書きます。
そのプロジェクトのルートディレクトリからファイルへのパスです。

your-projectのルートディレクトリが下記のような構成だとします。

/your-project
┣/.git
┣aaa.txt
┣bbb.txt
┗/ccc
  ┣ ddd.txt
  ┗ eee.txt
eee.txtを指定したい場合は、ccc/eee.txtと書いて下さい。

--

filter branchが実行されるコミットの範囲を指定するオプションです。
全てのブランチに対して実行したいので、--allを指定しています。

実際の削除手順

Gitの歴史を実際に確認する

この歴史改竄のゴールは、過去の任意のcommtitをチェックアウトしても対象のファイルが存在しない状態にすることです。

まずは削除したいファイルが、Gitの歴史上のどの期間にどのように存在していたかを確認します。
削除前の状態と削除後の状態を把握していなければ、正確な作業は出来ません。

長い歴史の中でファイル名が変更されてたりするとうまく検索で拾えなかったりすると思うので、いくつかのアプローチで、削除したいファイルの在り方を調査することをおすすめします。

例えば、以下のようなコマンドでGitのlogを確認します。

git log --all --name-status --pretty=short --graph

Gitの歴史上でどのようなファイルを追加・変更・削除してきたかの年表を見ることが出来ます。

あるいは、特定のファイルパスを指定しても良いでしょう。

git log --all --name-status --pretty=short --graph -- 検索するファイルパス

特定のファイルがどの時点で追加・変更・削除されたのかを知ることが出来ます。

あるいは、以下のコマンドも有用かもしれません。

git log --all --name-status --pretty=short --graph -G"検索したい文字列の正規表現"

特定の文字列を含むファイルを削除したい場合などに役立つでしょう。

上記のコマンドなどを用いて、削除前の状態のcommit idをいくつか適当にピックアップします。
ピックアップしたcommit idは記録しておきましょう。
ピックアップしたcommit idをcheckoutしても、削除したいファイルが存在しなければ歴史改竄は成功です。

ピックアップしたコミットを実際にチェックアウトしてみましょう。

git checkout commit_id

当然、削除したいファイルが存在することが確認出来たかと思います。

歴史を書き換えるコマンドを実行する

git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch 消したいファイルのパス' -- --all

を実行します。
歴史が書き換わりました。

コマンド実行後の状態

ここで下記のコマンドなどを実行して、現在の状態を確認して下さい。

git log --all --name-status --pretty=short --graph

refs/original以外のローカルブランチ(リモート追跡ブランチも含む)の歴史が書き換えられたことが確認出来ると思います。

あるいは、以下のコマンドなどを実行してみて下さい。

git log --all --name-status --pretty=short --graph -- 検索するファイルパス
git log --all --name-status --pretty=short --graph -G"検索したい文字列の正規表現"

refs/originalにのみ改竄される前の歴史が残っていると思います。

refs/originalを目障りに思う方は、下記のコマンドで消して下さい。
但し、貴重なバックアップなのでよく考えてから消して下さいね。

git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d

もう一度、下記のコマンドで結果を確認して下さい。

git log --all --name-status --pretty=short --graph
git log --all --name-status --pretty=short --graph -- 検索するファイルパス
git log --all --name-status --pretty=short --graph -G"検索したい文字列の正規表現"

書き換えられた歴史のみが存在することが分かると思います。

refs/original以外のブランチにも、書き換える前の歴史が残っている場合があると思います。
その場合は、リモートブランチとの同期に注意を読んでみて下さい。
リモートブランチを自動でfetchしていることが、原因の1つとして考えられます。

リモートブランチに反映させる

あとは,下記のコマンドで全てのローカルブランチをpushすれば、リモートにも反映されます。

git push --all --force origin

ローカルブランチの存在しないリモートブランチには変更が反映されないので注意して下さい。

ローカルリポジトリから改竄の痕跡を抹消する

ピックアップしておいたcommit idをチェックアウトして下さい。
削除したはずのファイルの存在を確認出来ると思います

今回の作業より前にクローンされたローカルリポジトリには、削除したファイルの痕跡が残り続けています。
それは、今あなたが作業しているリポジトリも含めて全てのローカルリポジトリにいえることです。

ローカルリポジトリから削除したファイルの痕跡を消すためには、新たにリポジトリをcloneし直すのが良いでしょう。
再びリモートリポジトリからcloneし直して下さい。
その場合は、元々のローカルリポジトリのバックアップはとっておくべきでしょう。

cloneし直したリポジトリから、ピックアップしておいたcommit idをチェックアウトして下さい。
削除したはずのファイルが含まれるコミットが存在しないことが確認出来ると思います

『ローカルリポジトリから改竄の痕跡を抹消する』に関しては、過去記事に詳しく書いてあります。

リモートブランチとの同期に注意

歴史の書き換えがいつまでたっても上手くいかない場合があるかも知れません。
リモートブランチと同期されている事が原因かもしれません。
そのケースに関しては過去記事に詳しく書いたので、是非ご覧ください。

Gitの歴史上から特定のファイルを削除された

ここまでの作業で、ゴールは達成されたかと思います。
すなわち、過去の任意のcommtitをチェックアウトしても対象のファイルが存在しない状態にすることです。
歴史改竄前にピックアップしていたcommit idからも、歴史改竄後の任意のcommit idからもどこからも、削除したファイルには辿り着けないはずです。

最後に

filter-branchに関しては、gitのcommitの名前やメールアドレスを過去からまとめて変更するにも書いたので良かったらご覧下さい。
また、私が参考にした偉大な先人の記事も良かったらご覧下さい。

「何かわからんがくらえッ!」

Discussion