internal/httpd/facts.go
95 lines · 2908 bytes
6 symbols in this file
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}