头图

前言

本篇文章主要讲解如何从零搭建一套 Git 提交规范化流程,主要分为 lint 校验和 Commit Message 规范化

在团队合作中,Git 提交如果没有规范化,会让代码库变得混乱不堪,影响协作效率,特别是在多人协作中。如何从零开始搭建一套高效的 Git 提交规范化流程,是每个团队都需要思考的问题

本篇文章结构紧凑,因果关系明确,最好从头读到尾,跟着逻辑思路走,不建议中途跳段

本文也是《通俗易懂的中后台系统建设指南》系列的第五篇文章,该系列旨在告诉你如何来构建一个优秀的中后台管理系统

写在前面

上一篇文章中,我们在代码层面从零搭建起了一套代码规范体系

而在这篇文章中,我们会聚焦在工程层面,致力于从零搭建起一套 Git 提交规范化流程,通过工具如 HuskyLint-stagedCommitlintCommitizen,一步步实现 Git 提交规范化,首先,我们需要先了解一下这些工具的基础概念,请看下文

孟子曰:不以规矩,不能成方圆

基础概念

你可以在这里先了解下贯穿全文的工具概念,我们后面会使用到它们:

  • husky:一个 Git hook 工具,使用 Husky 可以挂载 Git 钩子,使现代的原生 Git 钩子变得简单,即可以帮助我们在 commit 前做一些自定义操作
  • Lint-staged: 对暂存的 git 文件运行 linters,也就是只对暂存的文件进行 ESLint、Prettier、Stylelint 等检查
  • commitlint:对 commit 信息进行检查,即 lint Commit Message
  • Commitizen:基于 Node.js 的  git commit  命令行工具,辅助生成标准化规范化的 Commit Message

git-hook

在开始前,我们先来了解一下什么是 git-hook,这是前置概念

hook 翻译过来是钩子的意思,有点类似于 Vue 的生命周期钩子一样,Git 能在特定的重要动作发生时触发自定义脚本

在我们执行 git init 命令后,会在当前目录下生成一个 .git 文件,目录中就存在一个 hooks 文件夹

Mac 用户如果看不到 .git 文件,使用 Command + Shift + . 来显示隐藏文件
└── .git
    ├── COMMIT_EDITMSG  # 保存最新的commit message
    ├── config  # 仓库的配置文件
    ├── description  # 仓库的描述信息,主要给gitweb使用
    ├── HEAD    # 指向当前分支
    ├── hooks   # 存放一些shell脚本,可以设置特定的git命令后触发相应的脚本
    ├── index   # 二进制暂存区(stage)
    ├── info    # 仓库的其他信息
    │   └── exclude # 本地的排除文件规则,功能和.gitignore类似
    ├── logs    # 保存所有更新操作的引用记录,主要用于git reflog等
    ├── objects # 所有文件的存储对象
    └── refs    # 具体的引用,主要存储分支和标签的引用

参考资料:https://blog.cti.app/archives/1344解析.git 文件夹,深入了解 git 内部原理

.git/hooks 目录下存放着一些 shell 脚本,以 .sample 为后缀,表示默认不启动,这些 shell 脚本主要分为两大类:客户端和服务端,我们这里只关注客户端

在客户端下,也细分一些钩子种类,比如 提交工作流钩子、电子邮件工作流钩子和其它钩子:

Pasted image 20240718224019

  • pre-commit  钩子在键入提交信息前运行
  • prepare-commit-msg  钩子在启动提交信息编辑器之前,默认信息被创建之后运行
  • commit-msg  存有当前提交信息的临时文件,如果该钩子脚本以非零值退出,Git 将放弃提交
  • post-commit  钩子在整个提交过程完成后运行

在文章开头,我们说这篇文章要做的事分为两类: lint 校验和 Commit Message 规范化,结合上面的钩子,所以我们的主要思路是:

  • pre-commit:在键入提交信息前进行 lint 校验,比如 ESLint、Stylelint、Prettier 等
  • commit-msg:对当前 Commit Message 进行检查并使用工具来规范化 Message 信息

要在 git 钩子里去做这些事情,我们就需要借助工具 Husky 来配置 Git 钩子,请看下文

Husky

这是什么?借用 Chat GPT 的话来说:Husky 是一个 Git hooks 工具,它可以帮助我们在 Git 操作执行前或执行后自动执行脚本

跟官网一样,首先安装它

pnpm add --save-dev husky

并且使用 Husky 推荐的 init 命令

pnpm exec husky init

