Files
simplegit/gitcmd/stats.go
T
2026-07-15 11:06:48 -04:00

161 lines
3.7 KiB
Go

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
}