git commit 提交规范

Daotin 于 2022-08-10 发布 编辑

TLDR(太长不看版)

已经了解了或者想快速完成项目配置的,可以按照下面操作快速完成推荐的配置:

1、安装

# 1、安装husky
npm install husky -D

# 2、配置husky
npx husky-init && npm install

# 3、去.husky/pre-commit中删除 npm test

# 4、添加commit-msg hook(如果失败,需要升级npm)
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

# 5、安装commitlint
npm install --save-dev @commitlint/config-conventional @commitlint/cli

# 6、在工程目录下创建 commitlint.config.js,内容如下:
module.exports = {
  extends: ['@commitlint/config-conventional'],
};

# 7、安装自动生成changelog
npm install conventional-changelog-cli -g
npm install conventional-changelog-cli -D

#8、执行下面命令,即可生成angular提交格式的changelog
conventional-changelog -p angular -i CHANGELOG.md -s
# 1、安装commitizen
npm install -g commitizen

# 2、安装适配器
commitizen init cz-conventional-changelog --save-dev --save-exact

# 3、git add后,输入 cz 即可使用

💡 可以创建一个 node 脚本来执行上面步骤,简化操作:(🛑注意:脚本未经过验证

const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");

const executeCommand = (command) => {
  console.log(`执行命令: ${command}`);
  try {
    execSync(command, { stdio: "inherit" });
  } catch (error) {
    console.error(`执行命令失败: ${command}`);
    process.exit(1);
  }
};

// 1. 安装husky
executeCommand("npm install husky -D");

// 2. 配置husky
executeCommand("npx husky-init && npm install");

// 3. 删除 .husky/pre-commit 中的 npm test
const preCommitPath = path.resolve(".husky/pre-commit");
const preCommitContent = fs.readFileSync(preCommitPath, "utf8");
const updatedPreCommitContent = preCommitContent.replace(/npm test\n/, "");
fs.writeFileSync(preCommitPath, updatedPreCommitContent);

// 4. 添加 commit-msg hook
executeCommand(
  "npx husky add .husky/commit-msg 'npx --no -- commitlint --edit \"$1\"'"
);

// 5. 安装 commitlint
executeCommand(
  "npm install --save-dev @commitlint/config-conventional @commitlint/cli"
);

// 6. 在工程目录下创建 commitlint.config.js
const commitlintConfigContent = `module.exports = {
  extends: ['@commitlint/config-conventional'],
};`;

fs.writeFileSync("commitlint.config.js", commitlintConfigContent);

// 7. 安装自动生成changelog
executeCommand("npm install conventional-changelog-cli -g");
executeCommand("npm install conventional-changelog-cli -D");

2、vscode 安装插件(二选一)

(完)


为什么需要制定 git commit 提交规范

现在的项目大多是团队开发,由于每个人都有自己的编码习惯,其中为了提高代码质量和开发效率,有了 Eslint 和 Prettier 工具,帮助我们统一代码规范。

但是,一直以来,commit message 却总是被忽视。在日常开发中,大家的 commit message 千奇百怪,中英文混合使用、fix bug,优化代码等各种笼统的信息司空见怪,这就导致后续在回顾提交记录的时候特别混乱,有时自己都不知道自己的 fix bug 修改的是什么问题,需要看提交代码才知道。

一份良好的 commit 记录的好处如下:

业界通用的提交规范有哪些?

  1. Angular commit convention:Angular 项目的提交规范
  2. Conventional Commits:约定式提交规范,基于 Angular 提交规范,提供了更加通用、简洁和灵活的提交规范。

示例如下:(可以看出这些提交信息都是有固定格式的)

Untitled

标准格式

每个提交消息都由一个头部 header、一个正文 body 和一个页脚 footer 组成。

头部 header 部分

header 具有特殊格式,包括 type, scopesubject:

<type>[optional scope][!]: <subject>
# 空行
[optional body]
# 空行
[optional footer]

type(必填)说明此次提交的类型。比如 feat、fix,后跟冒号(英文)和一个空格

type 需要指定为下面其中一个:

问题:没有单独修改 UI 的 type。

解决方案:

  1. UI 的修改归类在 style 中
  2. 新增一个 UI 类型

scope(可选):表明本次提交影响的范围。用英文圆括号包围。

比如可以取值 api,表明只影响了接口。

!(可选):表示破坏性变更。(跟下面的BREAKING CHANGE 相同,可以理解成重大修改,类似版本升级、接口参数减少、接口删除等。如果使用了  !,那么脚注中可以不写  BREAKING CHANGE

subject(必填):本次提交的简短描述概括。

(建议不要过长,如果感觉太短说不清楚,可以在 body 中补充。)

正文 body 部分

body(可选):比较详细描述本次提交涉及的条目,罗列代码功能,不是必须的。

footer(可选):一般包括破坏性变更(BREAKING CHANGE) 或 解决关闭的 issue 。

示例

示例 1:最小描述

feat: 增加搜索功能

示例 2:加了影响范围和详细描述正文

feat(notice): 增加搜索功能

1、在通知页面增加搜索功能
2、搜索范围在一个月以内

示例:加入!(破坏性变更)

build!: 增加md5库,需要重新npm imstall

示例 3:加了 footer

feat: 增加搜索功能

Closes: http://10.8.40.130:8081/browse/QJYH-28

示例 4:加了 BREAKING CHANGE

build: 增加md5库

BREAKING CHANGE: 需要重新 npm install

示例 5:最全的

feat(notice): 增加搜索功能

功能描述:
1、在通知页面增加搜索功能
2、搜索范围在一个月以内

需要注意:接口超时提醒。

BREAKING CHANGE: 需要重新 npm install

Reviewed-by: XXX
Closes: #111, #222, #333

如何快速生成符合规范的 commit 信息?

虽然有了规范,但是如果要每次提交都需要自己手写的话还是比较麻烦的,而且还是无法保证每个人都能够遵守相应的规范,因此就需要使用一些工具来保证大家都能够提交符合规范的 Commit Message。

常用的工具包括了可视化工具命令行工具,其中Commitizen是常用的命令行工具,接下来将会先介绍 Commitizen 的使用方法。

Commitizen

什么是 Commitizen

Commitizen是一个撰写符合上面 Commit Message 标准的一款工具,可以帮助开发者提交符合规范的 Commit Message。

安装 Commitizen

1、全局安装 commitizen 命令行

npm install -g commitizen

2、安装适配器。

Commitizen 支持多种不同的提交规范,可以安装和配置不同的适配器实现。因为我们使用约定式提交规范,所以使用 cz-conventional-changelog 适配器初始化项目

commitizen init cz-conventional-changelog --save-dev --save-exact

它会在 package.json 中添加下面内容

"devDependencies": {
	"cz-conventional-changelog": "^3.3.0"
},
"config": {
	"commitizen": {
		"path": "./node_modules/cz-conventional-changelog"
	}
}

或者,可以将 Commitizen 配置添加到一个 .czrc 文件中:

{
  "path": "cz-conventional-changelog"
}

3、生成标准 commit 信息

在命令行输入cz,或者git cz ,系统会提示您填写一些必填字段,并且您的提交消息进行格式化。

Untitled

它会引导你一步步填下去,然后自动提交该 commit。

Untitled

遇到的问题:

在输入 cz 的是提示,cz-conventional-changelog 找不到,看了路径是默认找的.git 下面的 node_modules,因为我们项目是项目集的形式,所以要修改路径。正常的单个项目不需要修改。

Untitled

"config": {
	"commitizen": {
		"path": "cz-conventional-changelog"
	}
}

4、自定义适配器

从上面的截图可以看到,git cz终端操作提示都是英文的,如果想改成中文的或者自定义这些配置选项,我们使用  cz-customizable*适配器。

具体的配置可以参考:

最后可以配置成中文的,甚至可以跳过其中的某些步骤:

Untitled

可视化工具

除了使用 Commitizen 命令行工具来帮助我们规范 Commit Message 之外,我们也可以使用编译器自带的可视化提交工具。接下来,将会分别介绍 VSCode 和 Webstorm 可视化提交工具的使用方法。

vscode

插件一:Commit Message Editor

Untitled

Untitled

Untitled

插件二:Conventional Commits

Untitled

Untitled

缺点:没有对BREAKING CHANGE 的选择提示,需要自己手写。

webstorm

Untitled

Untitled

如何校验 commit message

虽然我们有命令行和可视化交互工具帮助大家规范自己的 commit message,但是依然没法保证大家都按照规范提交 commit message,因为即使是不规范的 commit message 也是依然可以提交成功的。

所以,有必要在提交的时候校验 commit message 是否按照规范填写的,如果不规范,则阻止提交。

commit message 的校验在两个地方都可以进行:

在此之前,先介绍下 git 常用的钩子 Git Hook。

Git Hook

hook,直译过来是“钩子”,通常是用于在某事件发生或者完成后添加自定义的动态事件/任务。在使用 git 时,git 的一些特定的重要动作(比如 git commit,git push 等)也会触发一些特定的事件,通过这些事件,我们可以完成一些自动测试、集成、构建等流程工作。

git hook 有很多,包括客户端和服务端两种。

常见服务端 git commit 钩子

操作步骤

(PS:以下内容,因没有相关资源暂无法实践)

1、对于 Gitlab 来说,对于本地部署的 Gitlab 服务器,需要 GitLab 管理员在 GitLab 服务器的文件系统上配置服务器钩子,可以通过 custom_hooks 配置:

假如我们要向名为 project 的仓库添加 git hooks 脚本:

hook 文件示例:

#!/bin/sh

echo "Say hi from gitlab server ok 😄"
exit 0

参考文档:

2、如果您没有文件系统访问权限,可以使用的是 Push Rules,用于用户可配置的 Git 钩子接口。(该方法适用于付费版 gitlab 用户)。

有两种配置方式:针对全部项目的和针对单独项目的。

针对全部项目:

  1. 在顶部栏上,选择“菜单”>管理员”。
  2. 在左侧边栏上,选择推送规则 Push Rules
  3. 展开“推送规则 Push Rules”。
  4. 设置所需的规则。
  5. 选择保存推送规则

针对单个项目的推送规则:(单个项目的推送规则将覆盖全局推送规则。)

  1. 在顶部栏上,选择“菜单>项目”,然后找到你的项目。
  2. 在左侧边栏上,选择“设置”>存储库”。
  3. 展开“推送规则”。
  4. 设置所需的规则。
  5. 选择保存推送规则

针对单个项目,如果开启了 Push Rules,则在仓库 --> 设置 --> Push Rules 中有 commit message,可以使用正则匹配,匹配成功才可以提交。

例如,如果每个提交都应该引用 Jira 问题(如 Fix: JIRA-123),则正则表达式将为 JIRA\-\d+

然后在 push 的时候,如果不符合规范就会报错:

remote: GitLab: Commit message does not follow the pattern '^(fix|feat|refactor|chore|style|docs|test):([\s\S]){5,}$|^Merge|^Resolve|^Feat'
...
master -> master (pre-receive hook declined)

PS:对于 github 企业版,也可以设置服务端 hook。

步骤可以参考:https://docs.github.com/cn/enterprise-server@3.6/admin/policies/enforcing-policy-with-pre-receive-hooks/managing-pre-receive-hooks-on-the-github-enterprise-server-appliance

其中需要创建的具体 hook 脚本参考示例:https://github.com/github/platform-samples/tree/master/pre-receive-hooks

常见客户端 git commit 钩子

一般用来做一些代码提交前的准备工作。比如判断提交的代码是否符合规范、是否对这份提交进行了测试、代码风格是否符合团队要求等等。

这个钩子用的机会不是太多,主要是用于能自动生成 commit message,合并压缩提交等场合。

包含有一个参数,用来指定提交 message 规范文件的路径。

该脚本可以用来验证提交的 message 是否规范,如果作者写的提交 message 不符合指定路径文件中的规范,提交就会被终止。

客户端的 hook,由于没有平台的限制,且配置方便,因而更加常用。我们需要在.git/hooks文件夹下修改相关的 hook 文件,但是.git 文件夹下的文件不能同步到项目仓库中,因而需要每个开发者在自己本地手动配置,比较麻烦,而且写法可能也不熟悉。 所以我们需要一个工具方便我们配置客户端 hook,并且可以同步到代码仓库,它就是 Husky。

Husky

Husky 是一个开源 Git Hook 工具,它可以让开发人员更加便捷快速使用 Git Hook。

1、安装 husky

npm install husky -D

2、配置 husky

自动配置(推荐):使用  husky-init命令快速在项目初始化一个 husky 配置。

npx husky-init && npm install

它将设置 husky,修改 package.json 并创建一个您可以编辑的示例 pre-commit 钩子。默认情况下,它会在你提交时运行 npm test

错误提示:还是因为我们是项目集的形式,每个项目下没有.git文件夹。所以我们需要在仓库根目录下,执行该命令。(可以参考最后的补充说明

Untitled

注意事项:由于它默认创建的 pre-commit 中会执行 npm test 指令,但是我们项目中没有对应的指令,所以后续在 commit 的时候会报错,所以需要在生成的 pre-commit 文件中,把 npm test 指令删除。

3、添加其他 hook(比如 commit-msg hook)

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

错误提示:

Untitled

解决方案:升级 npm

Untitled

commitlint

commitlint 是一个用来检测 commit message 的开源工具,我们可以用来检查您的提交消息是否符合 Conventional Commits(约定式提交规范)。配合husky,我们可以在提交 commit 的时候进行校验和拦截操作。

执行过程:具体的过程就是,在提交 message 的时候,通过 Git Hook 工具 Husky,执行 commitlint 校验,如果不通过,就阻止 message 提交。

1、安装

npm install --save-dev @commitlint/config-conventional @commitlint/cli

2、在工程目录下创建commitlint.config.js,配置 commitlint

module.exports = {
  extends: ["@commitlint/config-conventional"],
};

配置文件的格式可以是下面的几种都可以:

3、测试

Untitled

Untitled

4、自定义校验规则

以上使用的是 commitlint 的默认配置(遵循约定式提交规范),我们也可以自定义 commit message 的校验规则,比如增加 type 类型,正文之前是否需要空行等等。

rule 配置说明::rule 由名称配置数组组成,如:’name:[0, ‘always’, 72]’,

配置数组中有三个元素,分别为:

示例:

/**
 * 
- `Level` :为0,1,2三个数的其中一值。0代表让本规则无效;1代表以警告提示,不影响编译运行;2代表以错误提示,阻止编译运行。以上面的例子则代表该规则不会起作用。
- `Applicable`**:**第二位为应用与否,为always和never其中一值。
- `Value`:适用于该规则的值。
 */
module.exports = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    // case可选值
    // 'lower-case' 小写 lowercase
    // 'upper-case' 大写 UPPERCASE
    // 'camel-case' 小驼峰 camelCase
    // 'kebab-case' 短横线 kebab-case
    // 'pascal-case' 大驼峰 PascalCase
    // 'sentence-case' 首字母大写 Sentence case
    // 'snake-case' 下划线 snake_case
    // 'start-case' 所有首字母大写 start-case
    "type-enum": [
      2,
      "always",
      [
        "feat",
        "fix",
        "docs",
        "style",
        "ui",
        "refactor",
        "perf",
        "test",
        "build",
        "ci",
        "chore",
        "revert",
      ],
    ], // type的类型只能从中取值
    "type-case": [2, "always", "lower-case"], // <type>格式小写
    "type-empty": [2, "never"], // <type> 不能为空
    "type-min-length": [0, "always", 0],
    "type-max-length": [0, "always", Infinity],

    "scope-enum": [0, "always", []],
    "scope-case": [0, "always", "lower-case"], // <scope> 格式 小写
    "scope-empty": [2, "never"], // <scope>为空
    "scope-min-length": [0, "always", 0],
    "scope-max-length": [0, "always", Infinity],

    "subject-case": [0, "always", "lower-case"],
    "subject-empty": [2, "never"], // <subject> 不能为空 (默认)
    "subject-full-stop": [0, "always", "."], // <subject> 以.为结束标志
    "subject-min-length": [0, "always", 0],
    "subject-max-length": [0, "always", Infinity],
    "subject-exclamation-mark": [0, "always"], // 在:前有感叹号

    "header-case": [0, "always", "lower-case"],
    "header-full-stop": [0, "always", "."],
    "header-min-length": [0, "always", 0],
    "header-max-length": [0, "always", Infinity],

    "body-case": [0, "always", "lower-case"],
    "body-leading-blank": [2, "always"], // body前导空行
    "body-empty": [0, "never"], // body为空
    "body-min-length": [0, "always", 0],
    "body-max-length": [0, "always", Infinity],
    "body-max-line-length": [0, "always", Infinity],

    "footer-leading-blank": [2, "always"], // <footer> 前导空行
    "footer-empty": [0, "never"],
    "footer-min-length": [0, "always", 0],
    "footer-max-length": [0, "always", Infinity],
    "footer-max-line-length": [0, "always", Infinity],

    "references-empty": [0, "never"], // 参考至少有一个
    "signed-off-by": [0, "always", "Signed-off-by:"], // message中含有value
    "trailer-exists": [0, "always", "Signed-off-by:"], // message尾部含有value
  },
};

目前规则必须遵守如下:

  • type 必填(增加 ui 类型)
  • scope 必填,影响范围写模块名(无法准确描述模块名的可以写笼统。比如国际云翻译)
  • body 非必填,若填写则前面必须空一行
  • foot 非必填,若填写则前面必须空一行

官方手册:https://commitlint.js.org/#/reference-rules

参考示例:https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/index.js

自动生成 change log

使用 conventional-changelog-cli 可以自动生成 Change log,生成的文档只包括以下 3 个部分:

1、安装

npm install conventional-changelog-cli -g
npm install conventional-changelog-cli -D

提示:使用预设格式需要全局安装,使用自定义格式需要项目安装。

2、生成 changelog

conventional-changelog -p angular -i CHANGELOG.md -s

可以在 package.json 中加入配置方便使用

"scripts": {
  "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}

直接运行npm run changelog即可。

参数:

3、自定义 CHANGELOG 格式

由于默认的只能生成 feat,fix,breaking 的信息,如果想生成其他的 log 信息,就需要自定义 CHANGELOG 格式。需要从外部传入自定义配置文件。

npm i conventional-changelog-custom-config compare-func -D

在根目录新建changelog.config.js文件(这个文件可以覆盖掉conventional-changelog-custom-config 中我们需要的配置)

const compareFunc = require("compare-func");

const typeMap = {
  feat: "",
  fix: "🐛",
  docs: "📝",
  style: "💄",
  ui: "🎨",
  perf: "",
  refactor: "🧩",
  test: "",
  revert: "",
  build: "👷",
  ci: "🔩",
  chore: "🧱",
};

module.exports = {
  writerOpts: {
    transform: (commit, context) => {
      let discard = true;
      const issues = [];

      commit.notes.forEach((note) => {
        note.title = "BREAKING CHANGES";
        discard = false;
      });

      if (commit.type && commit.type !== null) {
        commit.type = typeMap[commit.type] + " " + commit.type;
      } else return;

      if (commit.scope === "*") {
        commit.scope = "";
      }

      commit.subject = `${commit.type}: ` + commit.subject;

      // if (commit?.authorName) {
      //   commit.subject += `-- ${commit.authorName}.`
      // }

      if (typeof commit.hash === "string") {
        commit.hash = commit.hash.substring(0, 7);
      }

      if (typeof commit.subject === "string") {
        let url = context.repository
          ? `${context.host}/${context.owner}/${context.repository}`
          : context.repoUrl;

        // console.log('==>', commit)

        if (url && commit.footer !== null) {
          // url = `${url}/issues/`
          // Issue URLs.
          const regexJIRA = /#([0-9A-Za-z]+-[0-9]+)/g;
          const regexZENTAO = /#([0-9]+)/g;
          // JIRA issues
          if (regexJIRA.test(commit.footer)) {
            let issues = commit.footer.match(regexJIRA);
            let issuesTxt = issues
              .map((issue) => {
                return `[${issue}](http://10.8.40.130:8081/browse/${issue.replace(
                  "#",
                  ""
                )})`;
              })
              .join("");

            commit.subject = commit.subject + "(closes " + issuesTxt + "";

            // commit.footer = commit.footer.replace(/#([0-9A-Za-z]+-[0-9]+)/g, (match, issue) => {
            //   console.log('==>', match, issue)
            // issues.push(issue)
            // return `[#${issue}](${url}${issue})`
            // })
          }

          // 禅道 issues
          if (regexZENTAO.test(commit.footer)) {
            let issues = commit.footer.match(regexZENTAO);
            let issuesTxt = issues
              .map((issue) => {
                return `[${issue}](http://10.8.8.162/zentao/bug-view-${issue.replace(
                  "#",
                  ""
                )}.html)`;
              })
              .join("");

            commit.subject = commit.subject + "(closes " + issuesTxt + "";
          }
        }

        if (context.host) {
          // User URLs.
          commit.subject = commit.subject.replace(
            /\B@([a-z0-9](?:-?[a-z0-9/]){0,38})/g,
            (_, username) => {
              if (username.includes("/")) {
                return `@${username}`;
              }
              return `[@${username}](${context.host}/${username})`;
            }
          );
        }
      }

      // remove references that already appear in the subject
      // commit.references = commit.references.filter(reference => {
      //   if (issues.indexOf(reference.issue) === -1) {
      //     return false
      //   }
      //   return false
      // })
      commit.references = "";
      return commit;
    },
    groupBy: "committerDate",
    commitGroupsSort: "type",
    commitsSort: ["scope", "subject"],
    noteGroupsSort: "title",
    notesSort: compareFunc,
  },
};

修改 package.json 中 scripts 字段

{
  "scripts": {
     "changelog": "conventional-changelog -p custom-config -i CHANGELOG.md -s -r 0  -n ./changelog.config.js"
  }
}

然后就可以生成下面的样子

Untitled

参考文档:

补充说明

以上只针对单个仓库只有单个项目的情况,如果一个仓库有多个独立项目,比如下面这种:

(以喻支付仓库为例)

就需要在仓库根目录下,新增package.json文件,然后 npm install 安装 commitizen,husky 和 commitlint 等操作(就是把上面所有的操作放到仓库根目录),才能够校验 commit。

这种情况下,每个子项目是不需要任何安装和配置的

(完)