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 | } | ||