git filter-branch代替ツール:git filter-repo のメリットと使い方

git filter-branch代替ツール:git filter-repo のメリットと使い方

バージョン管理システム Git は、現代のソフトウェア開発において不可欠なツールです。コードの変更履歴を効率的に管理し、チームでの共同作業を円滑に進める上で、その重要性は計り知れません。Git の最も強力な機能の一つに、リポジトリのコミット履歴を操作できる能力があります。これにより、過去のコミットを修正したり、不要なファイルを履歴から完全に削除したり、リポジトリ構造を変更したりすることが可能になります。

このような履歴の書き換えは、機密情報が誤ってコミットされてしまった場合や、巨大なバイナリファイルが履歴に残ってしまった場合など、リポジトリの健全性を保つために非常に重要です。しかし、履歴の書き換えは強力であると同時に、誤って使用するとリポジトリを壊してしまう可能性もあるため、慎重に行う必要があります。

従来、Git で複雑な履歴操作を行うための主要なコマンドとして git filter-branch が利用されてきました。これは非常に強力なツールでしたが、いくつかの深刻な問題を抱えていました。その複雑な構文、極端な実行速度の遅さ、そして環境変数に依存した不安定な挙動などが挙げられます。特に大規模なリポジトリや複雑なフィルタリング処理を行う場合、git filter-branch は実用的ではないことが多く、開発者にとって大きな負担となっていました。

こうした背景から、git filter-branch に代わる、より高速で安全かつ使いやすいツールが求められていました。その要求に応える形で登場したのが、今回ご紹介する git filter-repo です。git filter-repo は、git filter-branch の問題を解決し、よりモダンな方法でリポジトリの履歴操作を可能にするツールとして開発されました。現在では Git 自体のドキュメントでも git filter-branch の代替として推奨されており、多くの開発者に利用されています。

本記事では、まず git filter-branch が抱えていた問題点を掘り下げ、次に git filter-repo がどのようにそれらを解決しているのか、その主要なメリットを詳しく解説します。さらに、git filter-repo のインストール方法から、基本的な使い方、そしてPythonスクリプトを用いた高度なカスタマイズ方法まで、具体的なコマンド例を交えながら詳細に説明します。最後に、git filter-repo を使用する上での注意点やベストプラクティスについても触れ、読者が安全かつ効果的にこのツールを使えるようになることを目指します。

長い記事となりますが、最後までお付き合いいただければ幸いです。

git filter-branch の課題

git filter-branch は、その登場当時としては画期的なツールであり、Git の柔軟性を示すものでした。しかし、時間の経過とともに、その設計に起因するいくつかの根本的な課題が明らかになりました。これらの課題が、git filter-repo の開発を促した主要因です。

主な課題は以下の通りです。

1. 複雑なスクリプト記述

git filter-branch は、--tree-filter--index-filter--msg-filter--env-filter などのオプションを通じて、リポジトリの各コミットに対してシェルスクリプトを実行する方式をとります。これらのスクリプトは、フィルタリングロジックを記述するために必要ですが、往々にして複雑で読み解きにくく、デバッグが困難でした。特に、ツリーオブジェクトやインデックスに対する操作は、Git の内部構造に関する深い理解を必要としました。シェルスクリプトの構文自体も環境に依存する可能性があり、移植性の問題も発生しがちでした。

2. 極端な実行速度の遅さ

git filter-branch の最も大きな欠点の一つが、その実行速度の遅さです。これは、各コミットに対して新しいワークツリーをチェックアウトし、そこでスクリプトを実行し、再びインデックスに追加してコミットを作成するというプロセスを、リポジトリの歴史上の全てのコミットに対して逐次的に行うためです。大規模なリポジトリや長い歴史を持つリポジトリでは、この処理に数時間、場合によっては一日以上かかることも珍しくありませんでした。これは、開発者が気軽に履歴操作を試したり、パイプラインに組み込んだりすることを非常に困難にしていました。

3. セキュリティリスクと環境変数への依存

git filter-branch は、フィルタリング処理に必要な情報(オリジナルのコミッター情報など)を環境変数としてスクリプトに渡します。この設計は、特に GIT_AUTHOR_EMAILGIT_COMMITTER_DATE といった機密性の高い情報を扱う際に、意図しない情報漏洩やセキュリティ上の脆弱性を引き起こす可能性がありました。また、環境変数に依存した処理は、異なるシェル環境やOS上での挙動の不安定さにも繋がりました。

4. メンテナンス性の低さ

git filter-branch の内部実装は複雑であり、シェルスクリプトによるカスタマイズも読み書きが難しいため、一度作成したフィルタリングスクリプトの保守や修正は容易ではありませんでした。また、Git のバージョンアップによって内部挙動が変わった場合に、スクリプトの修正が必要になる可能性もありました。

5. 非推奨化

上記のような問題点を受けて、Git プロジェクトでは git filter-branch の利用を非推奨 (deprecated) としました。公式ドキュメントでも、新しい履歴書き換えツールとして git filter-repo を使うことが強く推奨されています。これは、git filter-branch が今後積極的に改善される見込みが薄いことを意味し、利用者は代替ツールへの移行を促されました。

これらの課題は、Git リポジトリの履歴操作という強力な機能を使う上での大きな障壁となっていました。特に、リポジトリの肥大化や機密情報の扱いは、現代の開発において頻繁に発生する問題であり、それらに対処するためのツールが非効率的であることは、多くの開発者にとって頭痛の種でした。git filter-repo は、これらの課題を克服するために設計されたのです。

git filter-repo の紹介

git filter-repo は、git filter-branch が抱えていた多くの問題を解決するために、Bradley Smith 氏によって開発された新しい Git 履歴操作ツールです。Python で書かれており、高速性、使いやすさ、安全性、そして柔軟性を兼ね備えています。

git filter-repo は、リポジトリのすべてのコミット、ツリー、ブロブ、タグオブジェクトを効率的に読み込み、指定されたフィルタリングルールに基づいて新しいオブジェクトを生成し、最終的に新しいリポジトリの歴史を構築するというアプローチをとります。この方式は、git filter-branch が各コミットでワークツリーを再現するよりもはるかに効率的です。

Git プロジェクトは、git filter-branch を非推奨とする際に、公式な代替ツールとして git filter-repo を推奨しました。これは、git filter-repo が Git プロジェクトの標準的なツールとして認められていることを意味し、その信頼性の高さを物語っています。

