GitContributors.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. const API = require("../../lib/API")
  2. const AccessControl = require("../../lib/AccessControl")
  3. const { BaseStdResponse } = require("../../BaseStdResponse")
  4. const db = require("../../plugin/DataBase/db")
  5. const redis = require('../../plugin/DataBase/Redis')
  6. const simpleGit = require('simple-git')
  7. class GitContributors extends API {
  8. constructor() {
  9. super()
  10. this.setMethod("GET")
  11. this.setPath("/Repos/Contributors")
  12. }
  13. async analyzeGitContributors(repoPath) {
  14. try {
  15. // 检查 Redis 中是否已存在缓存日志
  16. const redisKey = `gitLogs:${repoPath}`
  17. const cachedLogs = await redis.get(redisKey)
  18. let logs
  19. if (cachedLogs)
  20. logs = JSON.parse(cachedLogs)
  21. else {
  22. const git = simpleGit()
  23. await git.cwd(repoPath)
  24. // 获取详细日志,带上插入删除统计并排除 merge 提交
  25. logs = await git.raw([
  26. "log",
  27. "--no-merges",
  28. "--pretty=format:%H|%an|%ae|%ad|%s",
  29. "--shortstat"
  30. ])
  31. redis.set(redisKey, JSON.stringify(logs), {
  32. EX: 172800
  33. })
  34. }
  35. const lines = logs.split("\n")
  36. const contributors = {}
  37. let currentAuthor = null
  38. for (let i = 0; i < lines.length; i++) {
  39. const line = lines[i].trim()
  40. if (line.includes("|")) {
  41. const [commitHash, name, email, date, ...messageParts] = line.split("|")
  42. currentAuthor = email // 用邮箱作为唯一 key 更稳妥
  43. if (!contributors[currentAuthor]) {
  44. contributors[currentAuthor] = {
  45. name,
  46. email,
  47. commits: 0,
  48. linesAdded: 0,
  49. linesDeleted: 0,
  50. filesChanged: 0,
  51. linesChanged: 0 // total changes
  52. }
  53. }
  54. contributors[currentAuthor].commits += 1
  55. // 接下来的 shortstat 统计信息在下一行
  56. const statLine = lines[i + 1]?.trim() || ""
  57. const statMatch = {
  58. filesChanged: statLine.match(/(\d+) file[s]? changed/),
  59. insertions: statLine.match(/(\d+) insertion[s]?\(\+\)/),
  60. deletions: statLine.match(/(\d+) deletion[s]?\(-\)/),
  61. }
  62. const files = statMatch.filesChanged ? parseInt(statMatch.filesChanged[1], 10) : 0
  63. const insertions = statMatch.insertions ? parseInt(statMatch.insertions[1], 10) : 0
  64. const deletions = statMatch.deletions ? parseInt(statMatch.deletions[1], 10) : 0
  65. contributors[currentAuthor].filesChanged += files
  66. contributors[currentAuthor].linesAdded += insertions
  67. contributors[currentAuthor].linesDeleted += deletions
  68. contributors[currentAuthor].linesChanged += insertions + deletions
  69. if (statLine) i++ // 跳过 shortstat 行
  70. }
  71. }
  72. const sorted = Object.values(contributors).sort(
  73. (a, b) => b.linesChanged - a.linesChanged || b.commits - a.commits
  74. )
  75. return sorted
  76. } catch (err) {
  77. console.error("分析 Git 贡献者失败:", err)
  78. throw new Error("分析 Git 贡献者失败")
  79. }
  80. }
  81. async onRequest(req, res) {
  82. let { uuid, session, id } = req.query
  83. if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
  84. return res.json({
  85. ...BaseStdResponse.MISSING_PARAMETER
  86. })
  87. // 检查 session
  88. if (!await AccessControl.checkSession(uuid, session))
  89. return res.status(401).json({
  90. ...BaseStdResponse.ACCESS_DENIED
  91. })
  92. let sql = 'SELECT state, path FROM repos WHERE create_user = ? AND id = ?'
  93. let r = await db.query(sql, [uuid, id])
  94. if (!r || r.length === 0)
  95. return res.json({
  96. ...BaseStdResponse.ERR,
  97. msg: '未找到仓库'
  98. })
  99. if (r[0].state !== 1 || !r[0].path)
  100. return res.json({
  101. ...BaseStdResponse.ERR,
  102. msg: '仓库未成功克隆!'
  103. })
  104. let contributors
  105. try {
  106. const cachedContributors = await redis.get(`contributors:${r[0].path}`)
  107. if (cachedContributors)
  108. return res.json({
  109. ...BaseStdResponse.OK,
  110. data: JSON.parse(cachedContributors)
  111. })
  112. contributors = await this.analyzeGitContributors(r[0].path)
  113. } catch (error) {
  114. this.logger.error('获取仓库开发者失败!' + error.stack)
  115. return res.json({
  116. ...BaseStdResponse.ERR,
  117. msg: '获取仓库开发者失败!'
  118. })
  119. }
  120. res.json({
  121. ...BaseStdResponse.OK,
  122. data: contributors
  123. })
  124. redis.set(`contributors:${r[0].path}`, JSON.stringify(contributors), {
  125. EX: 172800
  126. })
  127. }
  128. }
  129. module.exports.GitContributors = GitContributors