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.

forge/index/contributors.go +117

@@ -0,0 +1,117 @@
1 package index
2
3 // A repository's contributors, counted from the work the forge records rather
4 // than from the author text in commits.
5 //
6 // A commit author is whatever the pusher's git config said; an account is a name
7 // the forge authenticated. There is no field that joins the two, so the overview
8 // ranks accounts by what the metadata branch proves they did: changes that
9 // merged, the issues those changes fixed, the issues they reported that were
10 // fixed, and the reviews that named the head that merged.
11 //
12 // Every count is for one repository. Whether a viewer may see that repository is
13 // the caller's question; the overview has already opened it before it asks.
14
15 // ContributorSummary is one account's counted work in one repository.
16 type ContributorSummary struct {
17 // Merged is the number of change requests the account authored that reached
18 // the merged state.
19 Merged int
20 // Fixed is the number of distinct issues closed by those merged changes.
21 Fixed int
22 // Reported is the number of distinct issues the account opened that a merged
23 // change closed. An account that reports and fixes the same issue is counted
24 // in both: diagnosing and fixing are two contributions.
25 Reported int
26 // Reviews is the number of merged changes whose approval names the head and
27 // base that merged. An approval invalidated by a later push is not a review
28 // that gated a landing, so it is not counted.
29 Reviews int
30 }
31
32 // Total is the plain sum of the four counts. It is used only to order the
33 // overview's short list; it is not a score with hidden weights.
34 func (c ContributorSummary) Total() int {
35 return c.Merged + c.Fixed + c.Reported + c.Reviews
36 }
37
38 // ContributorSummaries returns one row per account with counted work in the
39 // repository, keyed by account name. An account with nothing counted is absent
40 // rather than present with four zeroes.
41 func (s *Store) ContributorSummaries(repoID string) (map[string]ContributorSummary, error) {
42 out := map[string]ContributorSummary{}
43 if repoID == "" {
44 return out, nil
45 }
46 queries := []struct {
47 sql string
48 add func(*ContributorSummary, int)
49 }{
50 {
51 // Changes that landed, credited to the account that wrote them.
52 sql: `SELECT author, COUNT(*) FROM requests
53 WHERE repo_id=? AND state='merged'
54 GROUP BY author`,
55 add: func(c *ContributorSummary, n int) { c.Merged = n },
56 },
57 {
58 // Issues those changes closed. The issue must still be closed: a
59 // reopened issue is not a fix that stands.
60 sql: `SELECT r.author, COUNT(DISTINCT rc.issue_id)
61 FROM requests r
62 JOIN request_closes rc ON rc.repo_id=r.repo_id AND rc.request_id=r.id
63 JOIN issues i ON i.repo_id=rc.repo_id AND i.id=rc.issue_id
64 WHERE r.repo_id=? AND r.state='merged' AND i.state='closed'
65 GROUP BY r.author`,
66 add: func(c *ContributorSummary, n int) { c.Fixed = n },
67 },
68 {
69 // Issues the account opened that a merged change closed. A manual
70 // close without a landing is deliberately not counted: it does not
71 // prove the report led to a fix.
72 sql: `SELECT i.author, COUNT(DISTINCT i.id)
73 FROM issues i
74 JOIN request_closes rc ON rc.repo_id=i.repo_id AND rc.issue_id=i.id
75 JOIN requests r ON r.repo_id=rc.repo_id AND r.id=rc.request_id
76 WHERE i.repo_id=? AND i.state='closed' AND r.state='merged'
77 GROUP BY i.author`,
78 add: func(c *ContributorSummary, n int) { c.Reported = n },
79 },
80 {
81 // Approvals that still name the merged head and base, which is the
82 // same rule the landing gate applies to a current approval.
83 sql: `SELECT a.principal, COUNT(DISTINCT a.request_id)
84 FROM request_approvals a
85 JOIN requests r ON r.repo_id=a.repo_id AND r.id=a.request_id
86 WHERE a.repo_id=? AND r.state='merged'
87 AND a.head=r.head AND a.base=r.base
88 GROUP BY a.principal`,
89 add: func(c *ContributorSummary, n int) { c.Reviews = n },
90 },
91 }
92 for _, q := range queries {
93 rows, err := s.db.Query(q.sql, repoID)
94 if err != nil {
95 return nil, err
96 }
97 for rows.Next() {
98 var (
99 name string
100 n int
101 )
102 if err := rows.Scan(&name, &n); err != nil {
103 rows.Close()
104 return nil, err
105 }
106 sum := out[name]
107 q.add(&sum, n)
108 out[name] = sum
109 }
110 if err := rows.Err(); err != nil {
111 rows.Close()
112 return nil, err
113 }
114 rows.Close()
115 }
116 return out, nil
117 }