package git import ( "bufio" "bytes" "fmt" "sort" "strconv" "strings" "time" ) type RepoKPIStats struct { Contributors int64 KeyContributors int64 ContributorsAdded int64 CommitsAdded int64 CommitLinesModified int64 Authors []*UserKPITypeStats } type UserKPIStats struct { Name string Email string Commits int64 CommitLines int64 } type UserKPITypeStats struct { UserKPIStats isNewContributor bool //是否是4个月内的新增贡献者 } func GetRepoKPIStats(repoPath string) (*RepoKPIStats, error) { stats := &RepoKPIStats{} contributors, err := GetContributors(repoPath) if err != nil { return nil, err } timeUntil := time.Now() fourMonthAgo := timeUntil.AddDate(0, -4, 0) recentlyContributors, err := getContributors(repoPath, fourMonthAgo) newContributersDict := make(map[string]struct{}) if err != nil { return nil, err } if contributors != nil { stats.Contributors = int64(len(contributors)) for _, contributor := range contributors { if contributor.CommitCnt >= 3 { stats.KeyContributors++ } if recentlyContributors != nil { for _, recentlyContributor := range recentlyContributors { if recentlyContributor.Email == contributor.Email && recentlyContributor.CommitCnt == contributor.CommitCnt { stats.ContributorsAdded++ newContributersDict[recentlyContributor.Email] = struct{}{} } } } } } err = setRepoKPIStats(repoPath, fourMonthAgo, stats, newContributersDict) if err != nil { return nil, fmt.Errorf("FillFromGit: %v", err) } return stats, nil } //获取一天内的用户贡献指标 func GetUserKPIStats(repoPath string) (map[string]*UserKPIStats, error) { timeUntil := time.Now() oneDayAgo := timeUntil.AddDate(0, 0, -1) since := oneDayAgo.Format(time.RFC3339) args := []string{"log", "--numstat", "--no-merges", "--branches=*", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)} stdout, err := NewCommand(args...).RunInDirBytes(repoPath) if err != nil { return nil, err } scanner := bufio.NewScanner(bytes.NewReader(stdout)) scanner.Split(bufio.ScanLines) usersKPIStatses := make(map[string]*UserKPIStats) var author string p := 0 var email string for scanner.Scan() { l := strings.TrimSpace(scanner.Text()) if l == "---" { p = 1 } else if p == 0 { continue } else { p++ } if p > 4 && len(l) == 0 { continue } switch p { case 1: // Separator case 2: // Commit sha-1 case 3: // Author author = l case 4: // E-mail email = strings.ToLower(l) if _, ok := usersKPIStatses[email]; !ok { usersKPIStatses[email] = &UserKPIStats{ Name: author, Email: email, Commits: 0, CommitLines: 0, } } usersKPIStatses[email].Commits++ default: // Changed file if parts := strings.Fields(l); len(parts) >= 3 { if parts[0] != "-" { if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { usersKPIStatses[email].CommitLines += c } } if parts[1] != "-" { if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { usersKPIStatses[email].CommitLines += c } } } } } return usersKPIStatses, nil } func setRepoKPIStats(repoPath string, fromTime time.Time, stats *RepoKPIStats, newContributers map[string]struct{}) error { since := fromTime.Format(time.RFC3339) args := []string{"log", "--numstat", "--no-merges", "--branches=*", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)} stdout, err := NewCommand(args...).RunInDirBytes(repoPath) if err != nil { return err } scanner := bufio.NewScanner(bytes.NewReader(stdout)) scanner.Split(bufio.ScanLines) authors := make(map[string]*UserKPITypeStats) var author string p := 0 var email string for scanner.Scan() { l := strings.TrimSpace(scanner.Text()) if l == "---" { p = 1 } else if p == 0 { continue } else { p++ } if p > 4 && len(l) == 0 { continue } switch p { case 1: // Separator case 2: // Commit sha-1 stats.CommitsAdded++ case 3: // Author author = l case 4: // E-mail email = strings.ToLower(l) if _, ok := authors[email]; !ok { authors[email] = &UserKPITypeStats{ UserKPIStats: UserKPIStats{ Name: author, Email: email, Commits: 0, CommitLines: 0, }, isNewContributor: false, } } if _, ok := newContributers[email]; ok { authors[email].isNewContributor = true } authors[email].Commits++ default: // Changed file if parts := strings.Fields(l); len(parts) >= 3 { if parts[0] != "-" { if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { stats.CommitLinesModified += c authors[email].CommitLines += c } } if parts[1] != "-" { if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { stats.CommitLinesModified += c authors[email].CommitLines += c } } } } } a := make([]*UserKPITypeStats, 0, len(authors)) for _, v := range authors { a = append(a, v) } // Sort authors descending depending on commit count sort.Slice(a, func(i, j int) bool { return a[i].Commits > a[j].Commits }) stats.Authors = a return nil } func getContributors(repoPath string, fromTime time.Time) ([]Contributor, error) { since := fromTime.Format(time.RFC3339) cmd := NewCommand("shortlog", "-sne", "--all", fmt.Sprintf("--since='%s'", since)) stdout, err := cmd.RunInDir(repoPath) if err != nil { return nil, err } stdout = strings.Trim(stdout, "\n") contributorRows := strings.Split(stdout, "\n") if len(contributorRows) > 0 { contributorsInfo := make([]Contributor, len(contributorRows)) for i := 0; i < len(contributorRows); i++ { var oneCount string = strings.Trim(contributorRows[i], " ") if strings.Index(oneCount, "\t") < 0 { continue } number := oneCount[0:strings.Index(oneCount, "\t")] commitCnt, _ := strconv.Atoi(number) committer := oneCount[strings.Index(oneCount, "\t")+1 : strings.LastIndex(oneCount, " ")] committer = strings.Trim(committer, " ") email := oneCount[strings.Index(oneCount, "<")+1 : strings.Index(oneCount, ">")] contributorsInfo[i] = Contributor{ commitCnt, committer, email, } } return contributorsInfo, nil } return nil, nil }