git提交规范

我们使用husky检测git钩子,lint-staged规范化暂存区代码,commitlint规范化提交信息。

1 git钩子介绍

Git 有很多的 hooks, 让我们在不同的阶段,对代码进行不同的操作,控制提交到仓库的代码的规范性,和准确性, 以下只是几个常用的钩子

git钩子

描述

pre-commit

判断提交的代码是否符合规范

commit-msg

判断 commit 信息是否符合规范

pre-push

执行测试,避免对以前的内容造成影响

2 工具介绍

  • husky:操作git钩子的工具
  • lint-staged:本地暂存代码检查工具
  • commitlint:提交信息校验工具
  • commitizen:辅助提交信息 ,全局安装后可以使用cz命令,选项式提交git

3 安装和配置

3.1 husky

官方文档

yarn add -D husky lint-staged

package.json中添加脚本

npm set-script prepare "husky install"

初始化husky,将git hooks钩子交由,husky执行。会在根目录创建 .husky 文件夹

yarn prepare

3.2 lint-staged

检测pre-commit钩子,执行 npx lint-staged 指令

npx husky add .husky/pre-commit "npx lint-staged"

lint-staged配置

可以在根目录创建 .lintstagedrc.json文件控制检查和操作方式

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

也可以在package.json中配置:

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

3.3 commitlint

官网

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

添加配置文件commitlint.config.js

// commitlint.config.js
/** @type {import('cz-git').UserConfig} */

