detached 4bec9a0

refactor(web): render the issue body and its merge request links in their own helpers

commit
4bec9a08f1d16de05320cc93a6cc759e32faa7a4
author
brice · · 2026-10-20 16:29 UTC
parent
804d55b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
7/8
1 file · +38 -22
M internal/adapters/web/issues.go +38 -22
@@ -290,29 +290,11 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
290290 Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(i.Title.String()),
291291 State: string(i.State), Open: i.IsOpen(),
292292 }
293- if body := i.Body.String(); strings.TrimSpace(body) != "" {
294- var h template.HTML
295- var err error
296- if i.External {
297- // Written by an anonymous visitor: the untrusted policy.
298- h, err = s.md.renderUntrusted([]byte(body), "issue-desc-", s.numberResolver(ctx, d.name()))
299- } else {
300- h, err = s.md.renderIssue([]byte(body), d.name(), d.repo.DefaultBranch, "issue-desc-", s.numberResolver(ctx, d.name()))
301- }
302- if err != nil {
303- return nil, 0, err
304- }
305- p.Desc = h
306- }
307- if i.ClosedByMergeRequest > 0 {
308- p.ClosedBy = &Link{Label: "!" + strconv.FormatInt(i.ClosedByMergeRequest, 10), Href: mrHref(d.name(), i.ClosedByMergeRequest)}
309- }
310- for _, ref := range res.References {
311- p.References = append(p.References, IssueRefView{
312- Number: "!" + strconv.FormatInt(ref.Number, 10), Title: visibleText(ref.Title.String()),
313- State: string(ref.State), Href: mrHref(d.name(), ref.Number), Closes: ref.Closes,
314- })
293+ if p.Desc, err = s.issueBody(ctx, d, i); err != nil {
294+ return nil, 0, err
315295 }
296+ p.ClosedBy = numberLink(d.name(), i.ClosedByMergeRequest)
297+ p.References = issueRefViews(d.name(), res.References)
316298 p.Opened.Name = s.who(i.Author)
317299 if i.External {
318300 // The label is the visitor's, not the owner's: shown as plain text.
@@ -356,6 +338,40 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
356338 return IssueShow(l, p), http.StatusOK, nil
357339 }
358340
341+// issueBody renders the description of an issue; an issue from the public form
342+// was written by an anonymous visitor and gets the untrusted policy.
343+func (s *Server) issueBody(ctx context.Context, d *repoData, i domain.Issue) (template.HTML, error) {
344+ body := i.Body.String()
345+ if strings.TrimSpace(body) == "" {
346+ return "", nil
347+ }
348+ numbers := s.numberResolver(ctx, d.name())
349+ if i.External {
350+ return s.md.renderUntrusted([]byte(body), "issue-desc-", numbers)
351+ }
352+ return s.md.renderIssue([]byte(body), d.name(), d.repo.DefaultBranch, "issue-desc-", numbers)
353+}
354+
355+// numberLink links merge request n of repo; nil for 0.
356+func numberLink(repo string, n int64) *Link {
357+ if n == 0 {
358+ return nil
359+ }
360+ return &Link{Label: "!" + strconv.FormatInt(n, 10), Href: mrHref(repo, n)}
361+}
362+
363+// issueRefViews are the merge requests that mention an issue.
364+func issueRefViews(repo string, refs []ports.IssueReference) []IssueRefView {
365+ views := make([]IssueRefView, 0, len(refs))
366+ for _, ref := range refs {
367+ views = append(views, IssueRefView{
368+ Number: "!" + strconv.FormatInt(ref.Number, 10), Title: visibleText(ref.Title.String()),
369+ State: string(ref.State), Href: mrHref(repo, ref.Number), Closes: ref.Closes,
370+ })
371+ }
372+ return views
373+}
374+
359375 // pageOrRedirect is page(), except that a page that is not found may be the
360376 // page of the other kind of the number: target then names where to send the
361377 // visitor ("" when there is none).
@@ -290,29 +290,11 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
290 Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(i.Title.String()),290 Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(i.Title.String()),
291 State: string(i.State), Open: i.IsOpen(),291 State: string(i.State), Open: i.IsOpen(),
292 }292 }
293- if body := i.Body.String(); strings.TrimSpace(body) != "" {293+ if p.Desc, err = s.issueBody(ctx, d, i); err != nil {
294- var h template.HTML294+ return nil, 0, err
295- var err error
296- if i.External {
297- // Written by an anonymous visitor: the untrusted policy.
298- h, err = s.md.renderUntrusted([]byte(body), "issue-desc-", s.numberResolver(ctx, d.name()))
299- } else {
300- h, err = s.md.renderIssue([]byte(body), d.name(), d.repo.DefaultBranch, "issue-desc-", s.numberResolver(ctx, d.name()))
301- }
302- if err != nil {
303- return nil, 0, err
304- }
305- p.Desc = h
306- }
307- if i.ClosedByMergeRequest > 0 {
308- p.ClosedBy = &Link{Label: "!" + strconv.FormatInt(i.ClosedByMergeRequest, 10), Href: mrHref(d.name(), i.ClosedByMergeRequest)}
309- }
310- for _, ref := range res.References {
311- p.References = append(p.References, IssueRefView{
312- Number: "!" + strconv.FormatInt(ref.Number, 10), Title: visibleText(ref.Title.String()),
313- State: string(ref.State), Href: mrHref(d.name(), ref.Number), Closes: ref.Closes,
314- })
315 }295 }
296+ p.ClosedBy = numberLink(d.name(), i.ClosedByMergeRequest)
297+ p.References = issueRefViews(d.name(), res.References)
316 p.Opened.Name = s.who(i.Author)298 p.Opened.Name = s.who(i.Author)
317 if i.External {299 if i.External {
318 // The label is the visitor's, not the owner's: shown as plain text.300 // The label is the visitor's, not the owner's: shown as plain text.
@@ -356,6 +338,40 @@ func (s *Server) issueShow(ctx context.Context, r *http.Request) (templ.Componen
356 return IssueShow(l, p), http.StatusOK, nil338 return IssueShow(l, p), http.StatusOK, nil
357 }339 }
358 340
341+// issueBody renders the description of an issue; an issue from the public form
342+// was written by an anonymous visitor and gets the untrusted policy.
343+func (s *Server) issueBody(ctx context.Context, d *repoData, i domain.Issue) (template.HTML, error) {
344+ body := i.Body.String()
345+ if strings.TrimSpace(body) == "" {
346+ return "", nil
347+ }
348+ numbers := s.numberResolver(ctx, d.name())
349+ if i.External {
350+ return s.md.renderUntrusted([]byte(body), "issue-desc-", numbers)
351+ }
352+ return s.md.renderIssue([]byte(body), d.name(), d.repo.DefaultBranch, "issue-desc-", numbers)
353+}
354+
355+// numberLink links merge request n of repo; nil for 0.
356+func numberLink(repo string, n int64) *Link {
357+ if n == 0 {
358+ return nil
359+ }
360+ return &Link{Label: "!" + strconv.FormatInt(n, 10), Href: mrHref(repo, n)}
361+}
362+
363+// issueRefViews are the merge requests that mention an issue.
364+func issueRefViews(repo string, refs []ports.IssueReference) []IssueRefView {
365+ views := make([]IssueRefView, 0, len(refs))
366+ for _, ref := range refs {
367+ views = append(views, IssueRefView{
368+ Number: "!" + strconv.FormatInt(ref.Number, 10), Title: visibleText(ref.Title.String()),
369+ State: string(ref.State), Href: mrHref(repo, ref.Number), Closes: ref.Closes,
370+ })
371+ }
372+ return views
373+}
374+
359 // pageOrRedirect is page(), except that a page that is not found may be the375 // pageOrRedirect is page(), except that a page that is not found may be the
360 // page of the other kind of the number: target then names where to send the376 // page of the other kind of the number: target then names where to send the
361 // visitor ("" when there is none).377 // visitor ("" when there is none).

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 2

Tags 41

↑↓ moveenter openesc close