3

foreword

Some time ago, the front-end project of the department was migrated to the monorepo architecture, in which the author was responsible for things related to git workflow, including the engineering practice related to git hooks. Some commonly used related tools such as husky, lint-staged, commitizen, commit-lint, etc. are used, and this article records the entire practice process and the pits stepped on.

Note: The examples and commands below are all based on Mac OS. If you are a Windows user, don't worry. The article will also explain the general principles and operation logic, and the corresponding Windows commands can be inferred.

Git Hooks

What are Git Hooks

Most of the students should know about git hooks , but I still want to explain it in detail here.
The first is hook , which is actually a very common concept in the computer field, hook translated means hook or hook, and in the computer field, there are two types explain:

  1. Intercept the message and process the message in advance before the message reaches the target
  2. Monitor specific events, and when an event or action is triggered, the corresponding event will be triggered at the same time hook
    That is to say hook itself is also a program, but it will be triggered at a specific time.

Understand the concept of hook , then git hooks is not difficult to understand. Git hooks are the corresponding programs that are triggered when certain git commands are run.

In the front-end field, the concept of hooks is not uncommon, such as Vue declaration cycle hooks, React Hooks, webpack hooks, etc. After all, they are all methods or functions that are triggered at specific times.

What are the common Git Hooks

git hooks are divided into two categories

client hook

  • pre-commit hook, fired when running git commit command and before commit completes
  • commit-msg hook, which is triggered when the commit-msg is edited, and accepts a parameter, which is the path to the temporary file that stores the current commit-msg
  • pre-push hook, which is fired when the git push command is run and before the push command completes

server hook

  • pre-receive when the server receives the push and before the push process is complete
  • post-receive after the server receives the push and the push is complete

Only a part is listed here, more details about git hooks can be found in the official documentation

Commonly used git hooks examples can also be seen in the .git/hooks folder in the local git repository

file

As can be seen from the figure, the default git hooks are all shell scripts. Just remove the .sample extension of the sample file of git hooks, then the sample file will take effect.
Generally speaking, the application of git hooks in front-end projects is to run javaScript scripts, like this

 #!/bin/sh
node your/path/to/script/xxx.js

or so

 #!/usr/bin/env node
// javascript code ...

Disadvantages of native Git Hooks

A big problem with native git hooks is that the contents of the folder .git will not be tracked by Git. This means that there is no guarantee that all members of a repository will use the same git hooks unless all members of the repository manually sync the same git hooks, which is obviously not a good idea.

Husky

Use of Husky

  1. install husky
 pnpm install husky --save-dev
  1. husky initialization
 npx husky install
  1. Set the prepare of package.json. to ensure that husky can run normally
 npm set-script prepare "husky install"
  1. add git hooks
 npx husky add .husky/${hook_name} ${command}

what the husky install command does

In fact, the husky install command is the key to solving the git hooks problem

  • The first step: husky install will create .husky and .husky/_ folder in the project root directory (the folder can also be customized), and then in the .husky/_ folder Create the script file under husky.sh . The role of this file is to ensure that the script created by husky can run normally, and its actual application will be discussed later. More discussion of this script can be found here github issue .
  • The second step: husky install will run git config core.hooksPath ${path/to/hooks_dir} , this command is used to specify the path of git hooks, at this time observe the project .git/config file, there will be an additional configuration under [core]: hooksPath = xxx . When git hooks are triggered by some commands, Git will run core.hooksPath git hook in the specified folder.

For more husky configuration and command related documents, see here

It is worth noting that core.hooksPath is a new feature introduced by Git v2.9, and Husky also started to use core.hooksPath this feature in v6 version. In previous versions, Husky would directly overwrite all hooks in the .git/hooks folder to make the hooks configured by Husky take effect. In addition, after configuring core.hooksPath , Git will ignore the git hooks under the .git/hooks folder

what the husky add command does

When running the following command

 npx husky add .husky/pre-commit npx eslint

A pre-commit file will be added to the .husky directory with the contents of

 #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx eslint

At this point, a pre-commit git hook has been successfully added. This script will be executed when the git commit command is run.
In the second line of the script, the above-mentioned .husky.sh file is referenced, which means that when the git hook created by husky is triggered, the script will be executed.

To sort out, how does husky solve the problem of native git hooks. First of all, as mentioned above, the main problem of native git hooks is that git cannot track files under .git/hooks, but this problem has been solved by git core.hooksPath, Then the new problem is that developers still need to manually set git core.hooksPath. husky helped us set git core.hooksPath in the install command, and then added "prepare": "husky install" to the scripts of package.json, so that every time a dependency is installed, husky install will be executed, so This ensures that the set git hooks can be triggered.

Commonly used git related tool library

lint-staged

In the pre-commit hook, generally speaking, the files currently to be committed are checked, formatted, etc. Therefore, in the script, we need to know which files are currently in the Git temporary storage area, and Git itself does not report to The pre-commit script passes relevant parameters, lint-staged this package solves this problem for us. The first sentence of the lint-staged documentation says:

Run linters against staged git files and don't let 💩 slip into your code base!