module.exports = {
    rules: {
        // @see: https://commitlint.js.org/#/reference-rules
        // 配置规则:
        //     每个配置是一个个键值对,键值是array类型:
        //         第一个参数表示:重要等级,0表示关闭规则,1表示warning,2表示error
        //         第二个参数表示:应用与否,always | nerver
        //         第三个参数表示:配置规则的值
        // commit message的结构如下:
        //     <type>[optional scope]: <description>
        //     [optional body]
        //     [optional footer(s)]
        // 简短描述(subject)
        // 详细描述(body)
        'body-leading-blank': [1, 'always'], // body开头空行
        'body-max-line-length': [2, 'always', 100], // body最大内容长度
        'footer-leading-blank': [1, 'always'], // footer开头空行
        'footer-max-line-length': [2, 'always', 100], // footer最大内容长度
        'header-max-length': [2, 'always', 100], // header最大长度
        // subject单词格式
        'subject-case': [
            2,
            'never',
            ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
        ],
        'subject-empty': [2, 'never'], // subject是否为空
        'subject-full-stop': [2, 'never', '.'], // subject中止符
        'type-case': [2, 'always', 'lower-case'], // type单词格式
        'type-empty': [2, 'never'], // type是否为空
        // type可选值
        'type-enum': [
            2,
            'always',
            [
                'release',
                'build',
                'update',
                'docs',
                'add',
                'fix',
                'perf',
                'refactor',
                'revert',
                'style',
                'test',
            ],
        ],
    },
    prompt: {
        alias: {
            fd: 'docs: :memo: 文档更新',
            uv: 'release: :bookmark: update version',
        },
        messages: {
            type: '选择你要提交的类型 :',
            scope: '选择一个提交范围(可选):',
            customScope: '请输入自定义的提交范围 :',
            subject: '填写简短精炼的变更描述 :\n',
            body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
            breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
            footerPrefixsSelect: '选择关联issue前缀(可选):',
            customFooterPrefixs: '输入自定义issue前缀 :',
            footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
            confirmCommit: '是否提交或修改commit ?',
        },
        types: [
            {
                value: 'add',
                name: 'add:      ✨  新增功能',
                emoji: ':sparkles:',
            },
            {
                value: 'update',
                name: 'update:   🚀  更新',
                emoji: ':rocket:',
            },
            {
                value: 'fix',
                name: 'fix:      🐛  修复缺陷',
                emoji: ':bug:',
            },
            {
                value: 'release',
                name: 'release:  🔖  发布新版本',
                emoji: ':bookmark:',
            },
            {
                value: 'style',
                name: 'style:    💄  代码格式 (不影响功能,例如空格、分号等格式修正)',
                emoji: ':lipstick:',
            },
            {
                value: 'refactor',
                name: 'refactor: ♻️   代码重构 (不包括 bug 修复、功能新增)',
                emoji: ':recycle:',
            },
            {
                value: 'perf',
                name: 'perf:     ⚡️  性能优化',
                emoji: ':zap:',
            },
            {
                value: 'test',
                name: 'test:     ✅  添加疏漏测试或已有测试改动',
                emoji: ':white_check_mark:',
            },
            {
                value: 'build',
                name: 'build:    📦️  构建相关 (构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)',
                emoji: ':package:',
            },
            {
                value: 'docs',
                name: 'docs:     📝  文档更新',
                emoji: ':memo:',
            },
            {
                value: 'revert',
                name: 'revert:   ⏪️  回滚 commit',
                emoji: ':rewind:',
            },
        ],
        useEmoji: true, // 是否开启 commit message 带有 Emoji 字符。
        emojiAlign: 'center', // 设置 Emoji 字符 的 位于头部位置
        themeColorCode: '', // 设置提示查询器主题颜色, cyan青色
        scopes: [], // 自定义选择 模块范围 命令行显示信息
        allowCustomScopes: true, // 是否在选择 模块范围 显示自定义选项(custom)
        allowEmptyScopes: true, // 是否在选择 模块范围 显示为空选项(empty)
        customScopesAlign: 'bottom', // 设置 选择范围 中 为空选项(empty) 和 自定义选项(custom) 的 位置
        customScopesAlias: 'custom', // 自定义 选择范围 中 自定义选项(custom) 在命令行中显示的 名称
        emptyScopesAlias: 'empty', // 自定义 选择范围 中 为空选项(empty) 在命令行中显示的 名称
        upperCaseSubject: false, // 是否自动将简短描述(subject)第一个字符进行大写处理
        markBreakingChangeMode: false, // 添加额外的问题重大变更(BREAKING CHANGES)提问,询问是否需要添加 "!" 标识于
        allowBreakingChanges: ['feat', 'fix'], // 允许出现 重大变更(BREAKING CHANGES)的特定 type
        breaklineNumber: 100, // 详细描述(body)和重大变更(BREAKING CHANGES)中根据字符超过该数值自动换行
        breaklineChar: '|', // 详细描述(body)和重大变更(BREAKING CHANGES)中换行字符
        skipQuestions: ['scope', 'body', 'breaking', 'footerPrefix', 'footer'], // 自定义选择指定的问题不显示
        // 自定义选择issue前缀
        issuePrefixs: [
            // 如果使用 gitee 作为开发管理
            { value: 'link', name: 'link:     链接 ISSUES 进行中' },
            { value: 'closed', name: 'closed:   标记 ISSUES 已完成' },
        ],
        customIssuePrefixsAlign: 'top', // 设置 选择 issue 前缀 中 跳过选项(skip) 和 自定义选项(custom) 的 位置
        emptyIssuePrefixsAlias: 'skip', // 自定义 选择 issue 前缀 中 跳过选项(skip) 在命令行中显示的 名称
        customIssuePrefixsAlias: 'custom', // 自定义 选择 issue 前缀 中 自定义选项(custom) 在命令行中显示的 名称
        allowCustomIssuePrefixs: true, // 是否在选择 ISSUE 前缀 显示自定义选项(custom)
        allowEmptyIssuePrefixs: true, // 是否在选择 ISSUE 前缀 显示为跳过选项(skip)
        confirmColorize: true, // 确定提交中模板 commit message 是否着色
        maxHeaderLength: Infinity, // 定义commit message中的 header 长度, 给予在命令行中的校验信息
        maxSubjectLength: Infinity, // 定义commit message中的 subject 长度, 给予在命令行中的校验信息
        minSubjectLength: 0, // 定义commit message中的 subject 长度, 给予在命令行中的校验信息
        scopeOverrides: undefined, // 自定义选择了特定类型后 覆盖模块范围 命令行显示信息
        defaultBody: '', // 在 详细描述 中是否使用显示默认值
        defaultIssues: '', // 在 输入ISSUE 中是否使用显示默认值
        defaultScope: '', // 如果 defaultScope 与在选择范围列表项中的 value 相匹配就会进行星标置顶操作。
        defaultSubject: '', // 在 简短描述 中是否使用显示默认值
    },
}

