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