这条命令会在根目录下新增一个 .husky 目录,目录下会创建  pre-commit  脚本

并在  package.json  中生成一个名为 prepare 的脚本

Pasted image 20240722220349

在终端运行这个命令,来初始化 husky

pnpm prepare

它会在 .husky 下生成一个文件夹 _

确保你的项目中存在 .git 文件夹,否则出现找不到 .git 问题

Pasted image 20240731171724

好了,上面我们说,pre-commit 可以在键入提交信息前进行 lint 校验,现在,我们可以在这个钩子里做些事情了,但我们先来想象一个场景:

假设我们修改了文件 A 的代码并准备提交时,团队规范要求在提交前通过 lint 校验进行检查。通常情况下,全量项目的 lint 校验可能会耗费大量时间,而且有时会出现文件 B 因为 lint 校验未通过而被标记为错误,尽管我们并没有修改这个文件,而是其他同事提交的

能不能有一个方案,只 lint 我当前修改的文件,而不是全量 lint ,这样就可以避免 lint 的耗时,提高效率且有针对性

Lint-staged 就是用来解决这样的问题的,帮助我们在暂存的文件上执行 lint,而不是对整个项目进行全量校验。这样可以显著减少 lint 校验的时间消耗,提高效率,并确保校验的针对性

Lint-staged

lint-staged 在 Git 暂存文件上运行 linter

安装

pnpm add --save-dev lint-staged

配置

lint-staged 安装完成后,我们需要来配置一下它,这个做法和 ESLint、Stylelint 等很像

可以先看看 lint-staged-Configuration,即可以通过多种方式来配置,一种普遍的方式是直接在 package.json 中配置,比如

  "lint-staged": {
    "*.{js,ts,jsx,tsx}": [
      "prettier --write",
      "eslint --fix"
    ],
    "*.vue": [
      "prettier --write",
      "eslint --fix",
      "stylelint --fix",
    ]
  },

还有种方式是创建一个配置文件

本文中使用 lint-staged.config.js 来配置,并采用 ESM 模式,请确保 package.json 中包含了  "type": "module",参阅 使用 JS 配置文件

在根目录下新建一个 lint-staged.config.js ,这个文件默认导出一个对象,写入以下基础模版

/** @type {import("lint-staged").Config} */
export default {
  //...
};
lint-staged 会自动找到这个配置文件

然后,就可以在里面写入你的个性化配置,比如

/** @type {import("lint-staged").Config} */
export default {
  "*.vue": ["prettier --write --cache", "eslint --fix", "stylelint --fix"],
  "*.{js,ts,jsx,tsx}": ["prettier --write --cache", "eslint --fix"],
  "*.{css,scss,less}": ["prettier --write --cache", "stylelint --fix"],
  "*.html": ["prettier --write --cache", "stylelint --fix"],
  "*.json": "prettier --write --cache",
};

你可以在官网找到配置说明

注意,在运行指定的 linter 和代码格式化工具时,建议先运行 Prettier ,因为它会对代码进行整体格式化,这样可以确保代码在进行 lint 校验之前是一致且格式良好的状态

接下来,我们需要确保 Git 钩子 pre-commit 能够正确调用 lint-staged

在根目录下的终端运行 shell 命令:

echo "pnpm lint-staged" > .husky/pre-commit

这条命令的作用是将 "pnpm lint-staged" 写入到 .husky/pre-commit 文件中,推荐你阅读 husky-v9.0.1

Pasted image 20241208150933

示例

接下来我们测试一下上述配置是否生效,在这之前,请确保你已经做好了如下准备:

在项目里添加一个 test.ts 文件,一个 test.vue 文件(位置随意)

随意写点内容,比如

//test.ts
const name = "fifteen";
//test.vue
<script setup lang="ts"></script>

<template>
  <div>测试!!</div>
</template>

<style lang="less"></style>

将这两个文件提交到暂存区里:在终端输入命令 git add . 或者在 VS Code 中可视化操作

Pasted image 20240723222132

在终端执行命令 git commit -m 'test'

Pasted image 20240723222404

出现类似这样的画面,即表示 lint-staged 起作用了,可以看到,因为校验未通过,Commit 提交并未通过而是直接退出了

Commit 规范

在写下面章节之前,我想先来讲讲 Commit 规范的意义

试想一个场景,在工作中你接手了一个别人的项目,如何能够快速入手呢?除了基本的浏览代码,如果这个项目每次提交都遵循一定规范,比如 “feat:userManage Module”,那么项目的提交历史将变得规范而清晰,这很有助你接下来的上手工作,能了解项目的发展和主要功能,并找到相关的变更记录!

