前端开发规范(三)-Git规范化提交

前言

在开发团队协作中,“开发规范” 是经常被讨论的话题。当然,除了代码上的规范,还有一个很重要的规范就是“提交规范”。

规范化提交的目的:

  • 提交统一的、有规则的信息;而不是混乱的、看不懂是什么意思的信息
  • 可以提供更加明朗的历史信息,便于后续快速定位问题、代码回滚等的操作
  • 可以自动化生成changelog

husky

husky 是一个 Git-Hooks 工具. 那么 hooks 是什么呢 ?

"hooks" 直译是 “钩子”,它并不仅是 react,甚至不仅是前端界的专用术语,而是整个行业所熟知的用语。通常指:系统运行到某一时期时,会调用被注册到该时机的回调函数。

规范化提交第一步就是要在 git commit之前先做一次Lint校验,限制不规范代码的提交,那 husky 这个工具就能做到。husky 继承了Git下所有的钩子,在触发pre-commit钩子的时候,阻止不合法的 commit、push 等等。

husky 配置

1、初始化 git 目录

如果没有先初始化 git, 需要重新装husky。

git init -y

2、安装 husky

# pnpm 安装
pnpm add husky  -D -w

# or npm 安装
npm install husky -D

3、添加 husky 脚本

package.json 文件 scripts 中手动添加

"prepare": "husky install"

或者直接执行命令添加

npm set-script prepare "husky install"

prepare 脚本会在npm install(不带参数)之后自动执行。也就是说当我们执行npm install安装完项目依赖后会执行 husky install命令,该命令会创建.husky/目录并指定该目录为git hooks所在的目录。

4、执行 npm run prepare

执行之后会发现项目根目录多了个.husky的目录及文件。

在该目录下添加 pre-commitcommit-msg 文件。

lint-staged

如果直接在 pre-commit 里执行 eslint --fix ,可能只是修改了一个文件,但依然会检查项目中所有需要检验的文件,体验非常的不友好。导致的问题就是:每次提交代码,无论改动多少,都会检查整个项目下的文件,当项目大了之后,检查速度也会变得越来越慢。

解决这个问题就需要用到lint-staged,lint-staged能够让lint只检测暂存区的文件

# pnpm 安装
pnpm add lint-staged  -D -w

# or npm 安装
npm install lint-staged -D

1、在 package.json 文件添加配置

"lint-staged": {
    "*.{js,jsx,ts,tsx,vue}": "eslint --fix"
}

2、在 pre-commit 添加脚本

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

pnpm exec lint-staged

# 当然也可以配合 prettier pretty-quick 这个两个插件来做代码格式化的修复,不需要可不用
# pnpm exec pretty-quick --staged

这样在使用 git commit 之前,会先执行 ./.husky/pre-commit 下的脚本,实现提交前的拦截修复

commitlint

在多人协作的项目中,每个人的 Commit message 可能都会不同,没有很明确的限定哪些是新增功能,哪些修复bug,哪些优化代码等等,那这时候就需要装一些工具来规范Commit message

需要使用的插件:

  • @commitlint/cli
  • @commitlint/config-conventional
  • czg
# pnpm 安装
pnpm add @commitlint/cli @commitlint/config-conventional czg  -D -w

# or npm 安装
npm install @commitlint/cli @commitlint/config-conventional czg -D

1、在 package.json 文件添加配置

"config": {
    "commitizen": {
      "path": "./node_modules/cz-git"
    }
}

2、在 package.json 文件添加脚本

 "cz": "git add . && czg",

3、在根目录加上 commitlint.config.js

module.exports = {
    extends: ['@commitlint/config-conventional'],
    rules: {
        // 'scope-enum': [2, 'always', scopes],
        'body-leading-blank': [1, 'always'],
        'footer-leading-blank': [1, 'always'],
        'header-max-length': [2, 'always', 72],
        'scope-case': [2, 'always', 'lower-case'],
        'subject-case': [
            1,
            'never',
            ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
        ],
        'subject-empty': [2, 'never'],
        'subject-full-stop': [2, 'never', '.'],
        'type-case': [2, 'always', 'lower-case'],
        'type-empty': [2, 'never'],
        'type-enum': [
            2,
            'always',
            [
                'feat',
                'fix',
                'style',
                'improvement',
                'perf',
                'build',
                'chore',
                'ci',
                'docs',
                'test',
                'refactor',
                'revert',
            ],
        ],
        'subject-full-stop': [0, 'never'],
        'subject-case': [0, 'never'],
    }
}

4、在 commit-msg添加脚本

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

pnpm exec commitlint --config commitlint.config.js --edit "${1}"

5、执行 npm run cz 就能看到效果了

自动生成 CHANGELOG

1、全局安装 conventional-changelog-cli 插件

npm install conventional-changelog-cli -g

2、在 package.json 文件添加脚本

"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 -n ./changelog-option.js"

参数说明:

  • -p angular,表示changelog标准为angular,现在有angular, atom, codemirror, ember, eslint, express, jquery 等项目的标准可供选择
  • -i CHANGELOG.md,表示指定输出的文件名称
  • -s 输出到infile,这样就不需要指定与outfile相同的文件
  • -r 从最新的版本的生成,默认值为1。如果为0,则将重新生成整个更新日志并覆盖输出文件
  • -n ./changelog-option.js 表示指定自定义配置文件

