コンテンツにスキップ

lefthook を使って git hook を管理する

導入

あらゆるプロジェクトで、コードの品質を機械的にチェックするための仕組みが欲しかった。git hook を使って、コミット時に自動でコードの品質がチェックされる仕組みを考えてみた。加えて、コミット時以外の任意のタイミングでもチェックできるように色々とやってみた。

lefthook の採用

git hook は、git の操作時に自動で実行されるスクリプトである。例えば、.git/hooks/pre-commitにコードの品質をチェックするようなスクリプトを書いておくと、コミット時に自動でコードの品質がチェックされるようになる。git hook を自分で書くこともできるが、pre-commithuskylefthook などのツールを使うと設定を良い感じにしてくれる。

これらのツールの中で、lefthookを次の理由で採用した。

  • シングルバイナリで依存関係が少ない
  • 並列実行ができる
  • 設定ファイルを YAML で記述する。GitHub Actions や Kubernetes マニフェストなどでスクリプトを書くのと同じ感覚で設定できる
  • プロジェクト用の設定ファイルを個人用の設定ファイルで上書きできる
  • git hook 以外にもタスクランナーとしても使える

lefthook の設定

基本的な使い方

lefthook の設定ファイルは .lefthook.yml に記述する。これを上書きするための設定は .lefthook-local.yml に記述する。他のパスにある設定ファイルを読み込んだり、Web 上の設定ファイルを読み込むこともできる。

設定ファイルには、pre-commitcommit-msgpre-push などのフックを記述する。フック以外にも、lefthook run <task> で任意のタスクを実行することもできる。次の例ではtestというタスクが定義されている。

.lefthook.ymlの例
pre-commit:
  parallel: true
  commands:
    rubocop:
      files: "app/**/*.rb"
      run: "bundle exec rubocop"
    eslint:
      files: "app/javascript/**/*.js"
      run: "yarn eslint"
test:
  parallel: true
  commands:
    rspec:
      run: "bundle exec rspec"

今回作ったもの

今回設定したものはこちら。Python や Terraform、Dockerfile、ShellScript、Kubernetes マニフェストなど複数の言語やファイル形式に対応している。これらを使うことで、あらゆるプロジェクトでコードの品質を機械的にチェックできる。 git hook 以外にも、lint や test などの名前でタスクの実行もできる。--all-files オプションをつけることで、stage されていないファイルを含む全てのファイルを対象に実行される。

今回作ったlefthook の設定を全てのプロジェクトで利用できるように、.lefthook.yml.lefthook-local.yml を自動で作成するlhコマンドを作った。作成した設定ファイルに、先のディレクトリの設定ファイルを読み込むようにしている。lhコマンドは、lefthookのエイリアスのようにも使える。

lefthook run pre-commit
lh run pre-commit

lh コマンドは、実行時に.lefthook.yml.lefthook-local.yml がなければ作成し、lefthook を実行する。これにより、プロジェクトごとに設定ファイルを作成する手間を効率化している。本当はプロジェクトごとに設定ファイルを作成せずに、1 箇所に設定ファイルを置いて、それを使うようにしたかった。調べたところlefthook もそれ以外のツールも、プロジェクトごとに設定ファイルを作成するようになっていたので、このような形にした。

lh コマンドの補完

私の環境ではargc-completionを使って、コマンドの補完をしている。argc-complesion に lefthook 用の補完スクリプトがあり、補完が効くようになっている。lh コマンドでも同様の補完が聞くように、lefthook 用の補完スクリプトを複製し、編集している。

lefthook の気になった挙動

今回の設定をするにあたり、lefthook の挙動についていくつか気になる点があったので、それについて記述する。

  1. extends のプロパティでマシン内の設定ファイルを読み込む。.lefthook.yml から設定ファイル A を読み込み、設定ファイル A から設定ファイル B を読み込んでみた。設定ファイル A に相対パスで B を読み込む記述をするのであれば、元の.lefthook.yml からの相対パスで指定しないといけない。そのため、設定ファイル A をあらゆるところから読み取って、共通で使い回すのは難しいように感じた。複数のファイルを経由せずに、対象のファイルを直接読み込むのが良さそう。

    graph TD
      A[.lefthook.yml] --> B[設定ファイルA]
      B --> C[設定ファイルB]
  2. groupというプロパティがあり、commandを論理的にグループ化できる。グループ化することで、並列処理や直接実行の制御がしやすくなる。group を利用する際の注意点として、個別のコマンドは実行できない。group を使わなければ、lefthook run pre-commit rubocopのように個別のコマンドを実行できる。group を使うと、lefthook run pre-commitのようにグループ内のコマンドを個別に実行できない。

  3. 実行の対象はfilesで記述したファイルをglob, excludeでフィルタリングしたものになる。filesの結果はファイルである必要があり、ディレクトリを含むことはできいない。--all-filesオプションを使うと、filesの設定は考慮されず、全てのファイルが対象になる。このことから、filesで細かいフィルタリングをしていると、--all-filesを使った時にフィルタリングが効かなくなる。オプションの有無で挙動が変わらないように、細かいフィルタリングはfilesでやらずに、runの中でやるのが良さそう。