git filter-repo は、単なる git filter-branch の機能的な置き換えにとどまらず、より直感的で安全なインターフェースと、Python の持つ強力な表現力による高度なカスタマイズ性を提供します。これにより、従来困難であった複雑な履歴操作も、比較的容易かつ安全に行うことができるようになりました。

git filter-repo の主なメリット

git filter-repogit filter-branch に比べて優れている点は多岐にわたります。ここでは、その主要なメリットを詳しく見ていきましょう。

1. 圧倒的な高速性

git filter-repo の最大のメリットの一つは、その実行速度です。git filter-branch が各コミットでワークツリーを再構築するのに対し、git filter-repo はリポジトリのオブジェクトデータベース(ブロブ、ツリー、コミット)を直接かつ効率的に走査し、インメモリで新しいオブジェクトを構築します。この根本的な設計の違いにより、git filter-branch と比較して桁違いに高速な処理を実現しています。

例えば、大規模なリポジトリから大きなファイルを削除するような場合、git filter-branch では数時間から一日かかった処理が、git filter-repo では数分から数十分で完了することも珍しくありません。この高速性により、開発者は履歴操作の結果をすぐに確認したり、試行錯誤を繰り返したりすることが容易になります。また、CI/CD パイプラインの一部として履歴操作を組み込む際にも、処理時間の短縮は大きなメリットとなります。

高速性の秘密は、以下の点にあります。

  • ワークツリーの回避: 各コミットでファイルシステム上にワークツリーを展開しない。
  • 効率的なオブジェクト走査: Git のオブジェクトデータベースを効率的に読み込む。
  • インメモリ処理: フィルタリング処理をメモリ上で行い、ディスクI/Oを最小限に抑える。
  • Pythonの効率的なデータ構造: Python のデータ構造を活用し、オブジェクト間の参照関係などを高速に処理。

2. シンプルで直感的な使いやすさ

git filter-repo は、よくある履歴操作のユースケースに対応するための、シンプルで理解しやすいコマンドラインオプションを多数提供しています。例えば、特定のファイルを削除する、ディレクトリを移動する、コミッター情報を変更するといった一般的な操作は、複雑なシェルスクリプトを書くことなく、短いオプションの組み合わせで実現できます。

例えば、特定のパス以下のファイルだけを残したい(他のパスは削除したい)場合は、--paths <path> オプションを、逆に特定のパス以下のファイルを削除したい場合は --invert-paths --paths <path> オプションを使用するなど、直感的な指定が可能です。

さらに、より複雑な処理が必要な場合でも、Pythonスクリプトによるコールバック関数を使用することで、柔軟かつ比較的容易にカスタマイズできます。Pythonはシェルスクリプトに比べて記述が容易で、ライブラリも豊富であり、デバッグもしやすいため、複雑なロジックの実装に適しています。

3. 高い安全性

git filter-repo は、git filter-branch が抱えていたセキュリティ上の問題(環境変数への依存など)を解決しています。フィルタリング処理に必要な情報は、安全な内部的な方法で管理され、外部の環境変数に依存することはありません。

また、git filter-repo はデフォルトで、処理を開始する前に元のリポジトリの状態を一時ディレクトリにバックアップします。これにより、万が一フィルタリング処理が失敗したり、意図しない結果になったりした場合でも、容易に元の状態に戻すことが可能です。さらに、処理対象のリポジトリとは別に一時的な作業領域を使用するため、オリジナルのリポジトリを直接破壊するリスクが低減されています。

ただし、git filter-repo も Git の履歴を書き換える強力なツールであることには変わりありません。そのため、常にリポジトリのクローンに対して実行し、元のリポジトリには変更を加えないという基本的な安全策は非常に重要です。

4. 豊富なフィルタリングオプションと柔軟性

git filter-repo は、様々な基準に基づいたフィルタリングを可能にする多様なオプションを提供しています。パスによるフィルタリング (--paths, --invert-paths, --path-glob)、ファイルサイズの指定 (--strip-blobs-bigger-than)、コミットメッセージやコミッター情報、日付によるフィルタリングなど、一般的なユースケースのほとんどに対応できます。

さらに、これらの組み込みオプションで対応できない複雑な処理が必要な場合は、Pythonスクリプトによるコールバック関数を定義することで、ほぼ無限の柔軟性を実現できます。コミットごと、タグごと、ブロブ(ファイルの内容)ごと、メッセージごと、コミッター情報ごとなど、様々なタイミングで独自のロジックを実行し、履歴を細かく制御できます。

5. 優れたメンテナンス性と拡張性

git filter-repo は Python で書かれており、そのコードベースはシェルスクリプトの集合体である git filter-branch よりも構造化されており、比較的読みやすく理解しやすいです。これにより、ツールの内部動作を理解したり、必要に応じて拡張したりすることが容易になります。Python コールバックを使用する場合も、デバッグやテストがしやすいというメリットがあります。

6. 公式推奨ツールとしての信頼性

Git プロジェクト自身が git filter-branch の代替として git filter-repo を推奨しているという事実は、このツールの信頼性と安定性を示す強力な根拠となります。継続的にメンテナンスされており、多くのユーザーによって利用されているため、安心して使用できます。

これらのメリットにより、git filter-repogit filter-branch を過去のツールとし、リポジトリ履歴操作のデファクトスタンダードとしての地位を確立しつつあります。

git filter-repo のインストール

git filter-repo は Python で書かれているため、使用するには Python 3.5 以降が必要です。また、Git 自体も比較的新しいバージョン、具体的には Git 2.22 以降が必要となります(Git 2.22 以降は git filter-repo が使用する Git の内部コマンドや機能に対応しています)。

