DEV Community

Cover image for Commitlint: custom commit message with emojis
Dragoș Străinu
Dragoș Străinu

Posted on • Edited on • Originally published at strdr4605.com

Commitlint: custom commit message with emojis

Commitlint@16.0.2 is the next step on enforcing rules in your JS project after eslint.

Installation and configuration is very simple:

echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
Enter fullscreen mode Exit fullscreen mode

Commitlint suggests Conventional commits which have this format:

type(scope?): subject
Enter fullscreen mode Exit fullscreen mode

But what if I want to use a custom format specific to my team?! Let's say I want to use emoji as a type, an optional ticket, and then the subject, like:

type [ticket]? subject
Enter fullscreen mode Exit fullscreen mode

To change the header format I need to change headerPattern from parserOpts config:

First I need to find a RegExp that will match "✅ [T-4605] Add tests", also we need to add at least one rule so let's add type-enum that is provided by commitlint to set allowed emojis

// commitlint.config.js
// emojis like "✅ ", "😂 ", ...
const matchAnyEmojiWithSpaceAfter =
  /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])\s/;
const matchOptionalTicketNumberWithSpaceAfter = /(?:\[(T-\d+)\]\s)?/; // "[T-4605] ", "[T-1]"
const subjectThatDontStartWithBracket = /([^\[].+)/; // "Add tests" but don't allow "[ Add tests"

module.exports = {
  parserPreset: {
    parserOpts: {
      headerPattern: new RegExp(
        "^" +
          matchAnyEmojiWithSpaceAfter.source +
          matchOptionalTicketNumberWithSpaceAfter.source +
          subjectThatDontStartWithBracket.source +
          "$"
      ),
      headerCorrespondence: ["type", "ticket", "subject"],
    },
  },
  rules: {
    "type-enum": [2, "always", ["⭐️", "🐞", "", "🚧", "♻️", "📝"]],
  },
};
Enter fullscreen mode Exit fullscreen mode

Testing locally:

> echo "✅ [T-4605] Add tests" | commitlint # passes
> echo "✅ Add tests" | commitlint # passes
> echo "Something else" | commitlint # should fail but still passes 🤔
Enter fullscreen mode Exit fullscreen mode

The problem is that there is no rule to make sure that the header matched our RegExp. I can add 2 other rules from commitlint:

"type-empty": [2, "never"],
"subject-empty": [2, "never"],
Enter fullscreen mode Exit fullscreen mode

but what if I have other variables names, emoji instead of type, desc instead of subject?

I need to create a custom rule using Commitlint plugins.
Let's name the rule header-match-team-pattern and also use emoji instead of type. In the rule, we check if all variables are null and return a message

...
      headerCorrespondence: ["emoji", "ticket", "subject"],
    },
  },
  plugins: [
    {
      rules: {
        "header-match-team-pattern": (parsed) => {
          const { emoji, ticket, subject } = parsed;

          if (emoji === null && ticket === null && subject === null) {
            return [
              false,
              "header must be in format '✅ [T-4605] Add tests' or '✅ Add tests'",
            ];
          }
          return [true, ""];
        },
      },
    },
  ],
  rules: {
    "header-match-team-pattern": [2, "always"],
...
Enter fullscreen mode Exit fullscreen mode

You can console.log({ parsed }), for debugging

Now let's create a better type-enum rule, explained-emoji-enum:

...
        "explained-emoji-enum": (parsed, _when, emojisObject) => {
          const { emoji } = parsed;
          if (emoji && !Object.keys(emojisObject).includes(emoji)) {
            return [
              false,
              `emoji must be one of:
${Object.keys(emojisObject)
                .map((emojiType) => `${emojiType} - ${emojisObject[emojiType]}`)
                .join("\n")}`,
            ];
          }
          return [true, ""];
        },
      },
    },
  ],
  rules: {
    ...
    "explained-emoji-enum": [
      2,
      "always",
      {
        "⭐️": "New feature",
        "🐞": "Bugfix",
        "": "Add, update tests",
        "🚧": "Work in progress",
        "♻️": "Refactor",
        "📝": "Documentation update",
      },
    ],
  },
...
Enter fullscreen mode Exit fullscreen mode

And when the engineer will set a wrong emoji it will have a error like:

> echo "😂 Add tests" | commitlint                                                                                                                                 
⧗   input: 😂 Add tests
✖   emoji must be one of:
⭐️ - New feature
🐞 - Bugfix
✅ - Add, update tests
🚧 - Work in progress
♻️ - Refactor
📝 - Documentation update [explained-emoji-enum]

✖   found 1 problems, 0 warnings
Enter fullscreen mode Exit fullscreen mode

Final result

// commitlint.config.js
// emojis like "✅ ", "😂 ", ...
const matchAnyEmojiWithSpaceAfter =
  /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])\s/;
const matchOptionalTicketNumberWithSpaceAfter = /(?:\[(T-\d+)\]\s)?/; // "[T-4605] ", "[T-1]"
const subjectThatDontStartWithBracket = /([^\[].+)/; // "Add tests" but don't allow "[ Add tests"

module.exports = {
  parserPreset: {
    parserOpts: {
      headerPattern: new RegExp(
        "^" +
          matchAnyEmojiWithSpaceAfter.source +
          matchOptionalTicketNumberWithSpaceAfter.source +
          subjectThatDontStartWithBracket.source +
          "$"
      ),
      headerCorrespondence: ["emoji", "ticket", "subject"],
    },
  },
  plugins: [
    {
      rules: {
        "header-match-team-pattern": (parsed) => {
          const { emoji, ticket, subject } = parsed;
          if (emoji === null && ticket === null && subject === null) {
            return [
              false,
              "header must be in format '✅ [T-4605] Add tests' or '✅ Add tests'",
            ];
          }
          return [true, ""];
        },
        "explained-emoji-enum": (parsed, _when, emojisObject) => {
          const { emoji } = parsed;
          if (emoji && !Object.keys(emojisObject).includes(emoji)) {
            return [
              false,
              `emoji must be one of:
${Object.keys(emojisObject)
                .map((emojiType) => `${emojiType} - ${emojisObject[emojiType]}`)
                .join("\n")}`,
            ];
          }
          return [true, ""];
        },
      },
    },
  ],
  rules: {
    "header-match-team-pattern": [2, "always"],
    "explained-emoji-enum": [
      2,
      "always",
      {
        "⭐️": "New feature",
        "🐞": "Bug fix",
        "": "Add, update tests",
        "🚧": "Work in progress",
        "♻️": "Refactor",
        "📝": "Documentation update",
      },
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

Next steps

I can add some rules from Commitlint or create other custom ones.

Put the config in a new package.

Add husky and use it in every company repo on the pre-commit hook.

Top comments (0)