👊

マルチプラットフォーム対応のGit hooksツール Lefthook

2024/05/03に公開

Lefthook とは

evilmartians/lefthook: Fast and powerful Git hooks manager for any type of projects.
Lefthook は Go 製の Git hooks を管理するクロスプラットフォーム対応のツールです。Git の各種イベント発生時にコマンドの並列実行、対象ファイルや実行ディレクトリの制御などが 1 ツールで行えます。1 バイナリで提供されているのでどの言語の開発環境にも利用できます。

インストール

lefthook/docs/install.md at master · evilmartians/lefthook
いくつかの言語のパッケージや各種 OS のパッケージシステムなど様々な形式でインストールする事ができます。筆者がよく利用する方法だけ記載しておきます。

go install github.com/evilmartians/lefthook@latest
pnpm add -D lefthook

コマンド

lefthook/docs/usage.md at master · evilmartians/lefthook

  • lefthook install
    • 設定ファイル lefthook.yml がなければ作成し、全ての Git hooks をインストールをします
  • lefthook uninstall
    • .git/hooks/ にインストールした Lefthook のフックを削除します
  • lefthook add [hook name]
    • 指定したフックを .git/hooks/ にインストールします
      • 例えば lefthook add pre-commit だと .git/hooks/pre-commit
    • -d, --dirs のオプションをつけるとそのフックのスクリプト用ディレクトリを作成します
      • 例: .lefthook/pre-commit.lefthook-local/pre-commit
  • lefthook run [hook name]
    • 指定したフックを直接実行します

Lefthook の仕組みを理解した方がこれらのコマンドの意味が理解しやすいと思うので先に説明します。

Lefthook.git/hooks/lefthook run [hook name] を実行するスクリプトを設置します。例えば .git/hooks/pre-commitlefthook run pre-commit を実行するスクリプトになっています。

lefthook run は設定ファイルに定義された内容を実行するコマンドなので、一度インストールしたフックは内容が変化しても反映されますが、新しいフックを追加時は lefthook install または lefthook add の実行が必要になります。

あとは当然ですがリポジトリをクローン時は何もインストールされていない状態なので lefthook install の実行が必要です。Node.js 環境では npm パッケージのインストール時に自動実行されるので不要な場合もあります。

設定ファイル

lefthook/docs/configuration.md at master · evilmartians/lefthook
設定項目が多いので使用頻度の高い項目についてだけ説明します。

設定ファイルは lefthook.yml が基本となっていますが、yaml, toml, json などの拡張子のファイルでも設定できます。また、先頭に . をつけた .lefthook.yml も有効なファイルとなります。自身にだけ反映したい設定がある時は lefthook-local.yml を作成し、オーバーライドする設定を書く事で行えます。

Top level options

extends
別の設定ファイルを拡張した設定を行う時に利用します。

extends:
  - ../lefthook.yml

<hook-name>
lefthook/internal/config/available_hooks.go at master · evilmartians/lefthook
フック名をキーに各種設定を行います。利用できるフックは上記のリンク先に一覧があります。

pre-commit:
  commands: ...
  scripts: ...

実は lefthook run はフック以外も呼び出せるのでタスクランナー的に使う事もできます。

lint:
  commands: ...

フックとして実行する内容は commands または scripts に定義します。まずは基本となる commands から説明します。

Commands

pre-commit:
  commands:
    name1:
      run: command1
    name2:
      run: command2

フックに対して実行する複数のコマンドをまとめたものが commands です。nameN 部分はコマンド名で run に実行するコマンドの内容を指定します。

run には以下のファイルテンプレートが利用できます。

  • {files}
    • 後述する files オプションに指定したコマンドの実行結果が入ります
  • {staged_files}
    • pre-commit などで利用できるステージングされたファイルのリストです
  • {push_files}
    • push 対象となるファイルのリストです
  • {all_files}
    • Git の追跡対象となっている全てのファイルのリストです
  • {cmd}
    • local ファイルで利用時に元ファイルのコマンド内容が入ります
  • {0}
    • フックに渡された引数を空白でつなげた 1 つの文字列です
  • {N}
    • フックに渡された引数の内の N 番目の引数です