类型

描述

release

发布新版

build

编译相关的修改,例如发布版本、对项目构建或者依赖的改动

update

更新

docs

文档修改

add

新特性、新功能

fix

修改bug

perf

优化相关,比如提升性能、体验

refactor

代码重构

revert

回滚到上一个版本

style

代码格式修改, 注意不是 css 修改

test

测试用例修改

3.4 commitizen

什么是commitizen:基于Node.js的 git commit 命令行工具,辅助生成标准化规范化的 commit message。

什么是适配器:适配器是为commitizen提供提示性交互的工具,提问功能就是适配器里面的

注意:要使用cz命令,必须全局安装commitizen

注意:要使用cz命令,必须全局安装commitizen

注意:要使用cz命令,必须全局安装commitizen

npm i -g commitizen

方式一:package.json 下 config.commitizen 下添加自定义配置,但过量的配置项会导致 package.json 臃肿,不介绍了。

方式二: (推荐)  使用适配器

commitlint 进行联动给予校验信息**,所以可以编写于 commitlint 配置文件之中。例如: (⇒ 配置模板)

cz-git适配器的效果挺好的,我们由于有特殊的提问需求,于是没采用cz-git适配器,但是在我们自己的适配器里面,实现了类似的功能。

commitlint.config.js中添加prompt对象:

// commitlint.config.js

