- Conventional Commits は
<type>[optional scope]: <description> 形式でコミットメッセージに意味を持たせようとするが、変更種別を先に置き、範囲を任意にすることで、実際の探索に必要な情報を後ろへ追いやっている
- コントリビューター・デバッガー・障害対応者はコミットログから変更が触れたコード領域を探すため、バグはどの種別の変更でも生じうる以上、範囲(scope) は種別より重要である
fix(compiler): prevent namespaced SVG <style> elements from being stripped のように説明だけでもバグ修正だと分かり、refactor(core): Update webmcp support to use document.modelContext のように1つのコミットが修正・リファクタリング・機能追加にまたがることもあるため、type は冗長で制約的である
- 自動 CHANGELOG 生成とセマンティックバージョンの増分判定は、コミットログと変更ログで読者が異なり、revert・偶発的な後方互換性破壊・後からの破壊解消によって結果がずれることがある
- 範囲プレフィックス のコミットメッセージは変更主体を先に示し、ビルド・デプロイ条件もタイトルの種別より
git diff で変わったファイルを基準にしたほうがよい
間違った優先順位
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
- タイトル行は
fix、feat、chore、docs、refactor のような <type>、任意の scope、description で構成される
- 中核的な欠陥は、変更の主体である scope より、変更の種類である type を優先する構造にある
- scope が任意である構造は、コミットでもっとも重要な情報が欠けうる状態を生み、type をタイトルの先頭に置くことで優先順位を逆転させている
なぜ scope は type より重要なのか
- コントリビューターは、最後の貢献以降の変化、プロジェクト全体の流れ、pull や rebase の際に進行中の作業と衝突しうるコミットを見つけるためにコミットログを読む
- デバッガーは、バグが現れたコンポーネントに関係する領域を触った変更を探すが、バグはどの type の変更でも発生しうるため、type 情報は役に立たない
- 障害対応者は、障害発生時点の前後のコミットログをざっと見て問題を引き起こした領域を探し、インバウンド API エラーが急増した地点に
auth scope のコミットがあれば、有力な原因候補になる
- コミットログを読む人にとって重要なのは、変更がどの種類だったかではなく、どの領域に触れたかである
type の冗長性と制限
自動化の約束の限界
- git-cliff や conventional-changelog のようなツールでコミットから CHANGELOG を自動生成する発想には、コミットログと変更ログでは読者が異なるという問題がある
- CHANGELOG はユーザー向けであり、バージョン間の機能的・ビジネス的な差分を理解することに焦点がある
- コミットログは開発者向けであり、コードベースが時間とともにどう変化したか、scope の観点で流れを読むことに焦点がある
- 中程度以上の複雑さを持つプロジェクトでは、意味のある1つの機能が複数のコミットで導入されることが多く、開発者には実装過程が有用でも、エンドユーザーにとって重要なのは最終的な新機能そのものだけである
- revert コミットは、開発者にとってはコミットログの流れの中で重要だが、エンドユーザーにとっては取り消された変更は最初から作られなかった変更と同じである
- コミット type に基づくセマンティックバージョンの増分は、後方互換性を壊す変更が revert されたのに major バージョンを上げたり、破壊に後から気づいて minor/patch として誤って上げたり、後続コミットと組み合わさって破壊が解消されたのに破壊的変更と判定したりする問題を生みうる
- このような状況では rebase で履歴を修正することもできるが、ワークフローによってはそれが妨げられたり破綻したりし、コミットログが伝える流れの信頼性を下げる
- コミットタイトルの type でビルド・デプロイプロセスをトリガーすると、
docs: fix typos というタイトルのコミットが認証サブシステムに脆弱性を入れる、といった形で自動ツールを回避できてしまう
- ビルド・デプロイ条件は、コミットタイトルより
git diff で変更ファイルを特定して決めるほうがよい
適用上の問題と代替案
- Conventional Commits はプロジェクトごとの type 集合を定義させるが、多くのプロジェクトは commitlint のデフォルト type をそのまま採用しており、個々のプロジェクト特性にうまく合わない可能性がある
- Conventional Commits の仕様は、技術的には
fix と feat だけを定義し、追加の type は各プロジェクトに委ねている
- 企業環境では、変更管理や監査要件により、すべてのコミットメッセージにチケット番号を入れなければならない場合があり、
<scope> がチケット番号の場所として使われると有用な metadata が失われる
- Linux、FreeBSD、Git、Go、NixOS、Node.js は、プロジェクトに適した scope プレフィックスのコミットメッセージを使っている
- Linux カーネルでは subsystem、Go プロジェクトでは package path、マイクロサービスアーキテクチャでは microservice 名が自然な scope になる
- scopedcommits.com は、コミットメッセージを scope 中心の形式へ戻し、CHANGELOG 生成とコミットログ管理を分離しようという方向性を扱っている
- Conventional Commits の利点は実際の利益につながらず、オープンソースプロジェクトでの人気と AI のデフォルト選好傾向が、アンチパターンを含むコミットメッセージの拡散を招いている
1件のコメント
Lobste.rsの意見
conventional commits への反論を、本能的な拒否感ではなく論理で整理した文章なのでうれしい。
なぜ嫌いなのか深く考えたことはなかったし、LLM が生成したコードと結び付けてしまうからかと思っていた。特に
chore:がいちばん嫌いで、ハンガリアン記法を再発明しないでほしい。そもそも生まれるべきではなかった。chore:は、もはや Angular コミットスタイルガイド にもなく、曖昧すぎると気付いたのかbuild:に吸収された。Angular スタイルにあった時代でも
chore:の説明はかなり具体的な用途を示していたが、一部のオープンソースプロジェクトでは、文字どおり面倒な雑務のように感じる作業へ雰囲気で付けているように見える。conventional commits は好きではないが、提案された代案は scope が任意である理由 を見落としている気がする。
はっきりしたモジュールが多くない小さなプロジェクトでは、「scope」という概念はあまり有用ではない。どちらも見落としている有用な慣行として、コミットタイトルに issue やチケット番号を入れると変更の追加コンテキストを把握しやすくなり、特にコードレビューで役立つ。ただしチケット番号を必須にすると、些細な変更にも無意味なチケットが量産されるので嫌だし、特定のバグや作業を扱う変更なら、そのバグや作業に結び付いているべきだ。
タイトル行を見ただけで分かるべき重複したコミットの「type」よりは、依然としてましだ。
変更がチケットと明確に対応しているなら「チケット番号」コミットを使い、そうでなければ別のやり方を使えばよい。ある変更は type にはよく合っても scope にはあまり合わず、その逆もあるので、scoped commits と conventional commits を混ぜて使うこともできる。
「段落テキストには 等幅フォント を使うな」と言いたい。
それでも、記事の前提にはおおむね同意する。
コミットメッセージがいまいちでも、変更範囲の感触をつかむには
git log --name-onlyやgit log --statをよく使ってみるのを勧める。ファイル名を見ると、各コミットをいちいち全部開かなくても何が変わったのか知るのにかなり役立つ。
本当に気に入っているやり方は、PR タイトルに conventional commit スタイルを強制することだ。
PR タイトルはマージ後でも maintainer が修正でき、コミット履歴を書き換える必要もなく、release-drafter のようなツールと組み合わせれば GitHub リリースで意味のある変更ログを自動化できる。筆者が言うステークホルダーに合った適切な粒度、つまり機能・修正・互換性破壊を分けて見せられ、次の GitHub リリース草案の妥当な semver も自動で処理してくれる。
parse-libのようなコンポーネントが任意であってはならないという記事の指摘はそのとおりだし、conventional commits を強制すると新規貢献者が萎縮するという点にも同意する。しかし代案が特に優れているわけでもない。それでも、互換性破壊識別子である
fix!(parse-lib): Don't leave sparse holes when parsing JSON arraysはかなり多くの情報を与えてくれる。特定コンポーネントのバグ修正であり、その修正に不可避に伴った互換性破壊であり、minor semver 増加のような意味を含んでいる。こういうものは PR タイトルに使える。コミット規律を促す手段として conventional commits にのめり込みすぎて、結局は習慣として固まってしまったことは認める。
今ではしばしば制約的で恣意的だと感じる。いくつかのプロジェクトでは、それが本当に実践的な慣例なのかも分からず、Linux/Go/Node スタイルに近くなってきたが、設定が多様な monorepo では type を無理に作るより
[service]: [what changed]と書くほうが自然だった。今後は厳格な慣例に合わせるより、何が有用そうかを基準に個人のコミットスタイルをもっと試してみるつもりで、scoped commits はよい出発点に思える。chore(lobsters): add my 2 cents on conventionals commits [JIRA-69420]ほぼ全面的に同意するが、「コミットログが語る物語の信頼性を下げる修正主義的な記録を貢献者に見せる」という部分だけは少し見方が違う。筆者は主に公開ブランチのことを言っているようで、公開ブランチなら妥当な助言だ。しかし非公開ブランチには当てはまるべきではない。最終的な変更をレビューする人、つまり maintainer や 10 年後の自分が理解しやすい形にすればよく、前後のつながらない思考の流れや、もっと悪く言えば
address reviewコミットの束を残す必要はない。「なぜ scope が任意なのか?」への答えは、小さなプロジェクトでは単に プロジェクト全体が scope だからだ。
コミットの「type」がそれほど有用でないという点には同意するが、scoped commits と conventional commits の間に大きな違いがあるのかはよく分からない。scoped は「type」を抜いた conventional にすぎず、fix・feat・refactor・chore の区別があっても別に構わない。
みんなが commitlint のデフォルトをそのまま持ってきているだけなら、人々がそれをうまく扱えるようにすればいいだけではないか、と思う。