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.

Say what each change is waiting for in the list

7cc36ba0f4ba96d3f4938b13758b2cc20c22375a · Keenan Nemetz · 5d ago · parent 946d2fd4

Say what each change is waiting for in the list

A change spends most of its life in states "open" cannot describe: held for a
maintainer to allow CI, running its checks, failed, waiting for a reviewer,
conflicting with its base. The list now says which of those it is, in one
column, with a tooltip that explains the label.

The answer comes from facts a list can afford: the approvals it already counts,
one batched run-queue query for every head on the page, the contributor
allowance (one ref listing, not one per row), and — only for a change that would
otherwise read as ready — whether it still merges into its base. The full
landing plan stays on the detail page.

The rest of the row follows the issue list: the author and opening moved under
the title, the comment count sits at the end of that line, and the last column
is "Updated", the newest commit over the request's directory. The list sorts by
it. The separate Author, Approvals and Age columns are gone.

Closes #194

forge/index/incremental_test.go +85

@@ -403,3 +403,88 @@ func TestEnsureDatesAnIssueFromItsNewestChange(t *testing.T) {
403 403 t.Fatalf("rebuilt updated = %v, incremental = %v", rebuilt, incremental)
404 404 }
405 405 }
406
407 // TestEnsureDatesARequestFromItsNewestChange is the request half of the list's
408 // activity sort. A comment or an approval moves the date; the incremental refresh
409 // and a full rebuild derive the same one.
410 func TestEnsureDatesARequestFromItsNewestChange(t *testing.T) {
411 s, m, g := incrementalRepo(t)
412 base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
413 at := func(offset time.Duration) {
414 t.Helper()
415 date := fmt.Sprintf("%d +0000", base.Add(offset).Unix())
416 t.Setenv("GIT_AUTHOR_DATE", date)
417 t.Setenv("GIT_COMMITTER_DATE", date)
418 }
419
420 at(time.Hour)
421 first := createIndexedRequest(t, g, "First change", base.Add(time.Hour), nil)
422 at(2 * time.Hour)
423 second := createIndexedRequest(t, g, "Second change", base.Add(2*time.Hour), nil)
424 if err := s.Ensure(m); err != nil {
425 t.Fatal(err)
426 }
427 rowOf := func(t *testing.T, id string) RequestRow {
428 t.Helper()
429 rows, err := s.Requests(m.ID, RequestFilter{})
430 if err != nil {
431 t.Fatal(err)
432 }
433 for _, row := range rows {
434 if row.ID == id {
435 return row
436 }
437 }
438 t.Fatalf("no indexed row for %s", id)
439 return RequestRow{}
440 }
441 if got := rowOf(t, first.ID).Updated; !got.Equal(base.Add(time.Hour)) {
442 t.Fatalf("a fresh request updated = %v, want its opening commit %v", got, base.Add(time.Hour))
443 }
444
445 // A comment moves the date, and the commented change takes the first row.
446 at(3 * time.Hour)
447 comment, err := metadata.NewRequestComment("A review note", "wilbur", base.Add(3*time.Hour))
448 if err != nil {
449 t.Fatal(err)
450 }
451 if err := metadata.AddRequestComment(g, first.ID, comment, "wilbur"); err != nil {
452 t.Fatal(err)
453 }
454 if err := s.Ensure(m); err != nil {
455 t.Fatal(err)
456 }
457 if got := rowOf(t, first.ID).Updated; !got.Equal(base.Add(3 * time.Hour)) {
458 t.Fatalf("updated after a comment = %v, want %v", got, base.Add(3*time.Hour))
459 }
460 if got := rowOf(t, second.ID).Updated; !got.Equal(base.Add(2 * time.Hour)) {
461 t.Fatalf("the untouched request updated = %v, want %v", got, base.Add(2*time.Hour))
462 }
463 rows, err := s.Requests(m.ID, RequestFilter{})
464 if err != nil {
465 t.Fatal(err)
466 }
467 if len(rows) != 2 || rows[0].ID != first.ID {
468 t.Fatalf("default order = %+v, want the commented change first", rows)
469 }
470
471 // An approval is activity too: it changes the request's directory.
472 at(4 * time.Hour)
473 if _, err := metadata.ApproveRequest(g, first.ID, "wilbur", "main", requestHead, base.Add(4*time.Hour), "wilbur"); err != nil {
474 t.Fatal(err)
475 }
476 if err := s.Ensure(m); err != nil {
477 t.Fatal(err)
478 }
479 incremental := rowOf(t, first.ID).Updated
480 if !incremental.Equal(base.Add(4 * time.Hour)) {
481 t.Fatalf("updated after an approval = %v, want %v", incremental, base.Add(4*time.Hour))
482 }
483
484 if err := s.IndexRepo(m); err != nil {
485 t.Fatal(err)
486 }
487 if rebuilt := rowOf(t, first.ID).Updated; !rebuilt.Equal(incremental) {
488 t.Fatalf("rebuilt updated = %v, incremental = %v", rebuilt, incremental)
489 }
490 }