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.

Store the overview's summaries when a push lands

2a8f7cef47240564179a45126863d1ccd8d9d362 · Keenan Nemetz · 10d ago · parent 8dc56e4e

Store the overview's summaries when a push lands

The repository overview walked git on every read: `rev-parse` for the
tip, `log` for the last five commits, `for-each-ref` for tags, `ls-tree`
plus `shortlog` for languages and contributors, and a `cat-file` for the
README. All of those describe a ref except the README, and a ref moves
only when a push lands, so the push now records them.

- Schema v10 adds `repo_languages`, `repo_commits`, `repo_contributors`
  and `repo_tags`.
- `RefreshHead` stores the tip's newest commits and its language
  breakdown from one `ls-tree -r -l`, and returns whether the tip moved.
- `RefreshContributors` stores the top authors; the hook calls it only
  when the tip changed, never on a metadata or tag push.
- `RefreshTags` stores the tag rows beside the release events it already
  rewrote, and `IndexRepo` now refreshes tags and contributors too, so a
  rebuild restores everything a push would have written.
- The overview reads those rows when the recorded head is the branch tip
  (`RefCommit` is a loose-ref file read) and falls back to git when it is
  not, so a missed hook shows the correct page rather than a stale one.
  If one stored section is empty on a repository with commits, that
  section alone is re-read from git.
- The sidebar asks SQL for five open issues and changes instead of
  sorting the whole open queue in memory.
- The rendered README is cached by repository, commit and ref under a
  byte budget, so the overview's last git spawn is gone after the first
  read of a commit.
- The language fallback reads one `ls-tree -r -l` instead of `ls-tree`
  followed by a `cat-file --batch-check`.
- `Remove` deletes every derived row for a repository, not only its row.

Load harness at concurrency 4: overview goes from 473 to about 740 req/s,
p50 from 8 ms to 5 ms, and the server runs no git child process for the
warm requests.

forge/index/overview_test.go +144

@@ -0,0 +1,144 @@
1 package index
2
3 import (
4 "os/exec"
5 "testing"
6
7 "github.com/rodent-software/vole/core/git"
8 )
9
10 func TestRefreshHeadStoresRecentCommitsAndLanguages(t *testing.T) {
11 t.Parallel()
12 s, m, g := incrementalRepo(t)
13 m.DefaultBranch = "main"
14 first := seedHead(t, g, map[string][]byte{"main.go": []byte("package main\n")})
15 second, err := git.CommitFiles(g, "refs/heads/main", func(f map[string][]byte) error {
16 f["main.go"] = []byte("package main\n\nfunc main() {}\n")
17 f["README.md"] = []byte("# demo\n")
18 return nil
19 }, "the second commit", "ada")
20 if err != nil {
21 t.Fatal(err)
22 }
23 changed, err := s.RefreshHead(m)
24 if err != nil {
25 t.Fatal(err)
26 }
27 if !changed {
28 t.Fatal("the first refresh reported no change")
29 }
30 commits, err := s.RecentCommits(m.ID, 5)
31 if err != nil {
32 t.Fatal(err)
33 }
34 if len(commits) != 2 || commits[0].SHA != second || commits[1].SHA != first {
35 t.Fatalf("recent commits = %+v, want %s then %s", commits, second, first)
36 }
37 if commits[0].Subject != "the second commit" || commits[0].Author != "ada" {
38 t.Fatalf("tip commit = %+v", commits[0])
39 }
40 languages, err := s.Languages(m.ID)
41 if err != nil {
42 t.Fatal(err)
43 }
44 byName := map[string]int64{}
45 for _, l := range languages {
46 byName[l.Name] = l.Bytes
47 }
48 if byName["Go"] == 0 || byName["Markdown"] == 0 {
49 t.Fatalf("languages = %+v, want Go and Markdown", languages)
50 }
51 changed, err = s.RefreshHead(m)
52 if err != nil {
53 t.Fatal(err)
54 }
55 if changed {
56 t.Fatal("a second refresh at the same tip reported a change")
57 }
58 }
59
60 func TestRefreshContributorsStoresAuthors(t *testing.T) {
61 t.Parallel()
62 s, m, g := incrementalRepo(t)
63 m.DefaultBranch = "main"
64 if _, err := git.CommitFiles(g, "refs/heads/main", func(f map[string][]byte) error {
65 f["main.go"] = []byte("package main\n")
66 return nil
67 }, "first", "keenan"); err != nil {
68 t.Fatal(err)
69 }
70 if _, err := git.CommitFiles(g, "refs/heads/main", func(f map[string][]byte) error {
71 f["main.go"] = []byte("package main // two\n")
72 return nil
73 }, "second", "ada"); err != nil {
74 t.Fatal(err)
75 }
76 if _, err := s.RefreshHead(m); err != nil {
77 t.Fatal(err)
78 }
79 if err := s.RefreshContributors(m); err != nil {
80 t.Fatal(err)
81 }
82 list, err := s.Contributors(m.ID, 8)
83 if err != nil {
84 t.Fatal(err)
85 }
86 if len(list) != 2 {
87 t.Fatalf("contributors = %+v, want two authors", list)
88 }
89 commits := map[string]int{}
90 for _, c := range list {
91 commits[c.Name] = c.Commits
92 }
93 if commits["ada"] != 1 || commits["keenan"] != 1 {
94 t.Fatalf("contributors = %+v", list)
95 }
96 }
97
98 func TestRefreshTagsStoresTagsAndReleaseEvents(t *testing.T) {
99 t.Parallel()
100 s, m, g := incrementalRepo(t)
101 m.DefaultBranch = "main"
102 commit := seedHead(t, g, map[string][]byte{"main.go": []byte("package main\n")})
103 if err := g.WriteAnnotatedTag("v1.0.0", commit, "first release", "keenan", false); err != nil {
104 t.Fatal(err)
105 }
106 cmd := exec.Command("git", "-C", g.Dir, "tag", "light", commit)
107 cmd.Env = append(cmd.Environ(), "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.com")
108 if out, err := cmd.CombinedOutput(); err != nil {
109 t.Fatalf("lightweight tag: %v: %s", err, out)
110 }
111 if err := s.RefreshTags(m); err != nil {
112 t.Fatal(err)
113 }
114 tags, err := s.Tags(m.ID, 5)
115 if err != nil {
116 t.Fatal(err)
117 }
118 if len(tags) != 2 {
119 t.Fatalf("tags = %+v, want two", tags)
120 }
121 byName := map[string]TagRow{}
122 for _, tag := range tags {
123 byName[tag.Name] = tag
124 }
125 if !byName["v1.0.0"].Annotated || byName["v1.0.0"].Commit != commit || byName["v1.0.0"].Tagger != "keenan" {
126 t.Fatalf("annotated tag = %+v", byName["v1.0.0"])
127 }
128 if byName["light"].Annotated || byName["light"].Commit != commit {
129 t.Fatalf("lightweight tag = %+v", byName["light"])
130 }
131 if err := g.DeleteRef("refs/tags/v1.0.0"); err != nil {
132 t.Fatal(err)
133 }
134 if err := s.RefreshTags(m); err != nil {
135 t.Fatal(err)
136 }
137 tags, err = s.Tags(m.ID, 5)
138 if err != nil {
139 t.Fatal(err)
140 }
141 if len(tags) != 1 || tags[0].Name != "light" {
142 t.Fatalf("after deleting a tag: %+v", tags)
143 }
144 }