インストールは、Python のパッケージ管理システムである pip を使うのが最も一般的で簡単です。

  1. Python 3 のインストール:
    お使いのシステムに Python 3 がインストールされているか確認してください。インストールされていない場合は、Python の公式サイトからダウンロードしてインストールしてください。

  2. Git のバージョン確認:
    コマンドラインで git --version を実行し、Git のバージョンが 2.22 以上であることを確認してください。もし古い場合は、Git の公式サイトやパッケージマネージャーを使って最新版にアップデートしてください。

  3. pip を使ったインストール:
    ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行します。

    bash
    pip install git-filter-repo

    これにより、git-filter-repo パッケージがインストールされます。インストールが成功すると、システムに git-filter-repo コマンドが利用できるようになります。

  4. PATH の設定(必要に応じて):
    pip でインストールしたスクリプトがシステム標準の PATH に含まれていない場合、git-filter-repo コマンドが見つからないことがあります。その場合は、Python の Scripts ディレクトリ(Windowsの場合)や、ユーザーのローカルバイナリディレクトリ(Linux/macOSの場合、例: ~/.local/bin)をシステム環境変数 PATH に追加する必要があります。正確な場所は Python のインストール方法や OS によって異なりますので、ご自身の環境に合わせて確認してください。

    例(Linux/macOS bash/zsh の場合、~/.bashrc や ~/.zshrc に追加):
    bash
    export PATH=$PATH:~/.local/bin

    設定を反映させるために、シェルを再起動するか、設定ファイルを再読み込みしてください(例: source ~/.bashrc)。

  5. インストール確認:
    インストールが完了したら、以下のコマンドを実行して git filter-repo が正しくインストールされ、利用できるか確認してください。

    bash
    git filter-repo --version

    バージョン情報が表示されれば成功です。

これで git filter-repo を使う準備が整いました。

git filter-repo の基本的な使い方

git filter-repo は、処理対象のリポジトリの作業コピー(クローン)に対して実行することが強く推奨されます。決してオリジナルのリポジトリを直接変更しないでください。 万が一の失敗に備え、必ずクローンを作成して、そのクローンに対して操作を行います。

基本的なコマンドの構造は以下のようになります。

bash
git filter-repo [OPTIONS]

git filter-repo を実行するディレクトリは、フィルタリングしたい Git リポジトリのルートディレクトリである必要があります。

以下に、git filter-repo を使ったよくある履歴操作の例とそのコマンドを解説します。

準備:リポジトリのクローン

まず、操作したいリポジトリのクローンを作成します。

“`bash

オリジナルのリポジトリがあるディレクトリで実行

git clone <オリジナルのリポジトリのパスまたはURL> <作業用ディレクトリ名>
cd <作業用ディレクトリ名>
“`

以降のコマンドは、このクローンしたリポジトリのルートディレクトリで実行します。

1. 特定のファイルやディレクトリを履歴から完全に削除する

これは、誤ってコミットしてしまった機密情報を含むファイルや、リポジトリを肥大化させている巨大なファイルを履歴から抹消したい場合によく使う操作です。

特定のファイルを削除するには、--paths オプションと --invert-paths オプションを組み合わせて使用します。--paths は残したいパスを指定しますが、--invert-paths を同時に使うと、指定したパス 以外 を残す、つまり指定したパスを 削除する という意味になります。

例1:特定のファイル config/credentials.yml を履歴から完全に削除する

bash
git filter-repo --paths config/credentials.yml --invert-paths

--paths オプションは複数指定できます。

例2:複数のファイル secret.txtlarge_data.zip を削除する

bash
git filter-repo --paths secret.txt --paths large_data.zip --invert-paths

ディレクトリとその内容を丸ごと削除したい場合も同様です。

例3:ディレクトリ dump/ を履歴から完全に削除する

bash
git filter-repo --paths dump/ --invert-paths

ディレクトリを指定する場合、末尾に / を付けるのが一般的ですが、付けなくても動作します。

ワイルドカードを使う場合は --path-glob オプションを使用します。

例4:拡張子が .log のファイルを全て削除する

bash
git filter-repo --path-glob *.log --invert-paths

注意: これらの操作は、指定したファイルやディレクトリを、リポジトリの歴史上の全てのコミットから削除しようと試みます。もしあるコミットに指定したファイル/ディレクトリが存在しない場合、そのコミットはそのまま維持されます。また、指定したファイル/ディレクトリ のみ が含まれるコミットは、内容が空になるためデフォルトでは削除されます (--prune-empty オプション)。

2. リポジトリの特定のサブディレクトリを新しいリポジトリのルートにする

これは、モノレポ(複数のプロジェクトを含む巨大なリポジトリ)から特定のプロジェクトのディレクトリだけを切り出して、新しい独立したリポジトリにしたい場合によく使う操作です。

--subdirectory-filter オプションを使用します。

例:my-project/ ディレクトリの内容を新しいリポジトリのルートにする

bash
git filter-repo --subdirectory-filter my-project/

このコマンドを実行すると、my-project/ ディレクトリの内容が、リポジトリのルートディレクトリにあるかのように履歴が書き換えられます。元のリポジトリにあった my-project/ 以外のファイルやディレクトリは全て履歴から削除されます。

サブディレクトリを切り出した後、新しいリポジトリとして公開したい場合は、このクローンしたリポジトリを新しいリモートリポジトリにプッシュします。

3. 機密情報を含むコミットメッセージを書き換える

誤ってパスワードやAPIキーなどをコミットメッセージに書いてしまった場合、そのメッセージを修正する必要があります。

--message-callback オプションを使用して、メッセージ書き換えのロジックを記述したPythonコードを指定します。

例:コミットメッセージ中の特定の文字列 PASSWORD=secret[REDACTED] に置き換える

--message-callback は、Python の式またはファイルパスを指定できます。簡単な置換であれば式で記述できます。

bash
git filter-repo --message-callback 'message.replace(b"PASSWORD=secret", b"[REDACTED]")'

ここで message は、バイト列(bytes 型)としてのコミットメッセージです。文字列リテラルの前にある b は、それがバイト列であることを示します。置換後の文字列もバイト列である必要があります。

より複雑な処理が必要な場合は、Pythonスクリプトファイルを指定します(後述の高度な使い方を参照)。

4. コミッターやオーサーの情報(名前やメールアドレス)を変更・正規化する

これは、コミット時のユーザー名やメールアドレスが間違っていたり、組織内の標準的な形式に統一したりしたい場合に利用します。

