すぐ知らせるフックと、最後に止めるフックは別の場所にある
一言でいうと
クライアントフック(commit-msg・pre-commit・pre-push)は、素早いフィードバックを与え、サーバーフックとCIは、強制を行います。クライアントフックは、--no-verifyという1語で突破され、cloneで付いてくることもないため、強制力を期待して、ここにルールを課してはいけません。
なぜ必要なのか
チームにルールができます。コミットのタイトルは決まった形式で、100KBを超えるファイルはコミット禁止、mainには直接push禁止。ドキュメントに書いておいても守られず、レビューで指摘しても、すでにコミットが積み上がったあとなので、直すのが面倒です。
.git/hooks/にスクリプトを置くと、gitが特定の瞬間にそれを実行します。コミットの直前、メッセージを書いた直後、pushの直前のような場所です。終了コードが0でなければ、その動作が止まります。
ところが、.git/hooks/には、大きな問題が1つあります。.gitディレクトリは、cloneで付いてきません。自分がどれだけうまく作っておいても、他の人のマシンにはありません。そのため、チームごとに「セットアップスクリプトを実行してください」のような案内を付けましたが、誰も実行しませんでした。
どう動くのか
git 2.9から、core.hooksPathという設定ができました。フックを探す場所を変える設定です。
git config core.hooksPath .githooks
これで、.githooks/は、リポジトリが追跡する通常のディレクトリになるため、フックのスクリプトがコミットされて共有されます。ただし、core.hooksPathの設定自体は.git/configにあるため、やはり各自が一度はオンにする必要があります。その1行をREADMEやブートストラップスクリプトに入れておくのが、今の慣例です。
よく使うフック3つは、次のとおりです。
| フック | いつ | 受け取るもの | 止めると |
|---|---|---|---|
pre-commit |
コミットが作られる直前 | なし(インデックスを直接見ます) | コミットできません |
commit-msg |
メッセージを書き終えたあと | メッセージファイルのパス($1) | コミットできません |
pre-push |
push直前 | リモート名・アドレス($1,$2)と、標準入力で参照の一覧 | pushできません |
pre-commitには、落とし穴が1つあります。ワーキングツリーではなく、インデックスを見る必要があります。コミットされるのは、インデックスに載った内容で、ワーキングツリーは、そのあといくらでも変わることがあります。ファイルに機密情報を書いてgit addしたあとで、ファイルをきれいに直しておくと、ワーキングツリーを読むフックは何も検知できず、機密情報がそのままコミットされます。インデックスの内容は、git show :<경로>やgit cat-file -p <blob>で読みます(プレースホルダーはパスです)。
pre-pushは、標準入力で行ごとに로컬참조 로컬해시 원격참조 원격해시を受け取ります(プレースホルダーは、ローカルの参照、ローカルのハッシュ、リモートの参照、リモートのハッシュです)。원격참조がrefs/heads/mainである行があれば止める、というように使います(プレースホルダーはリモートの参照です)。
クライアントフックでは止められないもの
3つあります。
1つ目は、--no-verifyによる回避です。git commit --no-verifyはpre-commitとcommit-msgを、git push --no-verifyはpre-pushを、スキップします。これは欠陥ではなく設計です。クライアントフックは、自分のマシンで自分がオンにするツールであり、急ぐときにオフにできる必要があります。
2つ目は、設定をオンにしていない人です。core.hooksPathは、各自がオンにする必要があります。
3つ目は、別の経路で入ってくるコミットです。Web画面で直接直したり、別のツールがプッシュしたり、フックをサポートしていないクライアントを使ったりすると、そもそも実行されません。
そのため、ルールを実際に強制する場所は、サーバーです。サーバー側のpre-receive・updateフックや、ホスティングサービスのブランチ保護、そしてCIの検査です。その場所には、--no-verifyのようなものはありません。その代わり、フィードバックが遅いです。すでにコミットを積んでpushしたあとで、ようやく拒否されます。
まとめると、同じルールを2か所に課します。クライアントフックは30秒で知らせてくれる側で、サーバーは最後に止める側です。クライアントにだけ課すと突破され、サーバーにだけ課すと、人々は毎回pushしてからはじめて気づきます。
現場での姿
- 機密情報スキャナーを
pre-commitに設定しておくと、ほとんどの事故を、コミット前に検知できます。それでも、履歴の掃除の手順は、別に準備しておきます。突破される経路が残っているからです。 commit-msgでタイトルの形式をそろえておくと、リリースノートを履歴から自動で作れます。- フックが遅いと、人々は
--no-verifyを習慣として使うようになります。コミットのたびに動く検査は、変更されたファイルだけを見るようにして、1秒以内に終える必要があります。 - フックをリポジトリに入れるときは、実行権限(
chmod +x)を必ず確認します。権限がないと、gitは静かにスキップします。何のエラーもなく、ルールだけが消えます。
次のラボですること
core.hooksPathでフックをリポジトリに入れ、タイトルのルールと、大きなファイル・機密情報のブロックを、自分で作ります。ワーキングツリーだけを見るフックがなぜ突破されるかを、インデックスにだけ機密情報がある状態で再現し、--no-verifyという1語で、3つのフックをすべて通り抜けてみます。cloneして、フックのファイルは付いてくるのに、設定は付いてこないことを確認し、pre-pushでmainへの直接pushを止めてみます。
公式ドキュメントは、githooks、git-config、Pro Git 8.3 Git Hooksです。