再比如团队的协同开发,你需要理解同事的部分业务代码,是否可以通过清晰、规范的提交信息来帮助我们了解这块的变更历史,从而简化交流达到提高效率的作用呢?

所以,commit 规范的意义体现在几个方面:

  1. 提高代码可读性和项目质量、改善项目可维护性
  2. 简化协作和沟通
  3. 方便版本管理和代码回溯
  4. 有助于自动化生成变更日志

这里不得不提到两个重要的提交规范:

  1. Angular 规范,由 Angular 团队制定并使用,也被社区广泛接受,推荐你阅读以下内容:
  1. Conventional Commits 规范则是由 Angular 规范发展调整而来的一个更通用的规范,官网的介绍是:约定式提交,一种用于给提交信息增加人机可读含义的规范,提供一套规则来创建清晰的提交历史:

如果你在开发 Angular 项目,那么遵循 Angular 提交规范是最优选择

如果你想要更通用的项目提交规范,那么选择 Conventional Commits 规范就行了,下文内容我们约定使用此规范

要让我们的提交信息能够符合 Conventional Commits 规范,可通过使用工具如 commitlint 来实现,请看下文

Commitlint

Commitlint 是一个用于检查 Git 提交信息是否符合规范的工具,用于在提交前自动检查提交信息

安装

pnpm add --save-dev @commitlint/{cli,config-conventional}

这条命令会在你的 package.json 文件的 devDependencies 中生成两个依赖:
Pasted image 20241208151333

  • @commitlint/cli,命令行工具
  • @commitlint/config-conventional,此配置遵循 Conventional Commits 规范,与 @commitlint/cli 配合使用,参阅 @commitlint/config-conventional

配置

类似的,我们需要一个配置文件,这里我们使用 commitlint.config.js

它也可以是一个 ts 格式的配置文件,但为了统一,我们这里使用 js

有两种方式配置,第一,在项目根目录下终端输入以下命令:

echo "/** @type {import('@commitlint/types').UserConfig} */ \n export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

这个 shell 脚本会在根目录下生成一个 commitlint.config.js 文件,并且写入以下内容:

Pasted image 20241207225729

第二种方式是手动创建

在项目根目录下新建一个 commitlint.config.js 文件,然后写入内容:

/** @type {import('@commitlint/types').UserConfig} */ 
 export default { extends: ['@commitlint/config-conventional'] };

一个基本的提交约定遵循以下模式:

type(scope?): subject
body?
footer?

然后,我们就可以写入一些自定义的规则项
比如,可以是这样的:

/** @type {import('@commitlint/types').UserConfig} */
export default {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "type-enum": [
      2,
      "always",
      [
        "feat", // 新功能
        "fix", // 修复bug
        "docs", // 文档更新
        "style", // 代码格式(不影响代码运行的变动)
        "refactor", // 重构(既不是新增功能,也不是修改bug的代码变动)
        "perf", // 性能优化
        "test", // 增加测试
        "chore", // 构建过程或辅助工具的变动
        "revert", // 回滚到上一个版本
        "build", // 编译相关的修改,例如发布版本、对项目构建或者依赖的改动,
        "types", // 类型
        "ci", // CI 配置文件和脚本的更改
      ],
    ],
    "header-max-length": [2, "always", 100], //头部最大长度100
    "body-max-line-length": [2, "always", 100], //body最大长度100
    "footer-max-line-length": [2, "always", 100], //footer最大长度100
    "type-empty": [2, "never"], //type 不能为空
    "subject-empty": [2, "never"], //subject 不能为空
    // "scope-empty": [2, "never"], //scope 不能为空
    "type-case": [2, "always", "lower-case"], //type 小写
    "scope-case": [2, "always", "lower-case"], //scope 小写
  },
};

这些规则可以在 这里找到

在你写完自定义的配置规则后,我们要在创建提交之前对其进行 lint 校验,需要使用到 Husky 的 commit-msg 钩子,

commit-msg 概念:存有当前提交信息的临时文件,如果该钩子脚本以非零值退出,Git 将放弃提交

运行 shell 命令:

echo "pnpm dlx commitlint --edit \$1" > .husky/commit-msg

这个 shell 脚本会在 .husky 文件夹生成一个 commit-msg 文件,并且写入以下内容:

Pasted image 20241208151906

到这一步,我们已经实现了基本的 commit 信息校验,下面我们来试试

示例

测试一下对于提交信息的检测,看是否符合我们在 Commitlint 定义的规则

