|
@@ -23,52 +23,71 @@ class GitContributors extends API {
|
|
|
try {
|
|
|
const git = simpleGit()
|
|
|
await git.cwd(repoPath)
|
|
|
-
|
|
|
- // 获取提交日志,包括作者姓名和邮箱
|
|
|
+
|
|
|
+ // 获取详细日志,带上插入删除统计并排除 merge 提交
|
|
|
const logs = await git.raw([
|
|
|
"log",
|
|
|
- "--format=%an <%ae>",
|
|
|
+ "--no-merges",
|
|
|
+ "--pretty=format:%H|%an|%ae",
|
|
|
"--shortstat"
|
|
|
])
|
|
|
-
|
|
|
- const lines = logs.split("\n").map(l => l.trim()).filter(l => l)
|
|
|
- let contributors = {}
|
|
|
+
|
|
|
+ const lines = logs.split("\n")
|
|
|
+ const contributors = {}
|
|
|
let currentAuthor = null
|
|
|
- for (const line of lines) {
|
|
|
- if (line.includes("<") && line.includes(">")) {
|
|
|
- const match = line.match(/(.*?) <(.*?)>/)
|
|
|
- if (!match) continue
|
|
|
- const name = match[1].trim()
|
|
|
- const email = match[2].trim()
|
|
|
- currentAuthor = name
|
|
|
+
|
|
|
+ for (let i = 0; i < lines.length; i++) {
|
|
|
+ const line = lines[i].trim()
|
|
|
+
|
|
|
+ if (line.includes("|")) {
|
|
|
+ const [commitHash, name, email] = line.split("|")
|
|
|
+ currentAuthor = email // 用邮箱作为唯一 key 更稳妥
|
|
|
if (!contributors[currentAuthor]) {
|
|
|
contributors[currentAuthor] = {
|
|
|
name,
|
|
|
email,
|
|
|
avatar: this.getGravatarUrl(email),
|
|
|
commits: 0,
|
|
|
- linesChanged: 0
|
|
|
+ linesAdded: 0,
|
|
|
+ linesDeleted: 0,
|
|
|
+ filesChanged: 0,
|
|
|
+ linesChanged: 0 // total changes
|
|
|
}
|
|
|
}
|
|
|
contributors[currentAuthor].commits += 1
|
|
|
- } else if (line.includes("file changed")) {
|
|
|
- const match = line.match(/(\d+) insertions?\(\+\)|(\d+) deletions?\(-\)/g)
|
|
|
- if (match) {
|
|
|
- const changes = match.map(m => parseInt(m.match(/\d+/)[0], 10)).reduce((a, b) => a + b, 0)
|
|
|
- contributors[currentAuthor].linesChanged += changes
|
|
|
+
|
|
|
+ // 接下来的 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 sortedContributors = Object.values(contributors)
|
|
|
- .sort((a, b) => b.linesChanged - a.linesChanged || b.commits - a.commits)
|
|
|
-
|
|
|
- return sortedContributors
|
|
|
- } catch (error) {
|
|
|
- console.error("获取仓库开发者失败:", error)
|
|
|
- throw new Error("获取仓库开发者失败")
|
|
|
+
|
|
|
+ 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
|