Pre-commitフレームワーク導入ガイド:コミット前にコードスタイルとlintを自動チェックする

Git tutorial - IT technology blog
Git tutorial - IT technology blog

背景:なぜチームでPre-commitを導入したのか

チームが4〜5人だった頃は、コードレビューがかなり気軽でした — みんな顔見知りで、お互いのコーディングスタイルも把握していたからです。しかし8人に増え、シニア3人・ミドルレベル2人・新人ジュニア3人という体制になると、混乱が生じ始めました。PRには必ず「ファイル末尾のnewlineがない」「スペースの代わりにタブでインデント」「importが整理されていない」というコメントがつくようになりました。レビュアーは時間を無駄にし、レビューを受ける側はツールが自動的に検出すべきことをいちいち指摘されてうんざりしていました。

あるオープンソースリポジトリで使われているのを見て、pre-commitフレームワークを導入することにしました。結果は明らかでした。PRでのスタイル・フォーマット関連のコメントがほぼゼロになり、レビュアーは余分なカンマではなくロジックに集中できるようになりました。8人チームで既存のGit flowと並行してpre-commitを導入したところ、コードの一貫性が高まりフォーマットの衝突が減ったことで、マージコンフリクトも大幅に減少しました。

Pre-commitはGit hooksを管理するフレームワークです — 具体的には、git commitコマンドでコミットを作成する直前に実行されるpre-commitフックを管理します。フックのいずれかが失敗すると、コミットはブロックされます。手動でフックを書く場合との大きな違いは、pre-commitが各フックの依存関係を個別に管理し、プロジェクトのPythonやNode環境には干渉しない点です — 各フックは独自の分離されたvirtualenvで実行されます。

Pre-commitのインストール

必要要件

Pre-commitにはPython 3.8以上とpipが必要です。まず確認しましょう:

python --version   # Python 3.8.x以上
pip --version

グローバルまたはvirtualenvへのインストール

# グローバルにインストール
pip install pre-commit

# またはpipxで(分離されていてクリーン — 自分はこちらを使用)
pipx install pre-commit

# バージョン確認
pre-commit --version
# pre-commit 3.7.x

リポジトリへのフック有効化

インストールが完了したら、リポジトリのディレクトリに移動して次の1コマンドを実行します:

cd /path/to/your/repo
pre-commit install

このコマンドで.git/hooks/pre-commitファイルが作成されます — これ以降、git commitのたびにpre-commitが自動実行されます。git pushもブロックしたい場合は、次のコマンドも実行します:

pre-commit install --hook-type pre-push

.pre-commit-config.yamlによる詳細設定

すべての設定はリポジトリルートの.pre-commit-config.yamlファイルに記述します。このファイルをgitにコミットすることで、チーム全員が自動的に同じ設定を共有できます — 各自がセットアップする必要はありません。

Pythonプロジェクトの基本設定

# .pre-commit-config.yaml
repos:
  # pre-commit-hooksの基本フック
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace      # 行末の余分な空白を削除
      - id: end-of-file-fixer        # ファイルがnewlineで終わることを確認
      - id: check-yaml               # YAML構文をチェック
      - id: check-json               # JSON構文をチェック
      - id: check-merge-conflict     # コンフリクトマーカーの残留を検出
      - id: check-added-large-files  # 大きすぎるファイルをブロック(バイナリの誤コミット防止)
        args: ['--maxkb=500']
      - id: debug-statements         # print()、breakpoint()の残留を検出

  # Black — Pythonのコードフォーマッタ
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black
        language_version: python3

  # isort — importステートメントの整列
  - repo: https://github.com/PyCQA/isort
    rev: 5.13.2
    hooks:
      - id: isort
        args: ["--profile", "black"]  # Blackと互換性のある設定

  # Flake8 — ロジックとスタイルのエラーを検出するlinter
  - repo: https://github.com/PyCQA/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        args: ['--max-line-length=88']  # Blackのデフォルトに合わせる

JavaScript/TypeScript用フックの追加

PythonとJSが混在するプロジェクト(APIバックエンド+フロントエンド)の場合、reposセクションに追加します:

  # Prettier — JS/TS/CSS/JSONのフォーマット
  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v4.0.0-alpha.8
    hooks:
      - id: prettier
        types_or: [javascript, typescript, css, json]

  # ESLint
  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v9.4.0
    hooks:
      - id: eslint
        files: \.(js|ts|jsx|tsx)$
        types: [file]

