Back to Home

How to generate meaningful commits. Apply Conventional Commits Standard

Habitual chaos in commit names. Familiar picture? Surely you know git-flow. This is a great set of conventions for organizing work with branches in Git. It is well documented and widely ...

How to generate meaningful commits. Apply Conventional Commits Standard



    Habitual chaos in commit names. Familiar picture?

    Surely you know git-flow . This is a great set of conventions for organizing work with branches in Git. It is well documented and widely distributed. Usually we are familiar with the correct branching and talk a lot about it, but, unfortunately, we pay too little attention to the name of commits, so often the messages in Git are written haphazardly.

    My name is Yerzhan Tashbenbetov, I work in one of the Yandex.Market teams. And today I will tell Habr's readers what tools we use to create meaningful commits in the team. I invite you to join the discussion of this topic.


    The lack of agreement on commits makes it difficult to work with history in Git. That was on our team. Before using common for all regulations and implementation of automation, typical commits looked as follows:

    SECRETMRKT-700: ΠΏΡ€ΠΎΠΏΠ°Π»ΠΈ Π»ΠΎΠ³ΠΎΡ‚ΠΈΠΏΡ‹ ΠΏΠ°Ρ€Ρ‚Π½Π΅Ρ€ΠΎΠ²
    ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΏΠ°Π΄Π°Π΅Ρ‚, ΠΏΠΎΠΏΡ€Π°Π²ΠΈΠ».
    SECRETMRKT-701, SECRETMRKT-702: ΠžΡ‚Ρ†Π΅Π½Ρ‚Ρ€ΠΈΡ€ΠΎΠ²Π°Π» ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Π½Π° всСх ...

    First, each developer wrote messages as he wanted: someone described the task, someone listed the changes made, someone used a random phrase generator. Everything was at odds with each other. Second, task numbers that were present in commits often shortened useful text. All this made it difficult to work effectively with history in Git.

    For this reason, we implemented the Conventional Commits standard in the team , began to generate commits in the commitizen console utility and check the result using commitlint . As a result, commits have changed and become like this:

    refactor(tutorial): ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ€Π°Π±ΠΎΡ‚Ρƒ эпиков Π² Ρ‚ΡƒΠ»Ρ‚ΠΈΠΏΠ°Ρ…
    feat(products): Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π±Π°Π½Π΅Ρ€ с Π½ΠΎΠ²ΠΎΠ³ΠΎΠ΄Π½ΠΈΠΌΠΈ скидками
    fix(products): ΠΈΡΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π² Π±Π°Π½Π΅Ρ€Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ Π΄Π°Ρ‚Ρ‹

    Reading the history and recognizing the changes made easier. We didn’t refuse to specify task numbers, everything was neatly transferred inside commits according to Conventional Commits convention .

    Next, I'll tell you how to achieve a similar order in Git.



    Best practices, recommendations, and common solutions for commiting


    If you try to understand what practices are used in the industry, you can find the following options:

    • Articles with general tips on writing commits. For the most part, they are quite logical and fairly well open the topic, but there is a sense of confusion and lack of a comprehensive solution to the issue.
    • Standards for writing commits. They are few. They are documents with a clear list of rules, often written specifically for a large library or framework. These standards are captivating system approach, popularity and support in the open-source community.

    We need more order in commits!

    Methodology Conventional Commits stands out from the other standards and deserves scrutiny for several reasons:

    1. It is well documented and worked out. In its specification answers to the most common questions.
    2. The creators of the convention were inspired by the requirements for writing commits, which are used in the popular and time-tested framework AngularJS .
    3. Several large and popular open-source libraries (such as yargs and lerna ) follow the rules of the convention .
    4. The advantages include preparation for the automatic formation of the Release Notes and Change Log.

    An example of a commit for this standard:

    fix(products): ΠΏΠΎΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π΄Π»ΠΈΠ½Ρƒ строки с Ρ†Π΅Π½ΠΎΠΉ
    Π§Π°ΡΡ‚ΡŒ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ² Π½Π΅ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½ΠΎ отобраТаСтся Π² мобильной вСрсии ΠΈΠ·-Π·Π° ошибок
    Π² ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ ΡƒΠ½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½Ρ‹Ρ… ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ².
    ΠœΠ•Π’Π ДАННЫЕ: SECRETMRKT-578, SECRETMRKT-602



    Basic abstracts of Conventional Commits


    • The developer should adhere to the following commit structure:
      <type> (<scope>): <subject>

      <body>

      <footer>
    • A commit should have a header, maybe a body and a footer.
    • The commit title should start with a type ( type ) indicating the specifics of the changes made to the code base and end with a description.
    • Along with the mandatory feat , fix (the use of which is strictly regulated), other types are allowed.
    • A commit may have a scope . It characterizes the code snippet affected by the changes. The area follows the type of commit. The standard does not regulate a clear list of areas. Examples of domains: eslint, git, analytics, etc.
    • The commit description should be immediately after the type / area.
    • The commit body can be used to detail changes. The body should be separated from the description by an empty line.
    • A footer should be used to specify external links, commit context, or other meta information. The footer should be separated from the body by a blank line.


    In addition to the rules listed in the convention, we use the following popular recommendations:


    • In the body of a commit we write what was changed and why .
    • We use the following types of commits:
      buildBuild a project or change external dependencies
      ciConfiguring CI and working with scripts
      docsDocumentation update
      featAdding new functionality
      fixError correction
      perfChanges to improve performance
      refactorEditing the code without correcting errors or adding new features
      revertRollback to previous commits
      styleCode style edits (tabs, indents, full stops, commas, etc.)
      testAdding tests
    • We write the description in the imperative mood , just like Git itself.
      Merge branch 'fix / SECRETMRKT-749-fix-typos-in-titles'
    • We do not download the description of the commit with punctuation marks.




    Standard commits Conventional Commits use kotribyutory lerna



    How easy is it to go to the correct name of commits?


    Need to add automation and convenience. To solve this issue, we need two tools: the commit generator and the commit linter, which is configured to be checked before pushing to the repository.



    Configure the commitizen utility


    This tool allows you to generate commits using the built-in wizard. In addition, commitizen is well supported by the community and, thanks to additional modules, is perfectly customizable.

    1. Install the commitizen utility globally (you may need administrator privileges).

      npm i -g commitizen
    2. Next install the adapter cz-customizable . It is needed to set up a template with questions used by the commitizen utility .

      npm i -D cz-customizable
    3. Create a file commitizen.js, it is needed to configure cz-customizable. Place the created file in the directory ./config/git. I recommend not to litter the project root with configuration files and try to group the files in the folder prepared for this. Content:

      Show commitizen.js
      "use strict";
      module.exports = {
        // Π”ΠΎΠ±Π°Π²ΠΈΠΌ описаниС Π½Π° русском языкС ΠΊΠΎ всСм Ρ‚ΠΈΠΏΠ°ΠΌ
        types: [
          {
            value: "build",
            name: "build:     Π‘Π±ΠΎΡ€ΠΊΠ° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° ΠΈΠ»ΠΈ измСнСния Π²Π½Π΅ΡˆΠ½ΠΈΡ… зависимостСй"
          },
          { value: "ci", name: "ci:        Настройка CI ΠΈ Ρ€Π°Π±ΠΎΡ‚Π° со скриптами" },
          { value: "docs", name: "docs:      ОбновлСниС Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ" },
          { value: "feat", name: "feat:      Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠ³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»Π°" },
          { value: "fix", name: "fix:       Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ошибок" },
          {
            value: "perf",
            name: "perf:      ИзмСнСния Π½Π°ΠΏΡ€Π°Π²Π»Π΅Π½Π½Ρ‹Π΅ Π½Π° ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ"
          },
          {
            value: "refactor",
            name:
              "refactor:  ΠŸΡ€Π°Π²ΠΊΠΈ ΠΊΠΎΠ΄Π° Π±Π΅Π· исправлСния ошибок ΠΈΠ»ΠΈ добавлСния Π½ΠΎΠ²Ρ‹Ρ… Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ"
          },
          { value: "revert", name: "revert:    ΠžΡ‚ΠΊΠ°Ρ‚ Π½Π° ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰ΠΈΠ΅ ΠΊΠΎΠΌΠΌΠΈΡ‚Ρ‹" },
          {
            value: "style",
            name:
              "style:     ΠŸΡ€Π°Π²ΠΊΠΈ ΠΏΠΎ кодстайлу (Ρ‚Π°Π±Ρ‹, отступы, Ρ‚ΠΎΡ‡ΠΊΠΈ, запятыС ΠΈ Ρ‚.Π΄.)"
          },
          { value: "test", name: "test:      Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ тСстов" }
        ],
        // ΠžΠ±Π»Π°ΡΡ‚ΡŒ. Она Ρ…Π°Ρ€Π°ΠΊΡ‚Π΅Ρ€ΠΈΠ·ΡƒΠ΅Ρ‚ Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ ΠΊΠΎΠ΄Π°, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ Π·Π°Ρ‚Ρ€ΠΎΠ½ΡƒΠ»ΠΈ измСнСния
        scopes: [
          { name: "components" },
          { name: "tutorial" },
          { name: "catalog" },
          { name: "product" }
        ],
        // Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π·Π°Π΄Π°Ρ‚ΡŒ спСц ΠžΠ‘Π›ΠΠ‘Π’Π¬ для ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½ΠΎΠ³ΠΎ Ρ‚ΠΈΠΏΠ° ΠΊΠΎΠΌΠΌΠΈΡ‚Π° (ΠΏΡ€ΠΈΠΌΠ΅Ρ€ для 'fix')/*
        scopeOverrides: {
          fix: [
            {name: 'style'},
            {name: 'e2eTest'},
            {name: 'unitTest'}
          ]
        },
        */// ПомСняСм Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½Ρ‹Π΅ вопросы
        messages: {
          type: "КакиС измСнСния Π²Ρ‹ вноситС?",
          scope: "\nΠ’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ ΠžΠ‘Π›ΠΠ‘Π’Π¬, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ Π²Ρ‹ ΠΈΠ·ΠΌΠ΅Π½ΠΈΠ»ΠΈ (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ):",
          // Бпросим Ссли allowCustomScopes Π² true
          customScope: "Π£ΠΊΠ°ΠΆΠΈΡ‚Π΅ свою ΠžΠ‘Π›ΠΠ‘Π’Π¬:",
          subject: "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ ΠšΠžΠ ΠžΠ’ΠšΠžΠ• описаниС Π² ΠŸΠžΠ’Π•Π›Π˜Π’Π•Π›Π¬ΠΠžΠœ Π½Π°ΠΊΠ»ΠΎΠ½Π΅Π½ΠΈΠΈ:\n",
          body:
            'ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ ΠŸΠžΠ”Π ΠžΠ‘ΠΠžΠ• описаниС (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ). Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ "|" для Π½ΠΎΠ²ΠΎΠΉ строки:\n',
          breaking: "Бписок BREAKING CHANGES (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ):\n",
          footer:
            "ΠœΠ΅ΡΡ‚ΠΎ для ΠΌΠ΅Ρ‚Π° Π΄Π°Π½Π½Ρ‹Ρ… (Ρ‚ΠΈΠΊΠ΅Ρ‚ΠΎΠ², ссылок ΠΈ ΠΎΡΡ‚Π°Π»ΡŒΠ½ΠΎΠ³ΠΎ). НапримСр: SECRETMRKT-700, SECRETMRKT-800:\n",
          confirmCommit: "Вас устраиваСт ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ²ΡˆΠΈΠΉΡΡ ΠΊΠΎΠΌΠΌΠΈΡ‚?"
        },
        // Π Π°Π·Ρ€Π΅ΡˆΠΈΠΌ ΡΠΎΠ±ΡΡ‚Π²Π΅Π½Π½ΡƒΡŽ ΠžΠ‘Π›ΠΠ‘Π’Π¬
        allowCustomScopes: true,
        // Π—Π°ΠΏΡ€Π΅Ρ‚ Π½Π° Breaking Changes
        allowBreakingChanges: false,
        // ΠŸΡ€Π΅Ρ„ΠΈΠΊΡ для Π½ΠΈΠΆΠ½Π΅Π³ΠΎ ΠΊΠΎΠ»ΠΎΠ½Ρ‚ΠΈΡ‚ΡƒΠ»Π°
        footerPrefix: "ΠœΠ•Π’Π ДАННЫЕ:",
        // limit subject length
        subjectLimit: 72
      };


    4. Add the package.json links to the cz-customizable and previously created configuration file:

      Show part package.json
      {
        "config": {
          "commitizen": {
            "path": "node_modules/cz-customizable"
          },
          "cz-customizable": {
            "config": "config/git/commitizen.js"
          }
        },
      }

    5. Let's check the resulting result. Type the following command in the terminal:

      git cz

    The commitizen wizard will first collect information about the type and area of ​​the commit, then sequentially ask for the text that will be in the description, in the body, in the footer, and after your consent will create a commit.

    Be sure to look at an example of the work of the configured commitizen utility and the cz-cusomizable adapter connected to it



    Configure the husky and commitlint utilities


    1. Install husky and commitlint in the project :

      npm i -D husky @commitlint/cli
    2. With the help of husky we will add check of kommit. To do this, immediately after the scripts in package.json, add the following hook and specify a link to the commitlint.js file in it:

      Show part package.json
      {
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1"
        },
        "husky": {
          "hooks": {
            "commit-msg": "commitlint -E HUSKY_GIT_PARAMS -g './config/git/commitlint.js'"
          }
        },
        "devDependencies": {
          "@commitlint/cli": "^7.2.1",
          "husky": "^1.1.3",
      }


    3. Create a file commitlint.js, necessary for the correct operation of the linter. Place the created file in the directory ./config/git. File contents:

      Show commitlint.js
      // Ѐайл создан на основС @commitlint/config-conventionalmodule.exports = {
        rules: {
          // Π’Π΅Π»ΠΎ ΠΊΠΎΠΌΠΌΠΈΡ‚Π° Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π½Π°Ρ‡ΠΈΠ½Π°Ρ‚ΡŒΡΡ с пустой строки"body-leading-blank": [2, "always"],
          // НиТний ΠΊΠΎΠ»ΠΎΠ½Ρ‚ΠΈΡ‚ΡƒΠ» ΠΊΠΎΠΌΠΌΠΈΡ‚Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ Π½Π°Ρ‡ΠΈΠ½Π°Ρ‚ΡŒΡΡ с пустой строки"footer-leading-blank": [2, "always"],
          // Максимальная длина заголовка 72 символа"header-max-length": [2, "always", 72],
          // ΠžΠ±Π»Π°ΡΡ‚ΡŒ всСгда Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² Π½ΠΈΠΆΠ½Π΅ΠΌ рСгистрС"scope-case": [2, "always", "lower-case"],
          // ОписаниС Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ пустым"subject-empty": [2, "never"],
          // ОписаниС Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π·Π°ΠΊΠ°Π½Ρ‡ΠΈΠ²Π°Ρ‚ΡŒΡΡ '.'"subject-full-stop": [2, "never", "."],
          // Π’ΠΈΠΏ всСгда Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² Π½ΠΈΠΆΠ½Π΅ΠΌ рСгистрС"type-case": [2, "always", "lower-case"],
          // Π’ΠΈΠΏ Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ пустым"type-empty": [2, "never"],
          // ΠŸΠ΅Ρ€Π΅Ρ‡ΠΈΡΠ»ΠΈΠΌ всС Π²ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹Π΅ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚Ρ‹ ΠΊΠΎΠΌΠΌΠΈΡ‚ΠΎΠ²"type-enum": [
            2,
            "always",
            [
              "build",
              "ci",
              "docs",
              "feat",
              "fix",
              "perf",
              "refactor",
              "revert",
              "style",
              "test"
            ]
          ]
        }
      };



    Everything. Now all commits will be checked before being sent to the repository :)

    Be sure to look at the example of the work of the configured commitlint utility.



    So choose commitizen or commitlint?


    Both! In conjunction, they bring excellent results: the first generates commits, the second checks them.


    Why do standards recommend the use of imperative?


    This is a very interesting question. A commit is a change in code; a message in a commit can be interpreted as a guide to changing this code. Make, change, add, update, fix - all these are specific instructions for the developer.

    By the way, the imperative is recommended in the Git versioning system itself :

    [[imperative-mood]]
    Describe your changes in imperative mood, e.g. "make xyzzy do frotz"
    instead of "[This patch] makes xyzzy do frotz" or "[I] changed xyzzy
    to do frotz", as if you are giving orders to the codebase to change
    its behavior.


    Why stick to any conventions? Is it worth spending time on it? What is the profit in this?


    Worth it. In general, I noticed that we have become more willing to detail the changes made to the code base. In the body of a commit, we describe in detail why we had to use certain solutions. Understanding the history has become objectively easier. Plus, our product is developing, and we expect replenishment in the team. I am sure that thanks to the implementation of the standard and automation, it will be easier for new users to integrate into the development process.

    Try and share the result.


    Useful links:


    Read Next