2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIが生成するcommitメッセージをLinterで強制的にルール準拠させる

2
Posted at

注意

本記事は人間が書いた記事をAIが補完・編集したものです。

はじめに

作業中のcommitメッセージ、皆さんどうしていますか?

私はGitHub CopilotやCursorにカスタムコマンド(プロンプト)を設定して、AIに自動生成させる手法をここ暫く愛用しています。

これ、基本的には普段めんどくさくて書かない内容までcommitメッセージとして自動で書いてくれるので割と重宝するんですよね

ただ偶にプロンプトを無視したcommitメッセージを生成しちゃうこともあり......

まあ面倒なので、これを良い感じに100%私のルールに従ったcommitメッセージとなるようLinterを設定しちまえという内容です。

commit形式

とりあえず私の個人開発におけるcommitルールはConventional Commits形式かつ、タイトルやら本文やらが日本語で記載されていること
というひどくシンプルなものです。

Conventional Commits形式についてはこの辺を参照ください

この下ではそれに則り設定を書いていきますが、
必要があればチームに則ってその辺は自由に書き換えてください。

Linter設定

今回使用するLinterはcommitlintです。
その名前の通り、commitメッセージ用のLinterとして作られています。

使い方は以下の記事などが参考になるので、参照ください

設定ファイルの形式は

を見ると

.commitlintrc
.commitlintrc.json
.commitlintrc.yaml
.commitlintrc.yml
.commitlintrc.js
.commitlintrc.cjs
.commitlintrc.mjs
.commitlintrc.ts
.commitlintrc.cts
.commitlintrc.mts
commitlint.config.js
commitlint.config.cjs
commitlint.config.mjs
commitlint.config.ts
commitlint.config.cts
commitlint.config.mts

といろいろあるようなので今回はcommitlint.config.mjsで書いていきます。

パッケージインストール

まずは必要なパッケージを入れます。

npm install --save-dev @commitlint/cli @commitlint/config-conventional conventional-changelog-conventionalcommits

package.jsontypemoduleになっていない場合は変更しておいてください。
.mjsで設定を書くので。

commitlint.config.mjs

以下のように設定してみましょう。

// commitlint.config.mjs
export default {
  extends: ['@commitlint/config-conventional'],
  parserPreset: 'conventional-changelog-conventionalcommits',
  rules: {
    'type-enum': [2, 'always', ['build','chore','ci','docs','feat','fix','perf','refactor','revert','style','test']],
    'type-empty': [2, 'never'],
    'type-case': [2, 'always', 'lower-case'],
    'scope-empty': [2, 'never'],
    'subject-empty': [2, 'never'],
    'subject-case': [0], // 日本語主体のため無効化
    'subject-full-stop': [2, 'never', '.'],
    'subject-full-stop-japanese': [2, 'never'],
    'header-max-length': [2, 'always', 100],
    'header-trim': [2, 'always'],
    'body-leading-blank': [1, 'always'], // 空行は警告
    'body-empty': [2, 'never'],
    'body-max-line-length': [2, 'always', 100],
    'footer-leading-blank': [1, 'always'],
    'footer-max-line-length': [2, 'always', 100],

    // 日本語強制
    'subject-japanese': [2, 'always'],
    'body-japanese': [2, 'always'],
  },
  plugins: [
    {
      rules: {
        'subject-japanese': (parsed, when = 'always') => {
          const s = parsed.subject || '';
          const hasJa = /[\u3040-\u309F\u30A0-\u30FF\uFF66-\uFF9F\u4E00-\u9FFF]/.test(s);
          const ok = hasJa;
          return [when === 'never' ? !ok : ok, 'subjectは日本語で記述してください'];
        },
        'body-japanese': (parsed, when = 'always') => {
          const b = (parsed.body || '').trim();
          const hasJa = /[\u3040-\u309F\u30A0-\u30FF\uFF66-\uFF9F\u4E00-\u9FFF]/.test(b);
          const ok = hasJa;
          return [when === 'never' ? !ok : ok, 'bodyは日本語で記述してください'];
        },
        'subject-full-stop-japanese': (parsed, when = 'always') => {
          const s = parsed.subject || '';
          const endsWithJapanesePeriod = s.endsWith('');
          const ok = when === 'never' ? !endsWithJapanesePeriod : endsWithJapanesePeriod;
          return [ok, 'subjectは句点(。)で終わらないでください'];
        },
      },
    },
  ],
  prompt: {
    questions: {
      type: {
        description: "コミットする変更の種類を選択してください",
        enum: {
          feat: { description: '新しい機能', title: 'Features', emoji: '' },
          fix: { description: 'バグ修正', title: 'Bug Fixes', emoji: '🐛' },
          docs: { description: 'ドキュメントのみの変更', title: 'Documentation', emoji: '📚' },
          style: { description: '意味のないコードの変更(空白やフォーマットなど)', title: 'Styles', emoji: '💎' },
          refactor: { description: 'リファクタリング(機能追加やバグ修正を含まない構造改善)', title: 'Code Refactoring', emoji: '📦' },
          perf: { description: 'パフォーマンス向上', title: 'Performance Improvements', emoji: '🚀' },
          test: { description: 'テストの追加や修正', title: 'Tests', emoji: '🚨' },
          build: { description: 'ビルドシステムや依存パッケージの変更', title: 'Builds', emoji: '🛠' },
          ci: { description: 'CI構成・スクリプトの変更', title: 'Continuous Integrations', emoji: '⚙️' },
          chore: { description: "ソースやテスト以外のその他の変更", title: 'Chores', emoji: '♻️' },
          revert: { description: '以前のコミットの取り消し', title: 'Reverts', emoji: '🗑' },
        },
      },
      scope: { description: '変更のスコープ(必須)' },
      subject: { description: 'コミット内容の簡潔な要約(日本語・必須)' },
      body: { description: '詳細な説明(日本語・必須)' },
      isBreaking: { description: '破壊的な変更がありますか?' },
      breakingBody: { description: '破壊的変更の場合は詳細を入力(必須)' },
      breaking: { description: '破壊的変更の内容を記述してください' },
      isIssueAffected: { description: 'この変更はIssueに影響しますか?' },
      issuesBody: { description: 'Issueと関連付ける場合は内容を入力(必須)' },
      issues: { description: 'Issueの参照(例: fix #123, re #123)' },
    },
  },
};