module.exports = {
    prompt: {
        alias: {
            fd: 'docs: :memo: 文档更新',
            uv: 'release: :bookmark: update version',
        },
        messages: {
            type: '选择你要提交的类型 :',
            scope: '选择一个提交范围(可选):',
            customScope: '请输入自定义的提交范围 :',
            subject: '填写简短精炼的变更描述 :\n',
            body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
            breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
            footerPrefixsSelect: '选择关联issue前缀(可选):',
            customFooterPrefixs: '输入自定义issue前缀 :',
            footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
            confirmCommit: '是否提交或修改commit ?',
        },
        types: [
            {
                value: 'add',
                name: 'add:      ✨  新增功能',
                emoji: ':sparkles:',
            },
            {
                value: 'update',
                name: 'update:   🚀  更新',
                emoji: ':rocket:',
            },
            {
                value: 'fix',
                name: 'fix:      🐛  修复缺陷',
                emoji: ':bug:',
            },
            {
                value: 'release',
                name: 'release:  🔖  发布新版本',
                emoji: ':bookmark:',
            },
            {
                value: 'style',
                name: 'style:    💄  代码格式 (不影响功能,例如空格、分号等格式修正)',
                emoji: ':lipstick:',
            },
            {
                value: 'refactor',
                name: 'refactor: ♻️   代码重构 (不包括 bug 修复、功能新增)',
                emoji: ':recycle:',
            },
            {
                value: 'perf',
                name: 'perf:     ⚡️  性能优化',
                emoji: ':zap:',
            },
            {
                value: 'test',
                name: 'test:     ✅  添加疏漏测试或已有测试改动',
                emoji: ':white_check_mark:',
            },
            {
                value: 'build',
                name: 'build:    📦️  构建相关 (构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)',
                emoji: ':package:',
            },
            {
                value: 'docs',
                name: 'docs:     📝  文档更新',
                emoji: ':memo:',
            },
            {
                value: 'revert',
                name: 'revert:   ⏪️  回滚 commit',
                emoji: ':rewind:',
            },
        ],
        useEmoji: true, // 是否开启 commit message 带有 Emoji 字符。
        emojiAlign: 'center', // 设置 Emoji 字符 的 位于头部位置
        themeColorCode: '', // 设置提示查询器主题颜色, cyan青色
        scopes: [], // 自定义选择 模块范围 命令行显示信息
        allowCustomScopes: true, // 是否在选择 模块范围 显示自定义选项(custom)
        allowEmptyScopes: true, // 是否在选择 模块范围 显示为空选项(empty)
        customScopesAlign: 'bottom', // 设置 选择范围 中 为空选项(empty) 和 自定义选项(custom) 的 位置
        customScopesAlias: 'custom', // 自定义 选择范围 中 自定义选项(custom) 在命令行中显示的 名称
        emptyScopesAlias: 'empty', // 自定义 选择范围 中 为空选项(empty) 在命令行中显示的 名称
        upperCaseSubject: false, // 是否自动将简短描述(subject)第一个字符进行大写处理
        markBreakingChangeMode: false, // 添加额外的问题重大变更(BREAKING CHANGES)提问,询问是否需要添加 "!" 标识于
        allowBreakingChanges: ['feat', 'fix'], // 允许出现 重大变更(BREAKING CHANGES)的特定 type
        breaklineNumber: 100, // 详细描述(body)和重大变更(BREAKING CHANGES)中根据字符超过该数值自动换行
        breaklineChar: '|', // 详细描述(body)和重大变更(BREAKING CHANGES)中换行字符
        skipQuestions: ['scope', 'body', 'breaking', 'footerPrefix', 'footer'], // 自定义选择指定的问题不显示
        // 自定义选择issue前缀
        issuePrefixs: [
            // 如果使用 gitee 作为开发管理
            { value: 'link', name: 'link:     链接 ISSUES 进行中' },
            { value: 'closed', name: 'closed:   标记 ISSUES 已完成' },
        ],
        customIssuePrefixsAlign: 'top', // 设置 选择 issue 前缀 中 跳过选项(skip) 和 自定义选项(custom) 的 位置
        emptyIssuePrefixsAlias: 'skip', // 自定义 选择 issue 前缀 中 跳过选项(skip) 在命令行中显示的 名称
        customIssuePrefixsAlias: 'custom', // 自定义 选择 issue 前缀 中 自定义选项(custom) 在命令行中显示的 名称
        allowCustomIssuePrefixs: true, // 是否在选择 ISSUE 前缀 显示自定义选项(custom)
        allowEmptyIssuePrefixs: true, // 是否在选择 ISSUE 前缀 显示为跳过选项(skip)
        confirmColorize: true, // 确定提交中模板 commit message 是否着色
        maxHeaderLength: Infinity, // 定义commit message中的 header 长度, 给予在命令行中的校验信息
        maxSubjectLength: Infinity, // 定义commit message中的 subject 长度, 给予在命令行中的校验信息
        minSubjectLength: 0, // 定义commit message中的 subject 长度, 给予在命令行中的校验信息
        scopeOverrides: undefined, // 自定义选择了特定类型后 覆盖模块范围 命令行显示信息
        defaultBody: '', // 在 详细描述 中是否使用显示默认值
        defaultIssues: '', // 在 输入ISSUE 中是否使用显示默认值
        defaultScope: '', // 如果 defaultScope 与在选择范围列表项中的 value 相匹配就会进行星标置顶操作。
        defaultSubject: '', // 在 简短描述 中是否使用显示默认值
    },
}

3.5 编写adaptor

参考:cz-gitcommitizen/cz-clicz-customizableInquirer.js

修改整合了cz-customizable的代码:

先安装依赖:

