Python Monthly Topics

pre-commit互換のGitフック管理フレームワーク「prek」紹介

筒井@ryu22eです。今月の「Python Monthly Topics」は、Gitフックを管理するフレームワークprekを紹介します。prekは、Pythonプログラマーの間で人気があるpre-commitのRust実装です。

prekの使い方はpre-commitとほぼ同じですが、本記事ではprek / pre-commitをどちらも使ったことがない人向けに、使い方をていねいに解説します。また、pre-commitから移行したい人のために、最後に移行方法についても解説します。

なお、本記事の内容はprek 0.4.8(執筆時点での最新版)を前提にしています。

なぜprek/pre-commitを使うか

ここでは、prek/pre-commitを使うと便利な場面について説明します。

Gitには「Gitフック」という機能があります。Gitフックは特定の操作の前後にカスタムスクリプト(シェルスクリプト)を実行して操作内容に問題がないかを検証してくれます。たとえば、コミットの直前にRuffのようなPythonリンターを実行して、Ruffが問題を検出したらコミットを中断させることができます。

Gitフックに関する詳細は、以下の公式ドキュメントを参照してください。

Gitフックの作り方について説明します。Gitリポジトリには、ディレクトリ.git/hooks/以下に拡張子sampleのファイルがいくつかあります。

.git/hooks/以下のsampleファイル
$ mkdir git-example  # 専用のディレクトリを作成
$ cd git-example
$ git init
Initialized empty Git repository in /****/git-example/.git/
$ ls .git/hooks/  # 拡張子sampleのファイルが複数ある
applypatch-msg.sample     post-update.sample        pre-merge-commit.sample   pre-receive.sample        sendemail-validate.sample
commit-msg.sample         pre-applypatch.sample     pre-push.sample           prepare-commit-msg.sample update.sample
fsmonitor-watchman.sample pre-commit.sample         pre-rebase.sample         push-to-checkout.sample

これらのファイルの拡張子のsampleを取ると、Gitはそのファイルをカスタムスクリプトとして認識します。各ファイルの用途については以下の公式ドキュメントを参照してください。

なお、.git/hooks/以下に置いたカスタムスクリプトは、Gitのコミット対象になりません。

Gitフックは、誤操作を未然に防ぐなどの利点がある一方で、以下のような問題もあります。

  • シェルスクリプトなので書きにくい
  • 複数のプロジェクトで共通のカスタムスクリプトを作る場合の管理が難しい
  • カスタムスクリプトの中でPython、Ruby、Node.jsなどの言語に依存するツールを呼び出す際は、スクリプトファイルの設置とは別に初期化処理が必要な場合がある

prek/pre-commitは、上記の問題を解決するために作られたフレームワークです。prek/pre-commitは、フック名が書かれた設定ファイルを元にGitのカスタムスクリプトを作成してくれます。カスタムスクリプトの修正が必要な場合も該当ファイルを1つ1つ編集する必要はありません。生成元のフックの定義を修正し、カスタムスクリプトを再作成するだけで済みます。

また、実行に必要な初期化処理も自動的に行なってくれます。たとえば、カスタムスクリプトでPythonのツールを呼び出すのであれば、仮想環境の作成、ツールのインストールも自動化されます。

prekとpre-commitの違い

prekはpre-commitより後発のフレームワークです。pre-commitはPython製のため、実行にPythonが必要ですが、prekはRustで実装されています。Rustはシングルバイナリとしてコンパイルされるため、ランタイムを別途インストールする必要がありません。

また、prekはpre-commitと互換性があります。詳細は後述しますが、pre-commitの設定ファイルをそのまま使えるので、移行は容易です。以下の有名リポジトリでもpre-commitからprekに移行しています。

prekの主な特徴は以下のとおりです。prekはpre-commitより後発だけあって、性能面、機能面でさまざまな改善点があります。

  • ビルトインフックをRustで実装して高速化
  • ディスク容量の使用効率を改善
  • フックの並列実行を可能に

prekのインストール方法

prekのインストール方法は複数提供されています。以下で代表的な例を紹介します。

prekのインストール方法

uvを使う場合(公式の推奨)
$ uv tool install prek
pipを使う場合
$ pip install prek
Homebrewを使う場合
$ brew install prek

