GitContributors.js 4.9 KB

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