📘

コミットメッセージの体裁を強制してみたお話

2024/06/14に公開

自己紹介

こんにちは。若手の web エンジニアの柿です!
フロントエンド領域を生業としています :)
個人でなにかやるときには React/Next を使いますが、仕事では Vue/Nuxt を使用しています。

今回のお話

今回は、コミットメッセージの体裁を強制してみることを頑張ったときのお話です。

なぜ頑張ったのかというと、
「コミットメッセージ、lint できるようにしてみてよ!」
という大先輩からの一言があったからです。

チームの課題として、一時期「コミットメッセージの体裁が乱れていたこと」を提起させて頂いた過去があったので、
これできるようになったらおもろいな、と思った次第です。

少なくともコミットメッセージに prefix(feat:, fix:など)がついているだけで、どんな類のコミットなのか分かりやすくなりますし、なにより見た目がいいですよね!笑

今回のゴールとしては、

「✨ feat: 対応内容」

というコミットメッセージの形式でコミットできるようにすることです。
私は Gitmoji が好きなので、それも取り入れた形にします。

今回導入するもの

今回導入するツールは大きく分けて

  • husky
  • commitlint
  • commitizen

の3つです。

次のパートから早速やってみます。

husky の導入

今回は Git hooks を利用するので、それを簡単にするツール husky を導入して使用します。

terminal
pnpm add -D husky

今回の husky のバージョンは、9.0.11を使っていきます。

package.json
{
  "devDependencies": {
    "husky": "^9.0.11"
  }
}

パッケージが追加できたら初期設定を行っていきます。

terminal
pnpm exec husky init

上記コマンドを打つと、.huskyディレクトリが作成され、その中にpre-commitファイルが作成されると思います。
そして、scripts にprepareが追加されると思います。

package.json
{
  "scripts": {
    "prepare": "husky"
  }
}

今回はpre-commitファイルは使用しないので、削除していただいても構いません。
もし活用したい場合は、lint-staged も合わせて導入すると、コミット対象のファイルのみに eslintprettier をかけることができ、エラーが残っている場合はコミットできなくなるので、品質担保におすすめです。

機会があれば、別記事で導入記事を書こうと思います。

commitlint の導入

次に、コミットメッセージの体裁を強制するために commitlint を導入します。
lint という名前がついている通り、コミットメッセージのリントツールになります。

有名なコミットメッセージの規約として、
Conventional Commits
があります。

この規約のコミットメッセージの形式は、

コミットメッセージの形式(Conventional Commits)
<type>[optional scope]: <description(subject)>

[optional body]

[optional footer(s)]

となっています。
私は<type>の部分に Gitmoji を使っているため、このまま使うとコミットが失敗します。
そのため、他のルールセットを使うことにしました。

今回導入したのは、commitlint-config-gitmojiというものです。

このルールセットを使うと、Gitmoji を使ったコミットメッセージの形式を強制することができます。

コミットメッセージの形式(commitlint-config-gitmoji)
:gitmoji: <type>: <description(subject)>

[optional body]

[optional footer(s)]

という形式になります。

それでは、関連パッケージをインストールしておきましょう。

terminal
pnpm add -D @commitlint/cli commitlint-config-gitmoji

使用しているパッケージのバージョンは以下の通りです。

package.json
{
  "devDependencies": {
    "@commitlint/cli": "^19.3.0",
    "commitlint-config-gitmoji": "^2.3.1"
  }
}

パッケージを追加できたら、commitlintの設定ファイルを作成します。

terminal
touch commitlint.config.cts

今回は ts ファイルを作成しましたが、他にも

設定ファイル一覧
.commitlintrc
.commitlintrc.json
.commitlintrc.yaml
.commitlintrc.yml
.commitlintrc.js
.commitlintrc.cjs
.commitlintrc.mjs
.commitlintrc.ts
.commitlintrc.cts
commitlint.config.js
commitlint.config.cjs
commitlint.config.mjs
commitlint.config.ts
commitlint.config.cts

といったファイルの作成が可能です。
公式ドキュメントではmodule.exports={}を使用しているので、.cjsもしくは.ctsの拡張子を使うといいかもれません。
(私はexport defaultできるか試してみたかったので、.tsを使っています)

次に作成したファイルに以下の内容を記述します。

commitlint.config.cts
const config = {
  extends: ['gitmoji'],
};

module.exports = config;

これでcommitlint-config-gitmojiの設定を適用することができます。
(commonJS ではない場合はmodule.exports = config;export default config; に変更してください)

次に<type>の部分にどんな type が来るのかを設定します。
feat,fixといった prefix たちですね。

