internal/gitutil/facts.go

39535a8e16d8dfc0189bce59511cfb0d7d019fb5
gitbay/internal/gitutil/facts.go history · blame · raw

113 lines · 2967 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, 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", 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", 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}