183 lines
4.7 KiB
Go
183 lines
4.7 KiB
Go
package stats
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"fmt"
|
||
"os/exec"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/hanxuanyu/gitcodestatic/internal/logger"
|
||
"github.com/hanxuanyu/gitcodestatic/internal/models"
|
||
)
|
||
|
||
// Calculator 统计计算器
|
||
type Calculator struct {
|
||
gitPath string
|
||
}
|
||
|
||
// NewCalculator 创建统计计算器
|
||
func NewCalculator(gitPath string) *Calculator {
|
||
if gitPath == "" {
|
||
gitPath = "git"
|
||
}
|
||
return &Calculator{gitPath: gitPath}
|
||
}
|
||
|
||
// Calculate 计算统计数据
|
||
func (c *Calculator) Calculate(ctx context.Context, localPath, branch string, constraint *models.StatsConstraint) (*models.Statistics, error) {
|
||
// 构建git log命令
|
||
args := []string{
|
||
"-C", localPath,
|
||
"log",
|
||
"--no-merges",
|
||
"--numstat",
|
||
"--pretty=format:COMMIT:%H|AUTHOR:%an|EMAIL:%ae|DATE:%ai",
|
||
}
|
||
|
||
// 添加约束条件
|
||
if constraint != nil {
|
||
if constraint.Type == models.ConstraintTypeDateRange {
|
||
if constraint.From != "" {
|
||
args = append(args, "--since="+constraint.From)
|
||
}
|
||
if constraint.To != "" {
|
||
args = append(args, "--until="+constraint.To)
|
||
}
|
||
} else if constraint.Type == models.ConstraintTypeCommitLimit {
|
||
args = append(args, "-n", strconv.Itoa(constraint.Limit))
|
||
}
|
||
}
|
||
|
||
args = append(args, branch)
|
||
|
||
logger.Logger.Debug().
|
||
Str("local_path", localPath).
|
||
Str("branch", branch).
|
||
Interface("constraint", constraint).
|
||
Msg("running git log")
|
||
|
||
cmd := exec.CommandContext(ctx, c.gitPath, args...)
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to run git log: %w", err)
|
||
}
|
||
|
||
// 解析输出
|
||
stats, err := c.parseGitLog(string(output))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to parse git log: %w", err)
|
||
}
|
||
|
||
// 填充摘要信息
|
||
stats.Summary.TotalContributors = len(stats.ByContributor)
|
||
if constraint != nil {
|
||
if constraint.Type == models.ConstraintTypeDateRange {
|
||
stats.Summary.DateRange = &models.DateRange{
|
||
From: constraint.From,
|
||
To: constraint.To,
|
||
}
|
||
} else if constraint.Type == models.ConstraintTypeCommitLimit {
|
||
stats.Summary.CommitLimit = &constraint.Limit
|
||
}
|
||
}
|
||
|
||
return stats, nil
|
||
}
|
||
|
||
// parseGitLog 解析git log输出
|
||
func (c *Calculator) parseGitLog(output string) (*models.Statistics, error) {
|
||
stats := &models.Statistics{
|
||
Summary: models.StatsSummary{},
|
||
ByContributor: make([]models.ContributorStats, 0),
|
||
}
|
||
|
||
contributors := make(map[string]*models.ContributorStats)
|
||
|
||
var currentAuthor, currentEmail, currentDate string
|
||
commitCount := 0
|
||
|
||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||
commitPattern := regexp.MustCompile(`^COMMIT:(.+?)\|AUTHOR:(.+?)\|EMAIL:(.+?)\|DATE:(.+)$`)
|
||
numstatPattern := regexp.MustCompile(`^(\d+|-)\s+(\d+|-)\s+(.+)$`)
|
||
|
||
for scanner.Scan() {
|
||
line := strings.TrimSpace(scanner.Text())
|
||
|
||
if line == "" {
|
||
continue
|
||
}
|
||
|
||
// 匹配提交行
|
||
if matches := commitPattern.FindStringSubmatch(line); matches != nil {
|
||
currentAuthor = matches[2]
|
||
currentEmail = matches[3]
|
||
currentDate = matches[4]
|
||
commitCount++
|
||
|
||
// 初始化贡献者统计
|
||
if _, ok := contributors[currentEmail]; !ok {
|
||
// 第一次遇到该贡献者,这是最新的提交(git log从新到旧)
|
||
contributors[currentEmail] = &models.ContributorStats{
|
||
Author: currentAuthor,
|
||
Email: currentEmail,
|
||
LastCommitDate: currentDate, // 第一次遇到就是最新的
|
||
FirstCommitDate: currentDate, // 暂时设为相同,会不断更新
|
||
}
|
||
} else {
|
||
// 继续更新首次提交日期,因为git log从新到旧,越往后越早
|
||
contributors[currentEmail].FirstCommitDate = currentDate
|
||
}
|
||
contributors[currentEmail].Commits++
|
||
continue
|
||
}
|
||
|
||
// 匹配文件变更行
|
||
if matches := numstatPattern.FindStringSubmatch(line); matches != nil && currentEmail != "" {
|
||
additionsStr := matches[1]
|
||
deletionsStr := matches[2]
|
||
|
||
// 处理二进制文件(显示为 -)
|
||
additions := 0
|
||
deletions := 0
|
||
|
||
if additionsStr != "-" {
|
||
additions, _ = strconv.Atoi(additionsStr)
|
||
}
|
||
if deletionsStr != "-" {
|
||
deletions, _ = strconv.Atoi(deletionsStr)
|
||
}
|
||
|
||
contrib := contributors[currentEmail]
|
||
contrib.Additions += additions
|
||
contrib.Deletions += deletions
|
||
}
|
||
}
|
||
|
||
if err := scanner.Err(); err != nil {
|
||
return nil, fmt.Errorf("error reading git log output: %w", err)
|
||
}
|
||
|
||
// 计算修改行数和净增加
|
||
for _, contrib := range contributors {
|
||
// 修改的定义:被替换的行数 = min(additions, deletions)
|
||
contrib.Modifications = min(contrib.Additions, contrib.Deletions)
|
||
contrib.NetAdditions = contrib.Additions - contrib.Deletions
|
||
stats.ByContributor = append(stats.ByContributor, *contrib)
|
||
}
|
||
|
||
stats.Summary.TotalCommits = commitCount
|
||
|
||
return stats, nil
|
||
}
|
||
|
||
// min 返回两个整数的最小值
|
||
func min(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|