はじめに
開発現場ではコミットメッセージが適当だと、後から見た時や他の人が見た時に???になって、開発のネックになってしまう事はままあると思う。今回はコミットメッセージが各人でバラバラで独自的なものにならないように、git hooksでコミットメッセージにリントを書ける方法についていきたいと思う。
ひとまずやってみる
私はNode.js(Javascript)で開発している事もあり、commitlintを利用する。
設定方法等はGuide: Local setupに書かれているのでそちらを参照して行っていくが、私はgit hooksを設定するのにsimple-git-hooksを使う事にした。
※simple-git-hooksを導入して設定する方法はPrettier のルールで自動整形するを参照。
[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
// echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.jsにより作成される
module.exports = {
extends: ['@commitlint/config-conventional']
};
{
...
"simple-git-hooks": {
"commit-msg": "npx commitlint -e"
}
}
ここまで設定した所で実際にgit commitを実行してみると、以下のようにコミットリントが走り、コミットが失敗している事が確認できる。
[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
ソースコード全体は以下。
詳細を見ていく
git hooksに関して
今回はsimple-git-hooksを利用していたが、このライブラリが一体何をしてくれているのか?というと、シンプルにgit hooksのshellを作成しているだけ。
公式を見ると分かるが、npx simple-git-hooks
を実行する事で、package.jsonのgit hooksのキー(git hooksの種類)に設定されているコマンドを./.git/hooks
以下に作成してくれる(git hooksについては8.3 Git のカスタマイズ - Git フックやgithooksを参照)。
{
...
"scripts": {
...
"prepare": "npx simple-git-hooks"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged",
"commit-msg": "npx commitlint -e"
}
}
[study@localhost hooks]$ tree .
.
├── commit-msg
└── pre-commit
そしてこのファイルの中身を見ると、以下のようにpackage.jsonに書いていたものがそのまま書かれている事が分かる。
[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) でコミットメッセージの書式チェックなどを参照)。
※prepareについてだが、npm/yarn(yarnは1.x系のみ)にはLife Cycle Scripts/scriptsという機能があり、これを利用するとnpm/yarnのコマンド実行時にイベントフックでコマンドを実行できる。このLife Cycle Scriptsのprepareを利用する事で、開発者全員で同じ設定を簡単にできるようにする事ができる。
※ちなみに、npx simple-git-hooks
を実行して動くコードはこれ(npx simple-git-hooks
を実行すると、simple-git-hooksのpackage.jsonのbinに設定したものが実行されるが、それについてはbinやnpm パッケージを CLI ツールとして機能させる仕組みについてを参照)。
commitlint.config.jsについて
Configuration object exampleにcommitlint.config.js
で設定できる内容が書かれている。
今回はextends
とローカルで独自のルールを適用する方法の2つについて以下で詳細を見ていく。
extendsの部分
今回は@commitlint/config-conventionalを継承して、Conventional Commitsで策定されている仕様を満たすようなコミットメッセージになるようにするために設定している(ルール一覧はGitHubのページに書かれている)。
※一部、@commitlint/config-conventionalのルールでwarnにレベルを下げたい、などの要望がある時には、以下のように上書き設定もできる(ルール適用のレベル(warn・errorなど)についてはRulesを参照)。
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)
ESLint同様、基本的にはルールはnpmに公開されているものを依存追加して利用する、という流れになるが、npmに追加する事なくローカルでプラグインを開発するという事もできる。今回見ていくものはそれに該当し、公式で言うとLocal Pluginsに詳細が書かれている。
実装方法はUsage Exampleにある通りだが、今回はRedmineとの紐づけをする際に必要になるsuffixが付いているか?を検証するルールを作成してみようと思う。実際の実装は以下。
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が出ない事も確認できる。
[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というキーの値のみを利用するので、このように実装している。もしオブジェクトを受け取るなら以下のような実装になる。
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で出力させてみると、以下のように何が引数に渡ってくるか?が確認できる。
[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に書かれているが、-e [string]
とすると、その後に指定した文字列のファイルパスのファイルからコミットメッセージを読み取り、lintを実行してくれる。-e
のみだと、
fallbacks to ./.git/COMMIT_EDITMSG(./.git/COMMIT_EDITMSG へのフォールバック)
と書かれているように、./.git/COMMIT_EDITMSG
の中身を読み取ってlintを実行してくれる(COMMIT_EDITMSG
はコミットメッセージ編集時の一時ファイルで、詳細は変更のコミットを参照)。
まとめとして
今回はコミットメッセージをgit hooksでチェックする方法についてみてきた。チームで開発している時などでは後から見て意味のあるコミットメッセージになっている事は重要と思われるので、今回やってみた設定を入れるのがBestだろうと思う。