最终效果:每次提交代码时,自动完成以下两个功能

  1. 执行代码增量检查,有代码风格错误抛出异常。

  2. 检查提交的文件列表,如果同时存在 dist 目录和非 dist 目录,抛出异常。

前期准备

在阅读本文之前,请确保熟悉或了解 eslinteslint-stagedhusky.

如果不了解,请点击传送门:Javascript代码检查那点事

代码增量检查

eslint 的具体配置不再赘述,在上述的传送门中有详细配置。

安装相关依赖

1
2
npm install --save-dev husky
npm install --save-dev lint-staged

package.json 中增加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"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 中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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 代码检查,当代码检查通过后,才会提交代码到远程仓库!