CIを待たずにphpcsの検証を自動実行するためのエディタ中立な手法を模索する
ランサーズ Advent Calendar 2021 3日目の記事です。
本稿ではgitの機能であるgit hooks
を使って、コミット前にコーディング規約違反を検知するという割とありきたりな内容となっております。しかしながら実際に自分でやってみると奥が深かったというか、gitの機能に対する理解が深まったのでgitのしくみについてあまり詳しくない方向けに執筆させて頂きました。
モチベーション
開発をしていると稀に良くこういう感じになります。
最終的にrebaseしてコミットをまとめてしまうのでmasterの履歴としては残らないのですが、リモートリポジトリにpushしてCIの実行を待ち、phpcsのコーディングチェックを受けるのは少々煩わしいです。かといって、いちいち開発環境のコンテナに入ってコマンドを叩くのも手間、docker run
コマンドで外からワンライナーでコマンドを実行しようとするとエントリーポイントの関係でpathが深くなるし補完が効かないので不便です。また筆者は、作業内容によってvim,vscode,phpstormを使い分けるスタイルで開発を行っているため、各エディタの機能やプラグインに依存した機能はあまり使いたくないです。この問題を改善に取り組んでみましょう。
要件
あらためて要件をまとめると以下の3点です。
- コーディング規約に違反しているコードをコミットできないようにしたい
- vscode,phpstorm,vimを選ばないエディタ中立な方法が良い
- dockerコンテナ内もしくはホスト上を選ばない、開発環境中立な方法が良い
そこでphpcsを実行するフィルタリング処理用のコンテナイメージを開発環境とは別で用意し、git hooks
で実行する方法を採用します。
コンテナイメージ
まずはコンテナイメージを用意します。
FROM php:7.4-alpine
COPY /usr/bin/composer /usr/bin/composer
RUN COMPOSER_HOME="/composer" composer global require --prefer-dist --no-progress --dev squizlabs/php_codesniffer
RUN COMPOSER_HOME="/composer" composer global require --prefer-dist --no-progress --dev slevomat/coding-standard
ENV PATH /composer/vendor/bin:${PATH}
WORKDIR "/app"
単純に最新のcomposerを取ってきて、phpcs本体と必要なコーディング規約を入れるだけとなっています。バージョン指定はあえてしません。漢は黙って最新版
一応レジストリにも置いおきました。
これでdocker run isanasan/phpcs
を実行するとphpcsをオンデマンドで実行できるようになりました。
git hooksとは
各種git操作を検知してスクリプトを実行する機能です。
公式のマニュアルはこちら
git管理下のディレクトリで.git/hooks/
配下を覗くとsampleが置いてあるのが確認できます。このスクリプトのことをフックスクリプトと呼びます。
$ ls .git/hooks/
applypatch-msg.sample* pre-merge-commit.sample*
commit-msg* pre-push.sample*
commit-msg.sample* pre-rebase.sample*
fsmonitor-watchman.sample* pre-receive.sample*
post-update.sample* prepare-commit-msg.sample*
pre-applypatch.sample* push-to-checkout.sample*
pre-commit* update.sample*
git hooksについて調べていて疑問だったのはwindows環境上だとフックスクリプトを実行する主体は何なのかという点です。
結論としてはgit bash
でした。なので、powershell
を普段使っている場合も普通にshell script
を書けば動きます。また、フックスクリプトの検証をする際はgit bash
内で実行すれば問題ありません。
ちなみにフックスクリプトから外部コマンドも呼べるので、あんまり複雑なことをする場合はgo
なりphp
なりdeno
なり、shell script
ほど辛みのない言語をつかうのが良いかと思います。
今回はpre-commit
を使い、commitの前にフックスクリプトが実行されるようにします。
成果物
とりあえず作成した.git/hooks/pre-commit
はこちらになります。
#!/bin/bash
docker run --rm -v `pwd -W`:/app isanasan/phpcs phpcs --standard=.circleci/phpcs.xml `git diff --cached --name-only` -n
docker run --rm -v
pwd -W
:/app isanasan/phpcs phpcs
ここまでがphpcsをコンテナで実行するコマンドです。--standard
オブションでコーディング規約の設定ファイルを指定しています。ここは各プロジェクトごとに必要なものを指定してください。-n
はwarningを抑制するoptionです。一行120文字を越えているとwarningが出るのですが、これはコーディング規約とは関係無いので抑制してしまいます。
phpcsの引数に渡しているgit diff --cached --name-only
は現在ステージング中のファイル名を取得するコマンドです。
問題点
上記のスクリプトでも十分機能するのですが、一点課題が残ってしまいました。
具体例を確認してみましょう。わざと規約違反のコードを作成してコミットします。
<?php
declare(strict_types=1);
namespace App\Domain;
class Hoge
{
}
するとphpcsが規約違反を検知してコミットが失敗します。
$ git commit -m "hoge"
FILE: /app/src/Hoge.php
----------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 2 LINES
----------------------------------------------------------------------
1 | ERROR | [x] End of line character is invalid; expected "\n" but
| | found "\r\n"
2 | ERROR | [x] Expected 1 line before declare statement, found 0.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
指摘内容を修正するためにphpcbfを実行します。
$ docker run --rm -v `pwd -W`:/app isanasan/phpcs phpcbf --standard=.circleci/phpcs.xml `git
diff --cached --name-only` -n
PHPCBF RESULT SUMMARY
----------------------------------------------------------------------
FILE FIXED REMAINING
----------------------------------------------------------------------
/app/src/Hoge.php 2 0
----------------------------------------------------------------------
A TOTAL OF 2 ERRORS WERE FIXED IN 1 FILE
----------------------------------------------------------------------
この時ステージング状態のファイルとワークツリー上のファイルの間には差分があります。
$ git diff
diff --git a/src/Hoge.php b/src/Hoge.php
index 61f1e49a..75b0cc23 100644
--- a/src/Hoge.php
+++ b/src/Hoge.php
@@ -1,4 +1,5 @@
<?php
+
declare(strict_types=1);
namespace App\Domain;
この状態だとフックスクリプトがエラーとならず、コミットできてしまいます。
$ git commit -m "hoge"
[test 15892951] hoge
1 file changed, 8 insertions(+)
create mode 100644 src/Hoge.php
現状のフックスクリプトだとphpcsがチェックしているのはワークツリー上のファイルであり、ステージングされたインデックス上のオブジェクトではないためこのような現象が発生してしまいます。このままではあまり便利ではないため、インデックス上のオブジェクトをphpcsで検証で出来るようにフックスクリプトをブラッシュアップしていきます。
ステージング状態の中身を取得する
gitの仕組み上、ステージングされたファイルはスナップショットが作られて.git/index
にblobオブジェクトとして保存されています。
.git/index
の中身はgit ls-files --stage
コマンドで確認できます。
また、引数にgit diff --cached --name-only
を渡すことで現在ステージング中のファイルに絞って表示することが出来ます。
$ git ls-files --stage `git diff --cached --name-only`
100644 61f1e49aa6341d7a125d845b7664537a937333a7 0 src/Hoge.php
戻り値の2つ目の要素がsrc/Hoge.php
に対応するblobオブジェクトのハッシュ値となっています。
このハッシュ値をgit cat-file -p
コマンドに渡すことでハッシュ値が示すオブジェクトの中身を確認することが出来ます。
$ git cat-file -p 61f1e49aa6341d7a125d845b7664537a937333a7
<?php
declare(strict_types=1);
namespace App\Domain;
class Hoge
{
}
git ls-files
コマンドでオブジェクトのハッシュ値を収集し、git cat-file -p
に渡すことでステージング中のコードを取得することが出来ました。
取得したコードをphpcsで検証する
phpcsは標準入力を受けとることが出来るのでgit cat-file -p
の標準出力をパイプで繋ぐだけで検証を実行することが出来ます。
docker run
コマンドはオプションに-i
を指定すればそのまま標準入力に渡してくれます。
$ git cat-file -p 61f1e49aa6341d7a125d845b7664537a937333a7 | docker run -i --rm -v `pwd -W`:
/app isanasan/phpcs phpcs --standard=.circleci/phpcs.xml
FILE: STDIN
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
2 | ERROR | [x] Expected 1 line before declare statement, found 0.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
出力結果がFILE: STDIN
となって分かりにくいので--stdin-path
オプションを使ってファイル名を教えてあげます。
$ git cat-file -p 61f1e49aa6341d7a125d845b7664537a937333a7 | docker run -i --rm -v `pwd -W`:/app isanasan/phpcs phpcs --standard=.circleci/phpcs.xml --stdin-path=src/Hoge.php
FILE: /app/src/Hoge.php
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
2 | ERROR | [x] Expected 1 line before declare statement, found 0.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
これで無事ステージング状態のオブジェクトをphpcsで検証出来るようになったので、フックスクリプトでループで回してやれば要件を実現することが出来ます。
最終版
こちらが完成したフックスクリプトになります。
#!/bin/bash
ret_phpcs=0
while read mode object stage_number file; do
if [ $stage_number -ne 0 ]; then
continue
fi
git cat-file -p $object | docker run -i --rm -v `pwd -W`:/app isanasan/phpcs phpcs --standard=.circleci/phpcs.xml --stdin-path=$file -n
if [ $? -ne 0 ]; then
ret_phpcs=1
fi
done < <(git ls-files --stage `git diff --cached --name-only`)
if [ $ret_phpcs -ne 0 ]; then
exit 1
fi
副産物
今回作成したdockerコンテナを使うとphpcbf
の実行がとても楽になりました。
下記のコマンドを実行することでステージング中のファイルにフォーマッタを掛けられるのでシェルでaliasを張っておくと便利です。
$ docker run --rm -v `pwd -W`:/app isanasan/phpcs phpcbf --standard=.circleci/phpcs.xml `git diff --cached --name-only`
まとめ
以上でエディタや開発環境に依存することなくphpcsを自動実行して規約違反のコードをコミットする前に検知することが出来るようになりました。
明日は@igaraさんによる「(仮) NextJS モノレポ運用」です。お楽しみに!!
謝辞
ほぼほぼ同じ内容の記事があって、参考にさせて頂きました。というか完成したスクリプトの内容は同じです。
参考
gitの仕組みについてはこちらの記事を参考にしました。
Discussion