コミットリント(commitlint)を導入し、コミットメッセージをlintする

## はじめに
開発現場ではコミットメッセージが適当だと、後から見た時や他の人が見た時に？？？になって、開発のネックになってしまう事はままあると思う。今回はコミットメッセージが各人でバラバラで独自的なものにならないように、git hooksでコミットメッセージにリントを書ける方法についていきたいと思う。

## ひとまずやってみる
私はNode.js（Javascript）で開発している事もあり、[commitlint](https://github.com/conventional-changelog/commitlint)を利用する。

設定方法等は[Guide: Local setup](https://commitlint.js.org/#/guides-local-setup?id=guide-local-setup)に書かれているのでそちらを参照して行っていくが、私はgit hooksを設定するのに[simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)を使う事にした。

※simple-git-hooksを導入して設定する方法は[Prettier のルールで自動整形する](https://note.com/shift_tech/n/n2a0b5561c27b#Prettier%20%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%A7%E8%87%AA%E5%8B%95%E6%95%B4%E5%BD%A2%E3%81%99%E3%82%8B)を参照。

```console
[study@localhost node-express]$ yarn add --dev @commitlint/{config-conventional,cli}

[study@localhost node-express]$ echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
```
```js:commitlint.config.js
// echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.jsにより作成される
module.exports = {
	extends: ['@commitlint/config-conventional']
};
```
```json:package.json
{
	...
	"simple-git-hooks": {
		"commit-msg": "npx commitlint -e"
	}
}
```

ここまで設定した所で実際にgit commitを実行してみると、以下のようにコミットリントが走り、コミットが失敗している事が確認できる。

```console
[study@localhost node-express]$ git commit -m "aaa"
⧗   input: aaa
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
```

ソースコード全体は以下。

https://github.com/yuta-katayama-23/node-express/commit/e543fd7652edc4368f0882ecbfd8e74f8637ce25

## 詳細を見ていく
### git hooksに関して
今回は[simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)を利用していたが、このライブラリが一体何をしてくれているのか？というと、シンプルにgit hooksのshellを作成しているだけ。

公式を見ると分かるが、`npx simple-git-hooks`を実行する事で、package.jsonのgit hooksのキー（git hooksの種類）に設定されているコマンドを`./.git/hooks`以下に作成してくれる（git hooksについては[8.3 Git のカスタマイズ - Git フック](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA-Git-%E3%83%95%E3%83%83%E3%82%AF)や[githooks](https://git-scm.com/docs/githooks)を参照）。

```json:package.json
{
	...
	"scripts": {
		...
		"prepare": "npx simple-git-hooks"
	},
	"simple-git-hooks": {
		"pre-commit": "npx lint-staged",
		"commit-msg": "npx commitlint -e"
	}
}
```
```console
[study@localhost hooks]$ tree .
.
├── commit-msg
└── pre-commit
```

そしてこのファイルの中身を見ると、以下のようにpackage.jsonに書いていたものがそのまま書かれている事が分かる。

```console
[study@localhost hooks]$ cat pre-commit
#!/bin/sh
npx lint-staged

[study@localhost hooks]$ cat commit-msg
#!/bin/sh
npx commitlint -e
```

なので、別に自分で`./.git/hooks`以下にshellを作成すれば同じ事は実現できる（自分でシェルを各パターンは[Git Hooks (commit-msg) でコミットメッセージの書式チェック](https://qiita.com/aKuad/items/f345331baa37c605e08a)などを参照）。

※prepareについてだが、npm/yarn(yarnは1.x系のみ)には[Life Cycle Scripts](https://classic.yarnpkg.com/en/docs/package-json#toc-scripts)/[scripts](https://classic.yarnpkg.com/en/docs/package-json#toc-scripts)という機能があり、これを利用するとnpm/yarnのコマンド実行時にイベントフックでコマンドを実行できる。このLife Cycle Scriptsのprepareを利用する事で、開発者全員で同じ設定を簡単にできるようにする事ができる。

※ちなみに、`npx simple-git-hooks`を実行して動くコードは[これ](https://github.com/toplenboren/simple-git-hooks/blob/master/cli.js)（`npx simple-git-hooks`を実行すると、simple-git-hooksのpackage.jsonのbinに設定したものが実行されるが、それについては[bin](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#bin)や[npm パッケージを CLI ツールとして機能させる仕組みについて](https://numb86-tech.hatenablog.com/entry/2020/06/03/203722)を参照）。

### commitlint.config.jsについて
[Configuration object example](https://commitlint.js.org/#/reference-configuration?id=configuration-object-example)に`commitlint.config.js`で設定できる内容が書かれている。

今回は`extends`とローカルで独自のルールを適用する方法の2つについて以下で詳細を見ていく。

#### extendsの部分
今回は[@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)を継承して、[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)で策定されている仕様を満たすようなコミットメッセージになるようにするために設定している（ルール一覧はGitHubのページに書かれている）。

※一部、@commitlint/config-conventionalのルールでwarnにレベルを下げたい、などの要望がある時には、以下のように上書き設定もできる（ルール適用のレベル（warn・errorなど）については[Rules](https://commitlint.js.org/#/reference-rules?id=rules)を参照）。

```js:commitlint.config.js
module.exports = {
	extends: ['@commitlint/config-conventional'],
	rules: {
		'subject-case': [
			1, // <- @commitlint/config-conventionalだと、2でerrorになるがそれを1（warn）に上書きしている
			'never',
			['sentence-case', 'start-case', 'pascal-case', 'upper-case']
		]
	}
};
```

#### 独自のルール（[Local Plugins](https://commitlint.js.org/#/reference-plugins?id=local-plugins)）
ESLint同様、基本的にはルールはnpmに公開されているものを依存追加して利用する、という流れになるが、npmに追加する事なくローカルでプラグインを開発するという事もできる。今回見ていくものはそれに該当し、公式で言うと[Local Plugins](https://commitlint.js.org/#/reference-plugins?id=local-plugins)に詳細が書かれている。

実装方法は[Usage Example](https://commitlint.js.org/#/reference-plugins?id=usage-example)にある通りだが、今回はRedmineとの紐づけをする際に必要になるsuffixが付いているか？を検証するルールを作成してみようと思う。実際の実装は以下。

```js
module.exports = {
	// 省略
	rules: {
		// 省略
		'redmine-rule': [1, 'always']
	},
	plugins: [
		{
			rules: {
				'redmine-rule': ({ subject }) => {
					const pattern = / refs#\d+$/;
					return [
						new RegExp(pattern).test(subject),
						`Your subject should contain suffix for redmine`
					];
				}
			}
		}
	]
};
```

このルールを設定してコミットをしてみると、以下のようにwarningが出るようになっている事が確認できる。また、正規表現に合致する場合にはwarningが出ない事も確認できる。

```console
[study@localhost node-express]$ git commit -m "feat: test commit"
⧗   input: feat: test commit
⚠   Your subject should contain suffix for redmine [redmine-rule]

⚠   found 0 problems, 1 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

[main 3bc8edc] feat: test commit
 1 file changed, 7 insertions(+)

[study@localhost node-express]$ git commit -m "feat: test commit refs#123"
[main f732291] feat: test commit refs#123
 1 file changed, 7 insertions(+)
```

※ちなみに、`({subject}) => {...}`のようにアロー関数の引数が`{subject}`になっているが、これは引数に渡ってくるオブジェクトの中のsubjectというキーの値のみを利用するので、このように実装している。もしオブジェクトを受け取るなら以下のような実装になる。

```js
module.exports = {
	// 省略
	plugins: [
		{
			rules: {
				'redmine-rule': (context) => {
					const pattern = / refs#\d+$/;
					return [
						new RegExp(pattern).test(context.subject), // <- ここが違う
						`Your subject should contain suffix for redmine`
					];
				}
			}
		}
	]
};
```

また、上記の実装の`context`をconsole.logで出力させてみると、以下のように何が引数に渡ってくるか？が確認できる。

```console
[study@localhost node-express]$ git commit -m "feat: aaa"
{
  type: 'feat',
  scope: null,
  subject: 'aaa',
  merge: null,
  header: 'feat: aaa',
  body: null,
  footer: null,
  notes: [],
  references: [],
  mentions: [],
  revert: null,
  raw: 'feat: aaa\n\n'
}
...
```

### npx commitlint -eについて
commitlintのCLIオプションは[reference-cli.md](https://github.com/conventional-changelog/commitlint/blob/master/docs/reference-cli.md)に書かれているが、`-e [string]`とすると、その後に指定した文字列のファイルパスのファイルからコミットメッセージを読み取り、lintを実行してくれる。`-e`のみだと、

> fallbacks to ./.git/COMMIT_EDITMSG（./.git/COMMIT_EDITMSG へのフォールバック）

と書かれているように、`./.git/COMMIT_EDITMSG`の中身を読み取ってlintを実行してくれる（`COMMIT_EDITMSG`はコミットメッセージ編集時の一時ファイルで、詳細は[変更のコミット](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E5%9F%BA%E6%9C%AC-%E5%A4%89%E6%9B%B4%E5%86%85%E5%AE%B9%E3%81%AE%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%B8%E3%81%AE%E8%A8%98%E9%8C%B2#r_committing_changes)を参照）。

## まとめとして
今回はコミットメッセージをgit hooksでチェックする方法についてみてきた。チームで開発している時などでは後から見て意味のあるコミットメッセージになっている事は重要と思われるので、今回やってみた設定を入れるのがBestだろうと思う。