Commitizen でコミットメッセージを綺麗にする

@Panda_Program

コミットに関する意思決定を楽にするため Commitizen に従う

私はコミットメッセージを書く際、 Commitizen の prefix を使っています。 2019年に Commitizen を知るまでは、コードの変更内容から適切なメッセージを書くことに頭を悩ませていました。

コミットメッセージの作成は小さな意思決定であり、自分の頭で一から考える上に繰り返し発生するタスクです。人が一日に下せる質の良い意思決定の数には上限があり、意志力は体力と同じように自然と消費されるものです。

このため、意志力の無駄遣いを避けて質の良い意思決定をするために余力を残すのであれば、コミットのたびに繰り返し発生する「コードの変更内容を自分で分類し、コミットメッセージを考える」というタスクのコストは極力抑えたいものです。

良いコードを書きたいという思いがあるなら尚更です。コミットメッセージで意志力を消費した結果、目の前のコードがリファクタリングできると気付いても「今は疲れているし、後でやるからいいか」とやり過ごしてしまうかもしれません。そして、その「後で」の機会が来ることは恐らくないでしょう。

意志力を回復させるためには、散歩をしたり風呂に入ったり映画やドラマを見るなど、決断が必要ない別のことに意識の対象を向けることが有効です。しかし、それならそもそも意思決定の数を減らせば良いのです。

このため自分は Commitizen に従っています。Commitizen はコミットを分類する prefix を自動で付与してくれるので、コミットメッセージを書く際の意思決定を軽くしてくれます。

本記事は、Commitizen の prefix とコミットメッセージの例を紹介するものです。 ただし、Commtizen の具体的な使い方は紹介しません。それは自分が Commitizen の prefix をコミットメッセージに付与しているものの、Commitizen の CLI ツールは使っていないからです。その理由は後述します(このため、本記事は Commitizen というより Conventional Commits の記事だとした方がより適切かもしれません)。

Commitizen に見るコミットのその分類

まず、Commtizen によるコミットの分類を見てみましょう。Commtizen は、コミットを11種類に分類し、それぞれの内容を簡潔に言い表す prefix を用意しています。

feat:     A new feature
fix:      A bug fix
docs:     Documentation only changes
style:    Changes that do not affect the meaning of the code
          (white-space, formatting, missing semi-colons, etc)
refactor: A code change that neither fixes a bug nor adds a feature
perf:     A code change that improves performance
test:     Adding missing tests or correcting existing tests
build:    Changes that affect the build system or external dependencies
          (example scopes: gulp, broccoli, npm)
ci:       Changes to our CI configuration files and scripts 
          (example scopes: Travis, Circle, BrowserStack, SauceLabs)
chore:    Other changes that don't modify src or test files
revert:   Reverts a previous commit

feat は feature の略で機能、fix は bug fix の fix というように、 prefix だけみても意味がわかりやすいものになっています。また、コード変更の種類が抜け漏れ重複なく(MECE)カバーされているため、コミットの内容はこれらのうちどれかに必ず当てはまるのではないでしょうか。

なお、日本語版は以下の通りです(cz-conventional-changelog-ja より引用)。

機能: 新機能
バグ修正: バグ修正
ドキュメント: ドキュメントのみの変更
スタイル: フォーマットの変更
        (コードの動作に影響しないスペース、フォーマット、セミコロンなど)
リファクタ: リファクタリングのための変更
          (機能追加やバグ修正を含まない)
パフォーマンス改善: パフォーマンスの改善のための変更
テスト:不足テストの追加や既存テストの修正 
ビルド: ビルドシステムや外部依存に関する変更
      (スコープ例: gulp, broccoli, npm)
CI: CI用の設定やスクリプトに関する変更
   (スコープ例: Travis, Circle, BrowserStack, SauceLabs)
雑務:その他の変更
    (ソースやテストの変更を含まない) 
復帰: 以前のコミットに復帰

コミットメッセージを考える際、上記の prefix の中から最適な prefix を選択すればそれだけでもコード変更の種類を簡潔に表現できます。

また、prefix はいずれか一つしか選択できません。このため「機能追加とリファクタリング」といったよくあるコード変更のパターンであっても、コミットを2つに分けておこうという意識が働きます。結果的に一つのコミットに複数の意味がある変更を盛り込まなくなるため、細かいことを忘れてしまった将来の自分や他のレビュワーが読みやすいコミット単位を自然に作れます。

