iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔥

Creating Interactive Commit Messages with Custom Commitizen Rules for Your Team

に公開

I'd like to introduce a great way to set up custom rules using commitizen, a tool that allows you to create commit messages interactively!

What is commitizen?

GitHub - commitizen/cz-cli: The commitizen command line utility. #BlackLivesMatter

commitizen is a tool that allows you to create commit messages in an interactive format.

By selecting from predefined labels like feat/fix/doc...etc, followed by the scope and message body,

feat(frontend): implement search feature!

you can create a message like this.

It's convenient for having people write commit messages according to conventions. While it's also valid to reject non-compliant messages using commitlint or similar tools, I personally think this is better because it automatically generates messages that follow the conventions, eliminating the need to fix them later.[1]

First, run cz directly

$ pnpm i -D commitizen cz-conventional-changelog

You can install it like this. commitizen is the CLI tool, and cz-conventional-changelog contains the configuration for the conventional-changelog convention.

After installation, you need to tell commitizen which convention to use, so add the configuration to package.json.

pakage.json
{
  // ...
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
}

Then, start the interaction with pnpm cz and answer the prompts to create a commit message.

$ pnpm cz
cz-cli@4.3.0, cz-custom-rule@1.0.0

? Select the type of change that you're committing: feat:     A new feature
? What is the scope of this change (e.g. component or file name): (press enter to skip) sample
? Write a short, imperative tense description of the change (max 86 chars):
 (15) add new feature
? Provide a longer description of the change: (press enter to skip)

? Are there any breaking changes? No
? Does this change affect any open issues? No

[main 90fb7ad] feat(sample): add new feature
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 tmp

It looks like this. It even creates the commit.

Creating custom rules

Now, while conventional-changelog is a good convention, I think it has a strong context of automatically generating CHANGELOGs for OSS or following SemVer operations, and there may be cases where it's not suitable for general web development conventions.

Therefore, we will create custom rules tailored to the project so that we can commit using commitizen.

In a monorepo setup, we'll add a dedicated package and write the configuration there.

# packages/cz-custom-rule
$ pnpm add cz-conventional-changelog
package.json
{
  "name": "cz-custom-rule",
  "description": "Custom commitizen rules for the project",
  "private": true,
  "version": "0.0.1",
  "main": "index.cjs",
  "dependencies": {
    "cz-conventional-changelog": "^3.3.0"
  }
}

We will write the settings in index.cjs. For this example, we'll prepare a simple convention that allows three types of labeling: Feature, Spec Change, and Refactoring.

index.cjs
// @ts-check
const engine = require('cz-conventional-changelog/engine')

/** @typedef {{ title: string, description: string, emoji?: string } } CommitType */

// ref: https://github.com/pvdlg/conventional-commit-types/blob/master/index.js#L93
/** @type {{ [K: string]: CommitType }} */
const commitTypes = {
  Feature: {
    description: 'Use this when adding a new feature!',
    title: 'Feature',
    emoji: '✨',
  },
  SpecChange: {
    description: 'Use this for commits that change the behavior of existing features!',
    title: 'Spec Change',
    emoji: '📚',
  },
  Refactoring: {
    description: 'Use this when improving code without changing specifications!',
    title: 'Refactoring',
    emoji: '📦',
  },
}

module.exports = engine({
  types: commitTypes,
  // config: configure these settings as needed
  defaultType: undefined,
  defaultScope: undefined,
  defaultSubject: undefined,
  defaultBody: undefined,
  defaultIssues: undefined,
  disableScopeLowerCase: undefined,
  disableSubjectLowerCase: undefined,
  maxHeaderWidth: 100,
  maxLineWidth: 100,
})

Now we have created the custom rules.

Next, add the dependency to the package.json in the workspace root and point the commitizen path setting to it.

pakcage.json
{
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-custom-rule"
    }
  },
  "devDependencies": {
    "cz-custom-rule": "workspace:*"
  }
}

Now you can create commit messages interactively with your own custom rules!

$ pnpm cz
? Select the type of change that you're committing: (Use arrow keys)
❯ Feature:     Use this when adding a new feature!
  SpecChange:  Use this for commits that change the behavior of existing features!
  Refactoring: Use this when improving code without changing specifications!

This explanation assumed a pnpm workspace monorepo configuration, but it's the same for other workspaces, and even if it's not a monorepo, it should work fine if you provide an index.cjs for configuration and reference it directly.

Executing via hooks

Now that we can automatically generate commit messages with pnpm cz, this method doesn't play very well with lint-staged setups using tools like husky (i.e., running a linter before committing).

Specifically, since the interactive prompt starts first and you think of a commit message before the linter runs, if the linter finds an issue, you'll have to re-enter the commit description you just typed in the interaction.

Therefore, if you are using this kind of setup, I recommend plugging it into prepare-commit-msg as a hook. Referencing the official documentation:

prepare-commit-msg
#!/usr/bin/env sh

exec < /dev/tty && node_modules/.bin/cz --hook || true

You should add this hook to prepare-commit-msg.

By doing this, when you run $ git commit, the git hooks will trigger, running the linter with lint-staged or similar tools, and then the interaction will start only after success. This flow provides a better experience because you don't have to re-enter your commit message.

Do not execute on amend or merge

I introduced the hook in the example above, but this hook also triggers for amend and merge commits.

This is a matter of preference, but personally, I feel that:

  • For an amend, I might be making minor corrections to an existing commit message or adding changes without changing the message, so I don't want a new commit message to be generated.
  • For merge commits, the default message is usually fine, so there's no need to create one.

So, I disable it by writing validation like this:

#!/usr/bin/env sh

# Do not trigger on amend or merge commits
first_line=$(head -n1 $1)
if [ "${first_line}" != "" ]; then
  exit 0
fi

exec < /dev/tty && node_modules/.bin/cz --hook || true

Summary

  • Using commitlint is good for organizing commit message conventions, but letting users write them interactively seems to be another great option.
      • Of course, you can use them together.
  • You can also set up custom rules specifically for your project.
  • If you are running linters via git hooks before committing, I recommend integrating via the prepare-commit-msg hook instead of calling cz directly.

That's all.
Thank you!

脚注
  1. Since it cannot handle cases where commit messages are created from a GUI, commitlint (or a combination of both) might be more suitable if you want to enforce conventions in those scenarios as well. ↩︎

Discussion