internal/gitutil/facts.go
114 lines · 2893 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", 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.
31func Contributors(dir, ref string, max int) []Contributor {
32 out, err := exec.Command("git", "-C", dir, "log", "--format=%an%x01%ae", ref).Output()
33 if err != nil {
34 return nil
35 }
36 byEmail := map[string]*Contributor{}
37 var order []string
38 for _, line := range strings.Split(string(out), "\n") {
39 name, email, ok := strings.Cut(line, "\x01")
40 if !ok || email == "" {
41 continue
42 }
43 c, seen := byEmail[email]
44 if !seen {
45 byEmail[email] = &Contributor{Name: name, Email: email, Commits: 1}
46 order = append(order, email)
47 continue
48 }
49 c.Commits++
50 }
51 list := make([]Contributor, 0, len(order))
52 for _, e := range order {
53 list = append(list, *byEmail[e])
54 }
55 sort.SliceStable(list, func(i, j int) bool { return list[i].Commits > list[j].Commits })
56 if max > 0 && len(list) > max {
57 list = list[:max]
58 }
59 return list
60}
61
62// Languages reports the byte share of each language in the tree at ref,
63// largest first, keyed by the extension map the caller supplies. Only
64// blobs count; git's own metadata does not.
65func Languages(dir, ref string, lang func(path string) string) []Language {
66 out, err := exec.Command("git", "-C", dir, "ls-tree", "-r", "-l", "--full-name", ref).Output()
67 if err != nil {
68 return nil
69 }
70 bytesBy := map[string]int64{}
71 var total int64
72 for _, line := range strings.Split(string(out), "\n") {
73 // <mode> blob <sha> <size>\t<path>
74 meta, path, ok := strings.Cut(line, "\t")
75 if !ok {
76 continue
77 }
78 f := strings.Fields(meta)
79 if len(f) < 4 || f[1] != "blob" {
80 continue
81 }
82 size, err := strconv.ParseInt(f[3], 10, 64)
83 if err != nil {
84 continue
85 }
86 name := lang(path)
87 if name == "" {
88 continue
89 }
90 bytesBy[name] += size
91 total += size
92 }
93 if total == 0 {
94 return nil
95 }
96 langs := make([]Language, 0, len(bytesBy))
97 for name, b := range bytesBy {
98 langs = append(langs, Language{Name: name, Bytes: b,
99 Percent: float64(b) * 100 / float64(total)})
100 }
101 sort.Slice(langs, func(i, j int) bool {
102 if langs[i].Bytes != langs[j].Bytes {
103 return langs[i].Bytes > langs[j].Bytes
104 }
105 return langs[i].Name < langs[j].Name
106 })
107 return langs
108}
109
110type Language struct {
111 Name string
112 Bytes int64
113 Percent float64
114}