Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Show real accounts as repository contributors

a32a859a08276681497be390d6f7195098a97ab4 · Keenan Nemetz · 5d ago · parent 802e0e52

Show real accounts as repository contributors

Count contributors from the change and issue records instead of the author text in commits: changes merged, issues those changes fixed, issues the reporter opened that were fixed, and reviews that named the merged head. The overview lists accounts by that work with compact icon counts and full labels in tooltips. The shortlog-derived contributor table and its refresh path are gone.

server/web/overview.go +126−29

@@ -32,11 +32,60 @@ type languageShare struct {
32 32 Percent int
33 33 }
34 34
35 // contributorView is one account in the overview's short list. The counts are
36 // the repository's own, so the same account can rank differently in another
37 // repository.
35 38 type contributorView struct {
36 Name string
37 Commits int
39 Name string
40 Merged int
41 Fixed int
42 Reported int
43 Reviews int
44 Bot bool
45 }
46
47 func (c contributorView) total() int { return c.Merged + c.Fixed + c.Reported + c.Reviews }
48
49 // contributorStat is one icon beside an account's name. The label is the whole
50 // sentence the icon stands for, shown as a tooltip and read by assistive tech;
51 // a zero count is left out rather than drawn as an empty mark.
52 type contributorStat struct {
53 Icon string
54 Count int
55 Label string
56 }
57
58 // Stats returns the counts that are not zero, in the order the section explains
59 // them. The labels name the outcome, not the record behind it, and each count is
60 // pluralised on its own so "1 issue reported" does not read like a schema dump.
61 func (c contributorView) Stats() []contributorStat {
62 all := []contributorStat{
63 {Icon: "git-merge", Count: c.Merged, Label: countLabel(c.Merged, "change merged", "changes merged")},
64 {Icon: "circle-check", Count: c.Fixed, Label: countLabel(c.Fixed, "issue fixed", "issues fixed")},
65 {Icon: "circle-dot", Count: c.Reported, Label: countLabel(c.Reported, "issue reported", "issues reported")},
66 {Icon: "check", Count: c.Reviews, Label: countLabel(c.Reviews, "review", "reviews")},
67 }
68 out := all[:0]
69 for _, stat := range all {
70 if stat.Count > 0 {
71 out = append(out, stat)
72 }
73 }
74 return out
75 }
76
77 func countLabel(n int, one, many string) string {
78 if n == 1 {
79 return "1 " + one
80 }
81 return fmt.Sprintf("%d %s", n, many)
38 82 }
39 83
84 // overviewContributorLimit is how many accounts the sidebar names. The list is
85 // a pointer to the work, not a directory of everybody who ever touched the
86 // repository.
87 const overviewContributorLimit = 8
88
40 89 // overviewEntity is the small shape the overview's issue and change sections
41 90 // need: enough to link, title, name the author and say how old it is. The list
42 91 // pages carry richer rows; the sidebar says only what fits beside a README.
@@ -64,14 +113,16 @@ func (w *Web) Overview(rw http.ResponseWriter, r *http.Request) {
64 113 // a packed ref, and the answer is what the index's summary is checked against.
65 114 commit, _ := g.RefCommit("refs/heads/" + ref)
66 115
67 commits, languages, contributors, tags := w.overviewSummaries(memoOf(r), m, commit)
116 memo := memoOf(r)
117 commits, languages, tags := w.overviewSummaries(memo, m, commit)
118 contributors := w.overviewContributors(memo, m)
68 119
69 120 var recentRuns []*runs.Run
70 121 if w.Runs != nil {
71 122 recentRuns, _ = w.Runs.Recent(m.Name, 5)
72 123 }
73 124
74 recentIssues, recentRequests := w.overviewRecent(memoOf(r), m)
125 recentIssues, recentRequests := w.overviewRecent(memo, m)
75 126
76 127 readme, readmeName := w.readmeAt(m, ref, commit)
77 128
@@ -107,12 +158,12 @@ func (w *Web) Overview(rw http.ResponseWriter, r *http.Request) {
107 158 }
108 159
109 160 // overviewSummaries returns the parts of the overview that describe the default
110 // branch: its newest commits, its languages, its authors, and the repository's
111 // tags. When the index has the summary for this exact tip they are rows; when it
112 // does not (no index, or a tip the push hook has not recorded) they are read from
113 // git, which is the fallback that keeps the page correct.
161 // branch: its newest commits, its languages, and the repository's tags. When the
162 // index has the summary for this exact tip they are rows; when it does not (no
163 // index, or a tip the push hook has not recorded) they are read from git, which
164 // is the fallback that keeps the page correct.
114 165 func (w *Web) overviewSummaries(memo *requestMemo, m *repo.Meta, commit string) (
115 commits []git.Commit, languages []languageShare, contributors []contributorView, tags []git.Tag) {
166 commits []git.Commit, languages []languageShare, tags []git.Tag) {
116 167 g := m.Git()
117 168 if w.Index != nil {
118 169 if err := w.ensureIndex(memo, m); err == nil {
@@ -126,12 +177,6 @@ func (w *Web) overviewSummaries(memo *requestMemo, m *repo.Meta, commit string)
126 177 if shares, err := w.Index.Languages(e.ID); err == nil {
127 178 languages = languageShares(shares)
128 179 }
129 if list, err := w.Index.Contributors(e.ID, 8); err == nil {
130 contributors = make([]contributorView, 0, len(list))
131 for _, c := range list {
132 contributors = append(contributors, contributorView{Name: c.Name, Commits: c.Commits})
133 }
134 }
135 180 if list, err := w.Index.Tags(e.ID, 5); err == nil {
136 181 tags = make([]git.Tag, 0, len(list))
137 182 for _, t := range list {
@@ -142,9 +187,9 @@ func (w *Web) overviewSummaries(memo *requestMemo, m *repo.Meta, commit string)
142 187 }
143 188 }
144 189 // A repository with commits has at least one of each; an empty
145 // section means the summary was written without it (a tree walk or
146 // a shortlog that failed), so read that part from git rather than
147 // leave the page blank until the next push.
190 // section means the summary was written without it (a tree walk
191 // that failed), so read that part from git rather than leave the
192 // page blank until the next push.
148 193 if commit != "" {
149 194 if len(commits) == 0 {
150 195 commits, _ = g.Log(commit, 5)
@@ -152,14 +197,8 @@ func (w *Web) overviewSummaries(memo *requestMemo, m *repo.Meta, commit string)
152 197 if len(languages) == 0 {
153 198 languages = w.languages(g, commit)
154 199 }
155 if len(contributors) == 0 {
156 contributors = w.contributors(g, commit)
157 if len(contributors) > 8 {
158 contributors = contributors[:8]
159 }
160 }
161 200 }
162 return commits, languages, contributors, tags
201 return commits, languages, tags
163 202 }
164 203 }
165 204 }
@@ -173,11 +212,69 @@ func (w *Web) overviewSummaries(memo *requestMemo, m *repo.Meta, commit string)
173 212 }
174 213 tags = list
175 214 }
176 contributors = w.contributors(g, commit)
177 if len(contributors) > 8 {
178 contributors = contributors[:8]
215 return commits, languages, tags
216 }
217
218 // overviewContributors ranks the accounts whose work the metadata branch
219 // records: changes that merged, issues those changes fixed, issues the account
220 // reported that were fixed, and reviews that named the merged head. Commit
221 // authors are deliberately absent: a git name is text somebody typed, not an
222 // account, and there is no field that joins the two.
223 func (w *Web) overviewContributors(memo *requestMemo, m *repo.Meta) []contributorView {
224 if w.Index == nil {
225 return nil
179 226 }
180 return commits, languages, contributors, tags
227 if err := w.ensureIndex(memo, m); err != nil {
228 return nil
229 }
230 sums, err := w.Index.ContributorSummaries(m.ID)
231 if err != nil {
232 return nil
233 }
234 list := make([]contributorView, 0, len(sums))
235 for name, sum := range sums {
236 view := contributorView{
237 Name: name, Merged: sum.Merged, Fixed: sum.Fixed,
238 Reported: sum.Reported, Reviews: sum.Reviews,
239 }
240 if w.Ident != nil {
241 // A name in metadata that the roster does not know is not an
242 // account. It can be left behind by a rewrite or a hand-edited
243 // branch, and the page should not present it as a person.
244 acct, err := w.Ident.Find(name)
245 if err != nil {
246 continue
247 }
248 view.Name = acct.Name
249 view.Bot = acct.Bot()
250 }
251 if view.total() == 0 {
252 continue
253 }
254 list = append(list, view)
255 }
256 sort.SliceStable(list, func(i, j int) bool {
257 if a, b := list[i].total(), list[j].total(); a != b {
258 return a > b
259 }
260 if list[i].Merged != list[j].Merged {
261 return list[i].Merged > list[j].Merged
262 }
263 if list[i].Fixed != list[j].Fixed {
264 return list[i].Fixed > list[j].Fixed
265 }
266 if list[i].Reported != list[j].Reported {
267 return list[i].Reported > list[j].Reported
268 }
269 if list[i].Reviews != list[j].Reviews {
270 return list[i].Reviews > list[j].Reviews
271 }
272 return list[i].Name < list[j].Name
273 })
274 if len(list) > overviewContributorLimit {
275 list = list[:overviewContributorLimit]
276 }
277 return list
181 278 }
182 279
183 280 // overviewRecent returns the newest open issues and change requests for the