internal/gitutil/facts.go

4744c629ed4f0158bdea7d0f1d207fdccb1f7f99
gitbay/internal/gitutil/facts.go history · blame · raw

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}