2026年10月14日
2026年10月14日
Gitのpre-commitフックで品質チェックを自動化する方法
Git の pre-commit フックを使うと、git commit を実行した瞬間に構文チェックやコード規約チェックを走らせ、問題があればコミットを止められます。チームで使うなら、.git/hooks に直接置くのではなく **core.hooksPath でリポジトリ内のフォルダを指定するか、pre-commit フレームワークで設定ファイルとして共有する** のが現実的です。この記事では、フックの仕組み、PHP プロジェクト向けの実例、共有方法、そして「フックだけに頼らない」運用までを説明します。
pre-commit フックの仕組み
Git には、特定の操作の前後に任意のスクリプトを実行する「フック」があります。pre-commit はその一つで、コミットメッセージを入力する前に実行されます。スクリプトが 0 以外の終了コードを返すと、コミットは中止されます。
押さえておきたい性質は次の3点です。
- フックの置き場所は既定で
.git/hooks/です。ここは Git の管理対象外 なので、クローンした人には配られません。 - Linux や macOS では、スクリプトに実行権限が要ります。権限がないとフックは実行されず、短いヒントが出るだけでコミットは通ってしまうため、「設定したのに動かない」原因の上位です。
git commit --no-verify(短縮形-n)でフックを飛ばせます。つまりフックは開発者の手元で早めに気づくための仕組みであり、最終的な品質の保証にはなりません。
まずは素のシェルスクリプトで書いてみる
仕組みを理解するために、PHP ファイルの構文チェックと PHP_CodeSniffer を行うフックを書いてみます。リポジトリ直下に .githooks/pre-commit を作ります。
#!/bin/sh
# ステージされた PHP ファイルだけを対象にチェックする
files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php')
if [ -z "$files" ]; then
exit 0
fi
status=0
for f in $files; do
if ! php -l "$f" > /dev/null; then
echo "構文エラー: $f"
status=1
fi
done
if [ "$status" -ne 0 ]; then
echo "PHP の構文エラーを直してからコミットしてください"
exit 1
fi
if [ -x vendor/bin/phpcs ]; then
# shellcheck disable=SC2086
vendor/bin/phpcs --standard=phpcs.xml.dist $files || exit 1
fi
exit 0
ポイントを補足します。
git diff --cached --name-only --diff-filter=ACMは、ステージ済みのうち追加(A)・コピー(C)・変更(M)されたファイル名を返します。削除されたファイルを除外しないと、存在しないファイルをチェックしようとして失敗します。- リポジトリ全体ではなく変更ファイルだけを対象にすることで、既存コードの指摘で毎回止まる事態を避けられます。チェックが遅いとフックを飛ばす人が増えるので、数秒で終わる範囲に絞るのがコツです。
- この簡易版は、ファイル名に空白を含む場合に正しく動きません。また、チェックしているのは作業ツリー上のファイルなので、一部だけステージした(
git add -p)場合はステージ内容とずれることがあります。後述の pre-commit フレームワークはこの点を自動で扱ってくれます。
core.hooksPath でチームに共有する
作ったスクリプトをリポジトリに含め、各自が一度だけ次のコマンドを実行すれば、.githooks/ がフックの置き場所になります。
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
core.hooksPath は比較的新しい Git(2.9 以降)で使える設定です。git config はリポジトリごとのローカル設定なので、クローンし直したら再設定が必要です。README の開発環境セットアップ手順に書いておくか、composer.json の scripts に登録して composer install 後に実行させると、設定漏れを減らせます。
Windows の場合、Git for Windows に同梱されたシェルでフックが実行されるため、先頭の #!/bin/sh のままで動くことが多いです。改行コードが CRLF になるとシェルが解釈できずに失敗するので、.gitattributes で *.sh や .githooks/* を eol=lf に固定しておくと安心です。
pre-commit フレームワークで管理する
フックが増えてくると、シェルスクリプトの保守が負担になります。そこで便利なのが、Python 製のツール pre-commit(pre-commit.com)です。.pre-commit-config.yaml にチェック内容を宣言すると、ツールの取得・キャッシュ・実行を代わりに行ってくれます。言語は Python に限らず、PHP や JavaScript のプロジェクトでも使えます。
pip install pre-commit # または pipx install pre-commit
pre-commit install # .git/hooks/pre-commit を設置
pre-commit run --all-files # 導入時に全ファイルを一度チェック
設定ファイルの例です。
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
- id: detect-private-key
- repo: local
hooks:
- id: php-lint
name: php -l
entry: bash -c 'for f in "$@"; do php -l "$f" > /dev/null || exit 1; done' --
language: system
files: \.php$
- id: phpcs
name: PHP_CodeSniffer
entry: vendor/bin/phpcs --standard=phpcs.xml.dist
language: system
files: \.php$
revはタグやコミットで固定します。上のv5.0.0は例なので、導入時にpre-commit autoupdateを実行して最新のタグに更新してください。最新のタグは各リポジトリで確認できます(2026年10月現在)。repo: localとlanguage: systemを使うと、プロジェクトに入っているvendor/bin/phpcsなどをそのまま呼べます。filesは正規表現で対象を絞ります。detect-private-keyは秘密鍵らしき文字列の混入を検出します。API キーなどのより広い検出には gitleaks のような専用ツールをフックに加える方法もあります。いずれも完璧な検出はできないので、.envを.gitignoreに入れるなど、そもそもコミットしない運用と組み合わせてください。
pre-commit フレームワークは、一部だけステージした場合に未ステージの変更を一時的に退避してからチェックするため、「ステージした内容」に対する検査になります。素のスクリプトで起きがちなずれを気にしなくてよいのは大きな利点です。
なお、npm を中心にしたフロントエンドのプロジェクトでは、Husky と lint-staged の組み合わせもよく使われます。Node.js を前提にできるなら選択肢になりますが、PHP 中心のリポジトリでは、ここで紹介した core.hooksPath か pre-commit フレームワークの方が依存が少なく済みます。
自動修正するか、止めるだけにするか
end-of-file-fixer や phpcbf(PHP_CodeSniffer の自動修正ツール)のように、ファイルを書き換えるフックもあります。書き換えが起きるとフックは失敗扱いになり、修正後のファイルを改めて git add してコミットし直す流れになります。
最初は「止めて知らせるだけ」のフックで始め、チームが慣れてから自動修正を足すのがおすすめです。いきなり大量のファイルが書き換わると、差分のレビューが難しくなるためです。自動修正を導入するときは、整形だけのコミットを一度作ってから本来の作業に戻ると、履歴が追いやすくなります。
フックだけに頼らず CI でも同じチェックを回す
先に触れたとおり、pre-commit フックは --no-verify で飛ばせますし、設定していない人の環境では動きません。そのため、同じチェックを CI(GitHub Actions や Jenkins など)でも実行し、プルリクエストのマージ条件にするのが定石です。pre-commit フレームワークを使っている場合は、CI で pre-commit run --all-files を実行すれば、手元と同じ定義で検査できます。
役割分担のイメージは次のとおりです。
- pre-commit フック: 数秒で終わる軽いチェック(構文、整形、秘密情報の混入)。開発者がすぐ気づけることが目的。
- CI: 時間のかかるテスト(PHPUnit、E2E テストなど)と、フックと同じ軽いチェックの再実行。マージの判断材料にする。
うまく動かないときの確認ポイント
- フックが実行されない: 実行権限(
ls -l .githooks/pre-commit)と、git config --get core.hooksPathの値を確認します。 - **
phpやvendor/bin/phpcsが見つからない**: GUI の Git クライアントから実行すると PATH がターミナルと異なることがあります。フック内でフルパスを使うか、クライアントの設定を見直します。 - 遅くてコミットのたびに待たされる: 対象を変更ファイルに絞る、重いテストは CI に移す、のいずれかで調整します。
まとめ
- pre-commit フックは、コミット直前に軽いチェックを走らせて問題を早めに見つける仕組み。
.git/hooksは共有されないので、core.hooksPathか pre-commit フレームワークでリポジトリに含めて配る。- 対象はステージされた変更ファイルに絞り、数秒で終わる内容にする。
--no-verifyで飛ばせるため、同じチェックを CI でも実行してマージ条件にする。