ファイルテンプレートは '{staged_files}' のように ' もしくは " で囲むと結果のファイル名を個々にクォートで囲んでくれます。例えば 'file1.ts' 'file2.ts' のようになります。

{cmd}lefthook-local.yml で利用時に lefthook.yml の同じフックの同じコマンドの内容が入るので拡張する際に便利です。例えば以下のような感じです。

lefthook.yml

pre-commit:
  commands:
    lint:
      run: pnpm biome lint

lefthook-local.yml

pre-commit:
  commands:
    lint:
      run: docker run -it --rm develop-container {cmd}

commandsrun に対する他のオプションについては scripts と共通する部分が多いので scripts の紹介後に説明します。

Scripts

pre-commit:
  scripts:
    "hello.sh":
      runner: bash
    "world.sh":
      runner: bash

フックに対して実行するスクリプトをまとめたものが scripts です。

commandsname にあたる部分にスクリプトファイル名を指定します。スクリプトファイルは .lefthook/<hook-name>/ ディレクトリに作成します。上記の例だと .lefthook/pre-commit/ ディレクトリの hello.shworld.sh のファイルが存在する事になります。スクリプトディレクトリは変更可能なので必要でしたら公式ドキュメントを参照ください。

  • runner
    • スクリプトを実行する際のコマンドを指定します
    • 例えば node を指定すると node .lefthook/pre-commit/<script-file> で実行します

scripts や各種ファイルに設定できる他のオプションは次で説明します。

Commands, Scripts options

前述の通り scriptscommands などに設定できるオプションは共通する部分が多いので、ある程度分類わけしつつまとめて紹介します。設定できる対象は以下の定義で説明します。

  • フックレベル
    • <hook-name> の直下に設定するオプション
    • commandsscripts を対象にフック全体に影響ある設定です
  • run レベル
    • commands の各 runscripts の各スクリプトに対して設定するオプション
    • 公式ドキュメント上でも run と同様のオプションと説明されています

実行制御オプション

  • parallel
    • true に設定するとコマンドが並列実行されます
    • piped オプションとは併用できません
    • フックレベルのオプションです
pre-commit:
  parallel: true
  commands:
    lint:
      run: pnpm lint
    test:
      run: pnpm test
  • piped
    • true に設定するとコマンドが失敗した時点で終了します
    • デフォルトは false で途中で失敗したコマンドがあっても全て実行されます
    • parallel オプションとは併用できません
    • フックレベルのオプションです
pre-commit:
  piped: true
  commands:
    # lint が失敗すればここで終了する
    1-lint:
      run: pnpm lint
    2-test:
      run: pnpm test
  • priority
    • 0 以上の数字でコマンドの実行順を設定します
    • 0 または設定が省略されたコマンドは最後に実行されるコマンドになります
    • このオプションは piped: true が設定された時のみ有効です
    • run レベルのオプションです
pre-commit:
  piped: true
  commands:
    lint:
      priority: 1
      run: pnpm lint
    test:
      priority: 2
      run: pnpm test
  • skip
    • コマンドをスキップする条件を設定します
    • 設定できる種類は以下の通りです
      • true にすると全てスキップするので local 設定などに利用できます
      • 文字列を指定する事で対象となる Git の操作時をスキップします
      • ref: GLOB の形式でスキップ対象となるブランチを設定できます
      • run: COMMAND の形式で実行したコマンドの終了ステータスを条件に設定できます
    • フックレベルと run レベルの両方に設定可能です
