Files
GitCodeStatic/internal/stats/calculator.go
2025-12-31 16:41:14 +08:00

183 lines
4.7 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 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
}