コミット時に自動でコードをチェックしてほしい！

# はじめに

:::note
この記事は[リンクアンドモチベーションアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/lmi)の15日目の記事です。
:::

コードをプッシュした後に、「あ、フォーマットかけるの忘れてた！」と思い出し、`フォーマット修正`とか`yarn fix適用`みたいなコミットをすることはありませんか？

自分は稀によくあります(^_^;)

チーム開発に携わるようになって、こうしたコミットをしないことの重要性がわかりました。例えば、最後のコミットがフォーマット修正系のコミットだと、何の変更をしたのかがわかりづらくなるといった問題があります。

（↓ 弊社の先輩の記事でも、コミットにこだわることの重要性が述べられています）

https://qiita.com/katsuya_tanaka/items/dade7c2138aea9afec2d

今回はコミット時に自動でそうしたチェックを行う仕組みを作る方法を調べました。

# 前提

自分が実現したいことは以下です。

- コミット前に`eslint`や`prettier`のチェックをして、引っかかるコードがあったらコミットを中断する
    - →フロントエンド開発をしているため
- モノレポ構成のプロジェクトに対応できる
    - →開発しているプロジェクトがそうなっているため
- GitHubにアップロードされるファイルは変更せず上記の要件を実現する
    - →あくまでも個人用途のため、このためのライブラリ（Huskyとか）をプロジェクトに導入したり、`.gitignore`に追記したりとかもあまりしたくないなあと思ったため

# pre-commitを探る

