git-hook简介与使用

599 阅读6分钟

hook

hook,钩子:即某个时期或事件之前、之后执行和一些操作或脚本。与我们常说的中间件很相似。

git-hooks

git有很多hook,每当我们git init初始化一个git仓库时,都会创建一个.git的隐藏文件夹,git的hook就放在.git/hooks下。hooks以.sample为后缀的是示例文件。git-hooks有客户端hooks和服务端的hooks。

这里介绍客户端会用到的hooks

  • pre-commit: 钩子在键入提交信息前运行。 它用于检查即将提交的快照,例如,检查是否有所遗漏,确保测试运行,以及核查代码。
  • commit-msg: 钩子在启动提交信息编辑器之前,默认信息被创建之后运行。 它允许你编辑提交者所看到的默认信息。
  • post-merge: 钩子在拉取代码合并后运行。

git仓库根目录的隐藏文件夹.git里的git-hooks是不被git管理的,只存在于本地。所以无法让仓库中所有成员使用同样的hooks。这时候就需要通过工具库来使用git-hooks,就是husky。

husky

  • 安装 husky
pnpm add --save-dev husky
  • 添加 git hooks
pnpm exec husky init

init命令简化了在项目中设置husky的过程。它在.husty/中创建了一个预提交脚本,并在package.json中更新了prepare脚本

  • 添加新钩子
# echo方式,是适用于linux/mac的。
echo "command" > .husky/pre-[hook_name]
#window
node --eval "fs.writeFileSync('./.husky/.pre-[hook-name]','commoand')" 

这样就可以在hooh_name钩子期间触发command命令

接下来我们就可以借助lint-staged来实现对暂存区的代码进行风格校验和格式化、借助commitlint校验commit信息。

lint-staged

一般情况下 lint-staged 搭配着 Husky 一起使用。需要保证lint-staged 会在 pre-commit hook 中被运行。在使用lint-staged校验代码和代码风格时这里使用的是prettiereslint

  • 安装
pnpm install lint-staged --save-dev
  • 添加 git hooks
#mac/linux
echo "npx lint-staged" > .husky/pre-commit
#window
node --eval "fs.writeFileSync('.husky/pre-commit','npx lint-staged\n')"
  • 配置

形式一:package.json 中添加一个 lint-staged 项。

"lint-staged": {
     "*.{vue,js,jsx,ts,tsx}": ["eslint --fix ", "prettier --write"],
      "*.{md,json,scss,css,sass,less,html}": ["prettier --write"]
  }

形式二:根目录添加一个 .lintstagedrc 文件。

//.lintstagedrc
{
  "*.{vue,js,jsx,ts,tsx}": ["eslint --fix ", "prettier --write"],
  "*.{md,json,scss,css,sass,less,html}": ["prettier --write"]
}

之后每次的pre-commit钩子触发都会执行代码校验和代码格式化。

commitlint

commitlint是对commit信息校验的一款npm包。

  • 安装
pnpm i @commitlint/cli @commitlint/config-conventional -D
  • 添加git-hook
#linux/mac
echo 'npx --no-install commitlint --edit "$1"' > .husky/commit-msg
#window
node --eval "fs.writeFileSync('./.husky/commit-msg','npx --no-install commitlint --edit "$1"')" 

现在我们只是增加了对commit message的校验,但是在使用 Git 过程中,填写 commit message其实是比较麻烦的。如果没有良好的 commit message 规范,commit历史就会很乱。这时就需要 commitizen 来协助开发者填写 commit 信息。

commitizen

commitizen是基于 Node.js 开发的 git commit 命令行工具,用来生成标准化的commit-message。commitizen 本身只提供命令行交互框架以及一些 git 命令的执行,实际的规则则需要通过适配器来定义,commitizen 留有对应的适配器接口。市面上有很多适配器,比如: cz-conventional-changelogcz-gitcz-conventional-changelog-zh-cn。常见适配器生产commit-message模板

<type>(<scope>): <subject>
<空行>
<body>
<空行>
<footer>
  • 安装commitizen和标准输出格式的 commitizen 适配器cz-git
pnpm add --save-dev commitizen cz-git 

配置 package.json 命令

//提交时只需使用用cz而不是git commit。您还可以使用git-cz,它是cz的别名。
"scripts": {
    "commit": "cz"
  },
"config": {
    "commitizen": {
      "path": "node_modules/cz-git"
    }
  },

运行pnpm run commit以触发git-hook规范commit message

配置模板

例如 Emoji 模板

//commitlint.config.js
import { defineConfig } from "cz-git";