その他のインストール方法は、以下の公式ドキュメントを参照してください。

prekの使い方

ここでは、prekの主なコマンドと使い方について説明します。本記事で紹介している以外の内容については、以下の公式ドキュメントを参照してください。

prek sample-configで初期設定を行う

prekの設定ファイルはYAML形式の.pre-commit-config.yaml(pre-commitでサポートする形式⁠⁠、またはTOML形式のprek.tomlです。本記事では、prek.tomlを採用する前提で解説します。

prek sample-configコマンドで設定ファイルのサンプルの生成ができます。--formatオプションでファイル形式(yamlまたはtoml。デフォルトはyaml)指定します。また、-fオプションをつけるとファイルに内容を出力します(デフォルトは標準出力⁠⁠。ファイル名はYAML形式なら.pre-commit-config.yaml、TOML形式ならprek.tomlです。

以下の手順でprek.tomlを含むGitリポジトリを作成してみましょう。

prek sample-configコマンドの実行例
$ mkdir prek-example  # 専用のディレクトリを作成
$ cd prek-example
$ git init  # まずGitリポジトリの初期化
Initialized empty Git repository in /***/prek-example/.git/
$ prek sample-config --format toml -f  # サンプルをprek.tomlに出力
Written to `prek.toml`
$ cat prek.toml
# Configuration file for `prek`, a git hook framework written in Rust.
# See https://prek.j178.dev for more information.
#:schema https://www.schemastore.org/prek.json


repo = "builtin"
hooks = [
    { id = "trailing-whitespace" },
    { id = "end-of-file-fixer" },
    { id = "check-added-large-files" },
]
$ git add prek.toml  # prek.tomlをGit管理対象に入れる
$ git commit -am "add prek.toml"
[main (root-commit) 45fbeae] add prek.toml 1 file changed, 11 insertions(+) create mode 100644 prek.toml

prek.tomlのrepo = "builtin"以下に書かれているのが、prekが標準で提供しているビルトインフックです。それぞれ、以下の機能があります。

フックID 説明
trailing-whitespace 行末にスペースがあるとエラーになる(自動的に除去も行う)
end-of-file-fixer ファイル末尾に改行がないとエラーになる(自動的に改行付与も行う)
check-added-large-files ファイルサイズが大きい(デフォルトは500KB)ファイルをコミットしようとするとエラーになる

argsにオプション引数を渡すことで、フックの挙動を変更できます。たとえば、check-added-large-filesでチェックする最大ファイルサイズを1024KBに変更する場合は、以下のように書きます。

check-added-large-fileでチェックする最大ファイルサイズを1024KBに変更

repo = "builtin"
hooks = [
    { id = "trailing-whitespace" },
    { id = "end-of-file-fixer" },
    {
        id = "check-added-large-files",
        args = ["--maxkb=1024"]  # これを追加
    },
]

複数のフックを並列実行したい場合は、フックの実行優先順位を表すpriorityオプションを指定します。priorityは値が小さいほど優先的に実行され、同じ値のフックは並列実行になります。以下の例では、trailing-whitespaceend-of-file-fixerを並列実行するようpriorityを設定しています。

priorityの設定例

repo = "builtin"
hooks = [
    # trailing-whitespace、end-of-file-fixerは並列実行
    { id = "trailing-whitespace", priority = 0 },
    { id = "end-of-file-fixer", priority = 0 },
    # 上記の後にcheck-added-large-filesを実行
    { id = "check-added-large-files", priority = 1 },
]

なお、priorityを指定しない場合は上のフックから順に実行されます。

その他のビルトインフックについては、以下の公式ドキュメントを参照してください。

また、ビルトインフック以外にサードパーティのフックも利用できます。サードパーティフックはpre-commit用のフックをそのまま使えます。詳細は、以下のpre-commit公式ドキュメントで紹介しているサードパーティフックを参照してください。

prek installでGitフックを作成

prek installコマンドを実行すると、prek.tomlを元にGitフックを作成します。

prek installコマンドの実行例
$ prek install
prek installed at `.git/hooks/pre-commit`
$ cat .git/hooks/pre-commit  # このGitフックが作成される
#!/bin/sh
# File generated by prek: https://github.com/j178/prek
# ID: 182c10f181da4464a3eec51b83331688

HERE="$(cd "$(dirname "$0")" && pwd)"
PREK="/****/.local/bin/prek"

# Check if the full path to prek is executable, otherwise fallback to PATH
if [ ! -x "$PREK" ]; then
    PREK="prek"
fi

exec "$PREK" hook-impl --hook-dir "$HERE" --script-version 4 --hook-type=pre-commit -- "$@"

デフォルトでは、コミット前に実行されるプレコミットフックのみが作成されます。他のフックも作成したい場合は、prek.tomlにdefault_install_hook_typesを追加します。

たとえば、プレコミットフックpre-commitに加えてプレプッシュフックpre-pushも加えたい場合は、以下のように書きます。

default_install_hook_typesの設定例
default_install_hook_types = [
  "pre-commit",
  "pre-push"
  ]


# (省略)

default_install_hook_typesでサポートする値は、以下の公式ドキュメントを参照してください。

prek uninstallでGitフックを削除

prek installコマンドで作成したGitフックを削除するには prek uninstallコマンドを実行します。一時的にGitフックを無効にしたい場合にこれを使います。

prek uninstallコマンドの実行例
$ prek uninstall
Uninstalled `pre-commit`
$ prek install  # 後述する手順を実行するために再度Gitフックを作成
prek installed at `.git/hooks/pre-commit`

変更をコミットしてGitフックを実行してみる

行末にスペースを入れたファイルを追加して、git commitを実行してみましょう。プレコミットフックの中でprekが初期化処理を行ってから、prek.tomlに定義されたフックを実行します。この例では、trailing-whitespaceでエラーを検出して、コミットに失敗します。

行末にスペースが入ったファイルをコミット
$ echo "行末にスペースを入れる→ " > test.txt
$ git add test.txt
$ git commit -am "add test.txt"
trim trailing whitespace

.................................................

Failed

- hook id: trailing-whitespace

- exit code: 1

- files were modified by this hook

Fixing test.txt fix end of files

.........................................................

Passed

check for added large files

..............................................

Passed

実行時にファイルを自動修正してくれるので、2度目のコミットは成功します。

2度目のコミット
$ git commit -am "add test.txt"
trim trailing whitespace

.................................................

Passed

fix end of files

.........................................................

Passed

check for added large files

..............................................

Passed

[main 6098a84] add test.txt 1 file changed, 1 insertion(+) create mode 100644 test.txt

プレコミットフックは、git commitでコミット対象になるファイルに対してのみ実行されます。すでにコミットされたファイルに対しては何もしません。後からprekを導入した場合にこの仕様が問題になる場合があります。

今までコミットしたファイルも含めてチェックを実行したい場合は、次に紹介するprek runコマンドに-aまたは--all-filesオプションをつけて実行します。

prek runでフックを手動実行する

prek runコマンドはgit commitコマンドでコミット対象になるファイルに対してGitフックと同じチェック処理を実行します。

prek runコマンドの実行例
$ echo "行末にスペースを入れる→ " > test2.txt
$ git add test2.txt
$ prek run
trim trailing whitespace
# (省略)

Failed

- hook id: trailing-whitespace

- exit code: 1

- files were modified by this hook

Fixing test2.txt fix end of files

# (省略)
$ git status  # 行末のスペースが除去されているので、さらに差分が生まれている
On branch main Changes to be committed: (use "git restore --staged <file>..." to unstage)

new file: test2.txt

Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory)

modified: test2.txt
$ git add test2.txt
$ prek run  # 行末のスペースが除去されているので、今度はエラーにならない
# (省略)
Passed
$ git commit -am "add test2.txt"
# (省略)
[main db1c5a9] add test2.txt
 1 file changed, 1 insertion(+)
 create mode 100644 test2.txt

また、--filesオプションでファイル名を指定すれば、git addでコミット対象に入れていないファイルもチェックできます。

prek run --filesコマンドの実行例
$ echo "行末にスペースを入れる→ " > test3.txt
$ prek run --files test3.txt  # --filesオプションでファイル名を指定すればgit addでコミット対象に入れていないファイルもチェックできる
trim trailing whitespace
# (省略)

Failed

- hook id: trailing-whitespace

- exit code: 1

Fixing test3.txt fix end of files

# (省略)
$ git add test3.txt
$ git commit -am "add test3.txt"
# (省略)
[main 81f8e14] add test3.txt
 1 file changed, 1 insertion(+)
 create mode 100644 test3.txt

-aまたは--all-filesオプションを付けると、すでにコミットされているファイルも対象にしてチェック処理を実行します。

以下の例では、prek uninstallコマンドで一旦Gitフックを無効にしてから行末にスペースが入ったファイルtrailing-whitespaceフックでエラーになる)をコミットし、その後prek run -aコマンドを実行しています。