表題のようなニーズを実現するツールとしては、[pre-commit](https://pre-commit.com/)が有名です。調べたら一番出てきます。
インストールをした後、`.pre-commit-config.yaml`という設定ファイルにチェックしたい項目を記述していき使用するようです。

:::note warn
この記事ではpre-commitのインストール方法や基本的な設定方法の説明は省略します。
:::

`eslint`や`prettier`といった外部のツールを使ったチェックをしたい場合は、pre-commitで使う用のプライグインのようなものが開発されているので、それを設定ファイルで指定します。
（pre-commitから提供されているものや、有志が開発しているものがあります。`eslint`や`prettier`はメジャーだからか、pre-commitから提供されているものがあります！）

↓例えば`eslint`を使う場合はこんな感じです。
```yaml:.pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-eslint # レポジトリのURLを指定
    rev: v8.51.0
    hooks:
      - id: eslint
        name: eslint
        entry: eslint
        language: node
        types: [javascript, typescript]
        files: \.jsx?$|\.tsx?$
```

今回は`prettier`と`eslint`を使いたいので、それぞれのレポジトリを見てみます。
ですが、[mirrors-prettier](https://github.com/pre-commit/mirrors-prettier/tree/main)　(pre-commitから提供されているprettierのプラグイン)をみてみると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3892166/b760fafb-7bb0-0f1c-a59d-f837b0e690a5.png)

public archiveになってる！

> prettier made some changes that breaks plugins entirely

なるほど。mirros-prettierを使うことはできなさそう。
軽く調べてみた感じ有志で開発されているものもなさそうでした。（あったらごめんなさい）

# 自分でやってみる

pre-commitは、**GitHooks**というGit標準機能を使っていることが調べていて分かったので、GitHooksを使って、pre-commitみたいなことができるようにします。（ツールとしてのpre-commitと、GitHooksのpre-commitがどちらも出てくるので、やや混乱しました）

## GitHooksとは

コミット時や、プッシュ時など、さまざまなGitの操作をトリガーにスクリプトを実行することができる機能です。
`.git/hooks`下に操作に対応したファイルが入っています。今回はこの中の`pre-commit`ファイルを編集していきます。

:::note
ツールのほうのpre-commitをレポジトリにインストールすると、`.git/hooks/pre-commit`が書き換えられます。そこで`.pre-commit-config.yaml`を使う設定などをしていました。

<details><summary>こんな感じのファイルでした👀</summary>

```shell:.git/hooks/pre-commit
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: *一応置き換え

# start templated
INSTALL_PYTHON=/opt/homebrew/opt/pre-commit/libexec/bin/python3.13
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi

```　
</details>
:::

## pre-commitを編集する

`.git/hooks`ディレクトリ下に`pre-commit.sample`というファイルがあるはずです。このファイル名を`pre-commit`に修正すると、コミット時に記述されているスクリプトが実行されるようになります。

`.git`ディレクトリはデフォルトの設定だとVSCodeに出てこないし、表示させるようにして意図せず触ってしまったということがあったら嫌なので、`.git/hooks/pre-commit`では、外部のスクリプトを読み込んで実行させることにします。

```sh:.git/hooks/pre-commit
#!/bin/sh

# 外部スクリプトのパス
HOOK_SCRIPT=".git-hooks/pre-commit"

if [ -x "$HOOK_SCRIPT" ]; then
    "$HOOK_SCRIPT"
else
    echo "Skipped: $HOOK_SCRIPT not found or not executable."
fi
```

指定したパスのスクリプトが存在すれば実行し、存在しなければスキップします。

## pre-commitで読み込む外部ファイルを作成

先ほど$HOOK_SCRIPTで指定したファイルを作成します。
例として、シンプルにlintとformatをチェックするスクリプトを記述します。
(ここでディレクトリを移動する処理を書いたりすれば、モノレポ構成のレポジトリにも対応できますね！)

```sh:.git-hooks/pre-commit
#!/bin/sh

pnpm eslint
pnpm prettier --check src/
```

そしてこのスクリプトに実行権限を与えましょう。

```sh
chmod +x .git-hooks/pre-commit
```

## コミットしてみる

テストとして、あえてlintが通らない状態でコミットしてみます。
```shell
# ...(前略)

  4:7  error  'hi' is assigned a value but never used  @typescript-eslint/no-unused-vars

✖ 1 problem (1 error, 0 warnings)

Checking formatting...
[error] src/components/WelcomeItem.vue: SyntaxError: Unexpected closing tag "div". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags (16:3)
[error]   14 |       <slot></slot>
[error]   15 |     </div>
[error] > 16 |   </div>
[error]      |   ^^^^^^
[error]   17 | </template>
[error]   18 |
[error]   19 | <style scoped>

# ...(後略)
```

コミット失敗しました！成功です🤗

:::note warn
もしうまくいかない場合は、実行権限が付与されていない可能性があるので、`.git/hooks/pre-commit`と`.git-hooks/pre-commit`どちらにも実行権限があるか確認してみてください。
:::

今回は極限までスクリプトをシンプルにしましたが、わかりやすいメッセージを出力させるようにしたり、出力したメッセージをスタイリングしたりすると、さらに使いやすくなりそうです。
また、今はチェック対象を全てのファイルにしていますが、ステージングされているファイルのみをチェックするといった工夫もできますね。

## おまけ: Gitの標準機能を使ってさらに便利に

### 1. テンプレートにする

今回作成した`.git/hooks/pre-commit`をテンプレートに登録して、今後Gitレポジトリを作成した際に、`.git/hooks/pre-commit`が一緒に作成されるようにします。

**ⅰ. テンプレートファイルの作成**

`~/.git-templates/hooks`ディレクトリを作成し、その中に`pre-commit`ファイルを作りましょう。ファイルに、今回書いたスクリプトを転記します。（`.git-hooks/pre-commit`の方ではなく、`.git/hooks/pre-commit`の方です）

**ⅱ. 実行権限の付与**

保存できたら、このファイルにも実行権限を付与します。

```shell
chmod +x ~/.git-templates/hooks/pre-commit
```

**ⅲ. テンプレートに登録**

そして、`.git-templates`ディレクトリをgitのtemplateとして設定しましょう！

```shell
git config --global init.templatedir "~/.git-templates"
```
この設定をした後にGitレポジトリを作成すると最初から`.git/hooks`下に`pre-commit`ファイルが登録されています。
`.git-hook/pre-commit`ファイルを作って、コミット前にチェックしたい項目を記述すれば、適用されます。（実行権限の付与もしてください！）

----

### 2. 設定ファイルをignoreする

ここまでの設定だけだと、`.git-hooks/pre-commit`がGitの追跡対象となり、レポジトリにプッシュされてしまいます。
前提でも書きましたが、GitHubにアップロードするコードは変更したくないので、このディレクトリを、`.gitignore`を使わずにignoreさせます。

**やり方**

`~/.config/git/ignore`を作成します。
ここに`.gitignore`に書くように追跡対象から外したい対象を記述し、保存するとそのPC上の全てのGitレポジトリで記述した対象が追跡されなくなります。
例えば以下のように記述すると、`.git-hooks`ディレクトリが追跡されなくなります。
全てのレポジトリに影響があるので、気をつけてください。

```:~/.config/git/ignore
.git-hooks
```

:::note warn
反映されない場合は、`~/.gitconfig`に違うファイルをグローバルな`.gitignore`として読み込むように設定されているかもしれません。`~/.gitconfig`ファイルを見てみてください。`excludesfile`が追跡対象外になるものを指定するファイルです。
:::

# おわりに

pre-commitのmirros-prettierが使えないのは残念でしたが、おかげでGitのさまざまな標準機能の存在を知ることができました。
Gitの知らない機能はまだまだありそうです。毎日使うツールなので最大限活用していきたいですね！

読んでいただきありがとうございましたm(_ _)m

# 参考

https://pre-commit.com/index.html

https://tech.excite.co.jp/entry/2024/06/24/125911#%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%92%E6%9B%B8%E3%81%8F

https://zenn.dev/sun_asterisk/articles/97d2b4be675c06

https://ama-tech.hatenablog.com/global-gitignore