自分のリポジトリの一つ(plugin-org-analyzer)に、コミットメッセージのルールが設定されていた気がしたので、実際にどうなっているか確認した。結論としては commitlint と Husky を組み合わせて、Conventional Commits 形式を強制する構成になっていた。
確認した設定
commitlint.config.cjs
module.exports = { extends: ["@commitlint/config-conventional"] };
@commitlint/config-conventional を継承しているだけのシンプルな設定。これにより、コミットメッセージがConventional Commitsのルールに沿っているかをチェックできるようになる。
.husky/commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit
Huskyのcommit-msgフックでcommitlint --editを実行している。commit-msgフックは、コミットメッセージが確定した直後・実際にコミットが作られる前に走るフックで、ここでcommitlintにメッセージファイルを渡してチェックしている。ルール違反があるとコミット自体が失敗する。
Conventional Commitsとは
コミットメッセージを次のような形式に統一する規約。
<type>(<scope>): <subject>
type: 変更の種類。feat(新機能)、fix(バグ修正)、docs(ドキュメント)、style(フォーマットのみ)、refactor(リファクタリング)、perf(パフォーマンス改善)、test(テスト)、build(ビルド関連)、ci(CI設定)、chore(雑務)、revert(取り消し)などが定番scope: 変更対象を表す任意項目(省略可)subject: 変更内容の要約
例えば以下のようなコミットメッセージになる。
feat: add package.xml export command
fix(parser): handle empty metadata list
docs: update README installation steps
@commitlint/config-conventional はこの形式をベースに、typeが定義済みの語彙に含まれているか、subjectが空でないか、といった基本的なルールをチェックしてくれる。
scopeには何を書くか
scopeはConventional Commits仕様上フォーマットが決まっておらず、リポジトリの構造や関心事に応じて自由に決めてよい項目。よく使われるのは次のような単位。
- モジュール・パッケージ名:
feat(parser): ... - 機能領域:
feat(auth): ...、fix(billing): ... - ファイル・コンポーネント名:
fix(button): ... - レイヤー:
feat(cli): ...、fix(db): ...
plugin-org-analyzerなら、package.xml出力機能の追加はfeat(export): add manifest generation、メタデータ解析部分の修正はfix(parser): handle empty metadata listのように、その変更が影響するモジュール・機能をひとことで表す使い方になる。
@commitlint/config-conventional自体はscopeの語彙を強制しない。scope-emptyのようなルールで必須化はできるが、デフォルトでは有効になっていないため省略も可能で、その場合は単にtype: subjectと書ける。プロジェクトによってはscope-enumルールを追加設定し、「使ってよいscope一覧」を定義してチェックさせるケースもある。
タイトル行だけの規約ではない
Conventional Commitsと聞くと「1行目のtype: subjectしか書けない」と誤解しそうになるが、そうではない。仕様上は従来の「タイトル行→空行→本文」というgitの慣習をそのまま活かせる構造になっている。
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
- 1行目(ヘッダー)だけが
type(scope): subjectの形式に従う必要がある - 空行を挟んだ後の
bodyは自由記述。変更の背景や詳細な説明をこれまで通り書ける footerにはBREAKING CHANGE: ...のような破壊的変更の告知や、Refs: #123のようなIssue番号の紐付けなど、構造化した情報を書く
例えば、次のようなコミットメッセージも問題なくConventional Commitsの形式に沿っている。
fix(parser): handle empty metadata list
package.xmlの生成時に、メタデータが1件も取得できなかった場合に
TypeErrorが発生していた問題を修正。
空配列を渡された場合は早期リターンし、警告ログを出力してから
処理を継続するようにした。
Refs: #42
@commitlint/config-conventionalがデフォルトで制限しているのは主にヘッダー行(1行目)で、以下のようなルールが中心になる。
header-max-length: ヘッダー行は100文字以内type-enum:typeが既定の語彙に含まれるsubject-empty/subject-full-stop: subjectが空でない・末尾にピリオドを付けないbody-leading-blank: bodyを書く場合はヘッダーとの間に空行が必要
body自体の分量や書き方そのものを制限するルールはデフォルトには含まれていない。つまり「1行目の型・要約だけは決まったフォーマットに揃えつつ、詳細な説明は従来通り自由に書ける」というのが実態。
この構成の効果
- コミットログのフォーマットがチームやリポジトリ全体で揃う
typeを見るだけで変更の性質(機能追加なのか修正なのか)がひと目でわかる- semantic-release のような、コミットメッセージからバージョンやCHANGELOGを自動生成するツールと相性がよい
- ルール違反はHuskyのフックでコミット時点で弾かれるので、レビュー段階まで持ち越さずに気づける