最终效果:每次提交代码时,自动完成以下两个功能
-
执行代码增量检查,有代码风格错误抛出异常。
-
检查提交的文件列表,如果同时存在
dist目录和非dist目录,抛出异常。
前期准备
在阅读本文之前,请确保熟悉或了解 eslint、 eslint-staged 和 husky.
如果不了解,请点击传送门:Javascript代码检查那点事
代码增量检查
eslint 的具体配置不再赘述,在上述的传送门中有详细配置。
安装相关依赖
npm install --save-dev husky
npm install --save-dev lint-staged
在 package.json 中增加如下配置
"husky": {
"hooks": {
"pre-commit": "node scripts/utils/pre-commit.js"
}
},
"lint-staged": {
"linters": {
"*.js": [
"eslint",
"git add"
],
"*.vue": [
"eslint",
"git add"
]
},
"ignore": ["dist/**"]
},
配置说明:当执行 git commit 时,会执行 husky.hooks.pre-commit, 也就是 node scripts/utils/pre-commit.js,该命令为执行一段脚本,脚本如下。
提交文件检查
检查提交内容的核心思路为:获取本次提交的改动文件列表信息 -> 检查文件路径 -> 判断是否同时存在
dist和非dist目录,如果同时存在,则抛出异常停止commit操作,如果不存在,则执行eslint代码检查。详细代码如下(可直接拷贝到scripts/utils/pre-commit.js中)
const chalk = require('chalk')
const symbols = require('log-symbols')
const spawn = require('child_process').spawn
require('lint-staged')
// 检查改动的文件目录
getDiffFiles().then(files => {
const filePaths = files.map(file => file.filename.split('/')[0])
let isDistFolder = false
let isOtherFiles = false
filePaths.forEach(path => {
if (path === 'dist') isDistFolder = true
else isOtherFiles = true
})
if (isDistFolder && isOtherFiles) {
throw new Error()
}
runCmd('lint-staged')
}).catch(() => {
/* eslint-disable */
console.error(`\n\n${symbols.error} ${chalk.redBright('Ops!Dist folder and other files cannot be submitted at the same time.')}`)
console.log(` (use "git reset" to cancel the "add" operation)\n`)
/* eslint-enable */
process.exit(1)
})
// 获取本次改动的文件列表
function getDiffFiles() {
return getHeadCommitId().then(head => {
if (head) {
const command = 'git -c core.quotepath=false diff-index --cached --name-status -M --diff-filter=ACM ' + head
return runCmd(command).then(({ err, stdout, stderr }) => {
return err || stderr ? err || new Error(stderr) : stdoutToResultsObject(stdout)
})
}
})
}
// 获取最近一次提交的commit_id
function getHeadCommitId() {
return runCmd('git rev-parse --verify HEAD').then(({ err, stdout, stderr }) => {
if (err && err.message.indexOf('fatal: Needed a single revision') !== -1) {
return getFirstCommitId()
} else {
return err || stderr ? err || new Error('STDERR: ' + stderr) : stdout.replace('\n', '')
}
})
}
// 获取第一次提交的commit_id
function getFirstCommitId() {
return runCmd('git hash-object -t tree /dev/null').then(({ err, stdout, stderr }) => {
return err || stderr ? err || new Error('STDERR: ' + stderr) : stdout.replace('\n', '')
})
}
// 执行命令,监听控制台信息
function runCmd(command) {
return new Promise((resolve) => {
// 解析命令获取参数
const bits = command.split(' ')
const args = bits.slice(1)
// 执行命令
const cmd = spawn(bits[0], args, { cwd: process.cwd() })
let stdout = ''
let stderr = ''
cmd.stdout.on('data', data => {
stdout += data.toString()
})
cmd.stderr.on('data', data => {
stderr += data.toString()
})
cmd.on('close', code => {
const err = code !== 0 ? new Error(stderr) : null
resolve({ err, stdout, stderr })
})
})
}
// 把stdout输出信息转化成Object对象
function stdoutToResultsObject(stdout) {
const results = []
const lines = stdout.split('\n')
let iLines = lines.length
while (iLines--) {
const line = lines[iLines]
if (line !== '') {
const parts = line.split('\t')
const result = {
filename: parts[2] || parts[1],
status: codeToStatus(parts[0]),
}
results.push(result)
}
}
return results
}
// git 枚举映射
function codeToStatus(code) {
const map = {
A: 'Added',
C: 'Copied',
D: 'Deleted',
M: 'Modified',
R: 'Renamed',
T: 'Type-Change',
U: 'Unmerged',
X: 'Unknown',
B: 'Broken',
}
return map[code.charAt(0)]
}
当执行 git commit 操作时,首先会执行上述脚本,当满足要求时,再执行 eslint 代码检查,当代码检查通过后,才会提交代码到远程仓库!