prek run -aコマンドの実行例
$ prek uninstall  # 一旦Gitフックを無効化
Uninstalled `pre-commit`
$ echo "行末にスペースを入れる→ " > test4.txt  # trailing-whitespaceフックでエラーになるファイル
$ git add test4.txt
$ git commit -am "add test4.txt"  # Gitフックを無効化しているのでコミットできる
$ prek install  # Gitフックを再び有効化
$ prek run -a  # すでにコミットしたファイルも対象にチェックを実行し、test4.txtがエラーになる
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook

  Fixing test4.txt
fix end of files.........................................................Passed
check for added large files..............................................Passed

-aまたは--all-filesオプションは以下のような場合に役立ちます。

  • 後からprekを導入する場合(既存のファイルにルール違反のファイルがないかチェックできる)
  • GitHub ActionsなどのCI上で実行する(Gitフックのチェックを通さずコミットしたファイルをここでチェックできる)

GitHub Actionsで実行する場合は、j178/prek-actionというアクションが提供されています。

.github/workflows/の下に以下のようなYAMLファイルを置くと、GitHub Actions上でprek run -aコマンドを実行してくれます。

j178/prek-actionの使用例
name: Prek checks
on: [push, pull_request]

jobs:
  prek:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: j178/prek-action@v2  # ここでprek run -aを実行

prek updateでフックのリビジョンを更新する

prekでは、サードパーティのフックはリビジョン番号の指定revが必須のため、必ずリビジョン番号を固定化して使います。ただ、カスタムスクリプトから呼ぶツールを最新版にしたい、既存の不具合を修正したリビジョンを使いたい、などの理由からリビジョン番号を更新したい場合もあります。

そんな時に役立つのがprek updateコマンドです。prek updateコマンドは、prek.tomlに書かれたフックのリビジョン番号を更新してくれます。

以下は、prek.tomlにサードパーティのフックastral-sh/ruff-pre-commitのv0.15.18(最新リビジョンではない)を指定している例です。revに書かれているのがフックのリビジョンです。

フックにastral-sh/ruff-pre-commitのv0.15.18を指定

repo = "https://github.com/astral-sh/ruff-pre-commit"
rev = "v0.15.18"  # これがフックのリビジョン
hooks = [
  { id = "ruff-check" },
  { id = "ruff-format" }
]

カレントディレクトリに上記のprek.tomlファイルがある状態でprek updateコマンドを実行すると、revに書かれたリビジョンを最新版に更新してくれます。

prek updateコマンドの実行例
$ prek update
https://github.com/astral-sh/ruff-pre-commit
  updating rev `v0.15.18` -> `v0.15.20`
$ cat prek.toml  # prek.tomlの現在の内容を確認

repo = "https://github.com/astral-sh/ruff-pre-commit"
rev = "v0.15.20"  # これがフックのリビジョン
hooks = [
  { id = "ruff-check" },
  { id = "ruff-format" }
]

次回のGitフック実行時には、更新後のリビジョンのフックをダウンロードしてチェックを実行してくれます。

なお、v0.15.20は本記事執筆時点でのastral-sh/ruff-pre-commitの最新リビジョンです。実行タイミングによっては結果が異なる場合があります。

AIエージェントのスキルとして使う

prekはAIエージェントからprekを呼ぶためのSKILL.mdを作成する機能もあります。SKILL.mdはAIエージェントに「prekとは何か」⁠prekの使い方」を伝え、prekを適切に使うように指示できます。

この機能はGitHub CLIと組み合わせて使います。gh skill install j178/prek prekコマンドを実行すると、SKILL.mdが作成されます。

gh skill install j178/prek prekコマンドの実行例
$ mkdir prek-skill  # 専用のディレクトリを作成
$ cd prek-skill
$ git init
Initialized empty Git repository in /****/prek-skill/.git/
$ gh skill install j178/prek prek
Using ref v0.4.5 (bb310828)

