package gitcmd import ( "sort" "strconv" "strings" "time" ) type Contributor struct { Author string `json:"author"` Email string `json:"email"` Commits int `json:"commits"` Additions int `json:"additions"` Deletions int `json:"deletions"` TotalLines int `json:"total_lines"` } type RepoStats struct { TotalCommits int `json:"total_commits"` TotalBranches int `json:"total_branches"` TotalTags int `json:"total_tags"` TotalSize int64 `json:"total_size"` Contributors []Contributor `json:"contributors"` FirstCommit time.Time `json:"first_commit"` LastCommit time.Time `json:"last_commit"` } func (r *Repository) GetContributors(ref string) ([]Contributor, error) { if empty, err := r.IsEmpty(); err != nil { return nil, err } else if empty { return []Contributor{}, nil } if ref == "" { ref = "HEAD" } out, _, err := NewCommand("log"). Add("--format=----COMMIT%x1f%an%x1f%ae"). Add("--numstat"). AddDynamicArguments(ref). WithDir(r.Path). RunStdString(r.ctx) if err != nil { return nil, err } byKey := map[string]*Contributor{} var curAuthor, curEmail string for _, line := range strings.Split(out, "\n") { if strings.HasPrefix(line, "----COMMIT") { fields := strings.Split(line, "\x1f") if len(fields) < 3 { continue } curAuthor = fields[1] curEmail = fields[2] key := curAuthor + " <" + curEmail + ">" if byKey[key] == nil { byKey[key] = &Contributor{Author: curAuthor, Email: curEmail} } byKey[key].Commits++ continue } if curAuthor == "" { continue } parts := strings.SplitN(line, "\t", 3) if len(parts) < 3 { continue } add, _ := strconv.Atoi(parts[0]) del, _ := strconv.Atoi(parts[1]) key := curAuthor + " <" + curEmail + ">" byKey[key].Additions += add byKey[key].Deletions += del byKey[key].TotalLines += add + del } result := make([]Contributor, 0, len(byKey)) for _, c := range byKey { result = append(result, *c) } sort.Slice(result, func(i, j int) bool { if result[i].Commits != result[j].Commits { return result[i].Commits > result[j].Commits } return result[i].TotalLines > result[j].TotalLines }) return result, nil } func (r *Repository) GetStats() (*RepoStats, error) { revOut, _, _ := NewCommand("rev-list", "--count", "HEAD"). WithDir(r.Path). RunStdString(r.ctx) totalCommits, _ := strconv.Atoi(strings.TrimSpace(revOut)) branchOut, _, _ := NewCommand("branch", "--list"). WithDir(r.Path). RunStdString(r.ctx) totalBranches := countNonEmptyLines(branchOut) tagOut, _, _ := NewCommand("tag", "--list"). WithDir(r.Path). RunStdString(r.ctx) totalTags := countNonEmptyLines(tagOut) firstOut, _, _ := NewCommand("log", "--reverse"). Add("--format=%aI"). Add("HEAD"). WithDir(r.Path). RunStdString(r.ctx) firstLine := strings.TrimSpace(firstOut) if i := strings.IndexByte(firstLine, '\n'); i >= 0 { firstLine = firstLine[:i] } firstCommit, _ := time.Parse(time.RFC3339, firstLine) lastOut, _, _ := NewCommand("log"). Add("--format=%aI"). Add("-n", "1"). Add("HEAD"). WithDir(r.Path). RunStdString(r.ctx) lastCommit, _ := time.Parse(time.RFC3339, strings.TrimSpace(lastOut)) contributors, _ := r.GetContributors("HEAD") var totalSize int64 if co, err := r.CountObjects(); err == nil { totalSize = co.TotalSize() } return &RepoStats{ TotalCommits: totalCommits, TotalBranches: totalBranches, TotalTags: totalTags, TotalSize: totalSize, Contributors: contributors, FirstCommit: firstCommit, LastCommit: lastCommit, }, nil } func countNonEmptyLines(s string) int { count := 0 for _, line := range strings.Split(strings.TrimSpace(s), "\n") { if line != "" { count++ } } return count }