commitlint.config.cts
const config = {
  extends: ['gitmoji'],
  rules: {
    'header-max-length': [2, 'always', 100],
    'type-enum': [
      2,
      'always',
      [
        'feat',
        'improve',
        'update',
        'fix',
        'hotfix',
        'refactor',
        'delete',
        'style',
        'docs',
        'move',
        'test',
        'chore',
        'package',
        'WIP',
      ],
    ],
  },
};

module.exports = config;

header-max-length は、コミットメッセージの長さを指定するルールです。
type-enum は、許可する type を指定するルールです。今回はこれらの prefix を許可するようにしました。

次にコミットしたときに、commitlintが動作するように設定します。

.huskyディレクトリ配下にcommit-msgファイルを作成します。

terminal
touch .husky/commit-msg

そして、以下の内容を記述します。

.husky/commit-msg
npx --no -- commitlint --edit "$1"

これでコミットしたときに、commitlintが動作するようになりました。

設定した prefix 以外のコミットメッセージや、prefix がないコミットメッセージを行った場合は、コミットが失敗するはずです。

commitizen の導入

commitlint だけでも十分効果を発揮しますが、いちいちどれが問題ない prefix なのかを覚えるのは大変なことです。

そのため、コミットメッセージの prefix 及び gitmoji を対話形式で選べるようにしたいと思います。
そのためには commitizen を導入します。

terminal
pnpm add -D commitizen cz-customizable

commitizen は、対話形式でコミットメッセージを作成するためのもので、
cz-customizable は、commitizen の設定をカスタマイズするためのものです。

使用しているパッケージのバージョンは以下の通りです。

package.json
{
  "devDependencies": {
    "commitizen": "^4.3.0",
    "cz-customizable": "^7.0.0"
  }
}

次に、commitizenの設定ファイルを作成します。

terminal
touch .cz-config.cts

このファイルはどうやら commonJS で書かないといけないようだったので、.ctsを使っています。(.cjsでも可)
これは、cz-customizableの設定ファイルで、対話形式でコミットメッセージを作成する際の設定を記述します。

.cz-config.cts
const config = {
  types: [
    {
      name: 'feat:     ✨ 新機能の追加',
      value: ':sparkles: feat',
    },
    {
      name: 'improve:  🎨 コードの構造/ロジックの改善',
      value: ':art: improve',
    },
    {
      name: 'update:   🩹 軽微な修正',
      value: ':adhesive_bandage: update',
    },
    {
      name: 'fix:      🐛 バグ修正',
      value: ':bug: fix',
    },
    {
      name: 'hotfix:   🚑 緊急バグ修正',
      value: ':ambulance: hotfix',
    },
    {
      name: 'refactor: ♻️ リファクタリング',
      value: ':recycle: refactor',
    },
    {
      name: 'delete:   🔥 ファイルやコードの削除',
      value: ':fire: delete',
    },
    {
      name: 'style:    💄 UIやスタイルファイルの追加/更新',
      value: ':lipstick: style',
    },
    {
      name: 'docs:     📝 ドキュメンテーションの追加/更新',
      value: ':memo: docs',
    },
    {
      name: 'move:     🚚 ファイルやディレクトリの移動',
      value: ':truck: move',
    },
    {
      name: 'test:     ✅ テストの追加/更新/合格',
      value: ':white_check_mark: test',
    },
    {
      name: 'chore:    🔧 設定ファイルの追加/更新',
      value: ':wrench: chore',
    },
    {
      name: 'package:  📦 パッケージの追加/更新',
      value: ':package: package',
    },
    {
      name: 'WIP:      🚧 作業途中',
      value: ':construction: WIP',
    },
  ],
  messages: {
    type: 'コミットの種類(型)を選択してください:\n',
    subject: 'コミットメッセージを入力してください:\n',
    body: '変更内容の詳細があれば書いてください:(enterでスキップ)\n',
    confirmCommit: '上記のコミットを続行してもよろしいですか?(Y/n)\n',
  },
  skipQuestions: ['scope', 'breaking', 'footer'],
  subjectLimit: 100,
};

module.exports = config;

types の配列には先程 commitlint の設定ファイルで設定した prefix を記述します。
name:には対話形式で表示される内容、value:には実際のコミットメッセージに追加される内容を記述します。

messages には対話形式で表示される質問の内容を記述します。
対話の種類としては

対話形式の質問一覧
type: <type>の部分、prefix を選択する質問
scope: <type>(scope)の部分、スコープを選択する質問(ui, api, etc...)
subject: <description(subject)>の部分、コミットメッセージを入力する質問
body: [optional body]の部分、変更内容の詳細を書く質問(3行目にあたる部分)
breaking: <type!>の部分、破壊的変更がある場合に書く質問(! がつく)
footer: [optional footer(s)]の部分、issue 番号などを書く質問

