找回密码
 注册
搜索
查看: 81|回复: 0

建议收藏!这个程序员一身反骨,不愿写周报,于是做了个神器

[复制链接]
52RD网友  发表于 2024-11-4 17:41:03 |阅读模式

作者:Ethan_Zhou

前言
之前看到一个有趣的说法:从员工的工位状态可以判断其工作状态——工位越整洁、个人物品越少,员工随时准备“跑路”的概率越高。这个观点让我觉得颇有道理。

那么同样的,从公司对待员工的态度和政策中,也可以看出一些东西,在公司上升期时,管理层的重心通常放在业务拓展上,专注于赚钱、做大蛋糕,因此对员工相对宽容,只要完成本职工作,不会过于苛责。但当公司发展遇到瓶颈或进入衰退期时,就开始苛求细节、抓考勤、缩减福利,各种压缩成本,卷形式主义,员工压力倍增。

有时,员工在这些高压环境下,还要应对额外的任务,比如写日报、周报,想必这也是程序员最烦的一件事,明明工作产出已经在代码中体现了,却还要花大量时间去写 ppt,领导写写也就罢了,毕竟这是他们的工作之一,一线干活的程序员也要写,这就很烦人了,每天做不完的需求还要挤出来时间整理总结。

为了解决这个痛点,今天我给大家介绍同事写的自动化写周报的脚本工具,能够一键抓取 git 提交记录,并按照你需要的格式生成日报、周报。

功能介绍
效果如视频所示,只要把脚本运行文件当到项目所在文件夹下,用 node 环境执行,即可抓取该文件夹下的所有 git 仓库,并读取 .git 配置文件的内容,根据 commit 记录来生成简易工作报告,列出规定时间内做过的所有需求记录和耗时情况。

同时如果你在 commit 时,填写了 jira 需求号,会根据 jira 需求号来抓取该需求详情,如对接人等信息,你可以根据自己公司的要求,稍微改下,填充更多需要的信息,使周报内容更丰富。

<顺便吆喝一句,技术大厂内推,前/后端or测试机会,尤其东莞、深圳等地紧缺!感兴趣来>→jinshuju.net/f/o38ijj这里

关键步骤解读

findGitRepos 函数会递归地在指定目录下搜索包含 .git 目录的文件夹,识别出所有的 Git 仓库路径。为了优化性能,避免无关文件夹(如 node_modules)的搜索,函数在遇到它们时会直接跳过。

getJIRA 函数利用 HTTP 请求调用 JIRA API,通过需求 ID 获取需求的详细信息,包括标题、优先级、描述和相关人员。它基于用户名和密码进行基础认证(Basic Auth),并返回解析后的需求详情。

getGitLogs 函数执行 Git 命令,提取最近 TOTALDAYS 天内的提交记录。通过 --author 参数筛选出指定用户的提交,并解析出包含需求 ID(如 ABC-123 格式)的提交信息。
日志信息会被收集并保存到 allLogs 数组中。

handleLog 函数将从 Git 日志中提取出的需求 ID 去重并统计提交次数,接着通过调用 getJIRA 获取每个需求的详细信息。
需求信息获取成功后,它会根据提交次数和 TOTALHOURS 分配每个需求的工时,并生成一份报告。
简易报告:列出每个需求的 ID、标题、对接人和工时。
详细报告:进一步包括需求的优先级、描述等信息。

根据每个需求的提交次数占比,脚本会将 TOTALHOURS 进行合理分配,确保每个需求的工时按比例分配精确到 MINUNIT。

