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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Sort the issue list by when it last moved

db4081ec934ddd9a7ce123b4d3cf6d6ea43a4537 · Keenan Nemetz · 4d ago · parent a32a859a

Sort the issue list by when it last moved

An issue queue is read for what changed, not for the order things were
filed. The list now shows the newest activity first and carries the columns
that answer the question: labels have their own column, assignees render like
authors do, and the creation time and author sit under the title.

The last column is "updated": the newest commit over the issue's directory,
which covers an edit, a comment, a label change, and a close or reopen. The
metadata branch has no such field on purpose — the commit carries the time —
so the index derives it from one `git log` per rebuild or one per refreshed
range, and stores it. An incremental refresh also reads the commits' paths, so
an edit reverted inside the range still counts as activity.

Bumps the index schema version, which drops the derived rows and rebuilds them
from the branch on the next read.

Closes #192

core/git/touch_test.go +106

@@ -0,0 +1,106 @@
1 package git
2
3 // TouchTimes answers "when was this file last changed?" for a whole subtree in
4 // one `git log`. These tests pin the two properties the issue list depends on:
5 // the newest commit wins, and a commit range only names the paths it touched.
6
7 import (
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14 )
15
16 func touchRepo(t *testing.T) *Repo {
17 t.Helper()
18 dir := t.TempDir()
19 run := func(at time.Time, args ...string) {
20 t.Helper()
21 cmd := exec.Command("git", args...)
22 cmd.Dir = dir
23 date := at.Format(time.RFC3339)
24 cmd.Env = append(os.Environ(),
25 "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null",
26 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.com",
27 "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.com",
28 "GIT_AUTHOR_DATE="+date, "GIT_COMMITTER_DATE="+date)
29 if out, err := cmd.CombinedOutput(); err != nil {
30 t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out)
31 }
32 }
33 write := func(name, content string) {
34 t.Helper()
35 p := filepath.Join(dir, name)
36 if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
37 t.Fatal(err)
38 }
39 if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
40 t.Fatal(err)
41 }
42 }
43 first := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
44 second := first.Add(24 * time.Hour)
45 third := second.Add(24 * time.Hour)
46
47 run(first, "init", "-q", "-b", "main")
48 write("issues/a/issue.md", "a\n")
49 write("issues/b/issue.md", "b\n")
50 run(first, "add", "-A")
51 run(first, "commit", "-qm", "open both")
52
53 write("issues/b/issue.md", "b, edited\n")
54 run(second, "add", "-A")
55 run(second, "commit", "-qm", "edit b")
56
57 write("issues/b/comments/c.md", "a comment\n")
58 run(third, "add", "-A")
59 run(third, "commit", "-qm", "comment on b")
60 return open(t, dir)
61 }
62
63 func TestTouchTimesKeepsTheNewestCommitPerPath(t *testing.T) {
64 t.Parallel()
65 r := touchRepo(t)
66 first := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
67 second := first.Add(24 * time.Hour)
68 third := second.Add(24 * time.Hour)
69
70 times, err := r.TouchTimes("HEAD", "issues")
71 if err != nil {
72 t.Fatalf("TouchTimes: %v", err)
73 }
74 want := map[string]int64{
75 "issues/a/issue.md": first.Unix(),
76 "issues/b/issue.md": second.Unix(),
77 "issues/b/comments/c.md": third.Unix(),
78 }
79 if len(times) != len(want) {
80 t.Fatalf("TouchTimes = %v, want %v", times, want)
81 }
82 for path, when := range want {
83 if got, ok := times[path]; !ok || got != when {
84 t.Fatalf("%s = %d (present %v), want %d", path, got, ok, when)
85 }
86 }
87 }
88
89 func TestTouchTimesReadsOnlyTheRangeItIsGiven(t *testing.T) {
90 t.Parallel()
91 r := touchRepo(t)
92 third := time.Date(2026, 1, 3, 12, 0, 0, 0, time.UTC)
93
94 times, err := r.TouchTimes("HEAD~1..HEAD", "issues")
95 if err != nil {
96 t.Fatalf("TouchTimes: %v", err)
97 }
98 if len(times) != 1 || times["issues/b/comments/c.md"] != third.Unix() {
99 t.Fatalf("range TouchTimes = %v, want only the comment at %d", times, third.Unix())
100 }
101
102 empty, err := r.TouchTimes("", "issues")
103 if err != nil || empty != nil {
104 t.Fatalf("TouchTimes with no rev = %v, %v; want no rows and no error", empty, err)
105 }
106 }