detached c83fe67

fix(web): bound concurrent highlighting and revalidate raw files at every request

commit
c83fe6794d7d3fe07705bb5d9b7d55bafdad6f6c
author
brice · · 2026-10-10 14:14 UTC
parent
56a2ec9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +22 -12
M cmd/ermite/web_files_test.go +2 -5
@@ -212,11 +212,8 @@ func TestWebRaw(t *testing.T) {
212212 assert.Equal(t, "default-src 'none'; sandbox", hdr.Get("Content-Security-Policy"))
213213 assert.Equal(t, "DENY", hdr.Get("X-Frame-Options"))
214214 assert.Equal(t, strconv.Itoa(len(body)), hdr.Get("Content-Length"))
215- if strings.Contains(tt.path, sha) {
216- assert.Equal(t, "public, max-age=31536000, immutable", hdr.Get("Cache-Control"))
217- } else {
218- assert.Equal(t, "no-cache", hdr.Get("Cache-Control"), "a branch or a tag moves")
219- }
215+ // Even at a full commit id: the repository can turn private.
216+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
220217 })
221218 }
222219
@@ -212,11 +212,8 @@ func TestWebRaw(t *testing.T) {
212 assert.Equal(t, "default-src 'none'; sandbox", hdr.Get("Content-Security-Policy"))212 assert.Equal(t, "default-src 'none'; sandbox", hdr.Get("Content-Security-Policy"))
213 assert.Equal(t, "DENY", hdr.Get("X-Frame-Options"))213 assert.Equal(t, "DENY", hdr.Get("X-Frame-Options"))
214 assert.Equal(t, strconv.Itoa(len(body)), hdr.Get("Content-Length"))214 assert.Equal(t, strconv.Itoa(len(body)), hdr.Get("Content-Length"))
215- if strings.Contains(tt.path, sha) {215+ // Even at a full commit id: the repository can turn private.
216- assert.Equal(t, "public, max-age=31536000, immutable", hdr.Get("Cache-Control"))216+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
217- } else {
218- assert.Equal(t, "no-cache", hdr.Get("Cache-Control"), "a branch or a tag moves")
219- }
220 })217 })
221 }218 }
222 219
M internal/adapters/web/highlight.go +16 -1
@@ -8,6 +8,7 @@ import (
88 "html"
99 "html/template"
1010 "path"
11+ "runtime"
1112 "strconv"
1213 "strings"
1314 "time"
@@ -78,12 +79,26 @@ func highlight(name, src string) codeView {
7879 // input can be slow, and the page then shows the file as plain text.
7980 const highlightBudget = 10 * time.Second
8081
82+// lexSlots bounds the lexers running at once, those left to finish after
83+// their budget included: hostile files requested over and over cannot pile
84+// up CPU-bound goroutines.
85+var lexSlots = make(chan struct{}, runtime.GOMAXPROCS(0))
86+
8187 // highlightCtx is highlight with a time budget and the cancellation of ctx.
8288 // A lexer cannot be interrupted, so one that overruns is left to finish on
8389 // its own (its regexps have their own timeout) and its result is dropped.
90+// When every lexing slot is taken the file is shown as plain text.
8491 func highlightCtx(ctx context.Context, name, src string) codeView {
92+ select {
93+ case lexSlots <- struct{}{}:
94+ default:
95+ return highlightPlain(src)
96+ }
8597 done := make(chan codeView, 1)
86- go func() { done <- highlight(name, src) }()
98+ go func() {
99+ defer func() { <-lexSlots }()
100+ done <- highlight(name, src)
101+ }()
87102 select {
88103 case v := <-done:
89104 return v
@@ -8,6 +8,7 @@ import (
8 "html"8 "html"
9 "html/template"9 "html/template"
10 "path"10 "path"
11+ "runtime"
11 "strconv"12 "strconv"
12 "strings"13 "strings"
13 "time"14 "time"
@@ -78,12 +79,26 @@ func highlight(name, src string) codeView {
78 // input can be slow, and the page then shows the file as plain text.79 // input can be slow, and the page then shows the file as plain text.
79 const highlightBudget = 10 * time.Second80 const highlightBudget = 10 * time.Second
80 81
82+// lexSlots bounds the lexers running at once, those left to finish after
83+// their budget included: hostile files requested over and over cannot pile
84+// up CPU-bound goroutines.
85+var lexSlots = make(chan struct{}, runtime.GOMAXPROCS(0))
86+
81 // highlightCtx is highlight with a time budget and the cancellation of ctx.87 // highlightCtx is highlight with a time budget and the cancellation of ctx.
82 // A lexer cannot be interrupted, so one that overruns is left to finish on88 // A lexer cannot be interrupted, so one that overruns is left to finish on
83 // its own (its regexps have their own timeout) and its result is dropped.89 // its own (its regexps have their own timeout) and its result is dropped.
90+// When every lexing slot is taken the file is shown as plain text.
84 func highlightCtx(ctx context.Context, name, src string) codeView {91 func highlightCtx(ctx context.Context, name, src string) codeView {
92+ select {
93+ case lexSlots <- struct{}{}:
94+ default:
95+ return highlightPlain(src)
96+ }
85 done := make(chan codeView, 1)97 done := make(chan codeView, 1)
86- go func() { done <- highlight(name, src) }()98+ go func() {
99+ defer func() { <-lexSlots }()
100+ done <- highlight(name, src)
101+ }()
87 select {102 select {
88 case v := <-done:103 case v := <-done:
89 return v104 return v
M internal/adapters/web/raw.go +4 -6
@@ -56,12 +56,10 @@ func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
5656 if etag := b.info.GetId(); etag != "" {
5757 h.Set("ETag", `"`+etag+`"`)
5858 }
59- // A commit named in full never changes; a branch or a tag moves.
60- if t.detached && len(t.rev) == 40 {
61- h.Set("Cache-Control", "public, max-age=31536000, immutable")
62- } else {
63- h.Set("Cache-Control", "no-cache")
64- }
59+ // Revalidated every time, even at a full commit id: a repository can turn
60+ // private, and a shared cache must not keep serving its files (AC2). The
61+ // ETag keeps the revalidation cheap.
62+ h.Set("Cache-Control", "no-cache")
6563 if match := r.Header.Get("If-None-Match"); match != "" && h.Get("ETag") != "" && match == h.Get("ETag") {
6664 w.WriteHeader(http.StatusNotModified)
6765 return
@@ -56,12 +56,10 @@ func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
56 if etag := b.info.GetId(); etag != "" {56 if etag := b.info.GetId(); etag != "" {
57 h.Set("ETag", `"`+etag+`"`)57 h.Set("ETag", `"`+etag+`"`)
58 }58 }
59- // A commit named in full never changes; a branch or a tag moves.59+ // Revalidated every time, even at a full commit id: a repository can turn
60- if t.detached && len(t.rev) == 40 {60+ // private, and a shared cache must not keep serving its files (AC2). The
61- h.Set("Cache-Control", "public, max-age=31536000, immutable")61+ // ETag keeps the revalidation cheap.
62- } else {62+ h.Set("Cache-Control", "no-cache")
63- h.Set("Cache-Control", "no-cache")
64- }
65 if match := r.Header.Get("If-None-Match"); match != "" && h.Get("ETag") != "" && match == h.Get("ETag") {63 if match := r.Header.Get("If-None-Match"); match != "" && h.Get("ETag") != "" && match == h.Get("ETag") {
66 w.WriteHeader(http.StatusNotModified)64 w.WriteHeader(http.StatusNotModified)
67 return65 return

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close