長いですね。上から順に見ていきます。

rulesのざっくり解説

rulesの書き方は[レベル, 適用条件, 値]の3要素です。
レベルは 0=無効、1=警告、2=エラー。

今回の設定で特に意識したところだけ抜き出すとこんな感じです。

  • scope-empty: [2, 'never'] ... scopeを必須にしています。何の領域に対する変更なのかを明確にしたいので
  • body-empty: [2, 'never'] ... body(本文)も必須。変更の意図は残しておきたい
  • subject-case: [0] ... 大文字小文字のチェックを無効化。日本語のsubjectだと誤検知するので切っています

あとは文字数制限を入れたり、空行のルールを設定したりしていますが、まあConventional Commitsのお作法通りです。

カスタムルール(日本語強制)

ここが今回の肝です。

commitlint標準のルールセットには当然ながら「日本語で書け」なんてルールは無いので、pluginsで自作しています。

'subject-japanese': (parsed, when = 'always') => {
  const s = parsed.subject || '';
  const hasJa = /[\u3040-\u309F\u30A0-\u30FF\uFF66-\uFF9F\u4E00-\u9FFF]/.test(s);
  const ok = hasJa;
  return [when === 'never' ? !ok : ok, 'subjectは日本語で記述してください'];
},

やっていることはシンプルで、正規表現でひらがな・カタカナ・漢字が含まれているかをチェックしているだけです。
body-japaneseもほぼ同じ。

もう一つ、subject-full-stop-japaneseはsubjectが句点()で終わっていないかをチェックするルールです。
英語のピリオド(.)は標準のsubject-full-stopで弾けるんですが、日本語の句点は別途対応が必要だったので追加しました。

promptセクション

promptはcommitlint公式の対話型commitツール(@commitlint/prompt-clicommitizenアダプタ)用の設定です。
直接AIの挙動に影響するわけではないですが、typeの日本語説明を書いておくとAIがcommitメッセージを生成するときに参照してくれたりするので、おまけ的に設定しています。

huskyでcommit時に自動実行

commitlintの設定ファイルを書いただけではcommit時に勝手にチェックしてくれないので、huskyを使ってGit hookに組み込みます。

npm install --save-dev husky
npx husky init

npx husky init.husky/ディレクトリとpre-commitフックが生成されます。
今回はcommitメッセージのチェックなので、commit-msgフックを作ります。

.husky/commit-msgを以下の内容で作成してください。

npx --no -- commitlint --edit $1

これだけです。git commitするたびにcommitlintが走って、ルール違反があればcommitが止まります。

実際に試してみる

設定ができたので、わざとルール違反のcommitを投げてみました。

GitHub Copilotに英語のcommitメッセージを生成させた場合:

chore(setup): add commitlint configuration and husky hooks

This commit sets up commitlint with custom configuration and husky pre-commit hooks.
The configuration includes Japanese language requirements and conventional commit rules.

commitlintの出力:

⧗   input: chore(setup): add commitlint configuration and husky hooks
✖   subjectは日本語で記述してください [subject-japanese]
✖   bodyは日本語で記述してください [body-japanese]

✖   found 2 problems, 0 warnings

ちゃんと弾いてくれました。

で、ここからが地味に嬉しいポイントなんですが、Copilot等のAgentモードだとこのエラーメッセージを読んで「あ、日本語じゃないとダメなのか」と理解して、自動的に日本語のcommitメッセージを再生成してくれます。

つまり、プロンプトでルールを伝えている以上、ほとんどの場合は1発で通るし、万が一プロンプトを無視されてもcommitlintが弾いてくれる。弾かれたらAIが自分でリトライしてくれる。人間は何もしなくていい。最高。

AI向けプロンプトファイルも一応用意する

commitlintだけでもガードレールとして機能するんですが、毎回リトライされるのも微妙なので、AIにルールを事前に伝えるプロンプトファイルも用意しておくと良いです。

.github/prompts/create-commit.prompt.mdみたいな感じで置いておけば、VS CodeのCopilot Agentモードからカスタムプロンプトとして呼び出せます。

中身はcommitメッセージのフォーマットや手順をそのまま書いたものです。
差分の取り方、Conventional Commits形式の書き方、「日本語で書け」という制約あたりを書いておけば、大体言うことを聞いてくれます。

詳細は以下のリポジトリに置いてあるので参考にどうぞ。

ファイル構成

最終的にはこんな構成になります。

.
├── .github/
│   └── prompts/
│       └── create-commit.prompt.md   # カスタムプロンプト
├── .husky/
│   └── commit-msg                    # commitlint実行フック
├── commitlint.config.mjs            # commitlintルール定義
└── package.json

おわりに

やっていることは「commitメッセージのLintをGit hookで自動実行する」というだけで、技術的には何も新しくないです。

ただ、AIにcommitメッセージを任せる運用と組み合わせると「プロンプトで伝える → Lintで弾く → AIがリトライ」のサイクルが勝手に回るので、人間が何も気にしなくてよくなります。

commitメッセージを作るのに毎回AIの出力を確認するのもだるいので、こういう仕組みで任せきりにできるのはだいぶ楽ですね。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?