Use of lint-staged

  1. Install lint-staged

     pnpm install lint-staged --save-dev
  2. Configure lint-staged
    In general, it is recommended to use lint-staged with Husky , of course, this is not necessary, just make sure that lint-staged will be run in the pre-commit hook That's it. When used with Husky, you can run the following command in the pre-commit hook lint-staged

     npx husky add .husky/pre-commit "npx lint-staged"

Regarding the configuration of lint-staged, it is similar in form to the configuration of common toolkits. You can add a lint-staged item in package.json, or you can add a .lintstagedrc.json in the root directory .lintstagedrc.json , etc. The following is an example of configuration in package.json:
The key in the configuration item is the glob pattern matching statement, and the value is the command to run (more than one can be configured). For example, if you want to run eslint check and ts type for all .ts and .tsx files in the src folder in the staging area Check, then the configuration is as follows:
For detailed configuration documentation see here
If the git hooks script fails (the status code returned at the end of the process is not 0), subsequent operations will be terminated. For example, in the above example, if the eslint check reports an error, the commit will be terminated directly, and the git commit command will fail.

How does lint-staged know which files are in the current staging area

In fact, there is no black magic inside lint-staged, it runs the git diff --staged --diff-filter=ACMR --name-only -z command, which returns the file information in the staging area, similar to the following code:

 const { execSync } = require('child_process');
const lines = execSync('git diff --staged --diff-filter=ACMR --name-only -z')
    .toString()

const stagedFiles = lines
    .replace(/\u0000$/, '')
    .split('\u0000')

commitizen

In the process of using Git, it is inevitable to fill in the commit message, which is actually quite a headache. If there is no good commit message specification, then you will only be confused* when viewing the historical commits.
And commitizen can assist developers to fill in the commit information

Use of commitizen

  1. install commitizen

     pnpm install commitizen -D
  2. Initialize commitizen

     npx commitizen init cz-conventional-changelog --save-dev --save-exact --pnpm

what commitizen init does

  1. install cz-conventional-changelog adapter npm module
  2. Save it to devDependencies in package.json
  3. The config.commitizen configuration is added to package.json as follows:

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

Commitizen itself only provides a command-line interaction framework and the execution of some git commands. The actual rules need to be defined by adapters, and commitizen has corresponding adapter interfaces. And cz-conventional-changelog is a commitizen adapter.

At this point, run npx cz command and the following command line interaction page will appear:

file

The commit message template generated by this adapter is as follows

 <type>(<scope>): <subject>
<空行>
<body>
<空行>
<footer>

This is also the most common commit convention. Of course, other adapters can be installed, or custom adapters can be customized to customize the commit message template you want.
When running npx cz , commitizen will run the git commit -m "XXX" command internally after getting the final commit message through the adapter template and user input. So far, a git commit has been completed. operate

For more details about commitizen, see github and cz-git

Custom commitizen adapter

If you want to customize the adapter, you can choose to use the cz-customizable toolkit.
Without this toolkit, if you want to customize a commitizen adapter, then you also need to master the API of inquirer , commitizen will only pass an inquirer object for the adapter, and the rules of the adapter need to pass this inquirer object to create rules, which is not very easy to use, but cz-customizable allows me to focus on the rules and not consider the inquirer's API.

Use of cz-customizable

  1. commitizen configuration

     "config": {
      "commitizen": {
     "path": "./node_modules/cz-customizable"
      }
    }
  2. cz-customizable configuration, add a .cz-config.js file in the root directory, the configuration example is as follows

     module.exports = {
      types: [
     { value: 'feat', name: 'feat: A new feature' },
     { value: 'fix', name: 'fix: A bug fix' },
      ],
      scopes: [{ name: 'accounts' }, { name: 'admin' }],
      allowTicketNumber: false,
      messages: {
     type: "Select the type of change that you're committing:",
     scope: '\nDenote the SCOPE of this change (optional):',
     customScope: 'Denote the SCOPE of this change:',
      },
      subjectLimit: 100,
    };

Here is more detailed example and configuration on cz-customizable

Run commitizen with git cz command

If the global PATH configuration is correct, you can also directly use the git cz command to run commitizen. If you installed commitizen in your project, you will see two scripts in the node_modules/.bin directory of your project: cz and git-cz , as follows As shown in the figure:

file

The content of these two scripts is exactly the same. The official documentation recommends adding the following content to the scripts of package.json:

 commit: "cz"

This will use npm run commit to run commitizen. But if you want to use the git cz command to run commitizen, then you need git-cz the directory where the file is located is under the global PATH, run the following command to view the PATH

 echo $PATH

PATHs are separated by colons, check if there is a cz script in all PATHs that matches your cz script, generally there are, if not, then you can add it in your ~/.zshrc or ~/.bash_profile Add one:

 PATH=$PATH:./node_modules/.bin

Then reload the configuration file and run source ~/.zshrc or source ~/.bash_profile , so that you can use the git cz command directly in the root directory of your project.
If you use npm to install commitizen globally, then you probably don't need to worry about the PATH problem, because the bin folder under the npm dependency installation path will be automatically added to the PATH by node or NVM.

