前言
之前看到一个有趣的说法:从员工的工位状态可以判断其工作状态——工位越整洁、个人物品越少,员工随时准备“跑路”的概率越高。这个观点让我觉得颇有道理。
那么同样的,从公司对待员工的态度和政策中,也可以看出一些东西,在公司上升期时,管理层的重心通常放在业务拓展上,专注于赚钱、做大蛋糕,因此对员工相对宽容,只要完成本职工作,不会过于苛责。但当公司发展遇到瓶颈或进入衰退期时,就开始苛求细节、抓考勤、缩减福利,各种压缩成本,卷形式主义,员工压力倍增。
有时,员工在这些高压环境下,还要应对额外的任务,比如写日报、周报,想必这也是程序员最烦的一件事,明明工作产出已经在代码中体现了,却还要花大量时间去写 ppt,领导写写也就罢了,毕竟这是他们的工作之一,一线干活的程序员也要写,这就很烦人了,每天做不完的需求还要挤出来时间整理总结。
为了解决这个痛点,今天我给大家介绍同事写的自动化写周报的脚本工具,能够一键抓取 git 提交记录,并按照你需要的格式生成日报、周报。
功能介绍
效果如视频所示,只要把脚本运行文件当到项目所在文件夹下,用 node 环境执行,即可抓取该文件夹下的所有 git 仓库,并读取 .git
配置文件的内容,根据 commit
记录来生成简易工作报告,列出规定时间内做过的所有需求记录和耗时情况。
同时如果你在 commit 时,填写了 jira
需求号,会根据 jira 需求号来抓取该需求详情,如对接人等信息,你可以根据自己公司的要求,稍微改下,填充更多需要的信息,使周报内容更丰富。
关键步骤解读
-
查找 Git 仓库:
findGitRepos
函数会递归地在指定目录下搜索包含.git
目录的文件夹,识别出所有的 Git 仓库路径。为了优化性能,避免无关文件夹(如node_modules
)的搜索,函数在遇到它们时会直接跳过。
-
获取 JIRA 需求:
getJIRA
函数利用 HTTP 请求调用 JIRA API,通过需求 ID 获取需求的详细信息,包括标题、优先级、描述和相关人员。它基于用户名和密码进行基础认证(Basic Auth),并返回解析后的需求详情。
-
获取 Git 日志:
getGitLogs
函数执行 Git 命令,提取最近TOTALDAYS
天内的提交记录。通过--author
参数筛选出指定用户的提交,并解析出包含需求 ID(如ABC-123
格式)的提交信息。- 日志信息会被收集并保存到
allLogs
数组中。
-
处理日志:
handleLog
函数将从 Git 日志中提取出的需求 ID 去重并统计提交次数,接着通过调用getJIRA
获取每个需求的详细信息。- 需求信息获取成功后,它会根据提交次数和
TOTALHOURS
分配每个需求的工时,并生成一份报告。- 简易报告:列出每个需求的 ID、标题、对接人和工时。
- 详细报告:进一步包括需求的优先级、描述等信息。
-
工时分配:
- 根据每个需求的提交次数占比,脚本会将
TOTALHOURS
进行合理分配,确保每个需求的工时按比例分配精确到MINUNIT
。
- 根据每个需求的提交次数占比,脚本会将
完整代码
const fs = require('fs')
const path = require('path')
const { exec } = require('child_process')
const http = require('http')
let allLogs = []
const GITAUTHOR = '' //git显示的用户名,用于分离出自己的提交记录
const CNNAME = '' //阁下大名,中文,需求参与人员可以把自己过滤掉
const USERNAME = '' //jira 用户名,根据需求号用API去获取需求详情
const PASSWORD = '' //jira 密码,同上
const TOTALHOURS = 80 //总工时, 建议适当向上浮动
const TOTALDAYS = 14 //拉取git的最近n天的提交记录
const MINUNIT = 0.1 //工时精度
const findGitRepos = (dir, repos = []) => {
const files = fs.readdirSync(dir)
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (fullPath.includes('node_modules')) return
if (stat.isDirectory()) {
if (file === '.git') {
console.log('已扫到仓库:', dir)
repos.push(dir)
break
} else {
findGitRepos(fullPath, repos)
}
}
}
return repos
}
const getJIRA = id => new Promise((resolve, reject) => {
const options = {
hostname: '10.xxx.80.xxx',
port: 8080,
path: '/rest/api/2/issue/' + id,
method: 'GET',
headers: {'Authorization': 'Basic ' + Buffer.from(USERNAME +':' + PASSWORD).toString('base64')}
}
const req = http.request(options, res => {
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => {
const res = JSON.parse(data)
if (res.errorMessages) {
resolve(null)
} else {
let obj = {}
const fields = res.fields
if (fields) {
if (fields.summary) {
obj.title = fields.summary
}
if (fields.customfield_10400) {
obj.linkUsers = fields.customfield_10400.map(item => item.displayName)
}
if (fields.reporter && obj.linkUsers) {
obj.linkUsers.unshift(fields.reporter.displayName)
}
if (fields.priority) {
obj.priority = fields.priority.name
}
if (fields.description) {
obj.description = fields.description
}
}
resolve(obj)
}
})
})
req.on('error', error => console.error('Error:', error))
req.end()
})
const getGitLogs = (repoPath, author, callback) => {
console.log('进入列队:', repoPath)
const gitCommand = `git log --since="${TOTALDAYS} days ago" --author="${author}" --pretty=format:"%s"`;
exec(gitCommand, { cwd: repoPath }, (error, stdout, stderr) => {
if (error) {
console.error(`获取日志时出错: ${error}`);
return;
}
if (stderr) {
console.error(`标准错误: ${stderr}`);
return;
}
if (stdout && stdout.trim()) {
const arr = stdout.trim().split('\n')
console.log(`${repoPath} --- ${arr.length} 条日志`)
allLogs = allLogs.concat(arr)
} else {
console.log(`${repoPath} --- 没有日志`)
}
callback && callback()
});
}
const handleLog = () => {
allLogs = allLogs.map(item => item.match(/[A-Z]+-\d+/g)).filter(Boolean).flat()
const logsDict = {}
allLogs.forEach(item => {
if (!logsDict[item]) {
logsDict[item] = 1
} else {
logsDict[item] = logsDict[item] += 1
}
})
console.log('\n\n\n\n\n需求提交次数:', logsDict, '\n\n\n\n\n开始获需求内容')
let keys = Object.keys(logsDict)
let keysRes = []
let successKeys = []
let failKeys = []
Promise.all(keys.map(item => getJIRA(item))).then(values => {
keysRes = values
keys.forEach((key, index) => {
if (keysRes[index]) {
successKeys.push({
key: key,
title: keysRes[index].title,
linkUsers: keysRes[index].linkUsers,
priority: keysRes[index].priority,
description: keysRes[index].description
})
} else {
failKeys.push({key: key})
}
})
console.log('\n\n\n\n\n成功需求:', successKeys.map(i => i.key))
if (failKeys.length) {
console.error('\n\n\失败需求:', failKeys.map(i => i.key))
}
if (successKeys.length) {
console.error('\n\n\n\n\n最终周报:\n')
const submissions = {}
successKeys.forEach(item => submissions[item.key] = logsDict[item.key])
const totalHours = TOTALHOURS
const minUnit = MINUNIT
const totalSubmissions = Object.values(submissions).reduce((a, b) => a + b, 0)
const allocatedHours = {};
let remainingHours = totalHours
Object.keys(submissions).forEach(key => {
const proportion = submissions[key] / totalSubmissions
let hours = Math.round((proportion * totalHours) / minUnit) * minUnit
allocatedHours[key] = hours
remainingHours -= hours
})
const keys = Object.keys(allocatedHours)
let i = 0
while (remainingHours > 0 && i < keys.length) {
allocatedHours[keys[i]] += minUnit
remainingHours -= minUnit
i++
}
const sortReport = successKeys.map(i => {
return i.key + ' ' + i.title + ' 对接人:' + ((i.linkUsers || []).filter(u => !u.includes(CNNAME)).join(', ') || '---') + ' 工时:' + (+(allocatedHours[i.key] || 0).toFixed(1)) + 'h'
})
console.log('\n\n简单报告:\n')
console.log(sortReport)
const report = successKeys.map(i => {
return {
content: i.key + ' ' + i.title,
linkUsers: (i.linkUsers || []).filter(u => !u.includes(CNNAME)).join(', ') || '---',
time: +(allocatedHours[i.key] || 0).toFixed(1) + 'h',
priority: i.priority,
description: i.description || '----'
}
})
console.log('\n\n详细报告:\n')
console.log(report)
}
})
}
const scanGitRepos = (rootDir, author) => {
const repos = findGitRepos(rootDir)
if (repos.length === 0) {
return console.log('未找到 Git 仓库')
}
console.log(`总共找到 ${repos.length} 个仓库,开始提取最近${TOTALDAYS}天的提交日志`)
repos.forEach((repo, i) => {
if (i < repos.length - 1) {
getGitLogs(repo, author)
} else {
getGitLogs(repo, author, handleLog)
}
})
}
scanGitRepos('./', GITAUTHOR) // 扫描./ 目录下的所有git 仓库,并读取 GITAUTHOR = 14 天内
以上就是本文所有内容了,更多好玩的,欢迎大家在留言区讨论。