export default defineConfig({
 extends: ["@commitlint/config-conventional"],
 rules: {
   // @see: https://commitlint.js.org/#/reference-rules
 },
 prompt: {
   alias: { fd: "docs: fix typos" },
   messages: {
     type: "Select the type of change that you're committing:",
     scope: "Denote the SCOPE of this change (optional):",
     customScope: "Denote the SCOPE of this change:",
     subject: "Write a SHORT, IMPERATIVE tense description of the change:\n",
     body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
     breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
     footerPrefixesSelect: "Select the ISSUES type of changeList by this change (optional):",
     customFooterPrefix: "Input ISSUES prefix:",
     footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
     generatingByAI: "Generating your AI commit subject...",
     generatedSelectByAI: "Select suitable subject by AI generated:",
     confirmCommit: "Are you sure you want to proceed with the commit above?",
   },
   types: [
     { value: "feat", name: "feat:     ✨  A new feature", emoji: ":sparkles:" },
     { value: "fix", name: "fix:      🐛  A bug fix", emoji: ":bug:" },
     { value: "docs", name: "docs:     📝  Documentation only changes", emoji: ":memo:" },
     { value: "style", name: "style:    💄  Changes that do not affect the meaning of the code", emoji: ":lipstick:" },
     {
       value: "refactor",
       name: "refactor: ♻️   A code change that neither fixes a bug nor adds a feature",
       emoji: ":recycle:",
     },
     { value: "perf", name: "perf:     ⚡️  A code change that improves performance", emoji: ":zap:" },
     {
       value: "test",
       name: "test:     ✅  Adding missing tests or correcting existing tests",
       emoji: ":white_check_mark:",
     },
     {
       value: "build",
       name: "build:    📦️   Changes that affect the build system or external dependencies",
       emoji: ":package:",
     },
     { value: "ci", name: "ci:       🎡  Changes to our CI configuration files and scripts", emoji: ":ferris_wheel:" },
     { value: "chore", name: "chore:    🔨  Other changes that don't modify src or test files", emoji: ":hammer:" },
     { value: "revert", name: "revert:   ⏪️  Reverts a previous commit", emoji: ":rewind:" },
   ],
   useEmoji: true,
   emojiAlign: "center",
   useAI: false,
   aiNumber: 1,
   themeColorCode: "",
   scopes: [],
   allowCustomScopes: true,
   allowEmptyScopes: true,
   customScopesAlign: "bottom",
   customScopesAlias: "custom",
   emptyScopesAlias: "empty",
   upperCaseSubject: null,
   markBreakingChangeMode: false,
   allowBreakingChanges: ["feat", "fix"],
   breaklineNumber: 100,
   breaklineChar: "|",
   skipQuestions: [],
   issuePrefixes: [{ value: "closed", name: "closed:   ISSUES has been processed" }],
   customIssuePrefixAlign: "top",
   emptyIssuePrefixAlias: "skip",
   customIssuePrefixAlias: "custom",
   allowCustomIssuePrefix: true,
   allowEmptyIssuePrefix: true,
   confirmColorize: true,
   scopeOverrides: undefined,
   defaultBody: "",
   defaultIssues: "",
   defaultScope: "",
   defaultSubject: "",
 },
});


中文模板

//commitlint.config.js
import { defineConfig } from 'cz-git';

export default defineConfig({
   extends: ['@commitlint/config-conventional'],
   rules: {
   	// @see: https://commitlint.js.org/#/reference-rules
   },
   prompt: {
   	alias: { fd: 'docs: fix typos' },
   	messages: {
   		type: '选择你要提交的类型 :',
   		scope: '选择一个提交范围(可选):',
   		customScope: '请输入自定义的提交范围 :',
   		subject: '填写简短精炼的变更描述 :\n',
   		body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
   		breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
   		footerPrefixsSelect: '选择关联issue前缀(可选):',
   		customFooterPrefixs: '输入自定义issue前缀 :',
   		footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
   		confirmCommit: '是否提交或修改commit ?'
   	},
   	types: [
   		{ value: 'feat: 特性', name: '特性: ✨ 新增功能', emoji: '✨' },
   		{ value: 'fix: 修复', name: '修复: 🧩 修复缺陷', emoji: '🧩' },
   		{ value: 'style: 格式', name: '样式: 🎨 样式调整(不涉及业务,UI调整)', emoji: '🎨' },
   		{ value: 'docs: 文档', name: '文档: 📚 文档变更', emoji: '📚' },
   		{ value: 'style: 格式', name: '格式: 🎨 代码格式(不影响功能,例如空格、分号等格式修正)', emoji: '🎨' },
   		{ value: 'refactor: 重构', name: '重构: ♻️ 代码重构(不包括 bug 修复、功能新增)', emoji: '♻️' },
   		{ value: 'perf: 性能', name: '性能: ⚡️ 性能优化', emoji: '⚡️' },
   		{ value: 'test: 测试', name: '测试: ✅ 添加疏漏测试或已有测试改动', emoji: '✅' },
   		{ value: 'chore: 杂务', name: '杂务: ☕ 零星工作(如升级 npm 包、修改配置等)', emoji: '☕' },
   		{ value: 'ci: 集成', name: '集成: 🛠️ 修改 CI 配置、脚本', emoji: '🛠️' },
   		{ value: 'revert: 回退', name: '回退: ⏪️ 回滚 commit', emoji: '⏪️' },
   		{ value: 'build: 打包', name: '打包: 📦️ 项目打包发布', emoji: '📦️' }
   	],
   	useEmoji: true
   }
});

拉去远程仓库代码自动安装依赖

  • 添加git-hook
#linux/mac
echo 'pnpm install' > .husky/post-merge
#window
node --eval "fs.writeFileSync('.husky/post-merge','pnpm install')"