3、执行 npm run changelog 就可以在根目录下看到 CHANGELOG.md 文件了

4、如果想自定义输出模板,可以在配置 changelog-option.js 文件

在根目录新建 changelog-option.js文件 和 templates/commit.hbs 文件

changelog-option.js文件:

const readFileSync = require('fs').readFileSync
const join = require('path').join

module.exports = {
    gitRawCommitsOpts: {
        format: '%B%n-hash-%n%H%n-gitTags-%n%d%n-committerDate-%n%ci%n-authorName-%n%an%n-authorEmail-%n%ae',
    },
    writerOpts: {
        commitPartial: readFileSync(
            join(__dirname, 'templates/commit.hbs'),
            'utf-8'
        ),
        // mainTemplate:  readFileSync(join(__dirname, 'templates/template.hbs'),'utf-8'),
        // headerPartial: readFileSync(join(__dirname, 'templates/header.hbs'),'utf-8'),
        // footerPartial: readFileSync(join(__dirname, 'templates/footer.hbs'),'utf-8'),
        ...getWriterOpts(),
    },
}

function getWriterOpts() {
    return {
        transform: (commit, context) => {
            let discard = true
            const issues = []

            commit.notes.forEach((note) => {
                note.title = 'BREAKING CHANGES'
                discard = false
            })
            if (commit.type === 'feat') {
                commit.type = '✨ Features | 新功能'
            } else if (commit.type === 'fix') {
                commit.type = '🐛 Bug Fixes | Bug 修复'
            } else if (commit.type === 'perf') {
                commit.type = '⚡ Performance Improvements | 性能优化'
            } else if (commit.type === 'revert' || commit.revert) {
                commit.type = '⏪ Reverts | 回退'
            } else if (commit.type === 'improvement') {
                commit.type = '💩 Improvement | 优化改进'
            } else if (commit.type === 'style') {
                commit.type = '💄 Styles | 风格'
            } else if (discard) {
                return
            } else if (commit.type === 'docs') {
                commit.type = '📝 Documentation | 文档'
            } else if (commit.type === 'refactor') {
                commit.type = '♻ Code Refactoring | 代码重构'
            } else if (commit.type === 'test') {
                commit.type = '✅ Tests | 测试'
            } else if (commit.type === 'build') {
                commit.type = '👷 Build System | 构建'
            } else if (commit.type === 'ci') {
                commit.type = '🔧 Continuous Integration | CI 配置'
            } else if (commit.type === 'chore') {
                commit.type = '🎫 Chores | 其他更新'
            }

            if (commit.scope === '*') {
                commit.scope = ''
            }

            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

                if (url) {
                    url = `${url}/issues/`
                    // Issue URLs.
                    commit.subject = commit.subject.replace(
                        /#([0-9]+)/g,
                        (_, issue) => {
                            issues.push(issue)
                            return `[#${issue}](${url}${issue})`
                        }
                    )
                }

                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 true
                }
                return false
            })
            return commit
        },
        groupBy: 'type',
        commitGroupsSort: 'title',
        commitsSort: ['scope', 'subject'],
        noteGroupsSort: 'title',
    }
}

templates/commit.hbs文件:

- {{header}}

{{~!-- commit link --}} {{#if @root.linkReferences~}}
  ([#{{hash}}](
  {{~#if @root.repository}}
    {{~#if @root.host}}
      {{~@root.host}}/
    {{~/if}}
    {{~#if @root.owner}}
      {{~@root.owner}}/
    {{~/if}}
    {{~@root.repository}}
  {{~else}}
    {{~@root.repoUrl}}
  {{~/if}}/
  {{~@root.commit}}/{{hash}}) by @{{authorName}})
{{~else}}
  {{~hash}}
{{~/if}}

{{~!-- commit references --}}
{{~#if references~}}
  , closes
  {{~#each references}} {{#if @root.linkReferences~}}
    [
    {{~#if this.owner}}
      {{~this.owner}}/
    {{~/if}}
    {{~this.repository}}#{{this.issue}}](
    {{~#if @root.repository}}
      {{~#if @root.host}}
        {{~@root.host}}/
      {{~/if}}
      {{~#if this.repository}}
        {{~#if this.owner}}
          {{~this.owner}}/
        {{~/if}}
        {{~this.repository}}
      {{~else}}
        {{~#if @root.owner}}
          {{~@root.owner}}/
        {{~/if}}
          {{~@root.repository}}
        {{~/if}}
    {{~else}}
      {{~@root.repoUrl}}
    {{~/if}}/
    {{~@root.issue}}/{{this.issue}})
  {{~else}}
    {{~#if this.owner}}
      {{~this.owner}}/
    {{~/if}}
    {{~this.repository}}#{{this.issue}}
  {{~/if}}{{/each}}
{{~/if}}

5、生成效果如下

changelog.jpg

流程说明

  • commit(规范化提交)
  • release(发布版本)
  • tag(创建tag,提交代码到远程仓库)
  • npm run changelog (生成 CHANGELOG)
  • 提交 CHANGELOG.MD

参考文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容