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

3 3

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.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay