我们使用husky
检测git
钩子,lint-staged
规范化暂存区代码,commitlint
规范化提交信息。
1 git钩子介绍
Git 有很多的 hooks, 让我们在不同的阶段,对代码进行不同的操作,控制提交到仓库的代码的规范性,和准确性, 以下只是几个常用的钩子
git钩子 | 描述 |
| 判断提交的代码是否符合规范 |
| 判断 commit 信息是否符合规范 |
| 执行测试,避免对以前的内容造成影响 |
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"
可以在根目录创建 .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-git、commitizen/cz-cli、cz-customizable、Inquirer.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 风格的换行符保存入库。但它也考虑到了跨平台协作的场景,并且提供了一个“换行符自动转换”功能。
为了避免出现CRLF
和LF
混合的情况,可以选择配置“.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"
}
参考文章: