Commit 7a781d1ccb

7a781d1ccb8bc55f191bb839fe3d1a360bae0266

parent: 2d315865c1

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 05:46 UTC

web: link names to their definitions

The blob view of the indexed tree links each name the index holds: to
its line when defined once, to /{owner}/{repo}/symbols otherwise, with
one lookup for the page's distinct names. The page lists the file's own
symbols. Users, Parity and CHANGELOG describe the index.

Ref #293

Layout: unified · split

.gitbay/wiki/Parity.org +10 −1
@@ -188,6 +188,8 @@ rather than the one the web page shows.
188188| one commit with its patch | yes | yes | yes |
189189| blame | yes | yes | yes |
190190| search file contents | yes | yes | yes |
191| find a definition (symbols) | yes | yes | no |
192| jump to definition from a file | n/a | yes | no |
191193| compare two refs | yes | yes | yes |
192194| branches and tags | yes | yes | yes |
193195| wiki (read) | yes | yes | yes |
@@ -323,6 +325,11 @@ now render the way the reference does. go-org is not yet on the corpus.
323325| activity feed | yes | yes | yes |
324326| command reference | yes | no | n/a |
325327
328=repo symbols= and =/{owner}/{repo}/symbols= answer from the same
329store query and ranking. Jumping to a definition is a link on a name in
330the blob view, which a terminal has no place for; =repo symbols= with
331the name is its CLI form.
332
326333=explore= is the listing without a query; =repo search= is the one with.
327334=search= is both plus the title and body of every issue and merge
328335request the caller can read, over FTS5; the web serves it at =/search=
@@ -464,6 +471,7 @@ when there is none.
464471| worker queues | yes | yes | no |
465472| runners | yes | no | no |
466473| repository list, archive, visibility | yes | no | no |
474| rebuild a symbol index | yes | no | no |
467475| audit log | yes | no | no |
468476| instance statistics | yes | no | no |
469477
@@ -489,7 +497,8 @@ key, the other cannot be undone.
489497
490498* Pagination
491499
492=issue list=, =mr list=, =repo list=, =feed= and =build list= take
500=issue list=, =mr list=, =repo list=, =feed=, =build list= and =repo
501symbols= take
493502=--limit <n>= and =--cursor <c>=. Cursors are opaque; each page carries
494503the next one. Without the flags a list stays as it was, so existing
495504scripts are unchanged — for =build list= that means the newest fifty
.gitbay/wiki/Users.org +42
@@ -242,12 +242,54 @@ gitbay repo settings git-daemon you/project on # expose over git://
242242gitbay repo topics add you/project cli forge # free-form tags, shown on the web
243243gitbay repo search forge # find repos by name/description/topic
244244gitbay repo grep you/project "some string" # literal git grep over the default branch
245gitbay repo symbols you/project Dispatch # where a name is defined, from the symbol index
245246gitbay repo pin you/project # pin to your web dashboard
246247gitbay repo unpin you/project
247248gitbay repo archive you/project # read-only: pushes and issue/MR
248249gitbay repo unarchive you/project # writes refused, browsing intact
249250#+end_src
250251
252A push to the default branch queues a symbol index of its tree, built in
253the background after the push completes; a tree that is already indexed
254is not indexed again. =repo symbols <owner/name> [--kind k] <query>=
255lists definitions whose name starts with the query, exact matches
256first, case-sensitive before case-insensitive, as name, kind, path and
257line; =--limit= and =--cursor= page it, and without them it stops at
258200 rows. A Go method is listed as =Type.Method= and also matches on
259=Method=. Only the default branch is indexed: =--ref= accepts another
260ref only when its tree is the indexed one. The index follows read
261access: a repository you cannot read has no symbols either.
262
263What is indexed:
264
265| language | kinds |
266|--------------------------------+-----------------------------------------------------------|
267| Go (parsed) | function, method, type, const, var |
268| Swift | function, class, struct, enum, interface (protocol), type |
269| Rust | function, struct, enum, interface (trait), type, module, const, macro |
270| Python | function, method, class |
271| JavaScript, TypeScript | function, class, interface, type, enum, const |
272| C and C++ headers (=.h=, =.hpp=) | function (prototype), struct, enum, class, type, macro |
273| shell (=.sh=, =.bash=, =.zsh=) | function |
274| org, Markdown | section (a heading) |
275
276Languages other than Go are matched a line at a time, so unusual
277definition shapes are missed. Files over 1 MiB, anything under
278=vendor/= or =node_modules/=, =*_gen.go=, =*.pb.go=, Go files marked
279=Code generated ... DO NOT EDIT.=, and =*.min.js= are skipped. One run
280stops at 200,000 symbols or two minutes and records the index as
281partial; an index that cannot be built is recorded as failed. Either
282record stands for its tree: a push that changes the tree builds again,
283one that does not is left alone. A repository that has not been pushed
284to since the index existed has none until its next push to the default
285branch. =admin symbols reindex <owner/name>= rebuilds one regardless.
286
287On the web, a file viewed at the indexed tree (the default branch's
288head, or any commit with the same tree) links each name the index
289holds: to its definition when there is one, to the results page at
290=/{owner}/{repo}/symbols?q=<name>= when there are several. The file's
291own definitions are listed above its source.
292
251293Import from another forge — git data first, then optionally the issue
252294and PR history from GitHub or any Forgejo instance such as Codeberg
253295(issues keep state/labels/comments; PRs land as closed or merged MRs
CHANGELOG.org +10
@@ -54,6 +54,16 @@ anything beyond "replace the binary and restart" is needed.
5454 attributes it emits; MathML written as raw HTML is still stripped.
5555 Anything outside the subset, and math past 1000 expressions or
5656 256 KiB of TeX in one document, shows as source (#294).
57- Code navigation. A push or merge to the default branch queues a symbol
58 index of its tree, built by a background worker and keyed by the tree
59 id, so an unchanged tree is not indexed again. =repo symbols
60 <owner/name> <query>= finds definitions by name or prefix, and the
61 blob view links names to their definitions, with a results page at
62 =/{owner}/{repo}/symbols= and a per-file symbol list. Go is parsed;
63 Swift, Rust, Python, JavaScript/TypeScript, C headers, shell, org and
64 Markdown are matched by line. =admin symbols reindex= forces a
65 rebuild. Migration: =symbol_indexes=, =symbols= and
66 =symbol_requests=. (#293)
5767
5868* v1.38.0 — 2026-09-29
5969
internal/httpd/routes.go +1
@@ -65,6 +65,7 @@ func (s *Server) Routes() []Route {
6565 Route{Method: "GET", Pattern: "/{owner}/{repo}/blob/{ref}/{path...}", Handler: s.blob},
6666 Route{Method: "GET", Pattern: "/{owner}/{repo}/blame/{ref}/{path...}", Handler: s.blame},
6767 Route{Method: "GET", Pattern: "/{owner}/{repo}/search", Handler: s.search},
68 Route{Method: "GET", Pattern: "/{owner}/{repo}/symbols", Handler: s.symbolsPage},
6869 Route{Method: "GET", Pattern: "/{owner}/{repo}/milestones", Handler: s.milestones},
6970 Route{Method: "GET", Pattern: "/{owner}/{repo}/labels", Handler: s.labels},
7071 Route{Method: "GET", Pattern: "/{owner}/{repo}/wiki", Handler: s.wiki},
internal/httpd/symbols.go added +144
@@ -0,0 +1,144 @@
1package httpd
2
3import (
4 "errors"
5 "html/template"
6 "net/http"
7 "net/url"
8 "regexp"
9 "strconv"
10 "strings"
11
12 "gitbay.org/gitbay/internal/control"
13 "gitbay.org/gitbay/internal/store"
14 "gitbay.org/gitbay/internal/symbols"
15)
16
17// nameSpan is a name token as chroma writes it with classes: n, nx, nf,
18// nc and the rest of the Name family. Keywords, literals and punctuation
19// carry other classes and are never linked.
20var nameSpan = regexp.MustCompile(`<span class="n[a-z]?">([\p{L}_$][\p{L}\p{N}_$]*)</span>`)
21
22// escapePath escapes each segment of a slash-separated path for a URL.
23func escapePath(p string) string {
24 parts := strings.Split(p, "/")
25 for i, s := range parts {
26 parts[i] = url.PathEscape(s)
27 }
28 return strings.Join(parts, "/")
29}
30
31// blobSymbols finds the viewed file's symbols and links the names in its
32// highlighted source to their definitions, when the ref being viewed has
33// the indexed tree. A name defined once links to that line at the same
34// ref; one defined more than once links to the results page. Names the
35// index does not hold stay plain. The whole page costs one lookup of the
36// file's distinct names, however many times each appears.
37func (s *Server) blobSymbols(p repoPage, filePath string, code template.HTML) (template.HTML, []store.SymbolRow) {
38 idx, err := s.st.SymbolIndexFor(p.Repo.ID)
39 if err != nil || idx.State == "failed" || !control.IndexedTree(p.Dir, p.Ref, idx) {
40 return code, nil
41 }
42 list, _ := s.st.SymbolsInFile(idx.ID, filePath)
43 if code == "" {
44 return code, list
45 }
46 seen := map[string]bool{}
47 var keys []string
48 for _, m := range nameSpan.FindAllStringSubmatch(string(code), -1) {
49 if !seen[m[1]] {
50 seen[m[1]] = true
51 keys = append(keys, m[1])
52 }
53 }
54 if len(keys) == 0 {
55 return code, list
56 }
57 targets, err := s.st.SymbolTargets(idx.ID, keys)
58 if err != nil || len(targets) == 0 {
59 return code, list
60 }
61 repoPath := "/" + p.Repo.Path()
62 ref := escapePath(p.Ref)
63 linked := nameSpan.ReplaceAllStringFunc(string(code), func(span string) string {
64 name := nameSpan.FindStringSubmatch(span)[1]
65 t, ok := targets[name]
66 if !ok {
67 return span
68 }
69 href := repoPath + "/symbols?q=" + url.QueryEscape(name)
70 if t.Count == 1 {
71 href = repoPath + "/blob/" + ref + "/" + escapePath(t.Path) + "#L" + strconv.Itoa(t.Line)
72 }
73 return `<a class="sym" href="` + template.HTMLEscapeString(href) + `">` + span + `</a>`
74 })
75 return template.HTML(linked), list
76}
77
78// symbolsPageSize is the results page's page length.
79const symbolsPageSize = 100
80
81// symbolsPage lists the definitions matching a query, ranked as `repo
82// symbols` ranks them, from the same store query.
83func (s *Server) symbolsPage(w http.ResponseWriter, r *http.Request) {
84 p, ok := s.repoFor(w, r, "")
85 if !ok {
86 return
87 }
88 p.Tab = "search"
89 q := strings.TrimSpace(r.URL.Query().Get("q"))
90 kind := r.URL.Query().Get("kind")
91 if !symbols.ValidKind(kind) {
92 kind = ""
93 }
94 var (
95 rows []store.SymbolRow
96 next string
97 problem string
98 note string
99 )
100 idx, err := s.st.SymbolIndexFor(p.Repo.ID)
101 switch {
102 case errors.Is(err, store.ErrNotFound):
103 problem = "no symbol index yet; one is built after a push to " + p.Repo.DefaultBranch
104 case err != nil:
105 http.Error(w, "internal error", http.StatusInternalServerError)
106 return
107 case idx.State == "failed":
108 problem = "the symbol index failed: " + idx.Note
109 case len(q) > 200:
110 problem = "query must be 1 to 200 characters"
111 case q != "":
112 if idx.State == "partial" {
113 note = "the index is partial: " + idx.Note
114 }
115 var after int64
116 if a, err := strconv.ParseInt(r.URL.Query().Get("after"), 10, 64); err == nil && a > 0 {
117 after = a
118 }
119 rows, err = s.st.SearchSymbols(idx.ID, q, kind, symbolsPageSize+1, after)
120 if err != nil {
121 http.Error(w, "internal error", http.StatusInternalServerError)
122 return
123 }
124 if len(rows) > symbolsPageSize {
125 rows = rows[:symbolsPageSize]
126 v := url.Values{"q": {q}, "after": {strconv.FormatInt(rows[len(rows)-1].ID, 10)}}
127 if kind != "" {
128 v.Set("kind", kind)
129 }
130 next = "/" + p.Repo.Path() + "/symbols?" + v.Encode()
131 }
132 }
133 s.render(w, "symbols.html", struct {
134 repoPage
135 Query string
136 Kind string
137 Kinds []string
138 Problem string
139 Note string
140 Rows []store.SymbolRow
141 Next string
142 Commit string
143 }{p, q, kind, symbols.Kinds, problem, note, rows, next, idx.Commit})
144}
internal/httpd/symbols_test.go added +132
@@ -0,0 +1,132 @@
1package httpd
2
3import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "gitbay.org/gitbay/internal/config"
12 "gitbay.org/gitbay/internal/control"
13 "gitbay.org/gitbay/internal/store"
14 "gitbay.org/gitbay/internal/symbols"
15)
16
17func symbolGit(t *testing.T, dir string, args ...string) {
18 t.Helper()
19 cmd := exec.Command("git", args...)
20 cmd.Dir = dir
21 cmd.Env = append(os.Environ(),
22 "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null",
23 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
24 "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test")
25 if out, err := cmd.CombinedOutput(); err != nil {
26 t.Fatalf("git %v: %v\n%s", args, err, out)
27 }
28}
29
30// symbolServer serves alice/app (public) and alice/secret (private), each
31// with main indexed and a side branch whose tree differs.
32func symbolServer(t *testing.T) *Server {
33 t.Helper()
34 st, err := store.Open(":memory:")
35 if err != nil {
36 t.Fatal(err)
37 }
38 t.Cleanup(func() { st.Close() })
39 if err := st.MigrateUp(); err != nil {
40 t.Fatal(err)
41 }
42 uid, err := st.CreateUser("alice", false)
43 if err != nil {
44 t.Fatal(err)
45 }
46 root := t.TempDir()
47 src := filepath.Join(root, "src")
48 symbolGit(t, root, "init", "-q", "-b", "main", "src")
49 files := map[string]string{
50 "main.go": "package main\n\nfunc main() { helper(); dup(); unknown() }\n\nfunc helper() {}\n",
51 "a/dup.go": "package a\n\nfunc dup() {}\n",
52 "b/dup.go": "package b\n\nfunc dup() {}\n",
53 }
54 for name, body := range files {
55 os.MkdirAll(filepath.Join(src, filepath.Dir(name)), 0o755)
56 os.WriteFile(filepath.Join(src, name), []byte(body), 0o644)
57 }
58 symbolGit(t, src, "add", ".")
59 symbolGit(t, src, "commit", "-q", "-m", "base")
60 symbolGit(t, src, "checkout", "-q", "-b", "side")
61 os.WriteFile(filepath.Join(src, "side.go"), []byte("package main\n"), 0o644)
62 symbolGit(t, src, "add", ".")
63 symbolGit(t, src, "commit", "-q", "-m", "side")
64
65 w := &symbols.Worker{St: st, RepoDir: func(owner, name string) string { return control.RepoDir(root, owner, name) },
66 MaxSymbols: symbols.DefaultMaxSymbols, MaxTime: symbols.DefaultMaxTime}
67 for _, r := range []struct{ name, vis string }{{"app", "public"}, {"secret", "private"}} {
68 id, err := st.CreateRepo("user", uid, r.name, r.vis)
69 if err != nil {
70 t.Fatal(err)
71 }
72 dir := control.RepoDir(root, "alice", r.name)
73 os.MkdirAll(filepath.Dir(dir), 0o755)
74 symbolGit(t, root, "init", "-q", "--bare", dir)
75 symbolGit(t, src, "push", "-q", dir, "main", "side")
76 if err := w.Index(context.Background(), id, false); err != nil {
77 t.Fatal(err)
78 }
79 }
80 cfg := config.Default()
81 cfg.Server.Root = root
82 cfg.Server.SiteURL = "https://forge.test/"
83 return New(cfg, st, nil)
84}
85
86func TestBlobLinksIndexedNames(t *testing.T) {
87 h := symbolServer(t).Handler()
88 w := get(t, h, "/alice/app/blob/main/main.go", nil)
89 if w.Code != 200 {
90 t.Fatalf("blob: %d", w.Code)
91 }
92 body := w.Body.String()
93 for _, want := range []string{
94 // defined once: straight to the line, at the ref being viewed
95 `<a class="sym" href="/alice/app/blob/main/main.go#L5"><span class="nf">helper</span></a>`,
96 // defined twice: to the results page
97 `<a class="sym" href="/alice/app/symbols?q=dup"><span class="nf">dup</span></a>`,
98 // the per-file list
99 `2 symbols in this file`,
100 } {
101 if !strings.Contains(body, want) {
102 t.Errorf("blob page lacks %s", want)
103 }
104 }
105 if strings.Contains(body, `>unknown</span></a>`) {
106 t.Error("a name the index does not hold is linked")
107 }
108
109 // A branch at another tree is not what the index describes.
110 w = get(t, h, "/alice/app/blob/side/main.go", nil)
111 if w.Code != 200 || strings.Contains(w.Body.String(), `class="sym"`) || strings.Contains(w.Body.String(), "in this file") {
112 t.Errorf("side branch: %d, links or list present", w.Code)
113 }
114}
115
116func TestSymbolsPage(t *testing.T) {
117 h := symbolServer(t).Handler()
118 w := get(t, h, "/alice/app/symbols?q=dup", nil)
119 body := w.Body.String()
120 if w.Code != 200 || !strings.Contains(body, "a/dup.go:3") || !strings.Contains(body, "b/dup.go:3") {
121 t.Fatalf("results page: %d\n%s", w.Code, body)
122 }
123 w = get(t, h, "/alice/app/symbols?q=dup&kind=type", nil)
124 if strings.Contains(w.Body.String(), "a/dup.go:3") {
125 t.Error("kind filter ignored")
126 }
127 for _, p := range []string{"/alice/secret/symbols?q=dup", "/alice/secret/blob/main/main.go"} {
128 if w := get(t, h, p, nil); w.Code != 404 {
129 t.Errorf("%s anonymously: %d, want 404", p, w.Code)
130 }
131 }
132}
internal/httpd/web.go +6 −1
@@ -706,6 +706,10 @@ func (s *Server) blob(w http.ResponseWriter, r *http.Request) {
706706 if !binary && !image {
707707 codeHTML = highlight(filePath, data)
708708 }
709 var fileSymbols []store.SymbolRow
710 if !binary && !image {
711 codeHTML, fileSymbols = s.blobSymbols(p, filePath, codeHTML)
712 }
709713 // Markdown and org render like a README, with the source one click
710714 // away; ?view=source shows the text instead.
711715 renderable := markupFile(filePath) && !binary
@@ -752,8 +756,9 @@ func (s *Server) blob(w http.ResponseWriter, r *http.Request) {
752756 Rendered bool // this response shows the rendering
753757 RenderedHTML template.HTML
754758 Nav fileNav
759 Symbols []store.SymbolRow
755760 }{p, cs, base, filePath, filePath, "blob", binary, image, len(data), lines,
756 entry.Mode == "100755", entry.Mode == "120000", branches, codeHTML, renderable, rendered, renderedHTML, nav})
761 entry.Mode == "100755", entry.Mode == "120000", branches, codeHTML, renderable, rendered, renderedHTML, nav, fileSymbols})
757762}
758763
759764// releases lists tag-anchored releases with notes and assets.
internal/web/static/style.css +8
@@ -1101,6 +1101,14 @@ article.release { padding: var(--sp-4); margin: var(--sp-4) 0; }
11011101ul.matchlist { list-style: none; margin: var(--sp-3) 0; padding: 0; }
11021102ul.matchlist li { margin-bottom: var(--sp-3); }
11031103.matchpath { font-family: var(--mono); font-size: var(--fs-1); }
1104/* a name in highlighted source that links to its definition keeps its
1105 token colour; the dotted underline is what marks it as a link */
1106.chroma a.sym { color: inherit; text-decoration: underline dotted; text-underline-offset: 2px; }
1107.chroma a.sym:hover { text-decoration-style: solid; }
1108details.symlist { margin: 0 0 var(--sp-3); font-size: var(--fs-1); }
1109details.symlist summary { cursor: pointer; color: var(--muted); }
1110details.symlist ul { list-style: none; margin: var(--sp-2) 0 0; padding: 0; columns: 16rem; }
1111details.symlist li { break-inside: avoid; padding: 2px 0; }
11041112pre.matchline {
11051113 background: var(--inset);
11061114 border: 1px solid var(--line);
internal/web/templates/blob.html +4
@@ -12,6 +12,10 @@
1212 <span class="actions">{{if .Renderable}}{{if .Rendered}}<strong>rendered</strong> · <a href="?view=source">source</a>{{else}}<a href="?">rendered</a> · <strong>source</strong>{{end}} · {{end}}<a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log/{{.Ref}}?path={{.Path}}">history</a> · {{if not .Binary}}<a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/blame/{{.Ref}}/{{.Path}}">blame</a> · {{end}}<a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}">raw</a>{{if .Viewer}} · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/edit/{{.Ref}}/{{.Path}}">edit</a>{{end}}</span>
1313</div>
1414<p class="filefacts">{{if .Lines}}{{.Lines}} lines · {{end}}{{.Size}} bytes{{if .Exec}} · executable{{end}}{{if .Symlink}} · symlink{{end}}</p>
15{{if .Symbols}}<details class="symlist"><summary>{{len .Symbols}} symbol{{if ne (len .Symbols) 1}}s{{end}} in this file</summary>
16<ul>{{range .Symbols}}<li><a href="{{if $.Rendered}}?view=source{{end}}#L{{.Line}}"><code>{{.Name}}</code></a> <span class="meta">{{.Kind}}, line {{.Line}}</span></li>
17{{end}}</ul>
18</details>{{end}}
1519{{if .Image}}<div class="blobimage"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}"><img src="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}" alt="{{.Base}}"></a><p class="meta">{{.Size}} bytes</p></div>
1620{{else if .Binary}}<p class="empty-note">binary file, {{.Size}} bytes — <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}">download</a></p>
1721{{else if .Rendered}}<section class="readme"><div class="rendered">{{.RenderedHTML}}</div></section>
internal/web/templates/symbols.html added +32
@@ -0,0 +1,32 @@
1{{define "width"}}wide{{end}}
2{{define "title"}}symbols · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
3{{define "content"}}
4<h1>Symbols</h1>
5<form method="get" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/symbols" class="searchform">
6 <input type="search" name="q" aria-label="Find a definition" value="{{.Query}}" placeholder="a name or its start, on {{.Repo.DefaultBranch}}">
7 <select name="kind" aria-label="Kind">
8 <option value="">any kind</option>
9 {{range .Kinds}}<option value="{{.}}"{{if eq . $.Kind}} selected{{end}}>{{.}}</option>
10 {{end}}
11 </select>
12 <button type="submit" class="btn">Find</button>
13</form>
14{{if .Problem}}<p class="empty-note">{{.Problem}}</p>
15{{else if .Query}}
16{{if .Note}}<p class="meta">{{.Note}}</p>{{end}}
17{{if .Rows}}
18<div class="tablewrap"><table class="refs">
19<thead><tr><th scope="col">Name</th><th scope="col">Kind</th><th scope="col">Defined at</th></tr></thead>
20<tbody>
21{{range .Rows}}<tr>
22 <td><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/blob/{{$.Commit}}/{{.Path}}#L{{.Line}}"><code>{{.Name}}</code></a></td>
23 <td>{{.Kind}}</td>
24 <td class="sha">{{.Path}}:{{.Line}}</td>
25</tr>
26{{end}}
27</tbody>
28</table></div>
29{{if .Next}}<p><a href="{{.Next}}">next page</a></p>{{end}}
30{{else}}<p class="empty-note">no definitions matching “{{.Query}}”</p>{{end}}
31{{end}}
32{{end}}
internal/web/web_test.go +1 −1
@@ -89,7 +89,7 @@ func TestWhenNamesTheZone(t *testing.T) {
8989// none. The merge request page picks wide for its diff view, so it gets
9090// a per-view define instead of a fixed one.
9191func TestMainWidthClass(t *testing.T) {
92 wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true, "edit.html": true, "dashboard.html": true, "issues.html": true, "mrs.html": true, "mrrangediff.html": true, "explore.html": true, "notifications.html": true, "settings.html": true, "account.html": true, "admin.html": true, "adminusers.html": true, "queries.html": true}
92 wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "symbols.html": true, "globalsearch.html": true, "edit.html": true, "dashboard.html": true, "issues.html": true, "mrs.html": true, "mrrangediff.html": true, "explore.html": true, "notifications.html": true, "settings.html": true, "account.html": true, "admin.html": true, "adminusers.html": true, "queries.html": true}
9393 bounded := map[string]bool{"landing.html": true, "fork.html": true, "login.html": true, "logout.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true}
9494 perView := map[string]string{"mr.html": `{{define "width"}}{{if eq .View "diff"}}wide{{else}}reading{{end}}{{end}}`}
9595 for _, name := range Pages() {