const API = require("../../lib/API") const AccessControl = require("../../lib/AccessControl") const { BaseStdResponse } = require("../../BaseStdResponse") const db = require("../../plugin/DataBase/db") const redis = require('../../plugin/DataBase/Redis') const simpleGit = require('simple-git') class GitContributors extends API { constructor() { super() this.setMethod("GET") this.setPath("/Repos/Contributors") } async analyzeGitContributors(repoPath) { try { // 检查 Redis 中是否已存在缓存日志 const redisKey = `gitLogs:${repoPath}` const cachedLogs = await redis.get(redisKey) let logs if (cachedLogs) logs = JSON.parse(cachedLogs) else { const git = simpleGit() await git.cwd(repoPath) // 获取详细日志,带上插入删除统计并排除 merge 提交 logs = await git.raw([ "log", "--no-merges", "--pretty=format:%H|%an|%ae|%ad|%s", "--shortstat" ]) redis.set(redisKey, JSON.stringify(logs), { EX: 172800 }) } const lines = logs.split("\n") const contributors = {} let currentAuthor = null for (let i = 0; i < lines.length; i++) { const line = lines[i].trim() if (line.includes("|")) { const [commitHash, name, email, date, ...messageParts] = line.split("|") currentAuthor = email // 用邮箱作为唯一 key 更稳妥 if (!contributors[currentAuthor]) { contributors[currentAuthor] = { name, email, commits: 0, linesAdded: 0, linesDeleted: 0, filesChanged: 0, linesChanged: 0 // total changes } } contributors[currentAuthor].commits += 1 // 接下来的 shortstat 统计信息在下一行 const statLine = lines[i + 1]?.trim() || "" const statMatch = { filesChanged: statLine.match(/(\d+) file[s]? changed/), insertions: statLine.match(/(\d+) insertion[s]?\(\+\)/), deletions: statLine.match(/(\d+) deletion[s]?\(-\)/), } const files = statMatch.filesChanged ? parseInt(statMatch.filesChanged[1], 10) : 0 const insertions = statMatch.insertions ? parseInt(statMatch.insertions[1], 10) : 0 const deletions = statMatch.deletions ? parseInt(statMatch.deletions[1], 10) : 0 contributors[currentAuthor].filesChanged += files contributors[currentAuthor].linesAdded += insertions contributors[currentAuthor].linesDeleted += deletions contributors[currentAuthor].linesChanged += insertions + deletions if (statLine) i++ // 跳过 shortstat 行 } } const sorted = Object.values(contributors).sort( (a, b) => b.linesChanged - a.linesChanged || b.commits - a.commits ) return sorted } catch (err) { console.error("分析 Git 贡献者失败:", err) throw new Error("分析 Git 贡献者失败") } } async onRequest(req, res) { let { uuid, session, id } = req.query if ([uuid, session, id].some(value => value === '' || value === null || value === undefined)) return res.json({ ...BaseStdResponse.MISSING_PARAMETER }) // 检查 session if (!await AccessControl.checkSession(uuid, session)) return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED }) let sql = 'SELECT state, path FROM repos WHERE create_user = ? AND id = ?' let r = await db.query(sql, [uuid, id]) if (!r || r.length === 0) return res.json({ ...BaseStdResponse.ERR, msg: '未找到仓库' }) if (r[0].state !== 1 || !r[0].path) return res.json({ ...BaseStdResponse.ERR, msg: '仓库未成功克隆!' }) let contributors try { const cachedContributors = await redis.get(`contributors:${r[0].path}`) if (cachedContributors) return res.json({ ...BaseStdResponse.OK, data: JSON.parse(cachedContributors) }) contributors = await this.analyzeGitContributors(r[0].path) } catch (error) { this.logger.error('获取仓库开发者失败!' + error.stack) return res.json({ ...BaseStdResponse.ERR, msg: '获取仓库开发者失败!' }) } res.json({ ...BaseStdResponse.OK, data: contributors }) redis.set(`contributors:${r[0].path}`, JSON.stringify(contributors), { EX: 172800 }) } } module.exports.GitContributors = GitContributors