现代本地git钩子变得很容易: husky!定制Git拦截规范

现代本地git钩子变得很容易: husky

由于一些同学没有协同工作得相关经验,或者说说缺乏相关得git、代码规范知识。写的代码、Git CommitGit BranchesGit Tags等比较随意,造成得问题呢就是越迭代,越不好维护等。

那么有没有手段能控制呢?有太多了,eslintstylelintprettierlint-staged等等,但是还需要控制把一些不规范得给拦截. lint-staged插件中有一句介绍得话:对阶段性git文件运行检查器,不要让💩溜进代码库!(Run linters against staged git files and don't let 💩 slip into your code base!)

那么咱们今天就是利用githooks得一些钩子来把控这些代码等相关得质量。那么你不使用Husky可以吗?也可以。由于git默认是没有这些钩子得如下可以测试

bash 复制代码
git init
cd .git/hooks/
ls

正常会打印出来如下这些钩子

bash 复制代码
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如下:

bash 复制代码
yarn add husky@3.0.0

看到多增加了好多钩子

bash 复制代码
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钩子如下:

bash 复制代码
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运行命令如下:

bash 复制代码
$ cat node_modules/husky/run.js
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得都是咱们可以直接用的钩子自己按需要选择哈):

json 复制代码
{
  "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如下:

bash 复制代码
yarn add lint-staged -D

package.json现在是如下:

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更改如下:

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链接):

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相关规范的具体如下:

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": "node scripts/verify-git-branch/index.js" // push 之前的钩子
    }
  }
}

使用node执行verify-git-branch相关脚本关键代码如下:

js 复制代码
const params  = process.env.HUSKY_GIT_STDIN; // 使用环境变量能获取到一些分子、tags信息如下面注释:
// //params: refs/heads/test1 7fe5f9d5ae41 refs/heads/test1 00000000000000000000000

那么咱们做的就是截取到需要的信息如下:

js 复制代码
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是标签名。

其实看到这里应该很能明白接下来要怎么做了吧如下:

js 复制代码
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等地方,使用也是非常简单,大致功能如下:

  1. 定义命令和选项 :通过commander,你可以定义和注册多个命令和选项,指定它们的名称、别名、描述等信息。

  2. 解析命令行参数和选项commander会解析用户在命令行中输入的参数和选项,并将它们与你定义的命令和选项进行匹配。

  3. 处理命令和选项逻辑 :一旦解析了命令行参数,你可以使用commander来处理不同命令和选项的逻辑。你可以为每个命令和选项定义相应的回调函数,以执行特定的操作或触发相应的业务逻辑。

  4. 生成帮助信息commander可以自动生成帮助文档,包括已定义的命令、选项以及它们的描述、用法示例等信息。这样用户可以通过--help选项或无效命令时获取帮助。

bash 复制代码
|bin
|commands 
  -- githooks.js
|scripts
  --verify-git-branch
  --verify-git-branch
|package.json
...

bin中就是定义的一些命令如下:

js 复制代码
#!/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如下:

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()方法接受三个个参数:要执行的命令和命令的参数(可选),还有一些配置。

js 复制代码
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如下:

json 复制代码
{
  "name": "git-scripts-test",
  "bin": {
    "git-scripts-test": "bin/index.js"
  },
  "version": "1.0.0.bate.1",
  //...
}

然后再发布到npm就可以下载到项目里面如下:

bash 复制代码
yarn add git-scripts-test --dev

使用git-scripts-test如下更改

回到咱们上面的项目里面package.json如下更改,不用再添加什么脚本文件了都在咱们封装的git-scripts-test包里面了:

json 复制代码
{
  "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的还是比较清晰的代码不是很多有相关需求的可以去看下。

使用如下:

bash 复制代码
npm install husky --save-dev
npx husky install
npm pkg set scripts.prepare="husky install" # 要在安装后自动启用Git钩子,请编辑package.json
json 复制代码
{
  "scripts": {
    "prepare": "husky install" 
  }
}

创建一个新的hooks可以使用如下:

bash 复制代码
npx husky add .husky/pre-commit "npm test"

这个时候可以看下cat .husky/pre-commit了如下:

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

npm test

到这里应该就能想到怎么使用升级版本的husky了吧。再封装插件的时候可以利用script的postinstall钩子,再安装的该插件的时候可以执行些命令"postinstall": "node initHusky.js"

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你得项目里也能应用起来相关规范拦截了。 如果对您有帮助欢迎点赞收藏

相关推荐
天天向上1024几秒前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y16 分钟前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁23 分钟前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry23 分钟前
Fetch 笔记
前端·javascript
拾光拾趣录24 分钟前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟25 分钟前
vue3,你看setup设计详解,也是个人才
前端
Lefan29 分钟前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson34 分钟前
青苔漫染待客迟
前端·设计模式·架构
vvilkim36 分钟前
Nuxt.js 全面测试指南:从单元测试到E2E测试
开发语言·javascript·ecmascript
写不出来就跑路1 小时前
基于 Vue 3 的智能聊天界面实现:从 UI 到流式响应全解析
前端·vue.js·ui