pre-commitでGitのhookを管理しておくと、
commit時にいろいろなチェックを行ってくれるので、
チェック漏れを事前に防げ便利なのですが、
プロジェクト毎に「.pre-commit-config.yaml」を用意するのも面倒なので、
設定ファイルを書かなくても、有効にできないかを検討してみました。

pre-commit
https://pre-commit.com/

実現方法の概要

「.pre-commit-config.yaml」の自動生成

「.pre-commit-config.yaml」が無いリポジトリでも、
リポジトリ内の設定ファイルを見ると、ある程度は実行すべきhookを判断することができます。

リポジトリ内の設定ファイルから「.pre-commit-config.yaml」を生成し、
生成した「.pre-commit-config.yaml」を使って、pre-commitを実行すれば、
リポジトリ個別に「.pre-commit-config.yaml」を用意する必要は無くなります。

Gitのテンプレートの活用

pre-commitのサイトでも紹介されていますが、
Gitのテンプレートを用意しておけば「pre-commit install」を実行せずとも、
Gitフックからpre-commitが呼ばれる状態にできます。

automatically enabling pre-commit on repositories | pre-commit
https://pre-commit.com/#automatically-enabling-pre-commit-on-repositories

設定の流れ

「pre-commit-config.yaml」の自動生成

「pre-commit-config.yaml」の自動生成するために、以下の2つのファイルを用意すします。

  • ~/.gitconfig.d/build-pre-commit-config.py … pre-commit-configを生成するためのスクリプト
  • ~/.gitconfig.d/pre-commit-config-template.yaml … pre-commit-configのテンプレートファイル

~/.gitconfig.d/build-pre-commit-config.py

#!/bin/env python
import os
import re

context = {
    k: "stages: [manual]"
    for k in [
        "sqlfluff",
        "prettier",
        "flake8",
        "black",
        "ruff"
    ]
}

# detect needed hooks
repo_root = os.path.abspath(".")
if not os.path.exists(os.path.join(repo_root, ".pre-commit-config.yaml")):
    if os.path.exists(os.path.join(repo_root, ".sqlfluff")):
        context["sqlfluff"] = ""
    if os.path.exists(os.path.join(repo_root, ".prettierrc")):
        context["prettier"] = ""
    if os.path.exists(os.path.join(repo_root, "pyproject.toml")):
        with open(os.path.join(repo_root, "pyproject.toml")) as fp:
            pyproject_body = fp.read()
        for ln in pyproject_body.split("\n"):
            if re.match("\[tool.flake8.*\]", ln):
                context["flake8"] = ""
            if re.match("\[tool.black.*\]", ln):
                context["black"] = ""
            if re.match("\[tool.ruff.*\]", ln):
                context["ruff"] = ""

template_path = os.path.join(os.getenv("HOME"), ".gitconfig.d", "pre-commit-config-template.yaml")
output_config_path = "/tmp/pre-commit-config-tmp.yaml"
with open(template_path) as fp:
    tpl_body = fp.read()
with open(output_config_path, "w") as fp:
    fp.write(tpl_body.format(**context))

~/.gitconfig.d/pre-commit-config-template.yaml

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    - id: mixed-line-ending
      args:
        - --fix=lf
    - id: detect-aws-credentials
    - id: detect-private-key
  - repo: https://github.com/sqlfluff/sqlfluff
    rev: 3.1.1
    hooks:
      - id: sqlfluff-lint
        additional_dependencies:
          - sqlfluff-templater-dbt==3.1.1
        {sqlfluff}
      - id: sqlfluff-fix
        additional_dependencies:
          - sqlfluff-templater-dbt==3.1.1
        {sqlfluff}
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black
        {black}
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.4
    hooks:
      - id: ruff
        # args:
        #  - --fix # commentin if you like autofix
        {ruff}
      - id: ruff-format
        {ruff}
  - repo: https://github.com/PyCQA/flake8
    rev: 7.1.1
    hooks:
      - id: flake8
        {flake8}
  - repo: local
    hooks:
      - id: prettier-yaml
        name: Prettier YAML
        entry: prettier --write
        language: system
        types: [yaml]
        {prettier}

Gitのテンプレートの設定

テンプレートディレクトリを設定、作成します。

$ git config --global init.templateDir ~/.gitconfig.d/git-template
$ mkdir -p ~/.gitconfig.d/git-template/hooks
$ touch ~/.gitconfig.d/git-template/hooks/pre-commit
$ chmod +x ~/.gitconfig.d/git-template/hooks/pre-commit

pre-commitで実行するスクリプトを作成します。

~/.gitconfig.d/git-template/hooks/pre-commit

#!/usr/bin/env bash

# generate pre-commit-config.yaml
~/.gitconfig.d/build-pre-commit-config.py

# start templated
INSTALL_PYTHON=/usr/bin/python3
ARGS=(hook-impl --config=/tmp/pre-commit-config-tmp.yaml --hook-type=pre-commit --skip-on-missing-config)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi

# default
# start templated
INSTALL_PYTHON=/usr/bin/python3
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit --skip-on-missing-config)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

if [ -f .pre-commit-config.yaml ]; then
    if [ -x "$INSTALL_PYTHON" ]; then
        exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
    elif command -v pre-commit > /dev/null; then
        exec pre-commit "${ARGS[@]}"
    else
        echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
        exit 1
    fi
fi

試してみる

動作確認用のGitリポジトリを作成します。

$ mkdir sample-repo && cd $_
$ git init

適当なファイルを作成して、commitを実行します。
「~/.gitconfig.d/pre-commit-config-template.yaml」で設定している既定のhookが実行されます。

$ touch README.md
$ git add README.md
$ git commit -a -m "commit test default"
mixed line ending........................................................Passed
detect aws credentials...................................................Passed
detect private key.......................................................Passed
[master (root-commit) bccd1d0] commit test default
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

リポジトリのpyproject.tomlに、flake8の設定を追記して、commitを実行します。
既定のhookに加えて、flake8のhookも実行されます。

$ echo "[tool.flake8]" > pyproject.toml
$ git commit --allow-empty -m "commit test +flake8"
mixed line ending....................................(no files to check)Skipped
detect aws credentials...............................(no files to check)Skipped
detect private key...................................(no files to check)Skipped
flake8...............................................(no files to check)Skipped
[master 4a18813] commit test +flake8

以上。