Gitでのコミットメッセージの良い書き方
1. 概要
コミットメッセージは、デバッグ時、コードレビュー時、あるいはChangelogを書く際に、あなたが最も頻繁に読み直すものであり、書いたばかりのコードよりも何度も目にすることになります。この記事では、コミットメッセージの標準的な構造を理解し、Conventional Commitsを適用し、commitlint + huskyでメッセージを自動検証する方法を紹介します。この記事を読めば、明確で一貫性のあるコミットを書くことができ、チームはgitの履歴から自動的にChangelogを生成することができます。
2. なぜコミットメッセージが重要なのか
良いコミットメッセージは3つの質問に答えます:何が変わったか、なぜ変わったのか、どのような影響があるか。メッセージがなければ、各コミットに対してgit showを実行して作者の意図を推測しなければなりません。これは最初からきちんとしたメッセージを書くのに比べて、何倍も時間がかかります。
同じ変更に対する2つのメッセージを比較してみましょう:
# 悪い例
fix bug
# 良い例
fix(payment): OrderServiceに顧客情報がない場合のnull pointer処理
悪いメッセージはどのバグについて、どこにあるのかを示していません。良いメッセージはモジュール(payment)、アクション(fix)、具体的な原因を示しており、5秒読めば理解できます。
3. コミットメッセージの標準的な構造
Gitは3部構成を推奨しています:subject(件名)、body(本文)、footer(フッター)。
<type>(<scope>): <subject>
<body>
<footer>
subject:短い要約、50文字以下、現在形で書く(“added”ではなく“add”を使う)。body:なぜ変更したのかを説明し、何を変更したのかではない(diffはすでに何が変わったかを示している)。footer:メタデータ(breaking change、関連するissue、co-author)。
完全な例:
fix(order): 支払い失敗時のリトライでの重複請求を回避
PaymentService.charge()はidempotency keyを持たないため、
ネットワークタイムアウト後、クライアントがリトライすると、
システムが同じOrderに対してAPIゲートウェイを2回呼び出してしまう。
idempotencyKey = order.getId() + "-" + attemptCountを
リクエストヘッダに追加し、ゲートウェイが自動的にデデュープするようにした。
Closes #482
3.1. subjectの行
Subjectは最も重要な行です。これはgit log --oneline、PR title、changelogに表示されます。ルール:
# 間違い - 大文字で始まり、ピリオドで終わり、過去形を使用
Fixed the bug in order service.
# 正しい - 小文字で始まり、ピリオドなし、現在形を使用
fix: 電子ウォレットでの支払い時に在庫が差し引かれない
注記: 50文字の制限は絶対的なルールではありませんが、72文字を超えると多くのターミナルとGitHub UIで切り詰められます。簡潔さを心がけましょう。
3.2. bodyセクション
Bodyは文脈を説明するために使用します。コード差分では伝わらないもの:なぜこのソリューションを選んだのか、他のソリューションが除外された理由、副作用がないかなど。
refactor(cart): CartServiceを同期から非同期に変更
以前のCartService.checkout()はInventoryServiceと
ShippingServiceを順次呼び出していました。
合計レイテンシ~800ms。CompletableFutureで
2つの呼び出しを並行実行するように変更し、
レイテンシを~450msに短縮しました。
公開APIは変更せず、内部実装のみを変更しました。
Subjectとbodyを1行の空行で区切ります。gitとコミット解析ツールはすべてこの規約に依存しています。
4. Conventional Commits
Conventional Commitsは現在最も一般的な標準であり、ツールが自動的に分類してchangelogを生成できるように、固定されたtypeを定義しています。
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
よく使用されるtype:
| Type | 意味 |
|---|---|
feat |
新機能の追加 |
fix |
バグ修正 |
docs |
ドキュメントのみの変更 |
style |
コードのフォーマット、ロジックの変更なし |
refactor |
コード構造の変更、動作の変更なし |
test |
テストの追加/修正 |
chore |
小さな作業(ビルド、依存関係、設定) |
perf |
パフォーマンスの改善 |
scopeがモジュール名である例:
feat(auth): Google OAuth2ログインのサポートを追加
GoogleOAuth2ProviderがOAuth2LoginProviderを実装。
application.propertiesにclient-id、client-secretを追加。
4.1. Breaking change
後方互換性を破壊する変更の場合、type/scopeの後に!を付けるか、footerBREAKING CHANGE:で標記します。
feat(api)!: GET /orders/{id}のレスポンスをOrderDtoに変更
BREAKING CHANGE: フィールド"customerName"をオブジェクト
"customer"(id、name、emailを含む)に変更。
クライアントはパーサーを更新する必要があります。
semantic-releaseとCI/CDツールはBREAKING CHANGE:行に基づいてメジャーバージョンを自動的にバンプします。
5. コミットメッセージの自動検証
良い規約はチームが一貫して従う場合にのみ有効です。commitlintを使用して、pushする前にローカルで形式が間違ったコミットをブロックします。
<!-- Nodeプロジェクトには適用されません。husky-git-hooksにMaven wrapperを使用する場合の依存関係の例のみです -->
実際にはcommitlintはNode上で実行され、npmでインストールされます:
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
設定ファイルを作成:
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
Huskyを通じてcommit-msgフックをアタッチ:
npx husky init
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
形式が間違ったコミットを試してみましょう:
git commit -m "fixed bug"
⧗ input: fixed bug
✖ subject may not be empty [subject-empty]
✖ type must be one of [feat, fix, docs, style, refactor, test, chore, perf]
✖ found 2 problems, 0 warnings
husky - commit-msg script failed (code 1)
コミットは即座にブロックされ、正しい形式に修正してからコミットを作成する必要があります。
5.1. Java/Spring Bootプロジェクトでの使用
プロジェクトがMavenの場合、husky + commitlintを並行して使用できます(Nodeランタイムのみが必要で、Javaビルドには影響しません)。あるいは、純粋なJavaを望む場合は、簡単な検証スクリプトを書いてcore.hooksPath経由で呼び出します:
#!/bin/bash
# .githooks/commit-msg
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf)(\([a-z-]+\))?: .{1,50}"
MSG=$(cat "$1")
if ! [[ "$MSG" =~ $PATTERN ]]; then
echo "コミットメッセージがConventional Commits形式に従っていません" >&2
exit 1
fi
git config core.hooksPath .githooks
chmod +x .githooks/commit-msg
5.2. 形式チェックスクリプトのテスト作成
カスタム検証スクリプトを保守している場合(commitlintを使用していない場合)、正規表現に対するテストが必要です。CI内で再利用するためにparse ロジックをJavaクラスにパッケージ化する場合を想定します:
public class CommitMessageValidator {
private static final Pattern PATTERN =
Pattern.compile("^(feat|fix|docs|style|refactor|test|chore|perf)" +
"(\\([a-z0-9-]+\\))?: .{1,50}");
public boolean isValid(String message) {
String subject = message.lines().findFirst().orElse("");
return PATTERN.matcher(subject).find();
}
}
class CommitMessageValidatorTest {
private final CommitMessageValidator validator = new CommitMessageValidator();
@Test
void shouldAcceptConventionalFormat() {
boolean result = validator.isValid("fix(order): リトライ時の重複請求を回避");
assertThat(result).isTrue();
}
@Test
void shouldRejectMessageWithoutType() {
boolean result = validator.isValid("fixed the bug");
assertThat(result).isFalse();
}
@Test
void shouldRejectUnknownType() {
boolean result = validator.isValid("update: 何かを追加");
assertThat(result).isFalse();
}
}
このテストにより、正規表現を後で修正するときにvalidateロジックがregression(回帰)しないことが保証されます。例えば、新しいtypeを追加したときにテストを更新し忘れていないかなど。
6. よくある間違い
大きすぎるコミット、曖昧なメッセージ。 15の異なるファイルを修正する1つのコミットに“update code”というメッセージを付けるのは、複数の小さなコミットに分割すべきというサイン。各コミットは明確な目的を持つべきです(atomic commit)。
1つのコミットに複数の種類の変更を混ぜる。 同じコミット内で無関係なバグ修正とrefactorを一緒に行うと、git revertとgit bisectが使いにくくなります。修正をrevertすると、refactorも一緒に失われる可能性があります。
bodyがsubjectを繰り返している。 Bodyは文脈を補足すべき。最初の行を長い文で言い換えるべきではありません。
# 繰り返し、無意味
fix(order): 合計金額計算のエラーを修正
OrderServiceの合計金額計算エラーを修正。
絵文字/装飾文字を内容の代わりに使用する。 絵文字は悪くありませんが、実際の説明の代わりに使用すると、自動生成されたchangelogが情報を失います。
注記: Conventional Commitsは、CI/CDで自動的にchangelogを生成したり、semantic versioningを行うチームに適しています。チームが小さく、コミット数が少ない場合、ルールを厳しく適用すると不要な摩擦が生じる可能性があります。機械的な形式の遵守よりも、明確さを優先してください。
7. まとめ
良いコミットメッセージは長である必要はなく、正しい構造が必要なだけです:現在形の簡潔なsubject、理由を説明するbody、必要なメタデータを記載するfooter。Conventional Commitsを適用することで、チームがgitの履歴をすばやく読むことができ、ツールがchangelogとversioningを自動化できます。commitlintまたはカスタムgit hookを設定して、規約を自動的な制約に変え、個々のチームメンバーの意識に依存することを避けましょう。