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/ui_test.go +54
| @@ -538,6 +538,60 @@ func TestOverviewShowsRecentOpenIssuesAndChanges(t *testing.T) { | |||
| 538 | 538 | } | |
| 539 | 539 | } | |
| 540 | 540 | ||
| 541 | func TestOverviewContributorsAreAccountsNotCommitAuthors(t *testing.T) { | ||
| 542 | t.Parallel() | ||
| 543 | f := newFixture(t) | ||
| 544 | f.publish(f.repo) | ||
| 545 | f.addWriter(t) // wilbur | ||
| 546 | |||
| 547 | const ( | ||
| 548 | keenanIssue = "01M5JR2JK29KKMAA1T9MDNTP11" | ||
| 549 | wilburIssue = "01M5JR2JK29KKMAA1T9MDNTP12" | ||
| 550 | changeID = "01M5JR2JK29KKMAA1T9MDNTR11" | ||
| 551 | ) | ||
| 552 | head := strings.Repeat("a", 40) | ||
| 553 | issues := f.issuesRepo(t, map[string]string{ | ||
| 554 | "issues/" + keenanIssue + "/issue.md": issueFile( | ||
| 555 | "Reported by keenan", "keenan", "open", "2026-10-22T21:00:00Z", nil, ""), | ||
| 556 | "issues/" + wilburIssue + "/issue.md": issueFile( | ||
| 557 | "Reported by wilbur", "wilbur", "open", "2026-10-22T21:01:00Z", nil, ""), | ||
| 558 | }) | ||
| 559 | if _, errs, err := f.git(issues, "push", f.sshURL(f.repo), "HEAD:refs/heads/vole"); err != nil { | ||
| 560 | t.Fatalf("push issues: %v\n%s", err, errs) | ||
| 561 | } | ||
| 562 | |||
| 563 | // Wilbur's change closes both reports and carries keenan's approval of the | ||
| 564 | // exact head that merged. | ||
| 565 | meta := f.newMetaRepo(t) | ||
| 566 | if err := meta.apply(map[string]string{ | ||
| 567 | "issues/" + keenanIssue + "/issue.md": issueFile( | ||
| 568 | "Reported by keenan", "keenan", "closed", "2026-10-22T21:00:00Z", nil, ""), | ||
| 569 | "issues/" + wilburIssue + "/issue.md": issueFile( | ||
| 570 | "Reported by wilbur", "wilbur", "closed", "2026-10-22T21:01:00Z", nil, ""), | ||
| 571 | "requests/" + changeID + "/request.md": requestFileMerged( | ||
| 572 | "Fix both reports", "main", head, "wilbur", "keenan", | ||
| 573 | []string{keenanIssue, wilburIssue}), | ||
| 574 | "requests/" + changeID + "/approvals/keenan.md": approvalFile("main", head), | ||
| 575 | }, "merge wilbur's change"); err != nil { | ||
| 576 | t.Fatalf("push merged change: %v", err) | ||
| 577 | } | ||
| 578 | |||
| 579 | code, body := f.get("/r/" + f.repo) | ||
| 580 | if code != 200 { | ||
| 581 | t.Fatalf("overview = %d", code) | ||
| 582 | } | ||
| 583 | for _, want := range []string{ | ||
| 584 | "Contributors", | ||
| 585 | `href="/u/wilbur"`, | ||
| 586 | `data-tip="1 change merged"`, `data-tip="2 issues fixed"`, `data-tip="1 issue reported"`, | ||
| 587 | `href="/u/keenan"`, `data-tip="1 review"`, | ||
| 588 | } { | ||
| 589 | if !strings.Contains(body, want) { | ||
| 590 | t.Fatalf("overview does not show %q:\n%s", want, body) | ||
| 591 | } | ||
| 592 | } | ||
| 593 | } | ||
| 594 | |||
| 541 | 595 | func TestCodeBrowserTreeAndFragments(t *testing.T) { | |
| 542 | 596 | t.Parallel() | |
| 543 | 597 | f := newFixture(t) | |