husky + lint-stagedの支配から解放されるLefthookという選択肢


HRBrain Advent Calendar 2023 25日目の記事です。

https://qiita.com/advent-calendar/2023/hrbrain

## はじめに

こんにちは。[@yug1224(Yuji Yamaguchi)](https://x.com/yug1224)です。

これまで、pre-commit/pre-push時のLint実行にhusky+lint-stagedを使っていましたが、Monorepo環境だと設定が少し複雑になってしまうので、Lefthookを試してみることにしました。

## Lefthookとは

https://github.com/evilmartians/lefthook

> > The fastest polyglot Git hooks manager out there
>
> A Git hooks manager for Node.js, Ruby and many other types of projects.
>
> - **Fast.** It is written in Go. Can run commands in parallel.
> - **Powerful.** It allows to control execution and files you pass to your commands.
> - **Simple.** It is single dependency-free binary which can work in any environment.

LefthookはGoで書かれたGit hooks managerです。commitやpushなど、Gitの特定イベントが発生した時に実行するカスタムスクリプトの管理ができます。

コマンドの並列実行が可能で、Node.jsやRuby、フロントエンドやバックエンドに限らず、さまざまな種類のプロジェクトに対応しています。

## 使い方

https://github.com/evilmartians/lefthook/blob/master/docs/install.md

インストール方法はいくつかありますが、今回はHomebrewでインストールします。それ以外の方法はドキュメントを参照してください。

```console
brew install lefthook
```

インストール後、`lefthook`コマンドが使えるようになります。

```console
lefthook install
```

`lefthook install`を実行すると、`lefthook.yml`が存在しない場合は、テンプレートとして`lefthook.yml`が生成されます。

```yaml:lefthook.yml
# EXAMPLE USAGE:
#
#   Refer for explanation to following link:
#   https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
#
# pre-push:
#   commands:
#     packages-audit:
#       tags: frontend security
#       run: yarn audit
#     gems-audit:
#       tags: backend security
#       run: bundle audit
#
# pre-commit:
#   parallel: true
#   commands:
#     eslint:
#       glob: "*.{js,ts,jsx,tsx}"
#       run: yarn eslint {staged_files}
#     rubocop:
#       tags: backend style
#       glob: "*.rb"
#       exclude: "application.rb|routes.rb"
#       run: bundle exec rubocop --force-exclusion {all_files}
#     govet:
#       tags: backend style
#       files: git ls-files -m
#       glob: "*.go"
#       run: go vet {files}
#   scripts:
#     "hello.js":
#       runner: node
#     "any.go":
#       runner: go run
```

コメントアウトされているテンプレート部分を参考にして、`lefthook.yml`に必要なコマンドを記述します。

```console
lefthook uninstall && lefthook install
```

`lefthook.yml`の編集後は、`lefthook uninstall && lefthook install`を実行し、設定を反映させることも忘れないようにしましょう！

## husky+lint-stagedからの移行

https://github.com/evilmartians/lefthook/wiki/Migration-from-husky-with-lint-staged

husky+lint-stagedからの移行方法はドキュメントにも書かれていますが、これまで`package.json`に書いていた設定を`lefthook.yml`に書き直すだけなのでとても簡単です。

```jsonc:package.json
// Before
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,jsx}": "eslint"
  }
}
```

```yaml:lefthook.yml
# After
pre-commit:
  parallel: true
  commands:
    eslint:
      glob: "*.{js,jsx}"
      run: npx eslint {staged_files} && git add {staged_files}
```

### glob

https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#glob

> You can set a glob to filter files for your command. This is only used if you use a file template in [`run`](#run) option or provide your custom [`files`](#files) command.

`glob`にはlint-stagedで設定していたパターンを記述します。

### run

https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#run

> This is a mandatory option for a command. This is actually a command that is executed for the hook.
>
> You can use files templates that will be substituted with the appropriate files on execution:
>
> - `{files}` - custom [`files`](#files) command result.
> - `{staged_files}` - staged files which you try to commit.
> - `{push_files}` - files that are committed but not pushed.
> - `{all_files}` - all files tracked by git.
> - `{cmd}` - shorthand for the command from `lefthook.yml`.
> - `{0}` - shorthand for the single space-joint string of git hook arguments.
> - `{N}` - shorthand for the N-th git hook argument.

`run`にはlint-stagedで設定していたコマンドを記述します。

`{staged_files}`はGitでステージングされたファイルのリストで、`glob`によってマッチしたファイルのみが対象になります。

`git commit`時に、`glob`でマッチしたファイルを`npx eslint`でLintチェックし、`git add`でステージングします。これでhusky+lint-stagedと同じように動作します。

今回は`pre-commit`を例に挙げましたが、`pre-push`なども同様に設定できます。

## Monorepoでの便利な使い方

LefthookはMonorepo環境での利用にも対応しています。

### root

https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#root

> You can change the CWD for the command you execute using `root` option.
>
> This is useful when you execute some `npm` or `yarn` command but the `package.json` is in another directory.
>
> For `pre-push` and `pre-commit` hooks and for the custom `files` command `root` option is used to filter file paths. If all files are filtered the command will be skipped.

`root`オプションを使うことでコマンドを実行するCWDを指定できるため、Monorepoそれぞれに対応した設定を書くことができます。

### 実際の動作確認

https://github.com/yug1224/sandbox-lefthook

```yml:lefthook.yml
pre-commit:
  parallel: true
  commands:
    ramen:
      root: ramen/
      glob: '*.{ts,tsx}'
      run: npx eslint --fix {staged_files} && git add {staged_files}
    sushi:
      root: sushi/
      glob: '*.{ts,tsx}'
      run: npx eslint --fix {staged_files} && git add {staged_files}
```

実際に動作するサンプルを作ってみました。`ramen`ディレクトリと`sushi`ディレクトリに対応する設定を書いています。

`ramen`配下のファイルを壊した状態で`git commit`をするとどうなるか確認してみましょう。

![ramen_error.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106236/9b588bc2-2f24-3834-ffff-70f782271cc6.gif)

`ramen`配下のファイルのみがLintチェックされ、`sushi`配下では対象ファイルがないためスキップされています。

`root` `glob` `run`の3つのオプションを組み合わせることで、Monorepo環境での設定ができそうですね。

## まとめ

husky+lint-stagedの代わりにLefthookでGit hooksを管理する方法を紹介しました。

husky+lint-stagedと異なり、Node.jsに依存しない環境でも使えるので、バックエンドのプロジェクトでも利用しやすいのではないでしょうか。

Monorepo環境での設定も簡単にできるので、ぜひ試してみてください！

## PR

HRBrainでは一緒に働く仲間を募集しています。歴史に残るトライをしよう！

https://www.hrbrain.co.jp/recruit