最も簡単な方法は .mailmap ファイルを使用することです。.mailmap ファイルは、正しい名前 <正しいメールアドレス> 誤った名前 <誤ったメールアドレス> の形式で、誤った情報を正しい情報にマッピングするリストを記述します。

  1. リポジトリのルートディレクトリに .mailmap という名前のファイルを作成します。
  2. ファイル内にマッピング情報を記述します。例:
    正しい名前 <正しいメールアドレス> 間違った名前 <間違ったメールアドレス1>
    正しい名前 <正しいメールアドレス> 間違った名前2 <間違ったメールアドレス2>
    別の正しい名前 <別の正しいメールアドレス> 別の間違った名前 <別の間違ったメールアドレス>

    間違った名前 <間違ったメールアドレス> の部分を省略することも可能です。正しい名前 <正しいメールアドレス> とだけ書くと、そのメールアドレスを持つ全てのコミットが指定した名前とメールアドレスに統一されます。

  3. git filter-repo を実行する際に、特にオプションを指定する必要はありません。git filter-repo はデフォルトでリポジトリルートにある .mailmap ファイルを認識し、それに基づいてコミッター/オーサー情報を書き換えます。

    bash
    git filter-repo

    もし、特定の .mailmap ファイルを指定したい場合は --mailmap <ファイルパス> オプションを使用します。

.mailmap ファイルを使う方法は非常に強力かつ柔軟です。Git 自体の git log など多くのコマンドでも .mailmap を認識するため、フィルタリング後だけでなく、普段の操作でも履歴表示を正規化するのに役立ちます。

.mailmap だけでは実現できない複雑なロジック(例: 特定の期間のコミットのみ名前を変える)が必要な場合は、--commit-callback--name-callback--email-callback などのPythonコールバックを使用します(後述)。

5. 大きなブロブ(ファイル)を履歴から削除する

特に大きなバイナリファイルが誤って追加され、その後のコミットでも履歴に残っている場合、リポジトリサイズが著しく増大します。git filter-repo は、指定したサイズよりも大きなブロブを履歴から削除する便利なオプションを提供しています。

--strip-blobs-bigger-than <size> オプションを使用します。サイズは、K (KB)、M (MB)、G (GB) などの単位を付けて指定できます。

例:サイズが 100MB を超えるブロブを全て履歴から削除する

bash
git filter-repo --strip-blobs-bigger-than 100M

この操作は、履歴中のすべてのブロブ(ファイルの内容)をスキャンし、指定されたサイズより大きいものを見つけたら、そのブロブを参照している全てのコミットからその参照を削除します。これにより、リポジトリのディスク使用量を大幅に削減できる場合があります。

6. 空になったコミットを削除する

フィルタリング処理(ファイルの削除やディレクトリの移動など)の結果、内容が空になったコミット(変更点が全くないコミット)が生成されることがあります。デフォルトでは、git filter-repo はこのような空のコミットを自動的に削除します。

この挙動は --prune-empty オプションによって制御されますが、デフォルトで有効になっています。特に指定する必要はありません。空のコミットをあえて残したい場合は --no-prune-empty オプションを使用します。

7. 特定のコミット範囲のみを処理する

デフォルトでは、git filter-repo はリポジトリの全てのブランチ、タグ、およびその他の参照を含む、完全な履歴を対象に処理を行います。しかし、特定のブランチやタグのみを対象としたい場合もあります。

--refs オプションを使用して、処理対象とする参照を指定します。これは glob パターンで指定できます。

例:master ブランチと develop ブランチ、そして v1.* というタグのみを処理する

bash
git filter-repo --refs master develop "refs/tags/v1.*"

refs/heads/refs/tags/ は省略可能ですが、明示的に指定することで誤解を避けられます。

8. フィルタリングの取り消し(バックアップからの復元)

git filter-repo はデフォルトで、処理を開始する前に元のリポジトリの状態を .git-filter-repo/backup/ ディレクトリにバックアップします。フィルタリング処理の結果が意図したものと異なっていた場合や、何らかの問題が発生した場合は、このバックアップから元の状態に復元できます。

復元は、バックアップされた .git ディレクトリを元のリポジトリの .git ディレクトリに上書きすることで行います。

“`bash

フィルタリングを実行したリポジトリのルートディレクトリで

rm -rf .git # 現在の .git ディレクトリを削除 (注意!バックアップがあることを確認してから行うこと)
mv .git-filter-repo/backup/.git . # バックアップを復元
“`

警告: この操作は現在の .git ディレクトリを完全に削除するため、細心の注意を払って実行してください。必ずバックアップが存在すること、そしてそれが復元したい元の状態であることを確認してください。

--no-backup オプションを指定するとバックアップは作成されません。ディスク容量が非常に厳しい場合などを除き、バックアップを有効にしておくことを強く推奨します。

これらの基本的な使い方を組み合わせることで、多くの一般的な履歴操作に対応できます。しかし、より複雑でカスタムな処理が必要な場合は、Pythonスクリプトによる高度な使い方に進むことになります。

git filter-repo の高度な使い方:Pythonスクリプトによるカスタマイズ

git filter-repo の真の力は、Pythonスクリプトによる柔軟なカスタマイズ機能にあります。特定の条件を満たすコミットに対してのみ処理を行ったり、ファイルの内容を動的に変更したり、コミットメッセージの形式を複雑なロジックに基づいて整形したりする場合など、組み込みオプションだけでは対応できないケースに対応できます。

カスタマイズは、主に「コールバック関数」を定義することで実現します。git filter-repo はリポジトリのオブジェクトを走査しながら、特定のイベント(コミットを見つけた時、ブロブを見つけた時など)が発生するたびに、ユーザーが定義したコールバック関数を呼び出します。コールバック関数内で、Git オブジェクトの情報にアクセスし、必要に応じてその情報を変更したり、関連する操作(ファイルの追加・削除・変更など)を行ったりします。

コールバック関数は、git filter-repo コマンドのオプションとして直接Pythonコードを指定するか、より一般的な方法として、スクリプトファイルを指定します。スクリプトファイルは、通常、.git-filter-repo/ というディレクトリを作成し、その中に配置します。

コールバック関数の種類と指定方法