yarn add -D editor temp word-wrap find-config

cz_customizable.js

// cz_customizable.js
// 依赖:editor temp word-wrap lodash find-config
// yarn add -D editor temp word-wrap lodash find-config

/* eslint-disable global-require */
// Inspired by: https://github.com/commitizen/cz-conventional-changelog and https://github.com/commitizen/cz-cli

const editor = require('editor')
const temp = require('temp').track()
const fs = require('fs')
const _ = require('lodash')
const wrap = require('word-wrap')
const findConfig = require('find-config')
const path = require('path')

const log = console
const readConfigFile = (CZ_CONFIG_NAME = 'commitlint.config.js') => {
    // First try to find the .cz-config.js config file
    // It seems like find-config still locates config files in the home directory despite of the home:false prop.
    const czConfig = findConfig.require(CZ_CONFIG_NAME, { home: false })

    if (czConfig?.prompt) {
        return czConfig.prompt
    }

    // fallback to locating it using the config block in the nearest package.json
    let pkg = findConfig('package.json', { home: false })

    if (pkg) {
        const pkgDir = path.dirname(pkg)
        pkg = require(pkg)
        if (
            pkg.config &&
            pkg.config['cz-customizable'] &&
            pkg.config['cz-customizable'].config
        ) {
            // resolve relative to discovered package.json
            const pkgPath = path.resolve(
                pkgDir,
                pkg.config['cz-customizable'].config
            )
            log.info(
                '>>> Using cz-customizable config specified in your package.json: ',
                pkgPath
            )
            return require(pkgPath)
        }
    }

    /* istanbul ignore next */
    log.error(
        'Unable to find a configuration file. Please refer to documentation to learn how to set up: https://github.com/leonardoanalista/cz-customizable#steps "'
    )
    return null
}

const defaultSubjectSeparator = ': '
const defaultMaxLineWidth = 100
const defaultBreaklineChar = '|'

const isNotWip = (answers) => answers.type.toLowerCase() !== 'wip'

const isValidateTicketNo = (value, config) => {
    if (!value) {
        return !config.isTicketNumberRequired
    }
    if (!config.ticketNumberRegExp) {
        return true
    }
    const reg = new RegExp(config.ticketNumberRegExp)
    if (value.replace(reg, '') !== '') {
        return false
    }
    return true
}

