internal/httpd/facts.go

94884a22329740d40876a987689c17ab77aa9965
gitbay/internal/httpd/facts.go history · blame · raw

92 lines · 2727 bytes

 1package httpd
 2
 3import (
 4	"fmt"
 5	"sort"
 6
 7	"gitbay.org/gitbay/internal/control"
 8	"gitbay.org/gitbay/internal/gitutil"
 9)
10
11// repoFacts is the "what is this repository" summary on a repo home: the
12// counts a visitor uses to size up a project before reading any code.
13type repoFacts struct {
14	Commits      int
15	Branches     int
16	Tags         int
17	Contributors []factContributor
18	Languages    []gitutil.Language
19	License      string
20	Release      string // newest release tag, if any
21	Build        string // latest build status on the default branch
22}
23
24type factContributor struct {
25	Name    string
26	User    string // account, when the email is verified here
27	Email   string // the account's busiest address, when several collapsed
28	Commits int
29}
30
31// Title is the hover text: which address the commits carry, and how many.
32func (c factContributor) Title() string {
33	if c.Commits == 1 {
34		return c.Email + " · 1 commit"
35	}
36	return fmt.Sprintf("%s · %d commits", c.Email, c.Commits)
37}
38
39const maxContributors = 12
40
41// collapseContributors folds the addresses of one account into a single
42// row. .mailmap has already merged whatever the repository claims about
43// its own history; this merges what an account has proven here, which a
44// repository cannot know about. Addresses with no account stay distinct —
45// two people sharing a git name are not one contributor.
46func (s *Server) collapseContributors(cs []gitutil.Contributor) []factContributor {
47	names := s.authorNames()
48	var out []factContributor
49	byUser := map[string]int{}
50	for _, c := range cs {
51		user, known := names.account(c.Email)
52		if known {
53			if i, seen := byUser[user]; seen {
54				out[i].Commits += c.Commits
55				continue
56			}
57			byUser[user] = len(out)
58		}
59		out = append(out, factContributor{
60			Name: names.name(c.Email, c.Name), User: user, Email: c.Email, Commits: c.Commits,
61		})
62	}
63	sort.SliceStable(out, func(i, j int) bool { return out[i].Commits > out[j].Commits })
64	if len(out) > maxContributors {
65		out = out[:maxContributors]
66	}
67	return out
68}
69
70func (s *Server) factsFor(p repoPage) repoFacts {
71	f := repoFacts{
72		Commits: gitutil.CountCommits(p.Dir, p.Ref),
73		License: control.DetectLicense(p.Dir, p.Ref),
74	}
75	if heads, err := gitutil.Refs(p.Dir, "heads"); err == nil {
76		f.Branches = len(heads)
77	}
78	if tags, err := gitutil.Refs(p.Dir, "tags"); err == nil {
79		f.Tags = len(tags)
80	}
81	f.Languages = gitutil.Languages(p.Dir, p.Ref, langOf)
82
83	f.Contributors = s.collapseContributors(gitutil.Contributors(p.Dir, p.Ref))
84
85	if rels, err := s.st.ListReleases(p.Repo.ID); err == nil && len(rels) > 0 {
86		f.Release = rels[0].Tag
87	}
88	if builds, err := s.st.ListBuilds(p.Repo.ID, 1); err == nil && len(builds) > 0 {
89		f.Build = builds[0].Status
90	}
91	return f
92}