internal/httpd/facts.go

dd06e80a071d451ebb7b083363e0580550481114
gitbay/internal/httpd/facts.go history · blame · raw

95 lines · 2908 bytes

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