脚手架中,大部分是使用 husky
来控制githook,在 antd-pro
脚手架中,是使用的 yorkie
,它是尤雨溪fork的 husky
,简化了一些代码
目前流行的githook方式有两种,一种是像 husky
这种插件,在安装依赖时写入文件到 .git >= hooks
中。一种是手动写hooks,可以写到 .git => hooks
中,也可以写到任意文件夹中,由于 .git
文件夹不会上传到仓库中,不方便同事共同使用配置,在这里我使用后者,自己配置 git hook
文件目录
我将通过仿 yorkie
来做一个githook流程
githook的写法有很多种,只要是脚本语言都可以,比如 python、node、shell等等。 这里copy一份尤雨溪写好的 pre-commit
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
has_hook_script () {
if [ $1 == 'pre-commit' ];then
return 0
fi
[ -f package.json ] && cat package.json | grep -q "\"$1\"[[:space:]]*:"
}
# OS X and Linux only
load_nvm () {
# If nvm is not loaded, load it
command_exists nvm || {
export NVM_DIR="$1"
[ -s "$1/nvm.sh" ] && . "$1/nvm.sh"
}
}
# OS X and Linux only
run_nvm () {
# If nvm has been loaded correctly, use project .nvmrc
command_exists nvm && [ -f .nvmrc ] && nvm use
}
cd "."
# Check if pre-commit is defined, skip if not
has_hook_script pre-commit || exit 0
# Node standard installation
export PATH="$PATH:/c/Program Files/nodejs"
# Export Git hook params
export GIT_PARAMS="$*"
# Run hook
node "./scripts/yorkie.js" pre-commit || {
echo
echo "pre-commit hook failed (add --no-verify to bypass)"
exit 1
}
所有的hook文件都可以用这个,除了需要改一下名字。
这里比较重要的是最后一步 node '/scripts/yorkie.js'
// scripts/yorkie.js
const fs = require('fs');
const path = require('path');
const execa = require('execa');
const cwd = process.cwd();
const pkg = fs.readFileSync(path.join(cwd, 'package.json'));
const pkgJson = JSON.parse(pkg);
const hooks = pkgJson.gitHooks || {};
if (pkgJson.scripts && pkgJson.scripts.precommit) {
hooks['pre-commit'] = 'npm run precommit';
}
if (!hooks) {
process.exit(0);
}
const hook = process.argv[2];
const command = hooks[hook];
if (!command) {
process.exit(0);
}
console.log(` > running ${hook} hook: ${command}`);
try {
execa.commandSync(command, { stdio: 'inherit' });
} catch (e) {
process.exit(1);
}
其实就是每个钩子在触发的时候,去执行一个js脚本,然后这个js脚本去执行命令行代码 比如:
提交之前,我们使用 lint
去检查校验代码是否没问题 package.json => scripts => precommit: lint-staged
代码校验通过后,会触发 commit-msg
钩子
// package.json
"gitHooks": {
"commit-msg": "node ./scripts/verifycommit.js",
"pre-push": "node ./scripts/prepush.js"
},
此时,会执行 scripts/verifycommit.js
脚本,其内容如下:
// Invoked on the commit-msg git hook by yorkie.
const chalk = require('chalk');
const msgPath = process.env.GIT_PARAMS || process.env.HUSKY_GIT_PARAMS;
const msg = require('fs').readFileSync(msgPath, 'utf-8').trim();
const commitRE =
/^(((\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]) )?(revert: )?(feat|fix|docs|style|refactor| perf|workflow|build|CI|ci|typos|chore|tests|types|wip|release|dep|locale)(\(.+\))?: .{1,50}/;
if (!commitRE.test(msg) && !msgPath.includes('MERGE_MSG')) {
console.error(
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(`提交日志不符合规范`)}\n\n${chalk.red(
` 合法的提交日志格式如下(emoji 和 模块可选填):\n\n`,
)}
${chalk.green(`✨ feat(模块): 添加新特性`)}
${chalk.green(`🐛 fix(模块): 修复bug`)}
${chalk.green(`📝 docs(模块): 修改文档`)}
${chalk.green(`🌈 style(模块): 修改样式`)}
${chalk.green(`♻️ refactor(模块): 代码重构`)}
${chalk.green(` chore(模块): 改变构建流程,或增加依赖库、工具库`)}
${chalk.green(` perf(模块): 优化相关,比如提升性能、体验`)}
${chalk.green(`🔧 build(模块): 依赖相关的内容`)}
${chalk.green(` test(模块): 测试相关`)}
${chalk.green(` types(模块): TS类型相关`)}
${chalk.green('推荐使用vscode插件:git-commit-plugin,快捷提交commit')}
${chalk.red(`See README.md for more details.\n`)}`,
);
process.exit(1);
}
若 commit-msg
符合我们的要求,此次的 commit 就成功了
也可以把commit-msg的内容直接写到githook脚本中:
#!/usr/bin/env node
const chalk = require('chalk');
const msgPath = process.env.GIT_PARAMS || process.env.HUSKY_GIT_PARAMS;
const msg = require('fs').readFileSync(msgPath, 'utf-8').trim();
const commitRE =
/^(((\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]) )?(revert: )?(feat|fix|docs|style|refactor| perf|workflow|build|CI|ci|typos|chore|tests|types|wip|release|dep|locale)(\(.+\))?: .{1,50}/;
if (!commitRE.test(msg) && !msgPath.includes('MERGE_MSG')) {
console.error(
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(`提交日志不符合规范`)}\n\n${chalk.red(
` 合法的提交日志格式如下(emoji 和 模块可选填):\n\n`,
)}
${chalk.green(`✨ feat(模块): 添加新特性`)}
${chalk.green(`🐛 fix(模块): 修复bug`)}
${chalk.green(`📝 docs(模块): 修改文档`)}
${chalk.green(`🌈 style(模块): 修改样式`)}
${chalk.green(`♻️ refactor(模块): 代码重构`)}
${chalk.green(` chore(模块): 改变构建流程,或增加依赖库、工具库`)}
${chalk.green(` perf(模块): 优化相关,比如提升性能、体验`)}
${chalk.green(`🔧 build(模块): 依赖相关的内容`)}
${chalk.green(` test(模块): 测试相关`)}
${chalk.green(` types(模块): TS类型相关`)}
${chalk.green('推荐使用vscode插件:git-commit-plugin,快捷提交commit')}
${chalk.red(`See README.md for more details.\n`)}`,
);
process.exit(1);
}
我这个网站有版本更迭,每次推送代码之前会在终端提示开发者是否更新版本,所以把脚本放在了 pre-push
钩子中
// scripts/prepush.js
#!/usr/bin/env node
const commander = require('commander');
const inquirer = require('inquirer');
const semver = require('semver');
const chalk = require('chalk');
const shell = require('shelljs');
const program = new commander.Command();
const { version } = require('../package.json');
function isValidNewVersion(oldVersion, newVersion) {
return !!(semver.valid(newVersion) || semver.inc(oldVersion, newVersion));
}
async function inputVersion() {
// 弹出当前版本,并提示用户输入最新版本号
const { newVersion } = await inquirer.prompt([
{
type: 'input',
message: `请输入版本号(当前版本:${version})`,
name: 'newVersion',
},
]);
if (isValidNewVersion(version, newVersion) && semver.lt(version, newVersion)) {
console.log('更新版本中,请稍等...');
try {
// 更改package的version
if (
shell.exec(`npm version ${newVersion} --no-commit-hooks --no-git-tag-version`).code !== 0
) {
console.log(chalk.red('更新失败'));
shell.echo('Error: npm version error');
shell.exit(1);
} else {
const addCode = shell.exec(`git add .`).code;
const commitCode = shell.exec(`git commit -m "v${newVersion}" --no-verify`).code;
if (addCode === 0 && commitCode === 0) {
console.log(chalk.green(`更新版本成功,最新版本:${newVersion}`));
} else {
shell.exit(1);
}
}
} catch (err) {
console.log(err, chalk.grey('err~!'));
shell.exit(1);
}
} else {
// 提示输入不合法,重新输入
console.error(
chalk.red('输入不合法,请遵循npm语义化'),
chalk.underline('https://semver.org/lang/zh-CN/'),
);
inputVersion();
}
}
async function main() {
if (!shell.which('git')) {
shell.echo('Sorry, this script requires git');
shell.exit(1);
}
// 只有在 master 或 develop 分支才会弹出
const allowBranch = ['master', 'develop'];
const gitBranchName = shell.exec(`git rev-parse --abbrev-ref HEAD`, { silent: true });
if (allowBranch.includes(gitBranchName.stdout.trim())) {
program.version(version).action(async () => {
const { update } = await inquirer.prompt([
{
type: 'confirm',
message: '是否更新网站版本?',
name: 'update',
},
]);
if (update) {
await inputVersion();
} else {
shell.exit(0);
}
});
program.parse(process.argv);
} else {
console.log(chalk.yellow(`当前分支为:${gitBranchName.stdout}不执行版本更新操作`));
}
}
main();
到此,我这个系统的git流程算是走完了。
脚本可以正常执行只是第一步,还有一个问题是必须要解决的,那就是如何和同一项目的其他开发人员共享 git hooks 配置。因为 .git/hooks 目录不会随着提交一起推送到远程仓库。对于这个问题有两种解决方案:第一种是模仿 husky 做一个 npm 插件,在安装的时候自动在 .git/hooks 目录添加 hooks 脚本;第二种是将 hooks 脚本单独写在项目中的某个目录,然后在该项目安装依赖时,自动将该目录设置为 git 的 hooks 目录。 接下来详细说说第二种方法的实现过程:
npm install
执行完成后,自动执行 git config core.hooksPath hooks
命令。git config core.hooksPath hooks
命令将 git hooks
目录设置为项目根目录下的 hooks 目录。.husky
文件夹,可以在这里写githook 比如要在commit之前执行lint的代码校验
既然有网站版本了,那么可以用这个版本来做一些事,比如[《根据网站版本号判断是否更新网页》](https://www.yuque.com/docs/share/f76fdbe2-2ffb-462e-b234-b7b09129f619?# 《根据网站版本号判断是否更新网页》)
为了更好的分享配置,我把 .vscode
从 .gitignore
中移除了,添加了一些公用配置
// extension.json
{
"recommendations": [
"bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint",
"redjue.git-commit-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
// setting.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"search.exclude": {
"**/node_modules": true,
".git": true,
".vscode": true
},
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
},
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
},
"typescript.tsdk": "node_modules\\typescript\\lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"javascript.suggestionActions.enabled": false
}