! Skills are not verified by GitHub and may contain prompt injections, hidden instructions, or malicious scripts. Always review skill contents before use.


? Select target agent(s):  [Use arrows to move, space to select, <right> to all, <left> to none, type to filter]
> [x]  GitHub Copilot
# agentは「GitHub Copilot」、Installation scopeは「install in current repository (recommended)」を選択
# (省略)
✓ Installed prek (from j178/prek@v0.4.5) in .agents/skills

  prek/
  └── SKILL.md

! Skills may contain prompt injections or malicious scripts.
  Review installed content before use:

    gh skill preview j178/prek prek@bb3108283fd4aab0d9eb59f23098e0c05a96cdbe
$ ls .agents/skills/prek  # ここにSKILL.mdが作成される
SKILL.md
$ git add .agents
$ git commit -am "add prek skill"
[main (root-commit) b2b91af] add prek skill
 1 file changed, 219 insertions(+)
 create mode 100644 .agents/skills/prek/SKILL.md

以下は、GitHub Copilot Chat拡張をインストールしたVisual Studio Codeから前述のスキルを呼び出している画面です。プロンプトはastral-sh/ruff-pre-commitフックのv0.15.20を入れたprek.tomlを作成してからprek installを実行し、prek.tomlをコミットしてください。です。

VSCode + GitHub Copilot Chatでprekスキルを呼び出し
VSCode + GitHub Copilot Chatでprekスキルを呼び出し

pre-commitからの移行方法

すでにpre-commitを導入しているプロジェクトでprekを導入する方法について解説します。

prekはpre-commitの設定ファイル.pre-commit-config.yamlをサポートします。そのため、pre-commitコマンドの呼び出しをprekコマンドの呼び出しに置き換えるだけで移行できます。

pre-commitとprekのサブコマンドはほぼ同じです(一部、名前やオプションが異なるものがあります⁠⁠。prekとpre-commitのサブコマンドの対比表は以下のとおりです。

また、prek.tomlを使いたい場合は、.pre-commit-config.yamlをprek.tomlに変換するprek util yaml-to-tomlコマンドが提供されています。以下がprek util yaml-to-tomlコマンドの使用例です。

まずprek sample-configコマンドで.pre-commit-config.yamlを作成してから、prek util yaml-to-tomlコマンドで.pre-commit-config.yamlをprek.tomlに変換しています。

prek util yaml-to-tomlコマンドの使用例
$ mkdir yaml-to-toml-example  # 専用のディレクトリを作成
$ cd yaml-to-toml-example
$ prek sample-config -f  # まず.pre-commit-config.yamlを作成
$ cat .pre-commit-config.yaml  # .pre-commit-config.yamlの内容を確認
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
  - repo: 'https://github.com/pre-commit/pre-commit-hooks'
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
$ prek util yaml-to-toml  # .pre-commit-config.yamlをprek.tomlに変換
Converted `.pre-commit-config.yaml` → `prek.toml`
$ rm .pre-commit-config.yaml  # .pre-commit-config.yamlは不要になるので消す
$ cat prek.toml  # prek.tomlの内容を確認
# Configuration file for `prek`, a git hook framework written in Rust.
# See https://prek.j178.dev for more information.
#:schema https://www.schemastore.org/prek.json


repo = "https://github.com/pre-commit/pre-commit-hooks"
rev = "v6.0.0"
hooks = [
  { id = "trailing-whitespace" },
  { id = "end-of-file-fixer" },
  { id = "check-yaml" },
  { id = "check-added-large-files" }
]

最後に

Python製のpre-commitはPythonがない環境では動かないため、Python以外で開発するプロジェクトに導入しにくいと感じることが個人的にありました。シングルバイナリで動くpre-commitのような類似プロダクトはないものか、と数年前からぼんやり願っていたのですが、そのものズバリのものが登場していたのですね。

実際にpre-commitを使っている自分のリポジトリに導入してみましたが、思った以上に移行が簡単で、かつ使い方もほぼpre-commitなので、すっかり気に入ってしまいました。この先も常用したいと思っています。

pre-commitを愛用している人はもちろん、はじめて知った人もぜひ使ってみてください!

おすすめ記事

記事・ニュース一覧