internal/gitutil/facts.go
113 lines · 3027 bytes
1package gitutil
2
3import (
4 "os/exec"
5 "sort"
6 "strconv"
7 "strings"
8)
9
10// CountCommits returns the number of commits reachable from ref, or 0 when
11// the ref does not resolve (an empty repository).
12func CountCommits(dir, ref string) int {
13 out, err := exec.Command("git", "-C", dir, "rev-list", "--count", "--end-of-options", ref).Output()
14 if err != nil {
15 return 0
16 }
17 n, _ := strconv.Atoi(strings.TrimSpace(string(out)))
18 return n
19}
20
21// Contributor is one author of a repository's history.
22type Contributor struct {
23 Name string
24 Email string
25 Commits int
26}
27
28// Contributors summarises authorship reachable from ref, most commits
29// first. Identities are keyed by email, since that is what the forge can
30// tie back to an account, after the repository's own .mailmap has had its
31// say — a bare repo resolves that from HEAD:.mailmap with no config.
32func Contributors(dir, ref string) []Contributor {
33 out, err := exec.Command("git", "-C", dir, "log",
34 "--use-mailmap", "--format=%aN%x01%aE", "--end-of-options", ref).Output()
35 if err != nil {
36 return nil
37 }
38 byEmail := map[string]*Contributor{}
39 var order []string
40 for _, line := range strings.Split(string(out), "\n") {
41 name, email, ok := strings.Cut(line, "\x01")
42 if !ok || email == "" {
43 continue
44 }
45 c, seen := byEmail[email]
46 if !seen {
47 byEmail[email] = &Contributor{Name: name, Email: email, Commits: 1}
48 order = append(order, email)
49 continue
50 }
51 c.Commits++
52 }
53 list := make([]Contributor, 0, len(order))
54 for _, e := range order {
55 list = append(list, *byEmail[e])
56 }
57 sort.SliceStable(list, func(i, j int) bool { return list[i].Commits > list[j].Commits })
58 return list
59}
60
61// Languages reports the byte share of each language in the tree at ref,
62// largest first, keyed by the extension map the caller supplies. Only
63// blobs count; git's own metadata does not.
64func Languages(dir, ref string, lang func(path string) string) []Language {
65 out, err := exec.Command("git", "-C", dir, "ls-tree", "-r", "-l", "--full-name", "--end-of-options", ref).Output()
66 if err != nil {
67 return nil
68 }
69 bytesBy := map[string]int64{}
70 var total int64
71 for _, line := range strings.Split(string(out), "\n") {
72 // <mode> blob <sha> <size>\t<path>
73 meta, path, ok := strings.Cut(line, "\t")
74 if !ok {
75 continue
76 }
77 f := strings.Fields(meta)
78 if len(f) < 4 || f[1] != "blob" {
79 continue
80 }
81 size, err := strconv.ParseInt(f[3], 10, 64)
82 if err != nil {
83 continue
84 }
85 name := lang(path)
86 if name == "" {
87 continue
88 }
89 bytesBy[name] += size
90 total += size
91 }
92 if total == 0 {
93 return nil
94 }
95 langs := make([]Language, 0, len(bytesBy))
96 for name, b := range bytesBy {
97 langs = append(langs, Language{Name: name, Bytes: b,
98 Percent: float64(b) * 100 / float64(total)})
99 }
100 sort.Slice(langs, func(i, j int) bool {
101 if langs[i].Bytes != langs[j].Bytes {
102 return langs[i].Bytes > langs[j].Bytes
103 }
104 return langs[i].Name < langs[j].Name
105 })
106 return langs
107}
108
109type Language struct {
110 Name string
111 Bytes int64
112 Percent float64
113}