internal/httpd/facts.go
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}