forked from Gitlink/gitlink-cli
291 lines
5.9 KiB
Go
291 lines
5.9 KiB
Go
package contrib
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||
)
|
||
|
||
// Contributor 贡献者信息
|
||
type Contributor struct {
|
||
Login string
|
||
Name string
|
||
Commits int
|
||
Additions int
|
||
Deletions int
|
||
Issues int
|
||
PRs int
|
||
Score float64
|
||
}
|
||
|
||
// AHP 权重(基于层次分析法计算)
|
||
const (
|
||
WeightCommits = 0.143
|
||
WeightCodeLines = 0.286
|
||
WeightPRs = 0.071
|
||
WeightIssues = 0.05
|
||
WeightIssueSolve = 0.1
|
||
WeightRelease = 0.1
|
||
WeightComments = 0.083
|
||
WeightPRReview = 0.083
|
||
WeightWiki = 0.083
|
||
)
|
||
|
||
// fetchContributors 从 issue 和 PR 数据中提取贡献者
|
||
func fetchContributors(ctx *common.RuntimeContext) ([]Contributor, error) {
|
||
contributorMap := make(map[string]*Contributor)
|
||
|
||
// 1. 从 Issue 列表中提取贡献者
|
||
issues, err := fetchAllIssues(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("fetch issues: %w", err)
|
||
}
|
||
for _, issue := range issues {
|
||
login := issue["login"]
|
||
name := issue["name"]
|
||
if login == "" {
|
||
continue
|
||
}
|
||
if _, exists := contributorMap[login]; !exists {
|
||
contributorMap[login] = &Contributor{
|
||
Login: login,
|
||
Name: name,
|
||
}
|
||
if contributorMap[login].Name == "" {
|
||
contributorMap[login].Name = login
|
||
}
|
||
}
|
||
contributorMap[login].Issues++
|
||
}
|
||
|
||
// 2. 从 PR 列表中提取贡献者
|
||
prs, err := fetchAllPRs(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("fetch PRs: %w", err)
|
||
}
|
||
for _, pr := range prs {
|
||
login := pr["login"]
|
||
name := pr["name"]
|
||
if login == "" {
|
||
continue
|
||
}
|
||
if _, exists := contributorMap[login]; !exists {
|
||
contributorMap[login] = &Contributor{
|
||
Login: login,
|
||
Name: name,
|
||
}
|
||
if contributorMap[login].Name == "" {
|
||
contributorMap[login].Name = login
|
||
}
|
||
}
|
||
contributorMap[login].PRs++
|
||
}
|
||
|
||
// 转换为切片
|
||
var contributors []Contributor
|
||
for _, c := range contributorMap {
|
||
contributors = append(contributors, *c)
|
||
}
|
||
|
||
if len(contributors) == 0 {
|
||
return nil, fmt.Errorf("no contributors found")
|
||
}
|
||
|
||
return contributors, nil
|
||
}
|
||
|
||
// fetchAllIssues 获取所有 Issue
|
||
func fetchAllIssues(ctx *common.RuntimeContext) ([]map[string]string, error) {
|
||
var results []map[string]string
|
||
page := 1
|
||
|
||
for {
|
||
q := url.Values{}
|
||
q.Set("page", fmt.Sprintf("%d", page))
|
||
q.Set("limit", "100")
|
||
q.Set("state", "all")
|
||
|
||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/issues", ctx.Owner, ctx.Repo), q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
data, ok := env.Data.(map[string]interface{})
|
||
if !ok {
|
||
break
|
||
}
|
||
|
||
issues, ok := data["issues"].([]interface{})
|
||
if !ok || len(issues) == 0 {
|
||
break
|
||
}
|
||
|
||
for _, item := range issues {
|
||
issue, ok := item.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
author, ok := issue["author"].(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
login := getString(author, "login")
|
||
name := getString(author, "name")
|
||
if login != "" {
|
||
results = append(results, map[string]string{
|
||
"login": login,
|
||
"name": name,
|
||
})
|
||
}
|
||
}
|
||
|
||
totalCount := getInt(data, "total_count")
|
||
if page*100 >= totalCount {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// fetchAllPRs 获取所有 PR
|
||
func fetchAllPRs(ctx *common.RuntimeContext) ([]map[string]string, error) {
|
||
var results []map[string]string
|
||
page := 1
|
||
|
||
for {
|
||
q := url.Values{}
|
||
q.Set("page", fmt.Sprintf("%d", page))
|
||
q.Set("limit", "100")
|
||
q.Set("state", "all")
|
||
|
||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/pulls", ctx.Owner, ctx.Repo), q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
data, ok := env.Data.(map[string]interface{})
|
||
if !ok {
|
||
break
|
||
}
|
||
|
||
// PR 列表字段是 "pulls"
|
||
prs, ok := data["pulls"].([]interface{})
|
||
if !ok || len(prs) == 0 {
|
||
break
|
||
}
|
||
|
||
for _, item := range prs {
|
||
pr, ok := item.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
// PR 的作者信息在 issue.author 中
|
||
login := ""
|
||
name := ""
|
||
if issue, ok := pr["issue"].(map[string]interface{}); ok {
|
||
if author, ok := issue["author"].(map[string]interface{}); ok {
|
||
login = getString(author, "login")
|
||
name = getString(author, "name")
|
||
}
|
||
}
|
||
if login != "" {
|
||
results = append(results, map[string]string{
|
||
"login": login,
|
||
"name": name,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 检查是否还有更多页
|
||
searchCount := getInt(data, "search_count")
|
||
if page*100 >= searchCount {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// calculateScores 计算加权贡献分数
|
||
func calculateScores(contributors []Contributor, issueCounts, prCounts map[string]int) []Contributor {
|
||
// 如果提供了额外的计数,更新贡献者数据
|
||
if issueCounts != nil {
|
||
for i := range contributors {
|
||
c := &contributors[i]
|
||
if count, ok := issueCounts[c.Login]; ok {
|
||
c.Issues = count
|
||
}
|
||
}
|
||
}
|
||
if prCounts != nil {
|
||
for i := range contributors {
|
||
c := &contributors[i]
|
||
if count, ok := prCounts[c.Login]; ok {
|
||
c.PRs = count
|
||
}
|
||
}
|
||
}
|
||
|
||
// 找到各指标的最大值(用于归一化)
|
||
maxIssues := 0
|
||
maxPRs := 0
|
||
|
||
for i := range contributors {
|
||
c := &contributors[i]
|
||
if c.Issues > maxIssues {
|
||
maxIssues = c.Issues
|
||
}
|
||
if c.PRs > maxPRs {
|
||
maxPRs = c.PRs
|
||
}
|
||
}
|
||
|
||
// 计算加权分数(归一化后)
|
||
for i := range contributors {
|
||
c := &contributors[i]
|
||
|
||
// 归一化到 0-1
|
||
normIssues := 0.0
|
||
normPRs := 0.0
|
||
|
||
if maxIssues > 0 {
|
||
normIssues = float64(c.Issues) / float64(maxIssues)
|
||
}
|
||
if maxPRs > 0 {
|
||
normPRs = float64(c.PRs) / float64(maxPRs)
|
||
}
|
||
|
||
// 加权求和(只用 issue 和 PR,因为 commits API 不可用)
|
||
c.Score = normIssues*WeightIssues + normPRs*WeightPRs
|
||
}
|
||
|
||
return contributors
|
||
}
|
||
|
||
// getString 从 map 中获取字符串
|
||
func getString(m map[string]interface{}, key string) string {
|
||
if v, ok := m[key]; ok {
|
||
if s, ok := v.(string); ok {
|
||
return s
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// getInt 从 map 中获取整数
|
||
func getInt(m map[string]interface{}, key string) int {
|
||
if v, ok := m[key]; ok {
|
||
switch n := v.(type) {
|
||
case float64:
|
||
return int(n)
|
||
case int:
|
||
return n
|
||
}
|
||
}
|
||
return 0
|
||
}
|