マルチプラットフォーム対応のGit hooksツール Lefthook
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-commit
は lefthook 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}
commands
や run
に対する他のオプションについては scripts
と共通する部分が多いので scripts
の紹介後に説明します。
Scripts
pre-commit:
scripts:
"hello.sh":
runner: bash
"world.sh":
runner: bash
フックに対して実行するスクリプトをまとめたものが scripts
です。
commands
の name
にあたる部分にスクリプトファイル名を指定します。スクリプトファイルは .lefthook/<hook-name>/
ディレクトリに作成します。上記の例だと .lefthook/pre-commit/
ディレクトリの hello.sh
と world.sh
のファイルが存在する事になります。スクリプトディレクトリは変更可能なので必要でしたら公式ドキュメントを参照ください。
-
runner
- スクリプトを実行する際のコマンドを指定します
- 例えば
node
を指定するとnode .lefthook/pre-commit/<script-file>
で実行します
scripts
や各種ファイルに設定できる他のオプションは次で説明します。
Commands, Scripts options
前述の通り scripts
や commands
などに設定できるオプションは共通する部分が多いので、ある程度分類わけしつつまとめて紹介します。設定できる対象は以下の定義で説明します。
- フックレベル
-
<hook-name>
の直下に設定するオプション -
commands
とscripts
を対象にフック全体に影響ある設定です
-
-
run
レベル-
commands
の各run
やscripts
の各スクリプトに対して設定するオプション - 公式ドキュメント上でも
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}
-
glob
やexclude
などのフィルタは全て適用される
-
- このオプションは
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: true
と priority
を設定する以外で制御したい場合は数値を接頭辞にすると制御できます。
pre-commit:
commands:
1-lint:
run: pnpm lint
2-test:
run: pnpm test
ドキュメント上でも数値を接頭辞にして制御している例が記載されていましたが、探した範囲では明言された振る舞いではないので将来的には変わる可能性があります。
終了ステータス
Lefthook
はコマンドの終了ステータスがエラーだった場合に Git のコマンドも失敗させてくれますが、run
や scripts
で複数コマンドを実行時は最後に実行したコマンドの終了ステータスが利用されるので注意が必要です。実行順で制御が難しい場合は 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