pre-commit:
  skip:
    - merge
    - rebase
  commands:
    lint:
      run: pnpm lint
      skip:
        - ref: dev/*
    test:
      run: pnpm test
      skip:
        # 環境変数 NO_HOOK=1 設定時はスキップする
        - run: test "$(NO_HOOK}" -eq 1
  • only
    • コマンドを実行する条件を設定します
    • 設定内容は skip と同様です
pre-commit:
  only:
    - ref: main
  commands:
    lint:
      run: pnpm lint
    test:
      run: pnpm test

対象ファイル

  • files
    • 独自のファイルリストが必要な時に使用します
    • 指定したコマンドの実行結果が {files} の値になります
    • 結果が空の時はコマンドの実行がスキップされます
    • フックレベルと run レベルの両方に設定可能です
pre-commit:
  files: git diff --name-only
  commands:
    lint:
      files: git diff --name-only
      run: pnpm biome lint --apply {files}
  • glob
    • ファイルテンプレートの結果を Glob 形式で絞り込みます
    • run レベルのオプションです
pre-commit:
  commands:
    lint:
      glob: '*.{js,jsx,ts,tsx}'
      run: pnpm biome lint --apply {staged_files}
  • file_types
    • ファイルテンプレートの結果を指定したファイル種別で絞り込みます
    • 以下の値が設定できます
      • text, binary, executable, not executable, symlink, not symlink
    • run レベルのオプションです
pre-commit:
  commands:
    lint:
      run: pnpm lint {staged_files}
      filetypes: text
  • exclude
    • ファイルテンプレートの結果から除外するファイルを正規表現で設定します
    • ファイルのパスはリポジトリルートからの相対パスとなります
    • run レベルのオプションです
pre-commit:
  commands:
    lint:
      glob: '*.{js,jsx,ts,tsx}'
      # リポジトリ上にある全ての .next ディレクトリ以下を対象外にする
      exclude: '(^|/)\.next/.*'
      run: pnpm biome lint {staged_files}

実行ディレクトリ

  • root
    • コマンドを実行するカレントディレクトリを設定します
    • ファイルテンプレートの結果も対象ディレクトリ以下に絞り込まれます
      • ファイルテンプレートの結果が空の場合はコマンドがスキップされます
      • monorepo で変更したアプリケーションのみ実行などにも便利です
    • run レベルのオプションです
pre-commit:
  commands:
    front-lint:
      root: flontend/
      run: pnpm biome lint {staged_files}
    server-lint:
      root: server/
      run: pnpm biome lint {staged_files}

環境変数

  • env
    • 環境変数を設定します
    • run レベルのオプションです
pre-commit:
  commands:
    test:
      env:
        NODE_ENV: test
      run: pnpm test

変更の反映

  • stage_fixed
    • true に設定するとコマンドを実行後に自動的に git add します
    • 対象ファイルは以下のルールで決定されます
      • files オプションが指定されていれば {files}
      • files オプションがなければ {staged_files}
      • globexclude などのフィルタは全て適用される
    • このオプションは pre-commit のフックでしか使用できません
    • run レベルのオプションです
pre-commit:
  commands:
    lint:
      run: pnpm biome lint --apply {staged_files}
      stage_fixed: true

対話処理

  • interactive

    • true に設定すると対話型のコマンドやスクリプトが実行可能になります
    • デフォルトは非対話型のコマンドの実行後に対話型のコマンドが実行されます
      • piped: true に設定するとコマンド名順に変更できます
    • run レベルのオプションです
  • use_stdin

    • true に設定すると lefthook run のコマンド引数をコマンドやスクリプトに渡します
    • run レベルのオプションです

振る舞いと注意点

動作確認してて気付いた振る舞いなどを記載しています。

実行順序

Lefthook のコマンドはデフォルトでは順次実行します。この時の実行順は定義された順ではなくコマンドの名前順になります。piped: truepriority を設定する以外で制御したい場合は数値を接頭辞にすると制御できます。

pre-commit:
  commands:
    1-lint:
      run: pnpm lint
    2-test:
      run: pnpm test

ドキュメント上でも数値を接頭辞にして制御している例が記載されていましたが、探した範囲では明言された振る舞いではないので将来的には変わる可能性があります。

終了ステータス

Lefthook はコマンドの終了ステータスがエラーだった場合に Git のコマンドも失敗させてくれますが、runscripts で複数コマンドを実行時は最後に実行したコマンドの終了ステータスが利用されるので注意が必要です。実行順で制御が難しい場合は set -e を最初に実行するとその時点でエラーとして終了できます。

# これは stage_fixed: true の方がいいけど例として
pre-commit:
  commands:
    lint:
      # ng: lint error があっても git add は必ず成功する
      run: |
        pnpm biome lint --apply {staged_files}
        git add {staged_files}
      # ok: set -e により lint 時点でエラーで終了する
      run: |
        set -e
        pnpm biome lint --apply {staged_files}
        git add {staged_files}

Lefthook に限らずによくあるやつですね。

Discussion