Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions tools/sponsorkit/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ type Band struct {
// for this band. Contributors land in the first band whose minimum
// they meet.
MinCredit int
// PRMinCredit replaces MinCredit when ranking by merged PRs
// (-metric prs): one merged PR is a much rarer unit than one commit,
// so the cut-off sits far lower.
PRMinCredit int
// Avatar is the squircle's edge length in CSS pixels.
Avatar float64
// Box is the horizontal space reserved per contributor.
Expand All @@ -131,50 +135,45 @@ type Band struct {
Hover bool
}

// prBandMins replaces the bands' MinCredit thresholds when ranking by
// merged PRs (-metric prs): one merged PR is a much rarer unit than one
// commit, so the cut-offs sit far lower.
var prBandMins = []int{300, 100, 40, 15, 6, 2, 0}

// bands is ordered from most to least prolific, with MinCredit thresholds
// tuned for the commit metric. The last entry is the catch-all for
// first-time and drive-by contributors.
var bands = []Band{
{
MinCredit: 1000,
MinCredit: 1000, PRMinCredit: 300,
Avatar: 116, Box: 190, RowGap: 26,
ShowName: true, NameSize: 15, MaxName: 22,
Ring: "animated", RingWidth: 4.5, RingGradient: "ringPartner", Hover: true,
},
{
MinCredit: 150,
MinCredit: 150, PRMinCredit: 100,
Avatar: 94, Box: 152, RowGap: 24,
ShowName: true, NameSize: 13.5, MaxName: 18,
Ring: "animated", RingWidth: 4, RingGradient: "ringChampion", Hover: true,
},
{
MinCredit: 60,
MinCredit: 60, PRMinCredit: 40,
Avatar: 74, Box: 116, RowGap: 22,
ShowName: true, NameSize: 12, MaxName: 15,
Ring: "static", RingWidth: 3, RingGradient: "ringGold", Hover: true,
},
{
MinCredit: 20,
MinCredit: 20, PRMinCredit: 15,
Avatar: 58, Box: 74, RowGap: 16,
Ring: "static", RingWidth: 2, RingGradient: "ringSilver", Hover: true,
},
{
MinCredit: 8,
MinCredit: 8, PRMinCredit: 6,
Avatar: 46, Box: 59, RowGap: 13,
Ring: "subtle", RingWidth: 1.5,
},
{
MinCredit: 3,
MinCredit: 3, PRMinCredit: 2,
Avatar: 37, Box: 48, RowGap: 12,
Ring: "subtle", RingWidth: 1.25,
},
{
MinCredit: 0,
MinCredit: 0, PRMinCredit: 0,
Avatar: 30, Box: 40, RowGap: 11,
Ring: "subtle", RingWidth: 1,
},
Expand Down
4 changes: 4 additions & 0 deletions tools/sponsorkit/contributors.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ func FetchContributors(client *http.Client, token, repo string, changelogs []str
}
// Authored merged PRs but absent from the commit history (e.g.
// unlinked commit email). Same validation as changelog mentions.
// The pause matches the paginated fetches: bursts of per-login
// lookups are what trip GitHub's secondary rate limits.
time.Sleep(100 * time.Millisecond)
u, err := lookupUser(client, token, key)
if err != nil || u.Type != "User" || isBot(u.Login, u.Type) {
continue
Expand All @@ -132,6 +135,7 @@ func FetchContributors(client *http.Client, token, repo string, changelogs []str
// Credited in a changelog but absent from the commit history (e.g.
// their PR was squashed under someone else's authorship). Look the
// login up so typos and non-user tokens are dropped.
time.Sleep(100 * time.Millisecond)
u, err := lookupUser(client, token, key)
if err != nil {
fmt.Printf(" skipping changelog mention @%s: %v\n", key, err)
Expand Down
104 changes: 104 additions & 0 deletions tools/sponsorkit/contributors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"os"
"path/filepath"
"testing"
)

func writeChangelog(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "changelog.mdx")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}

func TestChangelogMentions(t *testing.T) {
tests := []struct {
name string
content string
want map[string]int
}{
{
name: "bare mentions, case-folded and counted",
content: "- Fixed X by @Alice\n- Fixed Y by @alice and @bob",
want: map[string]int{"alice": 2, "bob": 1},
},
{
name: "profile link trusts the URL, not the (typo'd) link text",
content: "- Fixed X [@hkere](https://github.com/hkhere)",
want: map[string]int{"hkhere": 1},
},
{
name: "profile link without @ in the text is not a credit",
content: "- See [the repo](https://github.com/wailsapp) for details",
want: map[string]int{},
},
{
name: "npm scopes and display handles are not logins",
content: "- Bump @wailsio/runtime\n- Fixed by @ronaldinho_x86",
want: map[string]int{},
},
{
name: "emails and mid-word @ are not mentions",
content: "- Contact lea.anthony@gmail.com or user@@host",
want: map[string]int{},
},
{
name: "trailing hyphen is trimmed",
content: "- Fixed by @carol-.",
want: map[string]int{"carol": 1},
},
{
name: "bracketed mention without a URL still counts",
content: "- Fixed by [@superDingda] in #4236",
want: map[string]int{"superdingda": 1},
},
{
name: "pull-request links are not profile credits",
content: "- Fix Z by @dave in [PR](https://github.com/wailsapp/wails/pull/1)",
want: map[string]int{"dave": 1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := changelogMentions([]string{writeChangelog(t, tt.content)})
if len(got) != len(tt.want) {
t.Fatalf("got %v, want %v", got, tt.want)
}
for login, n := range tt.want {
if got[login] != n {
t.Errorf("mentions[%q] = %d, want %d (full: %v)", login, got[login], n, got)
}
}
})
}
}

func TestCredit(t *testing.T) {
defer func(m string) { creditMetric = m }(creditMetric)

tests := []struct {
name string
metric string
c Contributor
want int
}{
{"commits metric takes max of commits and mentions", "commits", Contributor{Commits: 10, Mentions: 3}, 10},
{"mentions win when larger", "commits", Contributor{Commits: 2, Mentions: 5}, 5},
{"prs metric uses merged PRs", "prs", Contributor{Commits: 231, PRs: 26}, 26},
{"prs metric still lets mentions win", "prs", Contributor{PRs: 25, Mentions: 90}, 90},
{"commit-only contributors floored into the tail", "prs", Contributor{Commits: 50}, 7},
{"small commit-only counts kept as-is", "prs", Contributor{Commits: 2}, 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creditMetric = tt.metric
if got := tt.c.Credit(); got != tt.want {
t.Errorf("Credit() = %d, want %d", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion tools/sponsorkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func main() {
case "prs":
creditMetric = "prs"
for i := range bands {
bands[i].MinCredit = prBandMins[i]
bands[i].MinCredit = bands[i].PRMinCredit
}
default:
fmt.Fprintf(os.Stderr, "error: unknown -metric %q\n", *metric)
Expand Down
10 changes: 6 additions & 4 deletions tools/sponsorkit/render_contributors.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,12 @@ func (r *renderer) contributor(c Contributor, bi int, cx, cy float64) {
r.body.WriteString(fmt.Sprintf(`%s stroke="#FFFFFF" stroke-opacity="0.65" stroke-width="%s" stroke-linecap="round" stroke-dasharray="%s %s"><animate attributeName="stroke-dashoffset" from="0" to="%s" dur="6s" repeatCount="indefinite"/></use>`,
ring, num(b.RingWidth*0.6), num(perim*0.12), num(perim*0.88), num(-perim)))
// Hover-only bloom and fast counter-marching sweep.
r.body.WriteString(fmt.Sprintf(`%s class="bloom" stroke="url(#%s)" stroke-width="%s" filter="url(#soften)"/>`,
ring, b.RingGradient, num(b.RingWidth+7)))
r.body.WriteString(fmt.Sprintf(`%s class="fast" stroke="#FFFFFF" stroke-width="%s" stroke-linecap="round" stroke-dasharray="%s %s"><animate attributeName="stroke-dashoffset" from="0" to="%s" dur="1.4s" repeatCount="indefinite"/></use>`,
ring, num(b.RingWidth*0.7), num(perim*0.07), num(perim*0.93), num(perim)))
if b.Hover {
r.body.WriteString(fmt.Sprintf(`%s class="bloom" stroke="url(#%s)" stroke-width="%s" filter="url(#soften)"/>`,
ring, b.RingGradient, num(b.RingWidth+7)))
r.body.WriteString(fmt.Sprintf(`%s class="fast" stroke="#FFFFFF" stroke-width="%s" stroke-linecap="round" stroke-dasharray="%s %s"><animate attributeName="stroke-dashoffset" from="0" to="%s" dur="1.4s" repeatCount="indefinite"/></use>`,
ring, num(b.RingWidth*0.7), num(perim*0.07), num(perim*0.93), num(perim)))
}
case "static":
r.body.WriteString(fmt.Sprintf(`%s stroke="url(#%s)" stroke-width="%s"/>`,
ring, b.RingGradient, num(b.RingWidth)))
Expand Down
Loading