internal/gitutil/facts.go

2867b44a48ea8afa6b7c21d5e1d69272a7b235fa
gitbay/internal/gitutil/facts.go history · blame · raw

115 lines · 3112 bytes

  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}