なお、Commitizen のレポジトリに掲載されている画像は 2015年に作成されたもので内容が古いです。 この画像を元に Commitizen の prefix が紹介されている記事を多く見かけるため注意が必要です。

commitizen が公式で紹介している prefix のリスト

特に chore に注意が必要です。 例えばビルド関係の変更をする際、画像内で chore のカバー範囲だと表現されている一方で、現行の prefix では chore ではなく build の prefix 設けられており、こちらを選択する方がより適切です。

本ブログのコミット履歴の例

次に、prefix をつけた実際のコミットを紹介します。以下の例はこのブログのコミット履歴です。 読み下せるように、古いコミットは上に、新しいコミットは下に並べ替えています。一連のコミット内容は RSS フィードの作成と、作成したフィード生成関数に対するテストの修正などです。

feat: createPostFeed を追加
fix: ビルドのたびに lastBuildDate が変わり flaky なテストになるため日付を固定
refactor: rss 作成のテストを snapshot にする
feat: /api/feed で RSS フィードを返す
feat: /feed で RSS を表示する
feat: feed の URL を Header で表示する
docs: update CHANGELOG.md

ここでは prefix に feat, fix, refactor, docs を使っています。ここには出てきませんが、他にも GitHub Actions でテストを実行する yml を追加するときは ci を、ESLint の fix や Prettier でコードをフォーマットするなら style を使っています。

正直どれの prefix を使うか迷う場面も出てきたりはします。 上記の例では、失敗するテストの修正コミットに test を使うか迷った結果 fix を使っていたり、その他にデザインを変更する際に Tailwind CSS のクラス名の追加や削除、変更は feat か fix か迷うことがあります。ただ、そのようなケースはごく稀であり、深く気にする必要はないと割り切っています。

fish の関数を使ってコミットメッセージの prefix 設定を楽にする

さて、記事の冒頭で自分は Commitizen の prefix を使っているものの、 Commitizen というツール自体は使っていないと書きました。その理由は Commitizen が対話型の CLI を備えており、自分はそれが煩雑に感じるからです。

対話型の CLI はプロジェクトのセットアップなど一回きりだと気にはならないものの、コミットは短いサイクルで何度も行うアクションであるため質問と回答のサイクルが煩わしく思えてしまいます。

対話型インターフェースについては、『UNIXという考え方』(Amazon) という書籍の中で「定理8: 過度の拘束的インターフェースは避ける」という項目で以下のように書かれています。これが煩雑さを感じる原因を簡潔に言い表してます。

一旦、そのアプリケーション(注: 対話型のアプリケーション)をコマンドインタープリタから起動すると、そのアプリケーションが終了するまでコマンドインタープリタとの対話ができなくなる。ユーザーはアプリケーションのユーザーインターフェースの内部に拘束され、拘束を解くための行動を起こさない限り、その拘束から逃れられない。

そこで、結局やりたいことはコミットメッセージに prefix を付与するだけであり、自分は fish を使っていることから、fish の関数を作ってコミットメッセージに prefix を付与するようにしました。

.config/fish/functions/commit.fish
function gcf
	git commit -m "feat: $argv"
end

function gcfi
	git commit -m "fix: $argv"
end

function gcd
	git commit -m "docs: $argv"
end

function gcdu
	git commit -m "docs: update changelog"
end

function gcs
	git commit -m "style: $argv"
end

function gcr
	git commit -m "refactor: $argv"
end

function gcp
	git commit -m "perf: $argv"
end

function gct
	git commit -m "test: $argv"
end

function gcb
	git commit -m "build: $argv"
end

function gcci
	git commit -m "ci: $argv"
end

function gcc
	git commit -m "chore: $argv"
end

function gcrv
	git commit -m "revert: $argv"
end

gc は git commit の略です。fish の関数といっても、各 prefix に対応するコマンドを用意し、引数をコミットメッセージにするだけです。例えば、機能の追加なら以下のように gcf とメッセージを入力するだけで済みます。

$ gcf 素晴らしい機能を追加 

→ No staged files match any configured task.
[main 712fc22] feat: 素晴らしい機能を追加
 3 files changed, 23 insertions(+), 1 deletion(-)

