internal/httpd/facts.go

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

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