があります。
私としては、scopebreakingfooter は使わないので、skipQuestionsに記述して対話をスキップします。

コミットメッセージの文字数制限はsubjectLimitで設定します。
今回は 100 文字にしておきました。
そんなに書くのであれば、bodyに書いてほしいですが笑

git commit したときに対話できるようにする

最後に、git commitしたときに対話形式でコミットメッセージを作成できるようにします。

まずは、.husky ディレクトリ配下にprepare-commit-msgファイルを作成します。

terminal
touch .husky/prepare-commit-msg

そして、以下の内容を記述します。

.husky/prepare-commit-msg
exec < /dev/tty && node_modules/.bin/cz --hook || true

次にpackage.jsonに以下の内容を追記します。

package.json
{
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-customizable"
    },
    "cz-customizable": {
      "config": ".cz-config.cts"
    }
  }
}

これで.cz-config.cstの設定が適用され、git commitしたときに対話形式でコミットメッセージを作成できるようになります。

まとめ

さて、これで一連の導入が終わりました。
一度コミットまでの流れを確認してみましょう!

まずgit commitすると、対話形式になり、まずは prefix を選択します。

type選択の画像

次にコミットメッセージを入力します。

コミットメッセージの入力画像

次になにか詳細があれば書きます。ときになければ enter でスキップ可能です。

コミットの詳細を入力する画像

最後にできあがったコミットメッセージの確認があります。

コミットメッセージの最終確認の画像

問題がなければYか enter を押してください。
なにか問題があればnを押してやり直してください

これでチーム全体でコミットメッセージの体裁を揃えることができます。
ぜひ皆様のチームでも導入を検討してみてください!

おまけ

今回変更したファイルの差分を記載しておきます!

.husky/commit-msg
npx --no -- commitlint --edit "$1"
.husky/prepare-commit-msg
exec < /dev/tty && node_modules/.bin/cz --hook || true
.cz-config.cts
const config = {
  types: [
    {
      name: 'feat:     ✨ 新機能の追加',
      value: ':sparkles: feat',
    },
    {
      name: 'improve:  🎨 コードの構造/ロジックの改善',
      value: ':art: improve',
    },
    {
      name: 'update:   🩹 軽微な修正',
      value: ':adhesive_bandage: update',
    },
    {
      name: 'fix:      🐛 バグ修正',
      value: ':bug: fix',
    },
    {
      name: 'hotfix:   🚑 緊急バグ修正',
      value: ':ambulance: hotfix',
    },
    {
      name: 'refactor: ♻️ リファクタリング',
      value: ':recycle: refactor',
    },
    {
      name: 'delete:   🔥 ファイルやコードの削除',
      value: ':fire: delete',
    },
    {
      name: 'style:    💄 UIやスタイルファイルの追加/更新',
      value: ':lipstick: style',
    },
    {
      name: 'docs:     📝 ドキュメンテーションの追加/更新',
      value: ':memo: docs',
    },
    {
      name: 'move:     🚚 ファイルやディレクトリの移動',
      value: ':truck: move',
    },
    {
      name: 'test:     ✅ テストの追加/更新/合格',
      value: ':white_check_mark: test',
    },
    {
      name: 'chore:    🔧 設定ファイルの追加/更新',
      value: ':wrench: chore',
    },
    {
      name: 'package:  📦 パッケージの追加/更新',
      value: ':package: package',
    },
    {
      name: 'WIP:      🚧 作業途中',
      value: ':construction: WIP',
    },
  ],
  messages: {
    type: 'コミットの種類(型)を選択してください:\n',
    subject: 'コミットメッセージを入力してください:\n',
    body: '変更内容の詳細があれば書いてください:(enterでスキップ)\n',
    confirmCommit: '上記のコミットを続行してもよろしいですか?(Y/n)\n',
  },
  skipQuestions: ['scope', 'breaking', 'footer'],
  subjectLimit: 100,
};

module.exports = config;
commitlint.config.cts
const config = {
  extends: ['gitmoji'],
  rules: {
    'header-max-length': [2, 'always', 100],
    'type-enum': [
      2,
      'always',
      [
        'feat',
        'improve',
        'update',
        'fix',
        'hotfix',
        'refactor',
        'delete',
        'style',
        'docs',
        'move',
        'test',
        'chore',
        'package',
        'WIP',
      ],
    ],
  },
};

module.exports = config;
package.json
{
  "scripts": {
    "prepare": "husky"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.3.0",
    "commitizen": "^4.3.0",
    "commitlint-config-gitmoji": "^2.3.1",
    "cz-customizable": "^7.0.0",
    "husky": "^9.0.11"
  },
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-customizable"
    },
    "cz-customizable": {
      "config": ".cz-config.cts"
    }
  }
}
GitHubで編集を提案

Discussion