ローカルフック — 自作スクリプト

個人的に最も強力だと思う機能がこれです:どこかに公開する必要なく、リポジトリ内に置くだけで独自のフックを作れます:

  # ローカルフック — リポジトリ内のスクリプトを実行
  - repo: local
    hooks:
      - id: no-leftover-todos
        name: Check for leftover TODO/FIXME in staged files
        entry: grep -n "TODO\|FIXME\|HACK"
        language: system
        types: [python]
        pass_filenames: true

      - id: run-unit-tests
        name: Run unit tests
        entry: python -m pytest tests/unit -q --tb=short
        language: system
        pass_filenames: false
        stages: [pre-push]  # コミットごとではなく、pushのときだけ実行

stages: [pre-push]に注意してください — 重いテストはpre-pushに、軽いlintはpre-commitに設定することで、日々のコミットワークフローが遅くなりません。

フックバージョンの自動更新

設定ファイルを書いたら、次のコマンドですべてのフックのrevを最新バージョンに更新します:

pre-commit autoupdate

このコマンドを月1回のスケジュールに登録しています — セキュリティ問題のある古いフックを使い続けることを防ぐためです。

チェックとモニタリング

コミット前の手動実行

ダミーコミットを作らずにコードベース全体をチェックしたい場合:

# すべてのファイルに対してすべてのフックを実行
pre-commit run --all-files

# 特定のフックだけ実行
pre-commit run black --all-files
pre-commit run flake8 --all-files

# 特定のファイルに対して実行
pre-commit run --files src/main.py src/utils.py

初回実行時、pre-commitは各フック用の専用環境をダウンロード・インストールするため、1〜2分ほどかかります。2回目以降はキャッシュが使われるため即座に実行されます。

フック失敗時の出力を読む

コミットがブロックされた場合の実際の出力例です:

$ git commit -m "Add user authentication"
[INFO] Stashing unstaged changes to tracked files.
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check for merge conflicts................................................Passed
black....................................................................Failed
- hook id: black
- files were modified by this hook

reformatted src/auth.py

isort....................................................................Failed
- hook id: isort
- files were modified by this hook

Fixing src/auth.py

Blackとisortはファイルを自動修正します — git add src/auth.pyしてから再度コミットするだけです。flake8は異なります:エラーを報告しますが自動修正はしないため、指示に従って手動で修正する必要があります。

本当に必要な場合のフックスキップ

# すべてのフックをスキップ(緊急hotfix)
git commit --no-verify -m "hotfix: production down"

# 特定のフックをスキップ
SKIP=flake8 git commit -m "WIP: will fix lint later"

チームのルールとして、--no-verifyを使う場合はコミットメッセージに理由を明記し、1日以内に修正するためのチケットを作成することにしています。厳格なルールというわけではありませんが、全員が責任感を持てる程度には機能しています。

CI/CDとの連携でスキップを防止する

ローカルフックはdev環境でしか実行されません — pre-commitをインストールせずにpushする人がいる可能性があります。GitHub Actionsに追加してCI側でもブロックします:

# .github/workflows/lint.yml
name: Lint Check
on: [push, pull_request]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - uses: pre-commit/[email protected]  # pre-commit公式Action

このActionはpre-commitのキャッシュを再利用するため、CIでは通常30〜60秒しかかかりません — フルテストスイートの実行よりはるかに軽量です。

新メンバーのオンボーディング

新しいメンバーがリポジトリをクローンしたら、2つのコマンドを実行するだけです:

pip install pre-commit
pre-commit install

この2つのコマンドをMakefilesetupターゲットとREADMEに追加して、誰も忘れないようにしています。もう一つのTips:オンボーディング時にすぐpre-commit run --all-filesを実行してもらう — 新メンバーがコードベースの現状を把握し、最初から既存のlintの問題をすべて修正できるため、技術的負債の積み上げを防げます。

8人チーム全体への導入から最初の1ヶ月で、スタイル起因のCI失敗率が約40%から5%未満に減少しました。さらに重要なことに、コードレビューがアーキテクチャとロジック — ツールが数秒で処理できる余分なカンマではなく、本当に人間が読む必要のあるもの — に集中するようになりました。

Share: