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.

Turn the user directory into a sortable table

4739664b94c4321b83a2650a3a2486ec090aed64 · Keenan Nemetz · 8d ago · parent c14588ef

Turn the user directory into a sortable table

The list became a table with the same counts, and the column headers
order it: user, repositories, issues, changes, reviews, joining date.
Counts start largest-first and the name A to Z; clicking the active
header flips it. The active column carries aria-sort and an arrow.

The search, the status chips (everyone, active, no longer active), the
order and the page all live in the URL and are carried into every link,
so a view can be bookmarked and paging cannot change the question. It is
all server-side, so it works without JavaScript.

Sorting by a count needs every matched account's summary before the page
is cut, so the index now answers for the whole directory in one grouped
query — ActorSummaries — instead of a lookup for a page of names. That
also drops the per-request name list, so a large roster cannot run into
SQLite's bound-variable limit.

Retired accounts are marked in their row and can be filtered to rather
than living in a separate group, and a roster larger than one page is
covered by a test.

forge/index/profile_test.go +5−7

@@ -199,7 +199,7 @@ func TestActorEventsFollowTheKindTypeAndVisibility(t *testing.T) {
199 199 }
200 200 }
201 201
202 func TestActorSummaryAnswersForAPageOfNames(t *testing.T) {
202 func TestActorSummariesAnswersForEveryone(t *testing.T) {
203 203 t.Parallel()
204 204 s, m, g := incrementalRepo(t)
205 205 now := time.Date(2026, 11, 1, 12, 0, 0, 0, time.UTC)
@@ -219,7 +219,7 @@ func TestActorSummaryAnswersForAPageOfNames(t *testing.T) {
219 219 t.Fatal(err)
220 220 }
221 221
222 sums, err := s.ActorSummary([]string{m.ID}, []string{"alice", "adam", "nobody"})
222 sums, err := s.ActorSummaries([]string{m.ID})
223 223 if err != nil {
224 224 t.Fatal(err)
225 225 }
@@ -229,13 +229,11 @@ func TestActorSummaryAnswersForAPageOfNames(t *testing.T) {
229 229 if got := sums["adam"]; got.Issues != 1 {
230 230 t.Fatalf("adam = %+v, want 1 issue", got)
231 231 }
232 // A name with nothing visible is absent rather than zero, so the directory can
233 // say "nothing you can see yet" instead of "0".
234 if _, ok := sums["nobody"]; ok {
235 t.Fatalf("a name with no visible work was reported: %+v", sums["nobody"])
232 if len(sums) != 2 {
233 t.Fatalf("summaries = %+v, want only the two authors with visible work", sums)
236 234 }
237 235 // An empty repository set is not an error, it is an empty answer.
238 if sums, err = s.ActorSummary(nil, []string{"alice"}); err != nil || len(sums) != 0 {
236 if sums, err = s.ActorSummaries(nil); err != nil || len(sums) != 0 {
239 237 t.Fatalf("empty repository set = %v, %v", sums, err)
240 238 }
241 239 }