$ git log --oneline 
712fc22 (HEAD -> main) feat: 素晴らしい機能を追加

これで prefix をコミットメッセージに記入するコストを削減できます。 これはとても楽なのでおすすめのテクニックです。これは生産性向上というよりは効率アップのテクニックですね。

関連記事: 書籍「時短の科学」に生産性向上の手段を学ぶ

パッケージの追加と削除のコミット

なお、パッケージの追加と削除に関してはコマンドをそのままコミットメッセージにすると良いという話を聞きました。なるほど、それはその通りだと思ったのでそれ以降実践しています。

自分はパッケージ追加のコミットを簡単にするため、fish の alias にyarn addyarn add -Dを登録して、functions/commit.fishgit commit の gc を追加した gcya gcyad関数を作成しています。

.config/fish/config.fish
alias ya 'yarn add'
alias yad 'yarn add -D'
.config/fish/functions/commit.fish
function gcya
  git commit -m "yarn add $argv"
end

function gcyad
git commit -m "yarn add -D $argv"
end

これにより、例えばプロジェクトに react-use を追加する際、以下のようにできます。

$ ya react-use
$ git add .
$ gcya react-use

gcyaya に gc をつけるだけなので、矢印の↑キーを2回押して history を遡ってya react-useを表示し、この最初に ga を書けば済むのでとても簡単です。

綺麗なコミットメッセージを習慣にする

コミットメッセージを綺麗に書くというのは一つの習慣です。 prefix を選び、コードの変更内容に適したメッセージを考えて書く。これはエンジニアにとってルーティンと言ってもいいでしょう。

ルーティンは人が見ているか見ていないかに関係なく実行するものです。「コードレビューがある業務以外はコミットメッセージは適当に書く」という方もおられると思いますが、自分しか見ない Private レポジトリの個人プロジェクトであってもコミットメッセージを綺麗に書くという習慣を身につけることはおすすめです。

自分も最初は個人プロジェクトのコミットメッセージに気を使っていませんでした。ただ、個人プロジェクトだからといって自分の中のハードルを一旦下げてしまうと、業務でもその意識を引きずってしまいます。いざ綺麗に書こうと思っても「すぐに良いのは思いつかないから、まあこのくらいで大丈夫か」とメッセージが荒くなってしまうことに気づきました。

そのきっかけから、手を抜く方が非効率的だと悟りました。以来、誰に見せるわけでもないコミットメッセージあっても、メッセージを綺麗にしようという意識は捨てないようにしています。

最初は面倒と思うかもしれませんが、何度も繰り返していくうちに誰もが身につけている代表的な習慣「ハミガキ」と同じになります。歯を磨かず寝てしまったときに口の中に違和感を感じるのと同様に、綺麗にコミットメッセージを書く習慣が身につけば、あまり良いとは思えない自分のコミットメッセージに違和感を覚えるはずです。 そして、もし他の人のコミットメッセージにその違和感があるならそれもレビューのポイントになります。

まとめ

コミットメッセージに限らず、一般的にレビューというものは違和感をピックアップして言語化していく作業であると考えています。

有り体に言ってしまえば、綺麗を知っていないと綺麗と汚いの見分けがつきません。違和感とは、ある対象が自分にとっての理想や常識とかけ離れているときに生じるものです。特にリファクタリングの文脈で使われる Code Smell(コードの嫌な臭い)という言葉はこの違和感を表現した秀逸なメタファーです。

正直なところコミットメッセージをレビューされることはほとんどないため、いくら書いてもうまくなった気がしません。このため、自分の書くコミットメッセージは自己採点で60 ~ 80点ばかりですが、それでも綺麗に書く意識は持ち続けたいと思っています。

この記事がコミットメッセージを綺麗に書きたいという方の参考になれば幸いです。

最後に、私の同僚である 02 さんの「リーダブルコミットのすゝめ」 というスライドも本記事と同様のテーマを取り扱っており、とても参考になるのでおすすめです。

Happy Coding 🎉

パンダのイラスト
パンダ

記事が面白いと思ったらツイートやはてブをお願いします!皆さんの感想が執筆のモチベーションになります。最後まで読んでくれてありがとう。

  • Share on Hatena
  • Share on Twitter
  • Share on Line
  • Copy to clipboard