在终端输入命令:

pnpm exec commitlint --from HEAD~1 --to HEAD --verbose

这个命令的作用是检测你上一次的提交信息(前提是你有上一次提交信息),并让 Commitlint 输出更详细的信息

Pasted image 20240725224220

也可以来测试一下提交当前commit时,lint 校验是否起作用

在暂存区加一些测试文件,然后,输入 commit 信息

Pasted image 20240725224527

在这里,我的 commit 信息是 "ttt: a test" ,它的报错信息是:我的 type 类型不对,只能是枚举内的类型

我们上面配置了 lint-staged,所以会先对暂存的文件进行 lint

解决方法也很简单,修改 commit 信息使其符合规则即可

但你是否想过另一种方式?即通过自动化程序来避免这种纯手敲 commit 信息格式的错误呢

换句严谨点的话说,使用工具来给我们提供交互式命令行界面,创建符合 Commitlint 规范的 Commit 信息

社区有这样的解决方案,通过 Commitizen 来辅助我们生成一套标准化规范化的 Commit 信息,请看下文

Commitizen

Commitizen 是一个用于生成标准化规范化的 Commit 信息的命令行工具

在 Commitlint 官网的介绍中,有推荐一个 @commitlint/prompt-cli 来让我们创建交互式命令行,参阅这里

另一种代替方案是使用 Commitizen

我们简单比较一下这两者的区别:

  • @commitlint/prompt-cli 是官方提供的包,是 Commitlint 生态一部分,与 Commitlint 无缝衔接
  • Commitizen 拥有着更大的社区、更广泛的使用、更成熟,灵活及定制化

本文中我们使用 Commitizen

安装

pnpm add --save-dev commitizen

安装完后,我们还需要安装一个适配器,这里介绍两个:

这里使用 @commitlint/cz-commitlint

pnpm add --save-dev @commitlint/cz-commitlint

然后,你的 package.json 文件下的 devDependencies 会生成这两个依赖:
Pasted image 20241209111014

我们接着在 package.json 中写入以下内容:

{
  "scripts": {
    "commit": "git-cz"
  },
  "config": {
    "commitizen": {
      "path": "@commitlint/cz-commitlint"
    }
  },
}

配置

在做完上述的安装及基础配置后,我们可以自定义自己的交互文本,在 commitlint.config.js 文件中配置 prompt 属性,比如:

/** @type {import('@commitlint/types').UserConfig} */
export default {
  extends: ["@commitlint/config-conventional"],
  ignores: [(commit) => commit === ""],
  rules: {
      //...规则,上文已经配置
  },
  prompt: {
    questions: {
      type: {
        description: "选择你要提交的变更类型",
        enum: {
          feat: {
            description: "新功能",
            title: "新功能",
            emoji: "✨",
          },
          fix: {
            description: "修复bug",
            title: "Bug修复",
            emoji: "🐛",
          },
          docs: {
            description: "仅文档更改",
            title: "文档",
            emoji: "📚",
          },
          style: {
            description: "不影响代码含义的更改(空白、格式化、缺少分号等)",
            title: "样式",
            emoji: "💎",
          },
          refactor: {
            description: "既不修复bug也不添加新功能的代码更改",
            title: "代码重构",
            emoji: "📦",
          },
          perf: {
            description: "提高性能的代码更改",
            title: "性能优化",
            emoji: "🚀",
          },
          test: {
            description: "添加缺失的测试或修正现有的测试",
            title: "测试",
            emoji: "🚨",
          },
          //更多...
        },
      },
    },
  },
};

这些配置你可以在这里找到

示例

在配置好上面的内容后,我们就可以来测试一下效果了

修改一些文件内容,然后 git add . 放入暂存区

在项目终端输入命令

pnpm commit

Pasted image 20240727002230

出现这个交互式命令界面即为成功

本文中的所有示例都可以在 vue-clean-admin 中找到

参考资料

了解更多

系列专栏地址:GitHub | 掘金专栏 | 思否专栏

实战项目:vue-clean-admin

专栏往期回顾:

  1. 收下这份 Vue + TS + Vite 中后台系统搭建指南,从此不再害怕建项目
  2. 中后台开发必修课:Vue 项目中 Pinia 与 Router 完全攻略
  3. 用了这些 Vite 配置技巧,同事都以为我开挂了
  4. 受够了团队代码风格不统一?7千字教你从零搭建代码规范体系

交流讨论

文章如有错误或需要改进之处,欢迎指正


十五
1 声望1 粉丝