-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathutil.js
488 lines (458 loc) · 13.6 KB
/
util.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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
const fs = require('fs')
const os = require('os')
const cp = require('child_process')
const path = require('path')
const moment = require('moment')
const mustache = require('mustache')
mustache.escape = text => text // 不转义字符
const stringSimilarity = require('string-similarity')
const opener = require('./lib/opener.js')
const unionby = require('lodash.unionby')
const pkg = require(`./package.json`)
function removeLeft(str) {
const lines = str.split(`\n`)
// 获取应该删除的空白符数量
const minSpaceNum = lines.filter(item => item.trim())
.map(item => item.match(/(^\s+)?/)[0].length)
.sort((a, b) => a - b)[0]
// 删除空白符
const newStr = lines
.map(item => item.slice(minSpaceNum))
.join(`\n`)
return newStr
}
function print(...arg) {
return console._log(...arg)
}
/**
* 获取所使用的 git log 命令
* @returns string
*/
function logLine({after, before}) {
// 由于 git --before 包含当天, 但 --after 不包含当天, 所以 after 需要再推前
const newAfter = moment(after).add({day: -1}).format(`YYYY-MM-DD`)
const str = [
`git`,
`log`,
`--branches`,
`--after=${newAfter}`,
`--before=${before}`,
`--no-merges`,
`--format=fuller`,
].join(` `)
return str
}
function help() {
print(fs.readFileSync(`${__dirname}/README.md`, `utf8`))
print(`\n访问 ${pkg.homepage} 查看详情`)
// opener(pkg.homepage)
}
function execSync(cmd, option) {
console.log(`>`, cmd)
return cp.execSync(cmd, option).toString().trim()
}
function dateFormater(t, formater) { // 时间格式化
let date = t ? new Date(t) : new Date()
return moment(date).format(formater)
}
function parseArgv(arr) { // 解析命令行参数
return (arr || process.argv.slice(2)).reduce((acc, arg) => {
let [k, ...v] = arg.split(`=`)
v = v.join(`=`) // 把带有 = 的值合并为字符串
acc[k] = v === `` // 没有值时, 则表示为 true
? true
: (
/^(true|false)$/.test(v) // 转换指明的 true/false
? v === `true`
: (
/[\d|.]+/.test(v)
? (isNaN(Number(v)) ? v : Number(v)) // 如果转换为数字失败, 则使用原始字符
: v
)
)
return acc
}, {})
}
function handleMsg(rawMsg) {
let msg = ((rawMsg.match(/(\n\n)([\s\S]+?$)/) || [])[2] || ``)
msg = removeLeft(msg)
let [, one, body] = msg.trim().match(/(.*)[\n]?([\s\S]*)/)
one = one.trim()
body = body.trim()
one = handleOneMsg(one)
let newMsg = ``
if(GET(`curReport`).messageBody === `none`) { // 不使用 body
newMsg = one
}
if(GET(`curReport`).messageBody === `compatible`) { // 当 body 含有可能破坏报告的内容时不使用
if(
body.match(/^#{1,6}\s+/gm) // 含有 # 标题
) {
newMsg = one
} else {
newMsg = `${one}\n${body}`
}
}
if(GET(`curReport`).messageBody === `raw`) { // 原样使用 body
newMsg = `${one}\n${body}`
}
newMsg = newMsg.trim()
return newMsg
}
/**
* 根据 git commit 规范转换 msg 风格
*/
function handleOneMsg(msg) {
const {type, scope, subject} = parseMsg(msg)
let newMsg = msg
const [key, val] = Object.entries(GET(`curReport`).messageConvert).find(([key, val]) => {
const {rating} = stringSimilarity.findBestMatch(type.toLocaleLowerCase(), [key, ...val.alias].map(item => item.toLocaleLowerCase())).bestMatch
return (rating > GET(`curReport`).messageTypeSimilarity)
}) || []
if(key) {
const template = scope ? val.des.text[1] : val.des.text[0]
const text = render( [template, subject].join(``), {scope})
const emoji = val.des.emoji
const newText = render(GET(`curReport`).messageTypeTemplate, {text, emoji})
newMsg = newText
}
return newMsg
}
/**
* 解析 git msg 中的 type scope subject
* arg0 string
* @param {*} msg
* @returns
*/
function parseMsg(msg) {
let type, scope, subject
;[, type = ``, subject = ``] = msg.trim().match(/(.+?)[::][\s+](.*$)/) || []
;[, type = ``, scope = ``] = type.trim().match(/\s{0}(.+?)\s{0}\(\s{0,}(.*)\s{0,}\)$/) || [, type]
type = type.trim()
scope = scope.trim()
subject = subject.trim()
const res = {type, scope, subject}
return res
}
/**
* 把 git log 的内容解析为数组
* @param {*} str
* @returns
*/
function paserLogToList({str}) {
const authorList = GET(`curReport`).author || []
str = `\n${str}`
const tag = /\ncommit /
let list = str.split(tag).filter(item => item.trim()).map(rawMsg => {
rawMsg = `commit ${rawMsg}`
const obj = {}
obj.raw = rawMsg
// 使用 CommitDate 而不是 AuthorDate
// CommitDate: 被再次修改的时间或作为补丁使用的时间
obj.date = (dateFormater((rawMsg.match(/CommitDate:(.*)/) || [])[1].trim(), 'YYYY-MM-DD HH:mm:ss'))
// commit sha
obj.commit = (rawMsg.match(/(.*)\n/) || [])[1].trim()
// 作者及邮箱
obj.author = (rawMsg.match(/Author:(.*)/) || [])[1].trim()
obj.msg = handleMsg(rawMsg)
return obj
}).filter(item => {
return (
( // 时间范围
moment(item.date).isSameOrBefore(GET(`curReport`).before)
&& moment(item.date).isSameOrAfter(GET(`curReport`).after)
)
&& ( // 作者
GET(`curReport`).ignoreAuthor ||
authorList.some(author => {
return item.author.startsWith(`${author} <`)
} )
)
)
})
// 去除重复的 msg
GET(`curReport`).noEqualMsg && (list = unionby(list, `msg`));
// 去除大于指定相似度的 msg
GET(`curReport`).similarity && (list = list = removeSimilarity({list, key: `msg`, similarity: GET(`curReport`).similarity}));
return list
}
/**
* 删除数组中指定字段的相似度大于指定值的数组
* arg.list 提供的数组
* arg.key 要比较相似度的字段
* arg.similarity 相似值
*/
function removeSimilarity({list, key = `msg`, similarity = 0.75}) {
if(list.length < 2) {
return list
}
let msgList = list.map(item => item[key])
let res = []
msgList.forEach((msg, index) => {
let newList = [...msgList]
newList.splice(index, 1)
const diff = stringSimilarity.findBestMatch(msg, newList)
.ratings
.filter(item => {
return item.rating > similarity
}).map(item => item.target)
const select = diff.length ? diff[0] : msg
res = res.includes(select) ? res : res.concat(msg)
})
const newList = list.filter(item => res.includes(item[key]))
return newList
}
/**
* 根据指定的 key 排序
* @param {boolean} showNew false 降序 true 升序
* @param {string} key 要排序的 key
* @returns function
*/
function sort(showNew, key) {
return (a, b) => showNew ? (b[key] - a[key]) : (a[key] - b[key])
}
/**
* 根据 title 标记输出 md
* @param {object} param0
* @param {array} param0.list log 数据
* @returns string
*/
function create({list, rootLevel}) {
const template = GET(`curReport`).template
const titleMap = {
'month': [
'@{year}年@{month}月',
],
'month-week': [
'@{year}年@{month}月',
'第@{week}周',
],
'month-week-day': [
'@{year}年@{month}月',
'第@{week}周',
'@{day}日 星期@{weekDay}',
],
'week': [
'@{year}年@{month}月 第@{week}周',
],
'week-day': [
'@{year}年@{month}月 第@{week}周',
'@{day}日 星期@{weekDay}',
],
'day': [
'@{year}年@{month}月@{day}日',
],
}
let titleList = titleMap[template]
if(!titleList) {
new Error(`不支持的标记`)
return process.exit()
} else { // 添加 # 标题标志
titleList = titleList.map((item, index) => {
const level = getTitleLevel({type: `date`, rootLevel}) + index
return `${`#`.repeat(level)} ${item}`
})
}
let oldTitle = []
const str = list.map(item => {
let titleStr = []
const newTitle = titleList.map((title, index) => {
// 模拟一段代码实现简单的模板语法
title = render(`\n${title}`, item.dateObj)
titleStr[index] = oldTitle[index] === title ? `` : title
return title
} )
const res = [
titleStr.join(``),
`\n- ${debug({item})}${ // 多行 msg 的时候在行前面加空格, 以处理缩进关系
item.msg.split(`\n`).map((msgLine) => ` ${msgLine}`).join(`\n`).trim()
}`
].filter(item => item).join(`\n`)
oldTitle = newTitle
return res
}).join(`\n`).trim()
return str
}
function debug({item}) {
if(GET(`cli`)[`--debug`]) {
return `${item.date} ${item.commit} ${item.author}\n `
} else {
return ``
}
}
/**
* 根据标志对应的时间转换为 markdown 格式
* @param {object} param0
* @param {string} [param0.tag = day] - 标记
* @param {array} param0.list - 数据
* @param {boolean} [param0.showNew = true] - 是否把新时间排到前面
*/
function toMd({
list = [],
showNew = true,
rootLevel = 1,
}){
list = list.map(obj => { // 先把每个类型的时间取出来方便使用
const date = new Date(obj.date)
obj.timeStamp = date.getTime()
obj.dateObj = {
year: dateFormater(obj.date, `YYYY`),
month: dateFormater(obj.date, `MM`),
day: dateFormater(obj.date, `DD`),
week: Math.ceil((date.getDate() + 6 - date.getDay()) / 7), // 第几周
weekDay: [`日`, `一`, `二`, `三`, `四`, `五`, `六`][date.getDay()], // 星期几
}
return obj
}).sort(sort(showNew, `timeStamp`))
const res = create({
list,
rootLevel,
})
return res
}
/**
* 获取模板对应的时间
*/
function handleLogTime({template}) {
const tag = template.split(`-`).shift() // month week day
// 处理 moment 是以周日作为每周的开始, 但现实中是以周一作为开始
const offset = tag === `week` ? {day: 1} : {}
const after = moment().startOf(tag).add(offset).format(`YYYY-MM-DD`)
const before = moment().endOf(tag).add(offset).format(`YYYY-MM-DD`)
return {after, before}
}
/**
* 获取标题级别
* 假设 rootLevel = 1, 效果为:
* # 文档名称
* ## 项目名称
* ### 时间级别1
* #### 时间级别2
*/
function getTitleLevel({type, rootLevel = 1}) {
return {
repository: rootLevel + 1,
date: rootLevel + 2,
}[type]
}
/**
* 模板处理器
* report 报告配置
*/
function handleTemplateFile({report, body}) {
const insertBody = render(``, GET(`curReport`))
const useFilePath = `${GET(`configdir`)}/${report.useFile}`
const useFilePathDefault = `${GET(`configdir`)}/default.template.md`
let mdBody = fs.existsSync(useFilePath) ? fs.readFileSync(useFilePath, `utf8`) : (print(`所指定的 useFile 值不存在, 将使用默认值`), fs.readFileSync(useFilePathDefault, `utf8`))
mdBody = render(mdBody, GET(`curReport`))
mdBody = mdBody.replace(/<!--\s+slot-body-start\s+-->([\s\S]+?)<!--\s+slot-body-end\s+-->/, ($0, $1) => (body || $1))
mdBody = mdBody.replace(/<!--\s+slot-insert-start\s+-->([\s\S]+?)<!--\s+slot-insert-end\s+-->/, ($0, $1) => (insertBody || $1))
return mdBody
}
/**
* 渲染模板
* @param {*} template
* @returns
*/
function render(template, view) {
return mustache.render(template, view, {}, [`@{`, `}`])
}
/**
* 处理报告配置,例如使用命令行参数覆盖报告中的配置
*/
function handleReportConfig({reportItem: cfg, query: cli}) {
const curPath = process.cwd()
const curPathName = path.parse(curPath).name
const newReport = {
// 默认值
...GET(`reportDefault`),
// 配置值
...cfg,
// 命令行值
...Object.entries(cli).reduce((acc, [key, val]) => ({...acc, [key]: val}), {})
}
// 根据 template 预处理时间
newReport.after = newReport.after || handleLogTime({template: newReport.template}).after
newReport.before = newReport.before || handleLogTime({template: newReport.template}).before
newReport.author = cli[`author`]
? cli[`author`].split(`,`)
: (
(cfg.author && cfg.author.length)
? cfg.author
: [getDefaultGitName()]
);
newReport.authorName = newReport.authorName || newReport.author[0]
newReport.repository = cli[`repository`]
? cli[`author`].split(`,`).map(item => ({path: item, name: path.parse(item).name}))
: (
(cfg.repository && cfg.repository.length)
? cfg.repository
: [{path: curPath, name: curPathName}]
);
return newReport
}
function getDefaultGitName() {
try {
return execSync(`git config user.name`)
} catch (error) {
errExit(`获取 git 的默认用户名失败`, error)
}
}
function errExit(msg, error) {
print(msg)
print(String(error))
process.exit()
}
/**
* 初始化程序
*/
function init() {
global.SET = (key, val) => {
console.log(`SET`, key, val)
global[`${pkg.name}_${key}`] = val
return val
}
global.GET = (key) => {
return global[`${pkg.name}_${key}`]
}
global.SET(`package`, pkg)
const configdir = `${os.homedir()}/.${pkg.name}/`
global.SET(`configdir`, configdir)
global.SET(`configFilePath`, `${configdir}/config.js`)
global.SET(`reportDefault`, require(`./config/config.js`).report.find(item => item.select === `default`))
if(fs.existsSync(configdir) === false) { // 创建配置文件目录
fs.mkdirSync(configdir, {recursive: true})
}
// 如果文件不存在, 则创建它们
[
[`${__dirname}/config/config.js`, GET(`configFilePath`)],
[`${__dirname}/config/default.template.md`, `${GET(`configdir`)}/default.template.md`],
].forEach(([form, to]) => {
if(
(fs.existsSync(to) === false)
|| (fs.readFileSync(to, `utf8`).trim() === ``)
) { // 创建配置文件
fs.copyFileSync(form, to)
}
})
global.SET(`config`, require(GET(`configFilePath`)))
}
module.exports = {
init,
opener,
errExit,
handleReportConfig,
handleTemplateFile,
getTitleLevel,
handleLogTime,
logLine,
toMd,
create,
sort,
help,
print,
removeLeft,
execSync,
parseArgv,
paserLogToList,
}