const getPreparedCommit = (context) => {
    let message = null

    // eslint-disable-next-line no-undef
    let preparedCommit = getPreviousCommit()

    if (preparedCommit) {
        preparedCommit = preparedCommit
            .replace(/^#.*/gm, '')
            .replace(/^\s*[\r\n]/gm, '')
            .replace(/[\r\n]$/, '')
            .split(/\r\n|\r|\n/)

        if (preparedCommit.length && preparedCommit[0]) {
            if (context === 'subject') [message] = preparedCommit
            else if (context === 'body' && preparedCommit.length > 1) {
                preparedCommit.shift()
                message = preparedCommit.join('|')
            }
        }
    }

    return message
}
const addTicketNumber = (ticketNumber, config) => {
    if (!ticketNumber) {
        return ''
    }

    if (config.ticketNumberPrefix) {
        return `${config.ticketNumberPrefix + ticketNumber.trim()} `
    }

    return `${ticketNumber.trim()} `
}

const addScope = (scope, config) => {
    const separator = _.get(config, 'subjectSeparator', defaultSubjectSeparator)

    if (!scope) return separator // it could be type === WIP. So there is no scope

    return `(${scope.trim()})${separator}`
}

const addSubject = (subject) => _.trim(subject)

const addType = (type, config) => {
    const prefix = _.get(config, 'typePrefix', '')
    const suffix = _.get(config, 'typeSuffix', '')

    return _.trim(`${prefix}${type}${suffix}`)
}

const addBreaklinesIfNeeded = (value, breaklineChar = defaultBreaklineChar) =>
    value.split(breaklineChar).join('\n').valueOf()

const escapeSpecialChars = (result) => {
    const specialChars = ['`', '"', '\\$', '!', '<', '>', '&']
    let newResult = result

    specialChars.forEach((item) => {
        // If user types `feat: "string"`, the commit preview should show `feat: \"string\"`.
        // Don't worry. The git log will be `feat: "string"`
        newResult = newResult.replace(new RegExp(item, 'g'), `\\${item}`)
    })

    return newResult
}

const addFooter = (footer, config) => {
    if (config && config.footerPrefix === '') return `\n\n${footer}`

    const footerPrefix =
        config && config.footerPrefix ? config.footerPrefix : 'ISSUES CLOSED:'

    return `\n\n${footerPrefix} ${addBreaklinesIfNeeded(
        footer,
        config.breaklineChar
    )}`
}
const buildCommit = (answers, config, emoji) => {
    const wrapOptions = {
        trim: true,
        newline: '\n',
        indent: '',
        width: defaultMaxLineWidth,
    }

    // Hard limit this line
    // eslint-disable-next-line max-len
    const head =
        addType(answers.type, config) +
        addScope(answers.scope, config) +
        addTicketNumber(answers.ticketNumber, config) +
        ` ${emoji} ` +
        addSubject(answers.subject.slice(0, config.subjectLimit))

    // Wrap these lines at 100 characters
    let body = wrap(answers.body, wrapOptions) || ''
    body = addBreaklinesIfNeeded(body, config.breaklineChar)

    const breaking = wrap(answers.breaking, wrapOptions)
    const footer = wrap(answers.footer, wrapOptions)

    let result = head
    if (body) {
        result += `\n\n${body}`
    }
    if (breaking) {
        const breakingPrefix =
            config && config.breakingPrefix
                ? config.breakingPrefix
                : 'BREAKING CHANGE:'
        result += `\n\n${breakingPrefix}\n${breaking}`
    }
    if (footer) {
        result += addFooter(footer, config)
    }

    return escapeSpecialChars(result)
}

const myQuestions = {
    getQuestions(config, cz) {
        // normalize config optional options
        const scopeOverrides = config.scopeOverrides || {}
        const messages = config.messages || {}
        const skipQuestions = config.skipQuestions || []
        const skipEmptyScopes = config.skipEmptyScopes || false

        messages.type =
            messages.type || "Select the type of change that you're committing:"
        messages.scope =
            messages.scope || '\nDenote the SCOPE of this change (optional):'
        messages.customScope =
            messages.customScope || 'Denote the SCOPE of this change:'
        if (!messages.ticketNumber) {
            if (config.ticketNumberRegExp) {
                messages.ticketNumber =
                    messages.ticketNumberPattern ||
                    `Enter the ticket number following this pattern (${config.ticketNumberRegExp})\n`
            } else {
                messages.ticketNumber = 'Enter the ticket number:\n'
            }
        }
        messages.subject =
            messages.subject ||
            'Write a SHORT, IMPERATIVE tense description of the change:\n'
        messages.body =
            messages.body ||
            'Provide a LONGER description of the change (optional). Use "|" to break new line:\n'
        messages.breaking =
            messages.breaking || 'List any BREAKING CHANGES (optional):\n'
        messages.footer =
            messages.footer ||
            'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n'
        messages.confirmCommit =
            messages.confirmCommit ||
            'Are you sure you want to proceed with the commit above?'

        let questions = [
            {
                type: 'list',
                name: 'type',
                message: messages.type,
                choices: config.types,
            },
            {
                type: 'list',
                name: 'scope',
                message: messages.scope,
                choices(answers) {
                    let scopes = []
                    if (scopeOverrides[answers.type]) {
                        scopes = scopes.concat(scopeOverrides[answers.type])
                    } else {
                        scopes = scopes.concat(config.scopes)
                    }
                    if (config.allowCustomScopes || scopes.length === 0) {
                        scopes = scopes.concat([
                            new cz.Separator(),
                            { name: 'empty', value: false },
                            { name: 'custom', value: 'custom' },
                        ])
                    }
                    return scopes
                },
                when(answers) {
                    let hasScope = false
                    if (scopeOverrides[answers.type]) {
                        hasScope = !!(scopeOverrides[answers.type].length > 0)
                    } else {
                        hasScope = !!(config.scopes && config.scopes.length > 0)
                    }
                    if (!hasScope) {
                        // TODO: Fix when possible
                        // eslint-disable-next-line no-param-reassign
                        answers.scope = skipEmptyScopes ? '' : 'custom'
                        return false
                    }
                    return isNotWip(answers)
                },
            },
            {
                type: 'input',
                name: 'scope',
                message: messages.customScope,
                when(answers) {
                    return answers.scope === 'custom'
                },
            },
            {
                type: 'input',
                name: 'ticketNumber',
                message: messages.ticketNumber,
                when() {
                    return !!config.allowTicketNumber // no ticket numbers allowed unless specifed
                },
                validate(value) {
                    return isValidateTicketNo(value, config)
                },
            },
            {
                type: 'input',
                name: 'subject',
                message: messages.subject,
                default:
                    config.usePreparedCommit && getPreparedCommit('subject'),
                validate(value) {
                    if (value.length == 0) return '描述不能为空'
                    const limit = config.subjectLimit || 100
                    if (value.length > limit) {
                        return `Exceed limit: ${limit}`
                    }
                    return true
                },
                filter(value) {
                    const upperCaseSubject = config.upperCaseSubject || false

                    return (
                        (upperCaseSubject
                            ? value.charAt(0).toUpperCase()
                            : value.charAt(0).toLowerCase()) + value.slice(1)
                    )
                },
            },
            {
                type: 'input',
                name: 'body',
                message: messages.body,
                default: config.usePreparedCommit && getPreparedCommit('body'),
            },
            {
                type: 'input',
                name: 'breaking',
                message: messages.breaking,
                when(answers) {
                    // eslint-disable-next-line max-len
                    if (
                        config.askForBreakingChangeFirst ||
                        (config.allowBreakingChanges &&
                            config.allowBreakingChanges.indexOf(
                                answers.type.toLowerCase()
                            ) >= 0)
                    ) {
                        return true
                    }
                    return false // no breaking changes allowed unless specifed
                },
            },
            {
                type: 'input',
                name: 'footer',
                message: messages.footer,
                when: isNotWip,
            },
            {
                type: 'list',
                name: 'release',
                message: messages.release,
                choices: [
                    { key: 'n', name: 'No', value: 'no' },
                    { key: 'y', name: 'Yes', value: 'yes' },
                ],
            },
            {
                type: 'expand',
                name: 'confirmCommit',
                choices: [
                    { key: 'y', name: '确认', value: 'yes' },
                    { key: 'n', name: '中断提交', value: 'no' },
                    { key: 'e', name: '编辑提交信息', value: 'edit' },
                ],
                default: 0,
                message(answers) {
                    let emoji = config.types.find((e) => {
                        return answers.type === e.value
                    })
                    if (emoji === undefined) {
                        emoji = ''
                    } else {
                        emoji = emoji.emoji
                    }
                    const SEP =
                        '###--------------------------------------------------------###'
                    log.info(
                        `\n${SEP}\n${buildCommit(answers, config, emoji)}${answers.release === 'yes' ? '  #release#' : ''
                        }\n${SEP}\n`
                    )
                    return messages.confirmCommit
                },
            },
        ]

        questions = questions.filter(
            (item) => !skipQuestions.includes(item.name)
        )

        if (config.askForBreakingChangeFirst) {
            const isBreaking = (oneQuestion) => oneQuestion.name === 'breaking'

            const breakingQuestion = _.filter(questions, isBreaking)
            const questionWithoutBreaking = _.reject(questions, isBreaking)

            questions = _.concat(breakingQuestion, questionWithoutBreaking)
        }

        return questions
    },
}

module.exports = {
    prompter(cz, commit) {
        const config = readConfigFile()
        config.subjectLimit = config.subjectLimit || 100
        log.info('除了首行,所有行将在100个字符位置折叠。')

        const questions = myQuestions.getQuestions(config, cz)

        cz.prompt(questions).then((answers) => {
            if (answers.release === 'yes') {
                // eslint-disable-next-line no-param-reassign
                answers.subject += '#release#'
            }
            let emoji = config.types.find((e) => {
                return answers.type === e.value
            })
            if (emoji === undefined) {
                emoji = ''
            } else {
                emoji = emoji.emoji
            }
            if (answers.confirmCommit === 'edit') {
                temp.open(null, (err, info) => {
                    /* istanbul ignore else */
                    if (!err) {
                        fs.writeSync(
                            info.fd,
                            buildCommit(answers, config, emoji)
                        )
                        fs.close(info.fd, () => {
                            editor(info.path, (code) => {
                                if (code === 0) {
                                    const commitStr = fs.readFileSync(
                                        info.path,
                                        {
                                            encoding: 'utf8',
                                        }
                                    )
                                    commit(commitStr)
                                } else {
                                    log.info(
                                        `你的Commit信息是:\n${buildCommit(
                                            answers,
                                            config,
                                            emoji
                                        )}`
                                    )
                                }
                            })
                        })
                    }
                })
            } else if (answers.confirmCommit === 'yes') {
                commit(buildCommit(answers, config, emoji))
            } else {
                log.info('已经取消Commit。')
            }
        })
    },
}

4 git换行符问题

4.1 知识背景

LF和CRLF都是换行符,在各操作系统下,换行符是不一样的,Linux/UNIX下是LF,而Windows下是CRLF,早期的MAC OS是CR,后来的OS X在更换内核后和UNIX一样也是LF。

Git 由大名鼎鼎的 Linus 开发,最初只可运行于 UNIX系统,因此推荐只将 UNIX 风格的换行符保存入库。但它也考虑到了跨平台协作的场景,并且提供了一个“换行符自动转换”功能。

为了避免出现CRLFLF混合的情况,可以选择配置“.gitattribute”文件来管理 Git 读取特定存储库中的行结束符的方式。 将此文件提交到存储库时,它将覆盖所有存储库贡献者的 core.autocrlf 设置。 这可确保所有用户的行为一致,而不管其 Git 设置和环境如何。

4.2 修改.gitattributes文件

* text eol=lf

# Denote all files that are truly binary and should not be modified.
*.png   binary
*.jpg   binary
*.ttf   binary

4.3 修改编辑器vscode配置

{
	"editor.codeActionsOnSave": {
    	"source.fixAll.eslint": true						//设置代码保存时需要做的工作——启用保存时自动修复,默认只支持.js文件
	},
	"editor.formatOnSave": true,
	"files.eol": "\n",										//设置编辑器行尾以LF格式结尾,以匹配git远程代码库
	"editor.defaultFormatter": "esbenp.prettier-vscode", 	//主要是因为vscode lint规则与project lint规则不一致,保存的时候按A规则format,编辑、提交时又按B规则校验,规则不一致则来回format。
}

4.4 eslint配置

.eslintrc.js中添加:

{
	'rules': {
		'linebreak-style': [
			'error',
			'unix'
		],
	}
}

4.5 prettier配置

.prettierrc中添加:

{
	"endOfLine": "lf"
}

参考文章:

win10与mac,CRLF与LF的冲突

linebreak-style

请把 .gitattributes 加到你的 Git 仓库中

Git 初始化配置及常用命令

[git官方文档]  配置 Git 处理行结束符

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值