gitlink-cli/shortcuts/contrib/data.go

291 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}