git filter-repo は様々な種類のコールバック関数をサポートしています。主なものには以下のようなものがあります。

  • --commit-callback <expr_or_file>: 各コミットオブジェクトが処理される直前に呼び出されます。コミット情報(親コミット、ツリー、メッセージ、作者、コミッターなど)にアクセスし、変更を加えることができます。ファイルの追加・削除・内容変更などもここで行います。
  • --tag-callback <expr_or_file>: 各タグオブジェクトが処理される直前に呼び出されます。タグ情報(参照するコミット、タグ付け者、メッセージなど)にアクセスし、変更を加えることができます。
  • --blob-callback <expr_or_file>: 各ブロブ(ファイルの内容)オブジェクトが処理される直前に呼び出されます。ブロブの内容にアクセスし、変更を加えることができます。
  • --message-callback <expr_or_file>: 各コミットメッセージが処理される直前に呼び出されます。メッセージ内容を直接変更するのに使います(前述の例を参照)。
  • --name-callback <expr_or_file>: 各コミットの作者名やコミッター名が処理される直前に呼び出されます。名前を直接変更できます。
  • --email-callback <expr_or_file>: 各コミットの作者メールアドレスやコミッターメールアドレスが処理される直前に呼び出されます。メールアドレスを直接変更できます。
  • --filepath-callback <expr_or_file>: 各ファイルのパスが処理される直前に呼び出されます。ファイルパスを変更したり、特定のパスのファイルをスキップしたりするのに使います。

expr_or_file の部分には、以下のいずれかを指定します。

  1. Python の式: 簡単な処理であれば、引用符で囲んだ Python の式を直接記述できます(例: --message-callback 'message.replace(b"old", b"new")')。コールバックに渡される変数(message など)にアクセスできます。
  2. Python スクリプトファイル: 複雑な処理の場合は、Python スクリプトファイルへのパスを指定します。ファイルパスの前に : を付けます(例: --commit-callback :path/to/commit_filter.py)。このスクリプトファイル内で、特定の関数(例: commit_callback)を定義します。

.git-filter-repo/ ディレクトリとスクリプトファイル

通常、カスタマイズ用のスクリプトファイルはリポジトリのルートディレクトリに .git-filter-repo/ という名前のディレクトリを作成し、その中に配置します。例えば、commit_filter.py という名前のコミットコールバックを定義したい場合は、ファイルパスは .git-filter-repo/commit_filter.py となります。

そして、git filter-repo を実行する際に、以下のように --commit-callback :.git-filter-repo/commit_filter.py と指定します。

bash
git filter-repo --commit-callback :.git-filter-repo/commit_filter.py

git filter-repo は、指定されたパスのスクリプトファイルを読み込み、そのファイル内で定義されている、オプション名に対応する関数(例: commit_callback)を呼び出します。

コールバック関数の構造と引数

コールバック関数は、それぞれ特定の引数を受け取ります。引数の内容はコールバックの種類によって異なります。最も強力でよく使われる --commit-callback を例に説明します。

--commit-callback に指定するスクリプトファイル(例: .git-filter-repo/commit_filter.py)には、以下のような関数を定義します。

“`python

.git-filter-repo/commit_filter.py

def commit_callback(commit):
“””
git filter-repo のコミットコールバック関数

Args:
    commit: filter_repo.commit.Commit オブジェクト
"""
# ここにフィルタリングロジックを記述
print(f"処理中のコミット: {commit.original_hash.decode()}")
print(f"  メッセージ: {commit.message.decode()}")
print(f"  作者: {commit.author_name.decode()} <{commit.author_email.decode()}>")
print(f"  コミッター: {commit.committer_name.decode()} <{commit.committer_email.decode()}>")
print(f"  タイムスタンプ: {commit.author_date}")

# 例1:特定の文字列を含むメッセージのコミットをスキップする
if b"SKIP THIS COMMIT" in commit.message:
    print("  -> このコミットをスキップ")
    return None # None を返すとこのコミットは無視される(履歴から削除される)

# 例2:コミッターのメールアドレスを変更する
if commit.committer_email == b"[email protected]":
    print("  -> コミッターのメールアドレスを変更")
    commit.committer_email = b"[email protected]"
    commit.committer_name = b"新しいコミッター名" # 名前も一緒に変えることが多い

# 例3:特定のファイルの内容を変更する(ツリーオブジェクトの操作)
# コミットのツリーオブジェクトは commit.tree でアクセスできます
# ファイルの操作は filter_repo.lib.walk.FileEntry オブジェクトに対して行います

# 例として、README.md ファイルを見つけ、その内容を置き換える
readme_entry = None
for entry in commit.tree:
    if entry.filepath == b"README.md":
        readme_entry = entry
        break

if readme_entry:
    # ブロブの内容を読み込む
    original_content = commit.repo.get_blob_content(readme_entry.blob.original_oid)
    # 新しい内容を生成
    new_content = original_content.replace(b"古い情報", b"新しい情報")

    if new_content != original_content:
        print("  -> README.md の内容を変更")
        # 新しい内容でブロブを作成し、ファイルエントリを更新
        # filter_repo は新しいブロブオブジェクトを自動的に管理します
        readme_entry.blob = commit.repo.new_blob(new_content)

# 例4:特定のファイルを削除する
# 例えば、誤ってコミットされた secret_key.txt ファイルを削除
# 削除したいファイルのパスをリストアップし、そのエントリをツリーから除外します
files_to_delete = [b"secret_key.txt", b"temp/log.log"]
commit.tree.entries = [
    entry for entry in commit.tree.entries
    if entry.filepath not in files_to_delete
]
# ツリーの内容を変更した場合、commit.tree は新しいツリーオブジェクトを指すようになります。
# filter_repo が自動的に新しいツリーオブジェクトを生成し、古いツリーとブロブは履歴から除外されます。

# 例5:ファイルをリネームまたは移動する
# old/path/to/file.txt を new/path/to/file.txt に移動
old_path = b"old/path/to/file.txt"
new_path = b"new/path/to/file.txt"
for entry in commit.tree:
    if entry.filepath == old_path:
        print(f"  -> ファイルをリネーム/移動: {old_path.decode()} -> {new_path.decode()}")
        entry.filepath = new_path # ファイルパスを変更
        # 注意: フォルダ構造の変更も必要になる場合があります。
        # 複雑な移動の場合は、ツリー構造を手動で再構築するか、
        # filter_repo の helper メソッドを使う方が簡単な場合があります。
        # 例:commit.tree.rename(old_path, new_path) のようなヘルパーは提供されていないので、
        # ファイルパス変更と必要ならディレクトリツリーの調整を組み合わせて行う。

# コミットオブジェクト(commit)を変更すると、その変更が新しいコミットとして適用されます。
# None を返すと、そのコミットはスキップされます。
# 何も返さない(または commit オブジェクト自身を返す)場合は、変更されたコミットが適用されます。
return commit # または何も返さない

“`

