现代本地git钩子变得很容易: husky!定制Git拦截规范
现代本地git钩子变得很容易: husky
由于一些同学没有协同工作得相关经验,或者说说缺乏相关得git、代码规范知识。写的代码、
Git Commit
、Git Branches
、Git Tags
等比较随意,造成得问题呢就是越迭代,越不好维护等。
那么有没有手段能控制呢?有太多了,eslint
、stylelint
、prettier
、lint-staged
等等,但是还需要控制把一些不规范得给拦截. lint-staged
插件中有一句介绍得话:对阶段性git文件运行检查器,不要让💩溜进代码库!(Run linters against staged git files and don't let 💩 slip into your code base!)
那么咱们今天就是利用githooks得一些钩子来把控这些代码等相关得质量。那么你不使用Husky
可以吗?也可以。由于git默认是没有这些钩子得如下可以测试
git init
cd .git/hooks/
ls
正常会打印出来如下这些钩子
applypatch-msg.sample* pre-push.sample*
commit-msg.sample* pre-rebase.sample*
fsmonitor-watchman.sample* pre-receive.sample*
post-update.sample* prepare-commit-msg.sample*
pre-applypatch.sample* push-to-checkout.sample*
pre-commit.sample* update.sample*
pre-merge-commit.sample*
安装并执行husky
官网命令v3
如下:
yarn add husky@3.0.0
看到多增加了好多钩子
applypatch-msg* pre-commit*
applypatch-msg.sample* pre-commit.sample*
commit-msg* pre-merge-commit.sample*
commit-msg.sample* pre-push*
fsmonitor-watchman.sample* pre-push.sample*
post-applypatch* pre-rebase*
post-checkout* pre-rebase.sample*
post-commit* pre-receive*
post-merge* pre-receive.sample*
post-receive* prepare-commit-msg*
post-rewrite* prepare-commit-msg.sample*
post-update* push-to-checkout*
post-update.sample* push-to-checkout.sample*
pre-applypatch* sendemail-validate*
pre-applypatch.sample* update*
pre-auto-gc* update.sample*
咱们查看下pre-commit
钩子如下:
cat pre-commit
#!/bin/sh
# husky
# Hook created by Husky
# Version: 3.0.0
# At: 2023/7/26 11:45:02
# See: https://github.com/typicode/husky#readme
# From
# Directory: /githooks
# Homepage: https://github.com/typicode/husky#readme
scriptPath="node_modules/husky/run.js"
hookName=`basename "$0"`
gitParams="$*"
debug() {
if [ "${HUSKY_DEBUG}" = "true" ] || [ "${HUSKY_DEBUG}" = "1" ]; then
echo "husky:debug $1"
fi
}
debug "$hookName hook started"
if [ "${HUSKY_SKIP_HOOKS}" = "true" ] || [ "${HUSKY_SKIP_HOOKS}" = "1" ]; then
debug "HUSKY_SKIP_HOOKS is set to ${HUSKY_SKIP_HOOKS}, skipping hook"
exit 0
fi
if [ "${HUSKY_USE_YARN}" = "true" ] || [ "${HUSKY_USE_YARN}" = "1" ]; then
debug "calling husky through Yarn"
yarn husky-run $hookName "$gitParams"
else
if [ -f "$scriptPath" ]; then
# if [ -t 1 ]; then
# exec < /dev/tty
# fi
if [ -f ~/.huskyrc ]; then
debug "source ~/.huskyrc"
. ~/.huskyrc
fi
node "$scriptPath" $hookName "$gitParams"
else
echo "Can't find Husky, skipping $hookName hook"
echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook"
fi
fi
node "$scriptPath" $hookName "$gitParams"
主要是这么一行可以看出来是使用node
运行命令如下:
$ cat node_modules/husky/run.js
// run.js
/* eslint-disable @typescript-eslint/no-var-requires */
const pleaseUpgradeNode = require('please-upgrade-node')
const pkg = require('./package.json')
// Node version isn't supported, skip
pleaseUpgradeNode(pkg, {
message(requiredVersion) {
return 'Husky requires Node ' + requiredVersion + ", can't run Git hook."
}
})
// Node version is supported, continue
require('./lib/runner/bin')
反正知道就是运行咱们下面配置的指令就好了,如果想知道他说怎么运行的可以再往源码看下
那么这些钩子就是咱们可以使用得了,那么husky
可以看出来干了什么嘛,他就是让你使用本地git钩子变得很容易。使用如下package.js添加如下(上面不带.sample
得都是咱们可以直接用的钩子自己按需要选择哈):
{
"name": "githooks",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"husky": "3.0.0"
},
"husky": { // 上面不带.sample得都是咱们可以直接用的钩子自己按需要选择哈
"hooks": {
"pre-commit": "指令",
"commit-msg": "指令",
"pre-push": "指令"
}
}
}
我经常用的就就上面几种钩子去拦截:
pre-commit
: 提交commit
信息前可以去校验提交得相关代码是否符合规范。commit-msg
: 提交得commit
信息可以做一些对commit
做一部分规范校验。pre-push
:git push
前得相关校验可以校验你提交得分支、tags是否符合规范。
相关规范拦截
如何定制化拦截规范呢?接下面一步步带你定制相关规范
- 代码规范拦截
- commit信息规范拦截
- 分支、tags规范拦截
代码规范定义
代码规范拦截使用lint-staged
如下:
yarn add lint-staged -D
package.json现在是如下:
{
"lint-staged": {
"*.{js,jsx,vue}": [
"eslint --fix --ext .js,.jsx,.vue,.ts,.tsx"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged", // commit 之前的钩子
"commit-msg": "XXXXXXX", // commit 时的钩子
"pre-push": "XXXXXXX" // push 之前的钩子
}
}
}
如果发现你得代码有eslint
未修复的就会报错误如下图**(必须修复方可提交,如果遇到无法修复切必须要提交的可以再后缀加个--no-ignore 或者简写 -n git commit -m 'feat: XXXXX' -n
,还可以配置.eslintignore
忽略掉)**:
定义commit提交信息规范
commit
信息规范咱们准寻如下也是大家通用的commit规范
如下:
类型 | 描述 |
---|---|
revert | 回复 |
feat | 提交新特性代码 |
fix | 修复bug |
docs | 编写文档 |
style | 修改样式 |
refactor | 代码重构 |
perf | 性能优化 |
test | 测试用例 |
workflow | 工作流 |
ci | 持续集成 |
chore | 构建过程的变化 |
build | 构建打包 |
例如:
git commit -m 'feat: 我完成了某个新模块的XXX开发'
git commit -m 'fix: 我修复了某个问题'
git commit -m 'style: 我修改了某块css'
git commit -m 'refactor: XX模块部分代码重构'
git commit -m 'test: XXX模块单元测试'
- ...
规范咱们约束好了那么如何制定拦截呢?package.json更改如下:
{
"lint-staged": {
"*.{js,jsx,vue}": [
"eslint --fix --ext .js,.jsx,.vue,.ts,.tsx"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged", // commit 之前的钩子
"commit-msg": "node scripts/verify-commit-msg.js", // commit 时的钩子
"pre-push": "XXXXXXX" // push 之前的钩子
}
}
}
使用node
执行了verify-commit-msg.js
脚本内容如下(Vue官方verifyCommit.js链接):
const msgPath = process.env.HUSKY_GIT_PARAMS; //使用环境变量获取到commit message
const msg = require('fs').readFileSync(msgPath, 'utf-8').trim();
const commitRE = /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build)(\(.+\))?: .{1,50}/;
if (!commitRE.test(msg)) {
// 下面是一些错误日志提醒
console.log('提交信息不符合规范格式!\n');
console.log('格式为:[类型]: [描述]\n');
console.warning('\n feat: 完成详情页面布局\n fix: 修复刷新时间不准确的问题\n docs: 某某文档编写\n');
process.exit(1); // 如果不规范退出运行进程
}
分支、tags规范拦截
下面也是使用拦截
commit
信息差不多的手段去拦截分支、tags相关规范的具体如下:
{
"lint-staged": {
"*.{js,jsx,vue}": [
"eslint --fix --ext .js,.jsx,.vue,.ts,.tsx"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged", // commit 之前的钩子
"commit-msg": "node scripts/verify-commit-msg.js", // commit 时的钩子
"pre-push": "node scripts/verify-git-branch/index.js" // push 之前的钩子
}
}
}
使用node
执行verify-git-branch
相关脚本关键代码如下:
const params = process.env.HUSKY_GIT_STDIN; // 使用环境变量能获取到一些分子、tags信息如下面注释:
// //params: refs/heads/test1 7fe5f9d5ae41 refs/heads/test1 00000000000000000000000
那么咱们做的就是截取到需要的信息如下:
const params = process.env.HUSKY_GIT_STDIN; // 使用环境变量能获取到一些分子、tags信息如下面注释:
// //params: refs/heads/test1 7fe5f9d5ae41 refs/heads/test1 00000000000000000000000
const list = params.trim().split('\n'); // 去掉前后的空格截取
const branchList = list.reduce((result, item) => {
result.push(item.split(' ')[2]); // 继续截取
return result;
}, []);
console.log(branchList, 'branchList'); // 会得到['refs/heads/test1']
拿到了branchList
后就需要会得到['refs/heads/test1']
这个一个数组,其中heads
代表是分支,test1
是分支名。如果是tags的话你能得到['refs/tags/v1.0.0']
的数组,其中tags
代表是标签,test1
是标签名。
其实看到这里应该很能明白接下来要怎么做了吧如下:
let errorMessage = []; //错误收集
const GIT_TYPE = {
BRANCH: 'heads',
TAGS: 'tags',
};
const patterns = {
branchPattern: '^(master|dev){1}$|^(feature|hotfix|release|bugfix)\\/.+$', // 这个是以mester或者dev 再或者feature|hotfix|release|bugfix)开头
// 比如:feature/XXX模块
// 比如:bugfix/XXX
// 比如:hotfix/XXX
// 比如:release/XXX
// 经常用的就这些了
/***
* 1. **Feature Branch(功能分支)**:Feature分支用于开发新功能或增加新的功能模块。这些分支通常从主分支(如master或develop)上创建,并在特性完成后合并回主分支。
2. **Bugfix Branch(修复分支)**:Bugfix分支用于修复已知的问题或缺陷。当在主分支上发现错误时,可以从主分支上创建一个修复分支,并在修复完成后将其合并回主分支。
3. **Hotfix Branch(热修复分支)**:Hotfix分支类似于Bugfix分支,但它们用于紧急修复生产环境中的严重错误或问题。热修复分支通常从与生产环境匹配的标记或版本上创建,并在修复完成后合并到主分支和其他相关分支。
4. **Release Branch(发布分支)**:Release分支用于准备发布版本。在软件开发周期的最后阶段,通常会从开发分支(如develop)上创建一个发布分支,并在版本稳定和经过测试后进行发布。在发布之后,发布分支通常会合并回主分支来包含最新的更改。
*
*/
tagPattern: '^v\\d+\\.\\d+\\.\\d+.*',
// 已v开头比如: v1.0.0.alpah.0
// 已v开头比如: v1.0.0.bate.0
// 已v开头比如: v1.0.0.rc.0
// 已v开头比如: v1.0.0
};
const validate = (name, pattern) => {
const regExp = new RegExp(pattern, 'g');
return regExp.test(name);
}
branchList.forEach((branch) => {
const nameRefs = branch.split('\/'); // 截取得到['refs', 'heads', 'test']
const type = nameRefs[1];
const name = nameRefs.slice(2).join('\/');
if (type === GIT_TYPE.BRANCH) {
const branchResult = validate(name, patterns.branchPattern);
if(!branchResult) {
errorMessage.push({ type, name });
}
}
if (type === GIT_TYPE.TAGS) {
const tagResult = validate(name, patterns.tagPattern);
if(!tagResult) {
errorMessage.push({ type, name });
}
}
});
if (errorMessage.length) {
const GIT_TYPE_MAP = {
[GIT_TYPE.BRANCH]: 'branch',
[GIT_TYPE.TAGS]: 'tag'
}
const formatMessage = errorMessage.map((item) => `${GIT_TYPE_MAP[item.type]}(${item.name})命名格式错误`).join('\n');
console.log(`
'[ERROR]: 提交的branch/tag不符合规范格式!',
'\n',
'branch格式:[feature|hotfix|release|bugfix]/[描述]',
'tag格式:v[major].[minor].[patch][额外版本信息]',
'\n',
'例如:',
"branch:feature/login",
"tag:v1.0.0",
'\n'
提交错误信息如下:
${formatMessage}
`)
process.exit(1); // 退出进程
}
通过以上方法就可以做一些相关规范拦截了,代码也很简单、清晰但是呢要是每个项目里面都有一个感觉有些不太友好。那么咱们做些什么优化呢?
优化拦截配置
使用
commander
封装成一个命令去运行相关的脚本,把相关的脚本放在这个commander
封装的插件中项目结构如下:
关于commander
相关配置可以去官方看下就是制作自定义指令的,经常用在制作CLI
等地方,使用也是非常简单,大致功能如下:
-
定义命令和选项:通过
commander
,你可以定义和注册多个命令和选项,指定它们的名称、别名、描述等信息。 -
解析命令行参数和选项:
commander
会解析用户在命令行中输入的参数和选项,并将它们与你定义的命令和选项进行匹配。 -
处理命令和选项逻辑:一旦解析了命令行参数,你可以使用
commander
来处理不同命令和选项的逻辑。你可以为每个命令和选项定义相应的回调函数,以执行特定的操作或触发相应的业务逻辑。 -
生成帮助信息:
commander
可以自动生成帮助文档,包括已定义的命令、选项以及它们的描述、用法示例等信息。这样用户可以通过--help
选项或无效命令时获取帮助。
|bin
|commands
-- githooks.js
|scripts
--verify-git-branch
--verify-git-branch
|package.json
...
bin中就是定义的一些命令如下:
#!/usr/bin/env node
const commander = require('commander');
const program = new commander.Command();
async function main() {
program
.version(require('../package.json').version)
.usage('<command> [options] ');
program
.command('githooks <name>')
.description('githooks一些规范拦截')
.action((...args) => {
require('../commands/githooks')(...args); // 加载githooks相关拦截脚本
});
await program.parseAsync(process.argv);
}
main().catch((e) => { // 错误日志要暴漏出来
error(e);
process.exit(1); // 退出进程
});
githooks.js如下:
const path = require('path');
const spawn = require('./spawn.js');
const GIT_HOOKS_MAP = {
PRE_COMMIT: 'pre-commit',
COMMIT_MSG: 'commit-msg',
PRE_PUSH: 'pre-push',
}
const reslove = (dir) => path.join(__dirname, '..', dir);
module.exports = async (name) => {
try {
switch (name) {
case GIT_HOOKS_MAP.PRE_COMMIT:
await spawn(
'lint-staged',
[],
{ stdio: 'inherit', shell: true },
);
break;
case GIT_HOOKS_MAP.COMMIT_MSG:
await spawn(
'node',
[resolve('/scripts/verify-commit-msg'), 'HUSKY_GIT_PARAMS'], // 把环境变量名带上
{ stdio: 'inherit', shell: true },
);
break;
case GIT_HOOKS_MAP.PRE_PUSH:
await spawn(
'node',
[resolve('/scripts/verify-git-branch'), 'HUSKY_GIT_STDIN'],// 把环境变量名带上
{ stdio: 'inherit', shell: true },
);
break;
default:
error(`未知的错误: ${name}`);
process.exit(1);
}
} catch (e) {
process.exit(1);
}
};
child_process.spawn()
是Node.js中一个用于创建子进程的方法,它可以执行外部命令并获得其输出。spawn()
方法接受三个个参数:要执行的命令和命令的参数(可选),还有一些配置。
const { spawn } = require('child_process');
module.exports = (shell, args, options = {}) => new Promise((resolve, reject) => {
const sh = spawn(shell, args, { shell: true, stdio: 'inherit', ...options });
sh.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(code));
}
});
sh.on('error', (err) => {
reject(err);
});
});
package.json如下:
{
"name": "git-scripts-test",
"bin": {
"git-scripts-test": "bin/index.js"
},
"version": "1.0.0.bate.1",
//...
}
然后再发布到npm
就可以下载到项目里面如下:
yarn add git-scripts-test --dev
使用git-scripts-test
如下更改
回到咱们上面的项目里面
package.json
如下更改,不用再添加什么脚本文件了都在咱们封装的git-scripts-test
包里面了:
{
"name": "githooks",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"husky": "3.0.0",
"git-scripts-test": "^1.0.0.bate.1",
},
"lint-staged": {
"*.{js,jsx,vue}": [
"eslint --fix --ext .js,.jsx,.vue,.ts,.tsx"
]
},
"husky": {
"hooks": {
"pre-commit": "git-scripts-test githooks pre-commit",
"commit-msg": "git-scripts-test githooks commit-msg",
"pre-push": "git-scripts-test githooks pre-push"
}
}
}
利用新版本的husky
再优化程序
其实使用上面发包自定义指令的方式使用的多了,会发现也挺费劲的每次
package.json
里面都要配置一个信息。那么husky
经过迭代升级也是考虑到这个问题了,V4+以上版本都支持set、add俩api了其实是可以做到不用再package.json配置了的。
husky api源码地址 整个api的还是比较清晰的代码不是很多有相关需求的可以去看下。
使用如下:
npm install husky --save-dev
npx husky install
npm pkg set scripts.prepare="husky install" # 要在安装后自动启用Git钩子,请编辑package.json
{
"scripts": {
"prepare": "husky install"
}
}
创建一个新的hooks
可以使用如下:
npx husky add .husky/pre-commit "npm test"
这个时候可以看下cat .husky/pre-commit
了如下:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test
到这里应该就能想到怎么使用升级版本的husky
了吧。再封装插件的时候可以利用script的postinstall
钩子,再安装的该插件的时候可以执行些命令"postinstall": "node initHusky.js"
// initHusky.js
const husky = require('husky');
const hooksPath = path.resolve('./node_modules/.husky/');
const GIT_HOOKS_MAP = {
PRE_COMMIT: 'pre-commit',
COMMIT_MSG: 'commit-msg',
PRE_PUSH: 'pre-push',
};
husky.install(hooksPath);
husky.set(path.join(hooksPath, GIT_HOOKS_MAP.PRE_COMMIT), 'XXXXX自定义命令'); //
husky.set(path.join(hooksPath, GIT_HOOKS_MAP.COMMIT_MSG), 'XXXXX自定义命令'); //
husky.set(path.join(hooksPath, GIT_HOOKS_MAP.PRE_PUSH), 'XXXXX自定义命令'); //
具体还没经过实践,比如怎么获取到
commit
信息,怎么获取到分支、tag版本号等相关还没相关思路。
结语
相信经过学习
husky
你得项目里也能应用起来相关规范拦截了。 如果对您有帮助欢迎点赞收藏
转载自:https://juejin.cn/post/7261600077914947643