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:
- Intercept the message and process the message in advance before the message reaches the target
- 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 sayhook
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 runninggit 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 thegit 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
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
- install husky
pnpm install husky --save-dev
- husky initialization
npx husky install
- Set the prepare of package.json. to ensure that husky can run normally
npm set-script prepare "husky install"
- 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 underhusky.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 runcore.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
Install lint-staged
pnpm install lint-staged --save-dev
Configure lint-staged
In general, it is recommended to uselint-staged
withHusky
, of course, this is not necessary, just make sure thatlint-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 hooklint-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
install commitizen
pnpm install commitizen -D
Initialize commitizen
npx commitizen init cz-conventional-changelog --save-dev --save-exact --pnpm
what commitizen init does
- install
cz-conventional-changelog
adapter npm module - Save it to devDependencies in package.json
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:
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
commitizen configuration
"config": { "commitizen": { "path": "./node_modules/cz-customizable" } }
cz-customizable configuration, add a
.cz-config.js
file in the root directory, the configuration example is as followsmodule.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:
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:
- is an executable
- The filename must be
git-XXX
- 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
- Install
pnpm install --save-dev @commitlint/config-conventional @commitlint/cli
- Add commit-msg hook with husky
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $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:
- 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. - 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
:
install @commitlint/cz-commitlint
pnpm install --save-dev @commitlint/cz-commitlint
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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。