このスクリプトは、commit_callback という名前の関数を定義しており、filter_repo.commit.Commit 型の commit オブジェクトを引数として受け取ります。commit オブジェクトを通じて、現在のコミットに関するあらゆる情報にアクセスできます。

  • commit.original_hash: オリジナルのコミットハッシュ (バイト列)
  • commit.message: コミットメッセージ (バイト列)
  • commit.author_name, commit.author_email, commit.author_date: 作者の情報
  • commit.committer_name, commit.committer_email, commit.committer_date: コミッターの情報
  • commit.parents: 親コミットのリスト
  • commit.tree: コミットに対応するツリーオブジェクト (filter_repo.tree.Tree オブジェクト)

特に重要なのが commit.tree オブジェクトです。ツリーオブジェクトは、ディレクトリ構造とファイル(ブロブ)への参照を表します。commit.tree.entries は、そのツリーに含まれるファイルやサブディレクトリ(エントリ)のリストです。各エントリは filter_repo.lib.walk.FileEntry オブジェクトで、filepath (パス)、mode (ファイルモード)、blob または tree (参照するオブジェクト) といった属性を持ちます。

コールバック関数内で commit.tree.entries リストを直接操作することで、ファイルの追加、削除、リネーム(パスの変更)を行うことができます。ファイルの内容を変更したい場合は、該当するエントリの blob 属性を、新しい内容を持つ filter_repo.blob.Blob オブジェクトに置き換えます。新しいブロブオブジェクトは commit.repo.new_blob(content_bytes) のようにして作成できます。

コールバック関数が None を返すと、そのコミットはフィルタリング後の履歴から削除されます。それ以外の値を返すか、何も返さない場合、変更された commit オブジェクトに基づいて新しいコミットが生成されます。

複雑な例:特定の条件を満たすコミットでのみ、特定のファイルの内容を書き換える

例えば、「特定の文字列 SECRET_KEY を含むコミットメッセージを持つコミット」において、「config.ini ファイルの内容にある DEBUG=TrueDEBUG=False に書き換える」という処理を考えます。

“`python

.git-filter-repo/commit_filter_complex.py

import re

def commit_callback(commit):
“””
特定の条件を満たすコミットでファイル内容を書き換えるコールバック
“””
print(f”処理中のコミット: {commit.original_hash.decode()}”)

# 条件1: コミットメッセージに特定の文字列が含まれているか確認
if b"SECRET_KEY" in commit.message:
    print(f"  -> メッセージに 'SECRET_KEY' を検出。ファイル内容をチェック。")

    # 条件2: config.ini ファイルが存在するか確認
    config_ini_entry = None
    for entry in commit.tree:
        if entry.filepath == b"config.ini":
            config_ini_entry = entry
            break

    if config_ini_entry:
        print(f"  -> config.ini を検出。内容を確認。")
        original_content = commit.repo.get_blob_content(config_ini_entry.blob.original_oid)
        new_content = original_content

        # 条件3: ファイル内容に特定の文字列が含まれているか確認し、書き換え
        if b"DEBUG=True" in original_content:
            print(f"  -> 'DEBUG=True' を検出。'DEBUG=False' に書き換え。")
            new_content = re.sub(b"^DEBUG=True$", b"DEBUG=False", original_content, flags=re.MULTILINE)
            # re.sub を使うことで、行頭/行末を考慮して正確に置換

        # 内容に変更があった場合、ブロブを更新
        if new_content != original_content:
            print(f"  -> config.ini の内容を更新。")
            config_ini_entry.blob = commit.repo.new_blob(new_content)
        else:
            print(f"  -> config.ini に変更なし。")

    else:
        print(f"  -> config.ini は存在しません。")

# 処理を終えたコミットオブジェクトを返す(または何も返さない)
return commit

“`

このスクリプトを .git-filter-repo/commit_filter_complex.py として保存し、以下のコマンドで実行します。

bash
git filter-repo --commit-callback :.git-filter-repo/commit_filter_complex.py

このように、Pythonスクリプトを使用することで、複雑な条件分岐やファイルの読み書き、正規表現による置換など、高度な履歴操作を柔軟に実現できます。filter_repo ライブラリが提供するオブジェクトやメソッド(例: commit.repo.get_blob_content, commit.repo.new_blob など)を適切に利用することが鍵となります。

その他のコールバック関数(tag_callback, blob_callback など)も同様に、対応するGitオブジェクト(タグ、ブロブなど)を引数として受け取り、その属性を操作することでフィルタリングを行います。

Pythonスクリプトによるカスタマイズは非常に強力ですが、リポジトリのオブジェクト構造や git filter-repo ライブラリの内部的な動作についてある程度の理解が必要となります。公式ドキュメントの「Python scripting and callbacks」セクションは、利用可能なオブジェクト、属性、メソッドの詳細な情報源となりますので、参照することをお勧めします。

注意点とベストプラクティス

git filter-repo は非常に強力なツールですが、Git の履歴を書き換える操作であるため、いくつかの重要な注意点があります。これらの点に留意しないと、データ損失や共同作業上の問題を引き起こす可能性があります。

1. 必ずリポジトリのクローンに対して作業する

これは最も重要な注意点です。決してオリジナルのリポジトリ(特に共有されているリポジトリ)に対して git filter-repo を直接実行しないでください。

必ず、オリジナルのリポジトリのクローンを作成し、そのクローンに対してフィルタリング処理を実行します。

“`bash
git clone <オリジナルのリポジトリのパスまたはURL> <作業用ディレクトリ名>
cd <作業用ディレクトリ名>

ここで git filter-repo コマンドを実行

“`

これにより、フィルタリング処理が失敗したり、意図しない結果になったりしても、オリジナルのリポジトリは無傷で残ります。問題が発生した場合は、クローンしたディレクトリを削除し、再度クローンしてやり直せばよいだけです。

2. フィルタリング結果を十分に確認する

git filter-repo の実行後、必ずリポジトリの状態が意図した通りになっているか十分に確認してください。

  • 削除したはずのファイル/ディレクトリが本当に消えているか?:
    bash
    git log --all -- <削除したファイル/ディレクトリのパス>
    # 何も表示されなければOK
  • 変更したはずのコミットメッセージやコミッター情報が反映されているか?:
    bash
    git log --all --format='%H %an <%ae> %cn <%ce> %s'
    # または git log --all --grep="古いメッセージの一部"
  • リポジトリのサイズが削減されているか?:
    bash
    git count-objects -vH
    git gc --prune=now # ガベージコレクションを実行して未使用オブジェクトを削除
    git count-objects -vH # サイズを再確認
  • 履歴の構造に問題がないか?:
    bash
    git log --all --graph --oneline
    gitk --all # GUIツールがあればより分かりやすい

    フィルタリングによって無関係なコミットが削除されたり、履歴が単純化されたりする場合があります。

3. 履歴の書き換えは新しいコミットハッシュを生成する

git filter-repo は、既存のコミットオブジェクトを変更するのではなく、指定されたフィルタリングに基づいて新しいコミットオブジェクトを生成します。元のコミットと新しいコミットは、内容が同一であっても異なるオブジェクトとして扱われ、完全に新しいコミットハッシュを持ちます。

これは、履歴を書き換えたリポジトリと、書き換え前の履歴を持つリポジトリの間には直接的な互換性がなくなることを意味します。つまり、元のリポジトリにプッシュするには、強制プッシュ (git push --force または git push --force-with-lease) が必要になります。

4. 共有リポジトリへの適用とチームへの影響

最も注意が必要なのは、既に複数の開発者がクローンしている共有リポジトリに対して履歴書き換えを適用する場合です。履歴を書き換えて強制プッシュすると、チームメンバーが持っているローカルリポジトリの履歴と、リモートリポジトリの履歴が一致しなくなります。

この状況で、チームメンバーが通常の方法でプル (git pull) しようとすると、「Fast-forward push ができない」というエラーや、「関連性のない履歴」といった警告が表示される可能性があります。彼らのローカルリポジトリは古い履歴を参照しているため、強制プッシュによって上書きされた新しい履歴をそのまま受け入れることができません。

このような場合、チームメンバーは以下のいずれかの対応が必要になります。

  • 最も簡単な方法: ローカルリポジトリを削除し、新しい履歴を持つリモートリポジトリから再度クローンし直す。これが最も安全で推奨される方法です。
  • 代替方法: ローカルの変更点をスタッシュするなどして退避させ、リモートの新しい履歴を強制的にプルまたはフェッチ+リセットする。例えば git fetch origin && git reset --hard origin/master のように。ただし、これはローカルで行った、まだプッシュしていない作業が失われる可能性があるため、注意が必要です。

共有リポジトリに履歴書き換えを適用する場合は、必ず事前にチームメンバーに周知し、対応方法を伝えてください。 可能であれば、履歴書き換え作業を行う間は他のメンバーがプッシュしないように協力を仰ぐのが理想的です。

場合によっては、履歴を書き換えたリポジトリを全く新しいリポジトリとして公開し、古いリポジトリの使用を中止する方が、混乱が少ないこともあります。

5. パフォーマンスに関する考慮事項

git filter-repogit filter-branch よりはるかに高速ですが、非常に巨大なリポジトリや複雑なフィルタリング処理では、 still 依然として時間がかかる場合があります。パフォーマンスを向上させるためのヒントをいくつか紹介します。

  • 高速なディスク(SSD)を使用する: git filter-repo は一時的な作業領域や新しいオブジェクトの構築にディスクを使用します。SSD は HDD よりアクセス速度が速いため、全体的な処理時間を短縮できます。一時ディレクトリの場所を環境変数などで指定できる場合、SSD 上に設定すると効果的です。
  • 十分なメモリを確保する: git filter-repo はリポジトリのオブジェクト情報をメモリ上にロードして処理します。十分なメモリがない場合、スワップが発生してパフォーマンスが著しく低下する可能性があります。大きなリポジトリを扱う場合は、十分なメモリを搭載したマシンで作業することをお勧めします。
  • 不要なフィルタリングを避ける: 必要なフィルタリングのみを行うようにします。不必要なコールバックやオプションは、処理時間を増加させる可能性があります。
  • Python スクリプトの最適化: コールバック関数内の Python コードは、実行速度に直接影響します。非効率なループやファイル操作を行わないように注意し、必要に応じてプロファイリングツールなどを使ってボトルネックを特定・解消します。特に、ブロブ内容の読み込みや書き換えはコストが高い操作になり得ます。

6. 公式ドキュメントの参照

git filter-repo は活発に開発されており、新しい機能が追加されたり、既存のオプションの挙動が変更されたりする可能性があります。また、本記事で紹介しきれていない詳細なオプションや高度な機能も多数存在します。常に最新かつ正確な情報を得るためには、git filter-repo の公式ドキュメント(通常は GitHub のリポジトリに README や doc ディレクトリとして含まれています)を参照することを強く推奨します。

git filter-repo の内部動作 (簡易版)

git filter-repo がなぜ git filter-branch より高速で効率的なのかを理解するために、その内部動作の概要に触れておきます。

Git リポジトリは、大きく分けて以下の4種類のオブジェクトから構成されています。

  1. ブロブ (Blob): ファイルの内容そのものを格納します。内容が同じファイルは、リポジトリ全体で一つのブロブとして格納されます。
  2. ツリー (Tree): ディレクトリ構造を表します。ディレクトリ内のファイルやサブディレクトリへの参照(モード、名前、参照先オブジェクトのハッシュ)を格納します。
  3. コミット (Commit): 特定時点のリポジトリのスナップショットを表します。参照するルートツリーのハッシュ、親コミットのハッシュ、作者/コミッター情報、コミットメッセージなどを格納します。
  4. タグ (Tag): 特定のコミットに別名(タグ名)を付けたり、タグ付け者情報やメッセージを付加したりします。軽量タグはコミットへのポインタ、注釈付きタグは独自のオブジェクトです。

git filter-branch は、各コミットを処理する際に、そのコミットに対応するツリーオブジェクトを基にファイルシステム上に実際のディレクトリ構造(ワークツリー)を再構築し、その中でユーザー指定のシェルスクリプトを実行します。スクリプトによる変更(ファイルの追加・削除・変更)を読み取り、それを元に新しいツリーオブジェクトとコミットオブジェクトを作成します。この「ワークツリーの再構築→スクリプト実行→変更取り込み」というサイクルをコミットごとに繰り返すため、非常にオーバーヘッドが大きく、特にファイル数が多いリポジトリでは非効率です。