Back to the git-cz script in the node_modules/.bin folder just mentioned, in fact, it is the key to the git cz command can run. I don't know if you are wondering why you can use Git to run an npm library, in fact, this is a git custom command. There are several requirements for adding a git custom command:

  1. is an executable
  2. The filename must be git-XXX
  3. The path to this file must be in your PATH

Therefore, in the previous article, it was mentioned that if you want to run the git cz command, you need to configure the global PATH correctly.

You can also try adding other custom git commands based on the above requirements. It should be noted that you need to check whether the shell script you added has executable permission. If there is no executable permission, the following error will be reported _git: 'your command' is not a git command_ , you can run _chmod a+x <path to your file>_ to modify the file permissions to make it runnable.

commitlint

Commitlint is a tool library that can verify whether the commit message is standardized by configuring some rules.
So why do we need commitlint when we already have commitizen? As mentioned above, the role of commitizen is to assist developers to fill in the commit message. Although the corresponding commit information specification and template can be formulated by selecting different adapters or custom adapters, it lacks the verification function of the commit message. It is still possible to use the native git commit command to commit inadvertently, and commitlint verifies the commit message in the commit-msg git hook, which just solves this problem.

Use of commitlint

  1. Install
 pnpm install --save-dev @commitlint/config-conventional @commitlint/cli
  1. Add commit-msg hook with husky
 npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1
  1. The commitlint configuration adds a commitlint.config.js file to the project root directory. The content of the file is as follows:
 module.exports = {
    extends: ['@commitlint/config-conventional'],
    // 自定义部分规则
    rules: {
        'scope-case': [0, 'always', 'camel-case'],
        'scope-empty': [2, 'never'],
        'scope-enum': [2, 'always', [...]],
    },
};

Commitlint, like commitizen, is divided into two parts, one part is the main program to be executed, and the other part is the rules or adapters. @commitlint/cli is the main program to be executed, and @commitlint/config-conventional is the rule. Commitlint and commitizen use strategy mode and adapter mode respectively, so they have very high availability and good scalability.
In the configuration file of commitlint, you can first refer to a commitlint rule package, and then define the rules you want in the definition part, just like the configuration of eslint.
It should be noted that when adding commitlint to commit-msg hooks, the --edit $1 parameter in the shell command that executes commitlint is required. The meaning of this parameter is: the temporary file path for storing the commit message $1 , and $1 is the parameter passed by Git to the commit-msg hook, its value is the path of the temporary storage file of the commit message, by default it is .git/COMMIT_EDITMSG . If this parameter is not passed, then commitlint will not be able to know what the current commit message is.

For more details about commitlint, see here

Commitlint is shared with commitizen's configuration

As mentioned above, commitlint solves the problem that commitizen does not verify the commit message, but after using commitlint, a new problem arises. If the ruleset of commitlint is inconsistent with the rules in the adapter of commitizen, it may lead to the use of commitizen. The generated commit message does not pass the check by commitlint and git commit fails.
There are two ways to solve this problem:

  1. Translate commitizen's adapter rules into commitlint rule set. The existing corresponding toolkit is commitlint-config-cz . This package requires the commitizen adapter you use to be cz-customizable , which is a custom adapter.
  2. Convert commitlint rule set into commitizen adapter, the corresponding toolkit is @commitlint/cz-commitlint

Here is an example of the second option @commitlint/cz-commitlint :

  1. install @commitlint/cz-commitlint

     pnpm install --save-dev @commitlint/cz-commitlint
  2. Modify the commitizen configuration in packages.json

     "config": {
     "commitizen": {
       "path": "./node_modules/@commitlint/cz-commitlint"
     }
      }

conventional-changelog ecology

Open the github repository of commitlint , you will find that it is under the organization conventional-changelog, and the README.md file of the repository commitizen/cz-cli also mentions the conventional-changelog ecology:

For this example, we'll be setting up our repo to use AngularJS's commit message convention, also known as conventional-changelog.

It's no wonder why commitlint also provides a @commitlint/cz-commitlint package to work with commitizen.

So what else does the conventional-changelog ecosystem contain?

Plugins that support Conventional Changelog

Important modules in the Conventional Changelog ecosystem

  • conventional-changelog-cli - the full-featured command line interface ---_feature-rich command line interface_
  • standard-changelog - command line interface for the angular commit format. --_angular-style command line interface_
  • conventional-github-releaser - Make a new GitHub release from git metadata --- Generate a new GitHub release from git metadata_
  • conventional-recommended-bump - Get a recommended version bump based on conventional --commits Generate recommended version changes based on conventional-style commits
  • conventional-commits-detector - Detect what commit message convention your repository is using -_Check the commit message convention used by the repository_
  • commitizen - Simple commit conventions for internet citizens.
  • commitlint - Lint commit messages

Since this article mainly talks about git hooks, I will not talk about the conventional-changelog ecology here. If you are interested, you can take a look at their github repository and this article.


袋鼠云数栈UED
286 声望38 粉丝

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。


« 上一篇
CSS SandBox