完整代码
  1. const fs = require('fs')
  2. const path = require('path')
  3. const { exec } = require('child_process')
  4. const http = require('http')

  5. let allLogs = []

  6. const GITAUTHOR = ''   //git显示的用户名,用于分离出自己的提交记录
  7. const CNNAME = ''      //阁下大名,中文,需求参与人员可以把自己过滤掉
  8. const USERNAME = ''  //jira 用户名,根据需求号用API去获取需求详情
  9. const PASSWORD = '' //jira 密码,同上
  10. const TOTALHOURS = 80  //总工时, 建议适当向上浮动
  11. const TOTALDAYS = 14  //拉取git的最近n天的提交记录
  12. const MINUNIT = 0.1   //工时精度

  13. const findGitRepos = (dir, repos = []) => {
  14.   const files = fs.readdirSync(dir)

  15.   for (const file of files) {
  16.     const fullPath = path.join(dir, file);
  17.     const stat = fs.statSync(fullPath);

  18.     if (fullPath.includes('node_modules')) return

  19.     if (stat.isDirectory()) {
  20.       if (file === '.git') {
  21.         console.log('已扫到仓库:', dir)
  22.         repos.push(dir)
  23.         break
  24.       } else {
  25.         findGitRepos(fullPath, repos)
  26.       }
  27.     }
  28.   }
  29.   return repos
  30. }

  31. const getJIRA = id => new Promise((resolve, reject) => {
  32.   const options = {
  33.     hostname: '10.xxx.80.xxx',
  34.     port: 8080,
  35.     path: '/rest/api/2/issue/' + id,
  36.     method: 'GET',
  37.     headers: {'Authorization': 'Basic ' + Buffer.from(USERNAME +':' + PASSWORD).toString('base64')}
  38.   }
  39.   
  40.   const req = http.request(options, res => {
  41.     let data = ''
  42.   
  43.     res.on('data', chunk => data += chunk)

  44.     res.on('end', () => {
  45.       const res = JSON.parse(data)
  46.       if (res.errorMessages) {
  47.         resolve(null)
  48.       } else {
  49.         let obj = {}
  50.         const fields = res.fields
  51.         
  52.         if (fields) {
  53.           if (fields.summary) {
  54.             obj.title = fields.summary
  55.           }
  56.           if (fields.customfield_10400) {
  57.             obj.linkUsers = fields.customfield_10400.map(item => item.displayName)
  58.           }
  59.           if (fields.reporter && obj.linkUsers) {
  60.             obj.linkUsers.unshift(fields.reporter.displayName)
  61.           }
  62.           if (fields.priority) {
  63.             obj.priority = fields.priority.name
  64.           }
  65.           if (fields.description) {
  66.             obj.description = fields.description
  67.           }
  68.         }
  69.         
  70.         resolve(obj)
  71.       }
  72.     })
  73.   })

  74.   req.on('error', error => console.error('Error:', error))
  75.   req.end()
  76. })

  77. const getGitLogs = (repoPath, author, callback) => {
  78.   console.log('进入列队:', repoPath)
  79.   const gitCommand = `git log --since="${TOTALDAYS} days ago" --author="${author}" --pretty=format:"%s"`;
  80.   exec(gitCommand, { cwd: repoPath }, (error, stdout, stderr) => {
  81.     if (error) {
  82.       console.error(`获取日志时出错: ${error}`);
  83.       return;
  84.     }

  85.     if (stderr) {
  86.       console.error(`标准错误: ${stderr}`);
  87.       return;
  88.     }

  89.     if (stdout && stdout.trim()) {
  90.       const arr = stdout.trim().split('\n')
  91.       console.log(`${repoPath} --- ${arr.length} 条日志`)
  92.       allLogs = allLogs.concat(arr)
  93.     } else {
  94.       console.log(`${repoPath} --- 没有日志`)
  95.     }

  96.     callback && callback()
  97.   });
  98. }

  99. const handleLog = () => {
  100.   allLogs = allLogs.map(item => item.match(/[A-Z]+-\d+/g)).filter(Boolean).flat()

  101.   const logsDict = {}

  102.   allLogs.forEach(item => {
  103.     if (!logsDict[item]) {
  104.       logsDict[item] = 1
  105.     } else {
  106.       logsDict[item] = logsDict[item] += 1
  107.     }
  108.   })

  109.   console.log('\n\n\n\n\n需求提交次数:', logsDict, '\n\n\n\n\n开始获需求内容')

  110.   let keys = Object.keys(logsDict)
  111.   let keysRes = []
  112.   let successKeys = []
  113.   let failKeys = []

  114.   Promise.all(keys.map(item => getJIRA(item))).then(values => {
  115.     keysRes = values

  116.     keys.forEach((key, index) => {
  117.       if (keysRes[index]) {
  118.         successKeys.push({
  119.           key: key,
  120.           title: keysRes[index].title,
  121.           linkUsers: keysRes[index].linkUsers,
  122.           priority: keysRes[index].priority,
  123.           description: keysRes[index].description
  124.         })
  125.       } else {
  126.         failKeys.push({key: key})
  127.       }
  128.     })

  129.     console.log('\n\n\n\n\n成功需求:', successKeys.map(i => i.key))

  130.     if (failKeys.length) {
  131.       console.error('\n\n\失败需求:', failKeys.map(i => i.key))
  132.     }

  133.     if (successKeys.length) {
  134.       console.error('\n\n\n\n\n最终周报:\n')
  135.       const submissions = {}
  136.       successKeys.forEach(item => submissions[item.key] = logsDict[item.key])
  137.       const totalHours = TOTALHOURS
  138.       const minUnit = MINUNIT
  139.       const totalSubmissions = Object.values(submissions).reduce((a, b) => a + b, 0)

  140.       const allocatedHours = {};
  141.       let remainingHours = totalHours

  142.       Object.keys(submissions).forEach(key => {
  143.         const proportion = submissions[key] / totalSubmissions
  144.         let hours = Math.round((proportion * totalHours) / minUnit) * minUnit
  145.         allocatedHours[key] = hours
  146.         remainingHours -= hours
  147.       })

  148.       const keys = Object.keys(allocatedHours)
  149.       let i = 0
  150.       while (remainingHours > 0 && i < keys.length) {
  151.         allocatedHours[keys[i]] += minUnit
  152.         remainingHours -= minUnit
  153.         i++
  154.       }

  155.       const sortReport = successKeys.map(i => {
  156.         return i.key + '  ' + i.title + '   对接人:' + ((i.linkUsers || []).filter(u => !u.includes(CNNAME)).join(', ') || '---') + '      工时:' +  (+(allocatedHours[i.key] || 0).toFixed(1)) + 'h'
  157.       })

  158.       console.log('\n\n简单报告:\n')
  159.       console.log(sortReport)

  160.       const report = successKeys.map(i => {

  161.         return {
  162.           content: i.key + '  ' + i.title,
  163.           linkUsers:  (i.linkUsers || []).filter(u => !u.includes(CNNAME)).join(', ') || '---',
  164.           time: +(allocatedHours[i.key] || 0).toFixed(1) + 'h',
  165.           priority: i.priority,
  166.           description: i.description || '----'
  167.         }

  168.       })

  169.       console.log('\n\n详细报告:\n')
  170.       console.log(report)
  171.     }
  172.   })
  173. }

  174. const scanGitRepos = (rootDir, author) => {
  175.   const repos = findGitRepos(rootDir)
  176.   
  177.   if (repos.length === 0) {
  178.     return console.log('未找到 Git 仓库')
  179.   }

  180.   console.log(`总共找到 ${repos.length} 个仓库,开始提取最近${TOTALDAYS}天的提交日志`)

  181.   repos.forEach((repo, i) => {
  182.     if (i < repos.length - 1) {
  183.       getGitLogs(repo, author)
  184.     } else {
  185.       getGitLogs(repo, author, handleLog)
  186.     }
  187.    
  188.   })
  189. }

  190. scanGitRepos('./', GITAUTHOR) // 扫描./ 目录下的所有git 仓库,并读取 GITAUTHOR = 14 天内
复制代码



以上就是本文所有内容了,更多好玩的,欢迎大家留言讨论。
…………………………………………

“爱码士” 集结号,唤醒 “猿” 气超能力 —— 福利暖冬 [免费] 派送活动火热进行中

代码工具 VIP 会员、文创 “爱码士” 礼包、开心麻花门票等,➡派送通道main.focussend.cn/p/Sc9Q7L ~

程序员狂欢继续~~~

高级模式
B Color Image Link Quote Code Smilies

本版积分规则

Archiver|手机版|小黑屋|52RD我爱研发网 ( 沪ICP备2022007804号-2 )

GMT+8, 2024-11-22 20:14 , Processed in 0.094492 second(s), 16 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表