一方、git filter-repo は、リポジトリのオブジェクトデータベースを直接読み込み、ブロブ、ツリー、コミット、タグのオブジェクトをメモリ上に構築される内部的なデータ構造として扱います。そして、ユーザー指定のフィルタリングロジックやコールバック関数を、これらのメモリ上のオブジェクトに対して適用します。

例えば、ファイルを削除する操作は、ワークツリーからファイルを削除するのではなく、メモリ上のツリーオブジェクトから該当するファイルエントリへの参照を削除する操作として行われます。ファイルの内容変更は、メモリ上のブロブオブジェクトの内容を変更するか、新しい内容を持つブロブオブジェクトを作成し、該当するファイルエントリのブロブ参照を新しいブロブに変更することで行われます。

この「ワークツリーを回避し、メモリ上のオブジェクトを直接操作する」という方式により、ディスク I/O が最小限に抑えられ、オブジェクト間の参照関係の変更も高速に行えます。フィルタリング処理が完了した後、git filter-repo はメモリ上の新しいオブジェクト構造を基に、新しいリポジトリのオブジェクトデータベースを構築します。

さらに、git filter-repo は Git の内部コマンド( plumbing コマンド)を効率的に利用しており、Python のマルチプロセッシング機能などを使って処理を並列化できる部分もあります。これらの要因が組み合わさることで、git filter-branch と比較して格段に高いパフォーマンスを実現しています。

この内部動作の理解は、特に Python コールバックを使って高度なフィルタリングを行う際に役立ちます。コールバック関数内で操作している対象が、ファイルシステム上のファイルではなく、Git の内部オブジェクト(ブロブ、ツリー、コミット)のメモリ上の表現であるということを意識することで、より効果的なスクリプトを作成できます。

よくある質問 (FAQ)

Q1: git filter-branchgit filter-repo は互換性がありますか?

A1: いいえ、直接的な互換性はありません。git filter-branch 用に書かれたシェルスクリプトは、git filter-repo ではそのまま実行できません。git filter-repo を使うには、新しいコマンドオプションや Python コールバックとしてロジックを記述し直す必要があります。ただし、実現できる機能の多くは共通しており、git filter-repo の方がより多くのユースケースに対応できる場合が多いです。

Q2: Windows で git filter-repo を利用できますか?

A2: はい、利用可能です。Windows に Python 3.5 以降と Git 2.22 以降がインストールされていれば、pip install git-filter-repo でインストールして使用できます。ただし、シェル環境(Git Bash, PowerShell, コマンドプロンプトなど)によっては、コマンドの記述方法やパスの指定方法に違いがある場合があるため注意が必要です。特に、Python コールバックでファイルパスなどを扱う際は、OS のパス区切り文字 (\ または /) やエンコーディング(通常は UTF-8 ですが、システムによっては異なる場合も)に注意してください。

Q3: 処理中に git filter-repo が中断した場合、どうなりますか?

A3: git filter-repo は処理の途中で状態を保存しており、多くの場合、中断したところから処理を再開できます。ただし、一時ファイルなどが残る可能性があります。最も安全なのは、作業用クローンディレクトリを一度削除し、再度クローンしてから処理をやり直すことです。デフォルトで作成されるバックアップ (.git-filter-repo/backup/) から復元することも可能です。

Q4: 特定のタグだけをフィルタリングしたいのですが?

A4: --refs オプションを使用して、対象とするタグの名前やパターンを指定します。例えば、特定のタグ v1.0 だけを処理対象にしたい場合は --refs refs/tags/v1.0 と指定します。複数のタグやブランチを組み合わせることも可能です。

Q5: 大きなリポジトリでメモリ不足やディスク容量不足になります。どうすればよいですか?

A5: 前述の「パフォーマンスに関する考慮事項」を参照してください。メモリについては、より多くの RAM を搭載したマシンを使用するか、仮想メモリ(スワップファイル/パーティション)を適切に設定することを検討してください。ディスク容量については、一時ディレクトリの場所を空き容量の多いドライブに指定できるか確認したり、処理前に不要なファイルをできるだけ削除したり(ただしフィルタリングとは別に)、ガベージコレクション (git gc) を実行して無駄なオブジェクトを減らすといった対応が考えられます。非常に大きなリポジトリでメモリが足りない場合は、処理対象を分割するなど、より高度な対応が必要になる可能性もあります。

まとめ

本記事では、Git リポジトリの履歴操作における従来の git filter-branch が抱えていた課題を確認し、その後継ツールである git filter-repo の登場背景、そしてその主要なメリットについて詳しく解説しました。git filter-repo は、その圧倒的な高速性、シンプルで直感的な使いやすさ、高い安全性、そして Python スクリプトによる柔軟なカスタマイズ性といった点で、git filter-branch をはるかに凌駕しており、現在では Git 履歴操作のデファクトスタンダードとなっています。

具体的な使い方として、特定のファイル/ディレクトリの削除、サブディレクトリの切り出し、メッセージやコミッター情報の変更、大きなブロブの削除など、よくあるユースケースに対する基本的なコマンドオプションを紹介しました。さらに、Python コールバック関数を用いた、より複雑で高度なフィルタリングロジックの実装方法についても、具体的なコード例を交えながら詳細に説明しました。

最後に、git filter-repo を安全かつ効果的に使用するための注意点とベストプラクティスとして、必ずクローンで作業すること、結果を十分に確認すること、履歴書き換えがコミットハッシュを変更すること、そして特に共有リポジトリに適用する場合のチームへの影響と周知の重要性を強調しました。

Git リポジトリの履歴操作は強力である一方、リスクも伴う操作です。しかし、git filter-repo という強力かつ安全なツールを使うことで、リポジトリの健全性を保ち、過去の誤りを修正し、リポジトリ構造を最適化することが、以前よりもはるかに容易になりました。

本記事が、読者の皆様が git filter-repo を理解し、自身のプロジェクトで効果的に活用するための一助となれば幸いです。 Git の履歴操作の必要に迫られた際には、ぜひ git filter-repo を選択肢の第一候補として検討してみてください。その使いやすさとパフォーマンスに驚かれることでしょう。

参考文献 / リソース


注記: この記事は、執筆時点での git filter-repo の情報に基づいて書かれています。最新の情報や詳細なオプションについては、必ず公式ドキュメントを参照してください。記事内のコード例は理解を助けるためのものであり、実際の使用においては、対象リポジトリの特性や意図する結果に合わせて適宜修正・テストを行ってください。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール