detached 0cea275

feat(web): live MR checks, draft merge requests, sticky review rail (#47)

commit
0cea2758ac9d47b10e83c4d30c134423025e7bed
author
brice · · 2026-10-17 17:44 UTC
committer
bnema · · 2026-10-17 17:44 UTC
parent
ab8e178
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
refs
tag: v0.18.0
59 files · +1177 -456
M internal/adapters/kvstore/mergerequests.go +7 -0
@@ -344,6 +344,13 @@ func (m *MergeRequests) ListOpen(ctx context.Context, repo domain.RepoName) ([]d
344344 return out, nil
345345 }
346346
347+// CountOpen counts the open merge requests of the repository from the keys of
348+// the index: no request is decoded.
349+func (m *MergeRequests) CountOpen(ctx context.Context, repo domain.RepoName) (int, error) {
350+ numbers, _, err := m.scanIndex(ctx, []string{mrOpenRepoPrefix(repo)}, 0)
351+ return len(numbers), err
352+}
353+
347354 // Update replaces the merge request with a version precondition.
348355 //
349356 //hexcheck:ignore suspicious-business-logic-in-adapter port method name only
@@ -344,6 +344,13 @@ func (m *MergeRequests) ListOpen(ctx context.Context, repo domain.RepoName) ([]d
344 return out, nil344 return out, nil
345 }345 }
346 346
347+// CountOpen counts the open merge requests of the repository from the keys of
348+// the index: no request is decoded.
349+func (m *MergeRequests) CountOpen(ctx context.Context, repo domain.RepoName) (int, error) {
350+ numbers, _, err := m.scanIndex(ctx, []string{mrOpenRepoPrefix(repo)}, 0)
351+ return len(numbers), err
352+}
353+
347 // Update replaces the merge request with a version precondition.354 // Update replaces the merge request with a version precondition.
348 //355 //
349 //hexcheck:ignore suspicious-business-logic-in-adapter port method name only356 //hexcheck:ignore suspicious-business-logic-in-adapter port method name only
M internal/adapters/web/actions_pages.go +2 -2
@@ -237,7 +237,7 @@ func (s *Server) runRow(repo string, run domain.CIRun, now time.Time) RunRow {
237237 Kind: ciKind(run.Status, run.Conclusion), State: ciState(run.Status, run.Conclusion),
238238 Workflow: visibleText(run.WorkflowName), Event: visibleText(run.Event), Title: visibleText(run.Title),
239239 Ref: visibleText(strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/")),
240- Short: short(run.Sha), CHref: repoHref(repo, "commit", run.Sha), Actor: visibleText(run.Actor),
240+ Short: short(run.Sha), CHref: repoHref(repo, "commit", run.Sha), Actor: s.who(run.Actor),
241241 Duration: duration(run.StartedAt, run.CompletedAt, now), Number: "#" + strconv.FormatInt(run.Number, 10), Attempt: run.Attempt,
242242 }
243243 row.RefHref = repoHref(repo, "tree", strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/"))
@@ -248,7 +248,7 @@ func (s *Server) runRow(repo string, run domain.CIRun, now time.Time) RunRow {
248248 if ok {
249249 // A pull_request run is for refs/pull/<n>/merge, a pull_request_target
250250 // run for the default branch: both show the merge request.
251- row.Ref, row.RefHref = "!"+strconv.FormatInt(n, 10), mrHref(repo, n)
251+ row.Ref, row.RefHref = "#"+strconv.FormatInt(n, 10), mrHref(repo, n)
252252 }
253253 row.When, row.WhenAt = whenOf(now, run.CreatedAt)
254254 return row
@@ -237,7 +237,7 @@ func (s *Server) runRow(repo string, run domain.CIRun, now time.Time) RunRow {
237 Kind: ciKind(run.Status, run.Conclusion), State: ciState(run.Status, run.Conclusion),237 Kind: ciKind(run.Status, run.Conclusion), State: ciState(run.Status, run.Conclusion),
238 Workflow: visibleText(run.WorkflowName), Event: visibleText(run.Event), Title: visibleText(run.Title),238 Workflow: visibleText(run.WorkflowName), Event: visibleText(run.Event), Title: visibleText(run.Title),
239 Ref: visibleText(strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/")),239 Ref: visibleText(strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/")),
240- Short: short(run.Sha), CHref: repoHref(repo, "commit", run.Sha), Actor: visibleText(run.Actor),240+ Short: short(run.Sha), CHref: repoHref(repo, "commit", run.Sha), Actor: s.who(run.Actor),
241 Duration: duration(run.StartedAt, run.CompletedAt, now), Number: "#" + strconv.FormatInt(run.Number, 10), Attempt: run.Attempt,241 Duration: duration(run.StartedAt, run.CompletedAt, now), Number: "#" + strconv.FormatInt(run.Number, 10), Attempt: run.Attempt,
242 }242 }
243 row.RefHref = repoHref(repo, "tree", strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/"))243 row.RefHref = repoHref(repo, "tree", strings.TrimPrefix(strings.TrimPrefix(run.Ref, "refs/heads/"), "refs/tags/"))
@@ -248,7 +248,7 @@ func (s *Server) runRow(repo string, run domain.CIRun, now time.Time) RunRow {
248 if ok {248 if ok {
249 // A pull_request run is for refs/pull/<n>/merge, a pull_request_target249 // A pull_request run is for refs/pull/<n>/merge, a pull_request_target
250 // run for the default branch: both show the merge request.250 // run for the default branch: both show the merge request.
251- row.Ref, row.RefHref = "!"+strconv.FormatInt(n, 10), mrHref(repo, n)251+ row.Ref, row.RefHref = "#"+strconv.FormatInt(n, 10), mrHref(repo, n)
252 }252 }
253 row.When, row.WhenAt = whenOf(now, run.CreatedAt)253 row.When, row.WhenAt = whenOf(now, run.CreatedAt)
254 return row254 return row
M internal/adapters/web/compare.go +1 -1
@@ -111,7 +111,7 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component,
111111 }
112112 switch {
113113 case existing != nil:
114- p.Existing = &Link{Label: "!" + itoa(int(existing.Number)) + " " + visibleText(existing.Title.String()), Href: mrHref(name, existing.Number)}
114+ p.Existing = &Link{Label: "#" + itoa(int(existing.Number)) + " " + visibleText(existing.Title.String()), Href: mrHref(name, existing.Number)}
115115 case SignedIn(ctx) && !same && len(ahead) > 0:
116116 title := ""
117117 if len(ahead) == 1 {
@@ -111,7 +111,7 @@ func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component,
111 }111 }
112 switch {112 switch {
113 case existing != nil:113 case existing != nil:
114- p.Existing = &Link{Label: "!" + itoa(int(existing.Number)) + " " + visibleText(existing.Title.String()), Href: mrHref(name, existing.Number)}114+ p.Existing = &Link{Label: "#" + itoa(int(existing.Number)) + " " + visibleText(existing.Title.String()), Href: mrHref(name, existing.Number)}
115 case SignedIn(ctx) && !same && len(ahead) > 0:115 case SignedIn(ctx) && !same && len(ahead) > 0:
116 title := ""116 title := ""
117 if len(ahead) == 1 {117 if len(ahead) == 1 {
M internal/adapters/web/format.go +3 -3
@@ -10,8 +10,8 @@ import (
1010
1111 func itoa(n int) string { return strconv.Itoa(n) }
1212
13-// ago renders a short relative date: now, 5m ago, 3h ago, 12d ago, 5mo ago,
14-// 2y ago.
13+// ago renders a short relative date: now, 5 min ago, 3h ago, 12d ago, 5mo
14+// ago, 2y ago. Minutes are "min", so that they never read as months.
1515 func ago(now, t time.Time) string {
1616 if t.IsZero() {
1717 return "never"
@@ -21,7 +21,7 @@ func ago(now, t time.Time) string {
2121 case d < time.Minute:
2222 return "now"
2323 case d < time.Hour:
24- return fmt.Sprintf("%dm ago", int(d.Minutes()))
24+ return fmt.Sprintf("%d min ago", int(d.Minutes()))
2525 case d < 48*time.Hour:
2626 return fmt.Sprintf("%dh ago", int(d.Hours()))
2727 case d < 60*24*time.Hour:
@@ -10,8 +10,8 @@ import (
10 10
11 func itoa(n int) string { return strconv.Itoa(n) }11 func itoa(n int) string { return strconv.Itoa(n) }
12 12
13-// ago renders a short relative date: now, 5m ago, 3h ago, 12d ago, 5mo ago,13+// ago renders a short relative date: now, 5 min ago, 3h ago, 12d ago, 5mo
14-// 2y ago.14+// ago, 2y ago. Minutes are "min", so that they never read as months.
15 func ago(now, t time.Time) string {15 func ago(now, t time.Time) string {
16 if t.IsZero() {16 if t.IsZero() {
17 return "never"17 return "never"
@@ -21,7 +21,7 @@ func ago(now, t time.Time) string {
21 case d < time.Minute:21 case d < time.Minute:
22 return "now"22 return "now"
23 case d < time.Hour:23 case d < time.Hour:
24- return fmt.Sprintf("%dm ago", int(d.Minutes()))24+ return fmt.Sprintf("%d min ago", int(d.Minutes()))
25 case d < 48*time.Hour:25 case d < 48*time.Hour:
26 return fmt.Sprintf("%dh ago", int(d.Hours()))26 return fmt.Sprintf("%dh ago", int(d.Hours()))
27 case d < 60*24*time.Hour:27 case d < 60*24*time.Hour:
M internal/adapters/web/fragments.go +75 -1
@@ -29,8 +29,15 @@ type liveStream struct {
2929 // sha is the commit of the run of the job shown, read once when the stream
3030 // is set up: the annotations link to files at it, and it never changes.
3131 sha string
32+ // headRuns says, on a merge request page, whether a run is one of the
33+ // head shown (its checks are on that head); a job event does not carry
34+ // its commit, so its run is read once. One stream reads it, no lock.
35+ headRuns map[int64]bool
3236 }
3337
38+// maxHeadRuns bounds headRuns: past it the map starts again.
39+const maxHeadRuns = 256
40+
3441 // renderFragment renders a component to the data of an event.
3542 func renderFragment(ctx context.Context, event string, c templ.Component) (fragment, error) {
3643 var b bytes.Buffer
@@ -51,7 +58,7 @@ func liveEvent(we ports.WatchEvent) bool {
5158 }
5259 switch we.Event.(type) {
5360 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
54- domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.CommentAdded,
61+ domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded,
5562 domain.RunUpdated, domain.JobUpdated:
5663 return true
5764 }
@@ -148,6 +155,8 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
148155 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was merged — reload")
149156 case domain.MergeRequestClosed:
150157 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
158+ case domain.MergeRequestReady:
159+ return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")
151160 case domain.CommentAdded:
152161 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")
153162 case domain.PushEvent:
@@ -431,10 +440,66 @@ func (l *liveStream) runEvent(ctx context.Context, ev domain.RunUpdated) ([]frag
431440 return nil, nil
432441 }
433442 return l.runBody(ctx)
443+ case "mr":
444+ ofHead := ev.Run.CheckSha() == l.head
445+ l.rememberRun(ev.Run.ID, ofHead)
446+ if !ofHead {
447+ return nil, nil
448+ }
449+ return l.mrChecks(ctx)
434450 }
435451 return nil, nil
436452 }
437453
454+// rememberRun records whether a run is one of the head of the page.
455+func (l *liveStream) rememberRun(id int64, ofHead bool) {
456+ if l.headRuns == nil || len(l.headRuns) >= maxHeadRuns {
457+ l.headRuns = make(map[int64]bool)
458+ }
459+ l.headRuns[id] = ofHead
460+}
461+
462+// jobOfHead tells whether the run of a job is one of the head of the page,
463+// reading the run the first time.
464+func (l *liveStream) jobOfHead(ctx context.Context, job domain.CIJob) (bool, error) {
465+ if ofHead, known := l.headRuns[job.RunID]; known {
466+ return ofHead, nil
467+ }
468+ run, err := l.s.cfg.Runs.GetRun(ctx, principal(ctx), job.Repo, job.RunID, 0)
469+ switch {
470+ case errors.Is(err, domain.ErrNotFound):
471+ return false, nil
472+ case err != nil:
473+ return false, err
474+ }
475+ ofHead := run.Run.CheckSha() == l.head
476+ l.rememberRun(job.RunID, ofHead)
477+ return ofHead, nil
478+}
479+
480+// mrChecks is the checks block of the merge request page, read again for the
481+// head the page shows: a run or a job of that head changed. A job event does
482+// not name its commit; reading the checks of the head is one store read, and
483+// a job of another commit leaves them as they were.
484+func (l *liveStream) mrChecks(ctx context.Context) ([]fragment, error) {
485+ if !isFullID(l.head) {
486+ return nil, nil
487+ }
488+ name, err := domain.ParseRepoName(l.repo)
489+ if err != nil {
490+ return nil, nil
491+ }
492+ repo, err := l.s.repoRecord(ctx, name)
493+ switch {
494+ case errors.Is(err, domain.ErrNotFound):
495+ return gap, nil
496+ case err != nil:
497+ return nil, err
498+ }
499+ f, err := renderFragment(ctx, "ci-run", MRChecks(checksSection(repo, l.s.checksFor(ctx, name, l.head))))
500+ return []fragment{f}, err
501+}
502+
438503 // recentActions is the "Recent actions" block of the summary rail, read again
439504 // as the principal of the stream: the newest runs change with every event, and
440505 // a viewer who cannot read CI any more gets the block hidden and empty.
@@ -470,6 +535,15 @@ func (l *liveStream) jobEvent(ctx context.Context, ev domain.JobUpdated) ([]frag
470535 return nil, nil
471536 }
472537 return l.jobState(ctx)
538+ case "mr":
539+ if !isFullID(l.head) {
540+ return nil, nil
541+ }
542+ ofHead, err := l.jobOfHead(ctx, ev.Job)
543+ if err != nil || !ofHead {
544+ return nil, err
545+ }
546+ return l.mrChecks(ctx)
473547 }
474548 return nil, nil
475549 }
@@ -29,8 +29,15 @@ type liveStream struct {
29 // sha is the commit of the run of the job shown, read once when the stream29 // sha is the commit of the run of the job shown, read once when the stream
30 // is set up: the annotations link to files at it, and it never changes.30 // is set up: the annotations link to files at it, and it never changes.
31 sha string31 sha string
32+ // headRuns says, on a merge request page, whether a run is one of the
33+ // head shown (its checks are on that head); a job event does not carry
34+ // its commit, so its run is read once. One stream reads it, no lock.
35+ headRuns map[int64]bool
32 }36 }
33 37
38+// maxHeadRuns bounds headRuns: past it the map starts again.
39+const maxHeadRuns = 256
40+
34 // renderFragment renders a component to the data of an event.41 // renderFragment renders a component to the data of an event.
35 func renderFragment(ctx context.Context, event string, c templ.Component) (fragment, error) {42 func renderFragment(ctx context.Context, event string, c templ.Component) (fragment, error) {
36 var b bytes.Buffer43 var b bytes.Buffer
@@ -51,7 +58,7 @@ func liveEvent(we ports.WatchEvent) bool {
51 }58 }
52 switch we.Event.(type) {59 switch we.Event.(type) {
53 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,60 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
54- domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.CommentAdded,61+ domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded,
55 domain.RunUpdated, domain.JobUpdated:62 domain.RunUpdated, domain.JobUpdated:
56 return true63 return true
57 }64 }
@@ -148,6 +155,8 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
148 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was merged — reload")155 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was merged — reload")
149 case domain.MergeRequestClosed:156 case domain.MergeRequestClosed:
150 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")157 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
158+ case domain.MergeRequestReady:
159+ return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")
151 case domain.CommentAdded:160 case domain.CommentAdded:
152 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")161 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")
153 case domain.PushEvent:162 case domain.PushEvent:
@@ -431,10 +440,66 @@ func (l *liveStream) runEvent(ctx context.Context, ev domain.RunUpdated) ([]frag
431 return nil, nil440 return nil, nil
432 }441 }
433 return l.runBody(ctx)442 return l.runBody(ctx)
443+ case "mr":
444+ ofHead := ev.Run.CheckSha() == l.head
445+ l.rememberRun(ev.Run.ID, ofHead)
446+ if !ofHead {
447+ return nil, nil
448+ }
449+ return l.mrChecks(ctx)
434 }450 }
435 return nil, nil451 return nil, nil
436 }452 }
437 453
454+// rememberRun records whether a run is one of the head of the page.
455+func (l *liveStream) rememberRun(id int64, ofHead bool) {
456+ if l.headRuns == nil || len(l.headRuns) >= maxHeadRuns {
457+ l.headRuns = make(map[int64]bool)
458+ }
459+ l.headRuns[id] = ofHead
460+}
461+
462+// jobOfHead tells whether the run of a job is one of the head of the page,
463+// reading the run the first time.
464+func (l *liveStream) jobOfHead(ctx context.Context, job domain.CIJob) (bool, error) {
465+ if ofHead, known := l.headRuns[job.RunID]; known {
466+ return ofHead, nil
467+ }
468+ run, err := l.s.cfg.Runs.GetRun(ctx, principal(ctx), job.Repo, job.RunID, 0)
469+ switch {
470+ case errors.Is(err, domain.ErrNotFound):
471+ return false, nil
472+ case err != nil:
473+ return false, err
474+ }
475+ ofHead := run.Run.CheckSha() == l.head
476+ l.rememberRun(job.RunID, ofHead)
477+ return ofHead, nil
478+}
479+
480+// mrChecks is the checks block of the merge request page, read again for the
481+// head the page shows: a run or a job of that head changed. A job event does
482+// not name its commit; reading the checks of the head is one store read, and
483+// a job of another commit leaves them as they were.
484+func (l *liveStream) mrChecks(ctx context.Context) ([]fragment, error) {
485+ if !isFullID(l.head) {
486+ return nil, nil
487+ }
488+ name, err := domain.ParseRepoName(l.repo)
489+ if err != nil {
490+ return nil, nil
491+ }
492+ repo, err := l.s.repoRecord(ctx, name)
493+ switch {
494+ case errors.Is(err, domain.ErrNotFound):
495+ return gap, nil
496+ case err != nil:
497+ return nil, err
498+ }
499+ f, err := renderFragment(ctx, "ci-run", MRChecks(checksSection(repo, l.s.checksFor(ctx, name, l.head))))
500+ return []fragment{f}, err
501+}
502+
438 // recentActions is the "Recent actions" block of the summary rail, read again503 // recentActions is the "Recent actions" block of the summary rail, read again
439 // as the principal of the stream: the newest runs change with every event, and504 // as the principal of the stream: the newest runs change with every event, and
440 // a viewer who cannot read CI any more gets the block hidden and empty.505 // a viewer who cannot read CI any more gets the block hidden and empty.
@@ -470,6 +535,15 @@ func (l *liveStream) jobEvent(ctx context.Context, ev domain.JobUpdated) ([]frag
470 return nil, nil535 return nil, nil
471 }536 }
472 return l.jobState(ctx)537 return l.jobState(ctx)
538+ case "mr":
539+ if !isFullID(l.head) {
540+ return nil, nil
541+ }
542+ ofHead, err := l.jobOfHead(ctx, ev.Job)
543+ if err != nil || !ofHead {
544+ return nil, err
545+ }
546+ return l.mrChecks(ctx)
473 }547 }
474 return nil, nil548 return nil, nil
475 }549 }
M internal/adapters/web/mr.go +23 -8
@@ -138,6 +138,9 @@ type MRCommit struct {
138138 type MergeBox struct {
139139 MergeAction string
140140 CloseAction string
141+ // ReadyAction takes a draft out of draft; empty when it is not one: a
142+ // draft shows it in place of the merge form.
143+ ReadyAction string
141144 ReadAction string
142145 Head string
143146 Modes []ModeOption
@@ -262,6 +265,15 @@ func mrNumber(r *http.Request) (int64, error) {
262265 return n, nil
263266 }
264267
268+// mrStateName is the state a page shows: "draft" for an open request whose
269+// title marks it so.
270+func mrStateName(m domain.MergeRequest) string {
271+ if m.IsOpen() && m.IsDraft() {
272+ return "draft"
273+ }
274+ return stateName(m.State)
275+}
276+
265277 func stateName(s domain.MRState) string {
266278 switch s {
267279 case domain.MRStateOpen:
@@ -287,10 +299,10 @@ func modeName(m domain.MergeMode) string {
287299 // mrRow converts a merge request of a listing.
288300 func (s *Server) mrRow(repo string, m domain.MergeRequest) MRRow {
289301 row := MRRow{
290- Number: "!" + strconv.FormatInt(m.Number, 10), Href: mrHref(repo, m.Number),
291- Title: visibleText(m.Title.String()), State: stateName(m.State),
302+ Number: "#" + strconv.FormatInt(m.Number, 10), Href: mrHref(repo, m.Number),
303+ Title: visibleText(m.Title.String()), State: mrStateName(m),
292304 Branches: visibleText(m.Source) + " → " + visibleText(m.Target),
293- Author: visibleText(m.Author),
305+ Author: s.who(m.Author),
294306 }
295307 ts := m.UpdatedAt
296308 if ts.IsZero() {
@@ -422,10 +434,10 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
422434 }
423435
424436 p := MRPage{
425- Number: "!" + strconv.FormatInt(n, 10), Title: visibleText(m.Title.String()),
426- State: stateName(m.State), Open: open, Signed: signed,
437+ Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(m.Title.String()),
438+ State: mrStateName(m), Open: open, Signed: signed,
427439 Source: visibleText(m.Source), Target: visibleText(m.Target),
428- Author: visibleText(m.Author),
440+ Author: s.who(m.Author),
429441 Desc: plainParts(m.Description.String()),
430442 Head: head, HeadShort: short(head),
431443 NFiles: res.Stats.FilesChanged,
@@ -529,6 +541,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
529541 p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split")
530542 if signed && open {
531543 p.Box = s.mergeBox(d, n, m.Source, head, lastRead)
544+ if m.IsDraft() {
545+ p.Box.ReadyAction = mrHref(d.name(), n, "ready")
546+ }
532547 }
533548 if canWrite {
534549 p.GeneralForm = &CommentForm{
@@ -553,7 +568,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
553568 l.Crumbs = append(l.Crumbs, Crumb{Label: "since", Current: true})
554569 }
555570 p.Live = true
556- l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source))
571+ l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source)+"&head="+url.QueryEscape(head))
557572 l.HasRail = true
558573 return MergeRequest(l, p), http.StatusOK, nil
559574 }
@@ -683,7 +698,7 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
683698 for _, c := range comments {
684699 cid := strconv.FormatInt(c.ID, 10)
685700 v := CommentView{
686- ID: cid, Author: visibleText(c.Author),
701+ ID: cid, Author: s.who(c.Author),
687702 Parts: plainParts(c.Text.String()), Resolved: c.Resolved,
688703 Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head,
689704 }
@@ -138,6 +138,9 @@ type MRCommit struct {
138 type MergeBox struct {138 type MergeBox struct {
139 MergeAction string139 MergeAction string
140 CloseAction string140 CloseAction string
141+ // ReadyAction takes a draft out of draft; empty when it is not one: a
142+ // draft shows it in place of the merge form.
143+ ReadyAction string
141 ReadAction string144 ReadAction string
142 Head string145 Head string
143 Modes []ModeOption146 Modes []ModeOption
@@ -262,6 +265,15 @@ func mrNumber(r *http.Request) (int64, error) {
262 return n, nil265 return n, nil
263 }266 }
264 267
268+// mrStateName is the state a page shows: "draft" for an open request whose
269+// title marks it so.
270+func mrStateName(m domain.MergeRequest) string {
271+ if m.IsOpen() && m.IsDraft() {
272+ return "draft"
273+ }
274+ return stateName(m.State)
275+}
276+
265 func stateName(s domain.MRState) string {277 func stateName(s domain.MRState) string {
266 switch s {278 switch s {
267 case domain.MRStateOpen:279 case domain.MRStateOpen:
@@ -287,10 +299,10 @@ func modeName(m domain.MergeMode) string {
287 // mrRow converts a merge request of a listing.299 // mrRow converts a merge request of a listing.
288 func (s *Server) mrRow(repo string, m domain.MergeRequest) MRRow {300 func (s *Server) mrRow(repo string, m domain.MergeRequest) MRRow {
289 row := MRRow{301 row := MRRow{
290- Number: "!" + strconv.FormatInt(m.Number, 10), Href: mrHref(repo, m.Number),302+ Number: "#" + strconv.FormatInt(m.Number, 10), Href: mrHref(repo, m.Number),
291- Title: visibleText(m.Title.String()), State: stateName(m.State),303+ Title: visibleText(m.Title.String()), State: mrStateName(m),
292 Branches: visibleText(m.Source) + " → " + visibleText(m.Target),304 Branches: visibleText(m.Source) + " → " + visibleText(m.Target),
293- Author: visibleText(m.Author),305+ Author: s.who(m.Author),
294 }306 }
295 ts := m.UpdatedAt307 ts := m.UpdatedAt
296 if ts.IsZero() {308 if ts.IsZero() {
@@ -422,10 +434,10 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
422 }434 }
423 435
424 p := MRPage{436 p := MRPage{
425- Number: "!" + strconv.FormatInt(n, 10), Title: visibleText(m.Title.String()),437+ Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(m.Title.String()),
426- State: stateName(m.State), Open: open, Signed: signed,438+ State: mrStateName(m), Open: open, Signed: signed,
427 Source: visibleText(m.Source), Target: visibleText(m.Target),439 Source: visibleText(m.Source), Target: visibleText(m.Target),
428- Author: visibleText(m.Author),440+ Author: s.who(m.Author),
429 Desc: plainParts(m.Description.String()),441 Desc: plainParts(m.Description.String()),
430 Head: head, HeadShort: short(head),442 Head: head, HeadShort: short(head),
431 NFiles: res.Stats.FilesChanged,443 NFiles: res.Stats.FilesChanged,
@@ -529,6 +541,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
529 p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split")541 p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split")
530 if signed && open {542 if signed && open {
531 p.Box = s.mergeBox(d, n, m.Source, head, lastRead)543 p.Box = s.mergeBox(d, n, m.Source, head, lastRead)
544+ if m.IsDraft() {
545+ p.Box.ReadyAction = mrHref(d.name(), n, "ready")
546+ }
532 }547 }
533 if canWrite {548 if canWrite {
534 p.GeneralForm = &CommentForm{549 p.GeneralForm = &CommentForm{
@@ -553,7 +568,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
553 l.Crumbs = append(l.Crumbs, Crumb{Label: "since", Current: true})568 l.Crumbs = append(l.Crumbs, Crumb{Label: "since", Current: true})
554 }569 }
555 p.Live = true570 p.Live = true
556- l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source))571+ l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.Source)+"&head="+url.QueryEscape(head))
557 l.HasRail = true572 l.HasRail = true
558 return MergeRequest(l, p), http.StatusOK, nil573 return MergeRequest(l, p), http.StatusOK, nil
559 }574 }
@@ -683,7 +698,7 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
683 for _, c := range comments {698 for _, c := range comments {
684 cid := strconv.FormatInt(c.ID, 10)699 cid := strconv.FormatInt(c.ID, 10)
685 v := CommentView{700 v := CommentView{
686- ID: cid, Author: visibleText(c.Author),701+ ID: cid, Author: s.who(c.Author),
687 Parts: plainParts(c.Text.String()), Resolved: c.Resolved,702 Parts: plainParts(c.Text.String()), Resolved: c.Resolved,
688 Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head,703 Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head,
689 }704 }
M internal/adapters/web/mr.templ +11 -4
@@ -368,7 +368,12 @@ templ mergeBox(p MRPage) {
368368 <dt>into</dt>
369369 <dd>{ p.Target }</dd>
370370 </dl>
371- if !p.Box.HeadGone {
371+ if p.Box.ReadyAction != "" {
372+ <p class="d-draft">this is a draft: it cannot be merged until it is ready for review.</p>
373+ <form class="d-btns" method="post" action={ templ.SafeURL(p.Box.ReadyAction) }>
374+ <button type="submit" class="btn primary" title="remove the draft prefix from the title">Ready for review</button>
375+ </form>
376+ } else if !p.Box.HeadGone {
372377 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>
373378 <input type="hidden" name="head_id" value={ p.Box.Head }/>
374379 <label class="sr" for="merge-mode">merge mode</label>
@@ -396,9 +401,11 @@ templ mergeBox(p MRPage) {
396401 <button type="submit" title="close without merging; the branch is kept" class="btn danger">Close</button>
397402 </form>
398403 </div>
399- if !p.Box.HeadGone {
400- <p class="mute d-or">or from a terminal</p>
401- @CloneBlock(p.Box.CLI)
404+ if !p.Box.HeadGone && p.Box.ReadyAction == "" {
405+ <details class="d-or">
406+ <summary class="mute">or from a terminal</summary>
407+ @CloneBlock(p.Box.CLI)
408+ </details>
402409 }
403410 </section>
404411 }
@@ -368,7 +368,12 @@ templ mergeBox(p MRPage) {
368 <dt>into</dt>368 <dt>into</dt>
369 <dd>{ p.Target }</dd>369 <dd>{ p.Target }</dd>
370 </dl>370 </dl>
371- if !p.Box.HeadGone {371+ if p.Box.ReadyAction != "" {
372+ <p class="d-draft">this is a draft: it cannot be merged until it is ready for review.</p>
373+ <form class="d-btns" method="post" action={ templ.SafeURL(p.Box.ReadyAction) }>
374+ <button type="submit" class="btn primary" title="remove the draft prefix from the title">Ready for review</button>
375+ </form>
376+ } else if !p.Box.HeadGone {
372 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>377 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>
373 <input type="hidden" name="head_id" value={ p.Box.Head }/>378 <input type="hidden" name="head_id" value={ p.Box.Head }/>
374 <label class="sr" for="merge-mode">merge mode</label>379 <label class="sr" for="merge-mode">merge mode</label>
@@ -396,9 +401,11 @@ templ mergeBox(p MRPage) {
396 <button type="submit" title="close without merging; the branch is kept" class="btn danger">Close</button>401 <button type="submit" title="close without merging; the branch is kept" class="btn danger">Close</button>
397 </form>402 </form>
398 </div>403 </div>
399- if !p.Box.HeadGone {404+ if !p.Box.HeadGone && p.Box.ReadyAction == "" {
400- <p class="mute d-or">or from a terminal</p>405+ <details class="d-or">
401- @CloneBlock(p.Box.CLI)406+ <summary class="mute">or from a terminal</summary>
407+ @CloneBlock(p.Box.CLI)
408+ </details>
402 }409 }
403 </section>410 </section>
404 }411 }
M internal/adapters/web/mr_post.go +19 -0
@@ -260,6 +260,25 @@ func (s *Server) mrClose(ctx context.Context, r *http.Request) (string, string,
260260 return back, back, nil
261261 }
262262
263+// mrReady is POST /<repo>/merge-requests/<n>/ready: the draft prefix leaves
264+// the title.
265+func (s *Server) mrReady(ctx context.Context, r *http.Request) (string, string, error) {
266+ repo := r.PathValue("repo")
267+ n, err := number(r)
268+ if err != nil {
269+ return "", "", err
270+ }
271+ back := mrHref(repo, n)
272+ name, err := domain.ParseRepoName(repo)
273+ if err != nil {
274+ return "", back, err
275+ }
276+ if _, err := s.cfg.MergeRequests.MarkReady(ctx, principal(ctx), name, n); err != nil {
277+ return "", back, err
278+ }
279+ return back, back, nil
280+}
281+
263282 // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode,
264283 // the head the owner was shown (head_id): the server merges only if the
265284 // branch still points to it, and delete_branch when the owner asked to delete
@@ -260,6 +260,25 @@ func (s *Server) mrClose(ctx context.Context, r *http.Request) (string, string,
260 return back, back, nil260 return back, back, nil
261 }261 }
262 262
263+// mrReady is POST /<repo>/merge-requests/<n>/ready: the draft prefix leaves
264+// the title.
265+func (s *Server) mrReady(ctx context.Context, r *http.Request) (string, string, error) {
266+ repo := r.PathValue("repo")
267+ n, err := number(r)
268+ if err != nil {
269+ return "", "", err
270+ }
271+ back := mrHref(repo, n)
272+ name, err := domain.ParseRepoName(repo)
273+ if err != nil {
274+ return "", back, err
275+ }
276+ if _, err := s.cfg.MergeRequests.MarkReady(ctx, principal(ctx), name, n); err != nil {
277+ return "", back, err
278+ }
279+ return back, back, nil
280+}
281+
263 // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode,282 // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode,
264 // the head the owner was shown (head_id): the server merges only if the283 // the head the owner was shown (head_id): the server merges only if the
265 // branch still points to it, and delete_branch when the owner asked to delete284 // branch still points to it, and delete_branch when the owner asked to delete
M internal/adapters/web/mr_templ.go +251 -229
@@ -1995,152 +1995,170 @@ func mergeBox(p MRPage) templ.Component {
19951995 if templ_7745c5c3_Err != nil {
19961996 return templ_7745c5c3_Err
19971997 }
1998- if !p.Box.HeadGone {
1999- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<form class=\"d-acts\" method=\"post\" action=\"")
1998+ if p.Box.ReadyAction != "" {
1999+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<p class=\"d-draft\">this is a draft: it cannot be merged until it is ready for review.</p><form class=\"d-btns\" method=\"post\" action=\"")
20002000 if templ_7745c5c3_Err != nil {
20012001 return templ_7745c5c3_Err
20022002 }
20032003 var templ_7745c5c3_Var101 templ.SafeURL
2004- templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2004+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
20052005 if templ_7745c5c3_Err != nil {
2006- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 372, Col: 79}
2006+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 79}
20072007 }
20082008 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
20092009 if templ_7745c5c3_Err != nil {
20102010 return templ_7745c5c3_Err
20112011 }
2012- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2012+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"><button type=\"submit\" class=\"btn primary\" title=\"remove the draft prefix from the title\">Ready for review</button></form>")
20132013 if templ_7745c5c3_Err != nil {
20142014 return templ_7745c5c3_Err
20152015 }
2016- var templ_7745c5c3_Var102 string
2017- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2016+ } else if !p.Box.HeadGone {
2017+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<form class=\"d-acts\" method=\"post\" action=\"")
20182018 if templ_7745c5c3_Err != nil {
2019- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 58}
2019+ return templ_7745c5c3_Err
2020+ }
2021+ var templ_7745c5c3_Var102 templ.SafeURL
2022+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2023+ if templ_7745c5c3_Err != nil {
2024+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 79}
2025+ }
2026+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
2027+ if templ_7745c5c3_Err != nil {
2028+ return templ_7745c5c3_Err
2029+ }
2030+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2031+ if templ_7745c5c3_Err != nil {
2032+ return templ_7745c5c3_Err
2033+ }
2034+ var templ_7745c5c3_Var103 string
2035+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2036+ if templ_7745c5c3_Err != nil {
2037+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 378, Col: 58}
20202038 }
2021- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var102)
2039+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var103)
20222040 if templ_7745c5c3_Err != nil {
20232041 return templ_7745c5c3_Err
20242042 }
2025- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "\"> <label class=\"sr\" for=\"merge-mode\">merge mode</label> <select id=\"merge-mode\" name=\"mode\">")
2043+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\"> <label class=\"sr\" for=\"merge-mode\">merge mode</label> <select id=\"merge-mode\" name=\"mode\">")
20262044 if templ_7745c5c3_Err != nil {
20272045 return templ_7745c5c3_Err
20282046 }
20292047 for _, m := range p.Box.Modes {
2030- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<option value=\"")
2048+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<option value=\"")
20312049 if templ_7745c5c3_Err != nil {
20322050 return templ_7745c5c3_Err
20332051 }
2034- var templ_7745c5c3_Var103 string
2035- templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2052+ var templ_7745c5c3_Var104 string
2053+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
20362054 if templ_7745c5c3_Err != nil {
2037- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 29}
2055+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 29}
20382056 }
2039- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var103)
2057+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var104)
20402058 if templ_7745c5c3_Err != nil {
20412059 return templ_7745c5c3_Err
20422060 }
2043- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\">")
2061+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\">")
20442062 if templ_7745c5c3_Err != nil {
20452063 return templ_7745c5c3_Err
20462064 }
2047- var templ_7745c5c3_Var104 string
2048- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2065+ var templ_7745c5c3_Var105 string
2066+ templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
20492067 if templ_7745c5c3_Err != nil {
2050- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 41}
2068+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 41}
20512069 }
2052- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
2070+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
20532071 if templ_7745c5c3_Err != nil {
20542072 return templ_7745c5c3_Err
20552073 }
2056- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "</option>")
2074+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "</option>")
20572075 if templ_7745c5c3_Err != nil {
20582076 return templ_7745c5c3_Err
20592077 }
20602078 }
2061- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</select> ")
2079+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "</select> ")
20622080 if templ_7745c5c3_Err != nil {
20632081 return templ_7745c5c3_Err
20642082 }
20652083 if p.Box.CanDeleteBranch {
2066- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2084+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
20672085 if templ_7745c5c3_Err != nil {
20682086 return templ_7745c5c3_Err
20692087 }
2070- var templ_7745c5c3_Var105 string
2071- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2088+ var templ_7745c5c3_Var106 string
2089+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
20722090 if templ_7745c5c3_Err != nil {
2073- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 381, Col: 101}
2091+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 101}
20742092 }
2075- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
2093+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
20762094 if templ_7745c5c3_Err != nil {
20772095 return templ_7745c5c3_Err
20782096 }
2079- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, " after the merge</label>")
2097+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, " after the merge</label>")
20802098 if templ_7745c5c3_Err != nil {
20812099 return templ_7745c5c3_Err
20822100 }
20832101 }
2084- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\">Merge</button></div></form>")
2102+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\">Merge</button></div></form>")
20852103 if templ_7745c5c3_Err != nil {
20862104 return templ_7745c5c3_Err
20872105 }
20882106 }
2089- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<div class=\"d-btns\">")
2107+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "<div class=\"d-btns\">")
20902108 if templ_7745c5c3_Err != nil {
20912109 return templ_7745c5c3_Err
20922110 }
20932111 if p.Box.CanMarkRead {
2094- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"")
2112+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<form method=\"post\" action=\"")
20952113 if templ_7745c5c3_Err != nil {
20962114 return templ_7745c5c3_Err
20972115 }
2098- var templ_7745c5c3_Var106 templ.SafeURL
2099- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
2116+ var templ_7745c5c3_Var107 templ.SafeURL
2117+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
21002118 if templ_7745c5c3_Err != nil {
2101- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 390, Col: 64}
2119+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64}
21022120 }
2103- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
2121+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
21042122 if templ_7745c5c3_Err != nil {
21052123 return templ_7745c5c3_Err
21062124 }
2107- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2125+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
21082126 if templ_7745c5c3_Err != nil {
21092127 return templ_7745c5c3_Err
21102128 }
2111- var templ_7745c5c3_Var107 string
2112- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2129+ var templ_7745c5c3_Var108 string
2130+ templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
21132131 if templ_7745c5c3_Err != nil {
2114- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 391, Col: 59}
2132+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 59}
21152133 }
2116- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107)
2134+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var108)
21172135 if templ_7745c5c3_Err != nil {
21182136 return templ_7745c5c3_Err
21192137 }
2120- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\">Mark read</button></form>")
2138+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\">Mark read</button></form>")
21212139 if templ_7745c5c3_Err != nil {
21222140 return templ_7745c5c3_Err
21232141 }
21242142 }
2125- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<form method=\"post\" action=\"")
2143+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<form method=\"post\" action=\"")
21262144 if templ_7745c5c3_Err != nil {
21272145 return templ_7745c5c3_Err
21282146 }
2129- var templ_7745c5c3_Var108 templ.SafeURL
2130- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
2147+ var templ_7745c5c3_Var109 templ.SafeURL
2148+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
21312149 if templ_7745c5c3_Err != nil {
2132- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64}
2150+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 64}
21332151 }
2134- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
2152+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
21352153 if templ_7745c5c3_Err != nil {
21362154 return templ_7745c5c3_Err
21372155 }
2138- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form></div>")
2156+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form></div>")
21392157 if templ_7745c5c3_Err != nil {
21402158 return templ_7745c5c3_Err
21412159 }
2142- if !p.Box.HeadGone {
2143- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<p class=\"mute d-or\">or from a terminal</p>")
2160+ if !p.Box.HeadGone && p.Box.ReadyAction == "" {
2161+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
21442162 if templ_7745c5c3_Err != nil {
21452163 return templ_7745c5c3_Err
21462164 }
@@ -2148,8 +2166,12 @@ func mergeBox(p MRPage) templ.Component {
21482166 if templ_7745c5c3_Err != nil {
21492167 return templ_7745c5c3_Err
21502168 }
2169+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "</details>")
2170+ if templ_7745c5c3_Err != nil {
2171+ return templ_7745c5c3_Err
2172+ }
21512173 }
2152- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</section>")
2174+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</section>")
21532175 if templ_7745c5c3_Err != nil {
21542176 return templ_7745c5c3_Err
21552177 }
@@ -2175,12 +2197,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
21752197 }()
21762198 }
21772199 ctx = templ.InitializeContext(ctx)
2178- templ_7745c5c3_Var109 := templ.GetChildren(ctx)
2179- if templ_7745c5c3_Var109 == nil {
2180- templ_7745c5c3_Var109 = templ.NopComponent
2200+ templ_7745c5c3_Var110 := templ.GetChildren(ctx)
2201+ if templ_7745c5c3_Var110 == nil {
2202+ templ_7745c5c3_Var110 = templ.NopComponent
21812203 }
21822204 ctx = templ.ClearChildren(ctx)
2183- templ_7745c5c3_Var110 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2205+ templ_7745c5c3_Var111 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
21842206 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
21852207 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
21862208 if !templ_7745c5c3_IsBuffer {
@@ -2192,263 +2214,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
21922214 }()
21932215 }
21942216 ctx = templ.InitializeContext(ctx)
2195- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<header class=\"show mr-h\"><h1>")
2196- if templ_7745c5c3_Err != nil {
2197- return templ_7745c5c3_Err
2198- }
2199- var templ_7745c5c3_Var111 string
2200- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2201- if templ_7745c5c3_Err != nil {
2202- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 17}
2203- }
2204- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2205- if templ_7745c5c3_Err != nil {
2206- return templ_7745c5c3_Err
2207- }
2208- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, " → ")
2217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "<header class=\"show mr-h\"><h1>")
22092218 if templ_7745c5c3_Err != nil {
22102219 return templ_7745c5c3_Err
22112220 }
22122221 var templ_7745c5c3_Var112 string
2213- templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2222+ templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
22142223 if templ_7745c5c3_Err != nil {
2215- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 34}
2224+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 17}
22162225 }
22172226 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
22182227 if templ_7745c5c3_Err != nil {
22192228 return templ_7745c5c3_Err
22202229 }
2221- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
2230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, " → ")
22222231 if templ_7745c5c3_Err != nil {
22232232 return templ_7745c5c3_Err
22242233 }
2225- var templ_7745c5c3_Var113 templ.SafeURL
2226- templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
2234+ var templ_7745c5c3_Var113 string
2235+ templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
22272236 if templ_7745c5c3_Err != nil {
2228- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 414, Col: 45}
2237+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 34}
22292238 }
22302239 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
22312240 if templ_7745c5c3_Err != nil {
22322241 return templ_7745c5c3_Err
22332242 }
2234- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\">")
2243+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
22352244 if templ_7745c5c3_Err != nil {
22362245 return templ_7745c5c3_Err
22372246 }
2238- var templ_7745c5c3_Var114 string
2239- templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2247+ var templ_7745c5c3_Var114 templ.SafeURL
2248+ templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
22402249 if templ_7745c5c3_Err != nil {
2241- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 414, Col: 58}
2250+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 45}
22422251 }
22432252 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))
22442253 if templ_7745c5c3_Err != nil {
22452254 return templ_7745c5c3_Err
22462255 }
2247- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</a></dd><dt>target</dt><dd><a href=\"")
2256+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "\">")
22482257 if templ_7745c5c3_Err != nil {
22492258 return templ_7745c5c3_Err
22502259 }
2251- var templ_7745c5c3_Var115 templ.SafeURL
2252- templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
2260+ var templ_7745c5c3_Var115 string
2261+ templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
22532262 if templ_7745c5c3_Err != nil {
2254- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 45}
2263+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 58}
22552264 }
22562265 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
22572266 if templ_7745c5c3_Err != nil {
22582267 return templ_7745c5c3_Err
22592268 }
2260- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "\">")
2269+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</a></dd><dt>target</dt><dd><a href=\"")
22612270 if templ_7745c5c3_Err != nil {
22622271 return templ_7745c5c3_Err
22632272 }
2264- var templ_7745c5c3_Var116 string
2265- templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2273+ var templ_7745c5c3_Var116 templ.SafeURL
2274+ templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
22662275 if templ_7745c5c3_Err != nil {
2267- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 58}
2276+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 45}
22682277 }
22692278 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
22702279 if templ_7745c5c3_Err != nil {
22712280 return templ_7745c5c3_Err
22722281 }
2273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</a></dd><dt>ahead</dt><dd>")
2282+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "\">")
22742283 if templ_7745c5c3_Err != nil {
22752284 return templ_7745c5c3_Err
22762285 }
22772286 var templ_7745c5c3_Var117 string
2278- templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
2287+ templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
22792288 if templ_7745c5c3_Err != nil {
2280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 21}
2289+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 58}
22812290 }
22822291 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
22832292 if templ_7745c5c3_Err != nil {
22842293 return templ_7745c5c3_Err
22852294 }
2286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</dd></dl></header>")
2295+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</a></dd><dt>ahead</dt><dd>")
2296+ if templ_7745c5c3_Err != nil {
2297+ return templ_7745c5c3_Err
2298+ }
2299+ var templ_7745c5c3_Var118 string
2300+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
2301+ if templ_7745c5c3_Err != nil {
2302+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 21}
2303+ }
2304+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2305+ if templ_7745c5c3_Err != nil {
2306+ return templ_7745c5c3_Err
2307+ }
2308+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</dd></dl></header>")
22872309 if templ_7745c5c3_Err != nil {
22882310 return templ_7745c5c3_Err
22892311 }
22902312 if p.Note != "" {
2291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"notice\">")
2313+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"notice\">")
22922314 if templ_7745c5c3_Err != nil {
22932315 return templ_7745c5c3_Err
22942316 }
2295- var templ_7745c5c3_Var118 string
2296- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
2317+ var templ_7745c5c3_Var119 string
2318+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
22972319 if templ_7745c5c3_Err != nil {
2298- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 422, Col: 29}
2320+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 29}
22992321 }
2300- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2322+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
23012323 if templ_7745c5c3_Err != nil {
23022324 return templ_7745c5c3_Err
23032325 }
2304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</p>")
2326+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "</p>")
23052327 if templ_7745c5c3_Err != nil {
23062328 return templ_7745c5c3_Err
23072329 }
23082330 }
2309- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, " ")
2331+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, " ")
23102332 if templ_7745c5c3_Err != nil {
23112333 return templ_7745c5c3_Err
23122334 }
23132335 if p.Existing != nil {
2314- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
2336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
23152337 if templ_7745c5c3_Err != nil {
23162338 return templ_7745c5c3_Err
23172339 }
2318- var templ_7745c5c3_Var119 templ.SafeURL
2319- templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
2340+ var templ_7745c5c3_Var120 templ.SafeURL
2341+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
23202342 if templ_7745c5c3_Err != nil {
2321- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 114}
2343+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 114}
23222344 }
2323- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
2345+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
23242346 if templ_7745c5c3_Err != nil {
23252347 return templ_7745c5c3_Err
23262348 }
2327- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "\">")
2349+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\">")
23282350 if templ_7745c5c3_Err != nil {
23292351 return templ_7745c5c3_Err
23302352 }
2331- var templ_7745c5c3_Var120 string
2332- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
2353+ var templ_7745c5c3_Var121 string
2354+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
23332355 if templ_7745c5c3_Err != nil {
2334- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 135}
2356+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 135}
23352357 }
2336- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2358+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
23372359 if templ_7745c5c3_Err != nil {
23382360 return templ_7745c5c3_Err
23392361 }
2340- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</a></p>")
2362+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "</a></p>")
23412363 if templ_7745c5c3_Err != nil {
23422364 return templ_7745c5c3_Err
23432365 }
23442366 }
2345- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, " ")
2367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, " ")
23462368 if templ_7745c5c3_Err != nil {
23472369 return templ_7745c5c3_Err
23482370 }
23492371 if p.Create != nil {
2350- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<form class=\"cform create\" method=\"post\" action=\"")
2372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<form class=\"cform create\" method=\"post\" action=\"")
23512373 if templ_7745c5c3_Err != nil {
23522374 return templ_7745c5c3_Err
23532375 }
2354- var templ_7745c5c3_Var121 templ.SafeURL
2355- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
2376+ var templ_7745c5c3_Var122 templ.SafeURL
2377+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
23562378 if templ_7745c5c3_Err != nil {
2357- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 428, Col: 83}
2379+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 83}
23582380 }
2359- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2381+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
23602382 if templ_7745c5c3_Err != nil {
23612383 return templ_7745c5c3_Err
23622384 }
2363- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\"><input type=\"hidden\" name=\"source\" value=\"")
2385+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\"><input type=\"hidden\" name=\"source\" value=\"")
23642386 if templ_7745c5c3_Err != nil {
23652387 return templ_7745c5c3_Err
23662388 }
2367- var templ_7745c5c3_Var122 string
2368- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
2389+ var templ_7745c5c3_Var123 string
2390+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
23692391 if templ_7745c5c3_Err != nil {
2370- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 62}
2392+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 436, Col: 62}
23712393 }
2372- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122)
2394+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
23732395 if templ_7745c5c3_Err != nil {
23742396 return templ_7745c5c3_Err
23752397 }
2376- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "\"> <input type=\"hidden\" name=\"target\" value=\"")
2398+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "\"> <input type=\"hidden\" name=\"target\" value=\"")
23772399 if templ_7745c5c3_Err != nil {
23782400 return templ_7745c5c3_Err
23792401 }
2380- var templ_7745c5c3_Var123 string
2381- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
2402+ var templ_7745c5c3_Var124 string
2403+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
23822404 if templ_7745c5c3_Err != nil {
2383- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 62}
2405+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 437, Col: 62}
23842406 }
2385- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
2407+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
23862408 if templ_7745c5c3_Err != nil {
23872409 return templ_7745c5c3_Err
23882410 }
2389- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
2411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
23902412 if templ_7745c5c3_Err != nil {
23912413 return templ_7745c5c3_Err
23922414 }
2393- var templ_7745c5c3_Var124 string
2394- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
2415+ var templ_7745c5c3_Var125 string
2416+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
23952417 if templ_7745c5c3_Err != nil {
2396- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 97}
2418+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 439, Col: 97}
23972419 }
2398- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
2420+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var125)
23992421 if templ_7745c5c3_Err != nil {
24002422 return templ_7745c5c3_Err
24012423 }
2402- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")
2424+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")
24032425 if templ_7745c5c3_Err != nil {
24042426 return templ_7745c5c3_Err
24052427 }
24062428 }
2407- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " ")
2429+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, " ")
24082430 if templ_7745c5c3_Err != nil {
24092431 return templ_7745c5c3_Err
24102432 }
24112433 if p.HasDiff {
2412- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">")
2434+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">")
24132435 if templ_7745c5c3_Err != nil {
24142436 return templ_7745c5c3_Err
24152437 }
2416- var templ_7745c5c3_Var125 string
2417- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
2438+ var templ_7745c5c3_Var126 string
2439+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
24182440 if templ_7745c5c3_Err != nil {
2419- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 442, Col: 57}
2441+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 57}
24202442 }
2421- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
2443+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
24222444 if templ_7745c5c3_Err != nil {
24232445 return templ_7745c5c3_Err
24242446 }
2425- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, " · <span class=\"plus\">+")
2447+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " · <span class=\"plus\">+")
24262448 if templ_7745c5c3_Err != nil {
24272449 return templ_7745c5c3_Err
24282450 }
2429- var templ_7745c5c3_Var126 string
2430- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
2451+ var templ_7745c5c3_Var127 string
2452+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
24312453 if templ_7745c5c3_Err != nil {
2432- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 442, Col: 97}
2454+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 97}
24332455 }
2434- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2456+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
24352457 if templ_7745c5c3_Err != nil {
24362458 return templ_7745c5c3_Err
24372459 }
2438- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</span> <span class=\"minus\">-")
2460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "</span> <span class=\"minus\">-")
24392461 if templ_7745c5c3_Err != nil {
24402462 return templ_7745c5c3_Err
24412463 }
2442- var templ_7745c5c3_Var127 string
2443- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
2464+ var templ_7745c5c3_Var128 string
2465+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
24442466 if templ_7745c5c3_Err != nil {
2445- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 442, Col: 142}
2467+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 142}
24462468 }
2447- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2469+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
24482470 if templ_7745c5c3_Err != nil {
24492471 return templ_7745c5c3_Err
24502472 }
2451- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "</span></span> <span class=\"grow\"></span>")
2473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</span></span> <span class=\"grow\"></span>")
24522474 if templ_7745c5c3_Err != nil {
24532475 return templ_7745c5c3_Err
24542476 }
@@ -2464,64 +2486,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
24642486 if templ_7745c5c3_Err != nil {
24652487 return templ_7745c5c3_Err
24662488 }
2467- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "</div>")
2489+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</div>")
24682490 if templ_7745c5c3_Err != nil {
24692491 return templ_7745c5c3_Err
24702492 }
24712493 if p.Invisible {
2472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\">&lt;U+XXXX&gt;</span> markers, never applied.</p>")
2494+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\">&lt;U+XXXX&gt;</span> markers, never applied.</p>")
24732495 if templ_7745c5c3_Err != nil {
24742496 return templ_7745c5c3_Err
24752497 }
24762498 }
2477- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " <div id=\"diffs\"")
2499+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " <div id=\"diffs\"")
24782500 if templ_7745c5c3_Err != nil {
24792501 return templ_7745c5c3_Err
24802502 }
24812503 if p.Both {
2482- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, " data-diff=\"")
2504+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, " data-diff=\"")
24832505 if templ_7745c5c3_Err != nil {
24842506 return templ_7745c5c3_Err
24852507 }
2486- var templ_7745c5c3_Var128 string
2487- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
2508+ var templ_7745c5c3_Var129 string
2509+ templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
24882510 if templ_7745c5c3_Err != nil {
2489- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 457, Col: 23}
2511+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 464, Col: 23}
24902512 }
2491- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128)
2513+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)
24922514 if templ_7745c5c3_Err != nil {
24932515 return templ_7745c5c3_Err
24942516 }
2495- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "\"")
2517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "\"")
24962518 if templ_7745c5c3_Err != nil {
24972519 return templ_7745c5c3_Err
24982520 }
24992521 } else {
2500- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " data-view=\"")
2522+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, " data-view=\"")
25012523 if templ_7745c5c3_Err != nil {
25022524 return templ_7745c5c3_Err
25032525 }
2504- var templ_7745c5c3_Var129 string
2505- templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
2526+ var templ_7745c5c3_Var130 string
2527+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
25062528 if templ_7745c5c3_Err != nil {
2507- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 23}
2529+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 466, Col: 23}
25082530 }
2509- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)
2531+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
25102532 if templ_7745c5c3_Err != nil {
25112533 return templ_7745c5c3_Err
25122534 }
2513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "\"")
2535+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "\"")
25142536 if templ_7745c5c3_Err != nil {
25152537 return templ_7745c5c3_Err
25162538 }
25172539 }
25182540 if p.Both && !p.Pinned {
2519- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
2541+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
25202542 if templ_7745c5c3_Err != nil {
25212543 return templ_7745c5c3_Err
25222544 }
25232545 }
2524- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, ">")
2546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, ">")
25252547 if templ_7745c5c3_Err != nil {
25262548 return templ_7745c5c3_Err
25272549 }
@@ -2531,14 +2553,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
25312553 return templ_7745c5c3_Err
25322554 }
25332555 }
2534- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "</div>")
2556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</div>")
25352557 if templ_7745c5c3_Err != nil {
25362558 return templ_7745c5c3_Err
25372559 }
25382560 }
25392561 return nil
25402562 })
2541- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var110), templ_7745c5c3_Buffer)
2563+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var111), templ_7745c5c3_Buffer)
25422564 if templ_7745c5c3_Err != nil {
25432565 return templ_7745c5c3_Err
25442566 }
@@ -2562,173 +2584,173 @@ func compareRail(p ComparePage) templ.Component {
25622584 }()
25632585 }
25642586 ctx = templ.InitializeContext(ctx)
2565- templ_7745c5c3_Var130 := templ.GetChildren(ctx)
2566- if templ_7745c5c3_Var130 == nil {
2567- templ_7745c5c3_Var130 = templ.NopComponent
2587+ templ_7745c5c3_Var131 := templ.GetChildren(ctx)
2588+ if templ_7745c5c3_Var131 == nil {
2589+ templ_7745c5c3_Var131 = templ.NopComponent
25682590 }
25692591 ctx = templ.ClearChildren(ctx)
2570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
2592+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
25712593 if templ_7745c5c3_Err != nil {
25722594 return templ_7745c5c3_Err
25732595 }
2574- var templ_7745c5c3_Var131 string
2575- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
2596+ var templ_7745c5c3_Var132 string
2597+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
25762598 if templ_7745c5c3_Err != nil {
2577- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 475, Col: 67}
2599+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 67}
25782600 }
2579- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
2601+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
25802602 if templ_7745c5c3_Err != nil {
25812603 return templ_7745c5c3_Err
25822604 }
2583- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</span></h2><ul class=\"pcommits\">")
2605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</span></h2><ul class=\"pcommits\">")
25842606 if templ_7745c5c3_Err != nil {
25852607 return templ_7745c5c3_Err
25862608 }
25872609 for _, c := range p.Ahead {
2588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "<li><a href=\"")
2610+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "<li><a href=\"")
25892611 if templ_7745c5c3_Err != nil {
25902612 return templ_7745c5c3_Err
25912613 }
2592- var templ_7745c5c3_Var132 templ.SafeURL
2593- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
2614+ var templ_7745c5c3_Var133 templ.SafeURL
2615+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
25942616 if templ_7745c5c3_Err != nil {
2595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 37}
2617+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 37}
25962618 }
2597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
2619+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
25982620 if templ_7745c5c3_Err != nil {
25992621 return templ_7745c5c3_Err
26002622 }
2601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "\">")
2623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "\">")
26022624 if templ_7745c5c3_Err != nil {
26032625 return templ_7745c5c3_Err
26042626 }
2605- var templ_7745c5c3_Var133 string
2606- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
2627+ var templ_7745c5c3_Var134 string
2628+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
26072629 if templ_7745c5c3_Err != nil {
2608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 49}
2630+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 49}
26092631 }
2610- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
2632+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
26112633 if templ_7745c5c3_Err != nil {
26122634 return templ_7745c5c3_Err
26132635 }
2614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</a> <span class=\"s\">")
2636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</a> <span class=\"s\">")
26152637 if templ_7745c5c3_Err != nil {
26162638 return templ_7745c5c3_Err
26172639 }
2618- var templ_7745c5c3_Var134 string
2619- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
2640+ var templ_7745c5c3_Var135 string
2641+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
26202642 if templ_7745c5c3_Err != nil {
2621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 32}
2643+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 32}
26222644 }
2623- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
2645+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))
26242646 if templ_7745c5c3_Err != nil {
26252647 return templ_7745c5c3_Err
26262648 }
2627- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "</span> <span class=\"mute\">")
2649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "</span> <span class=\"mute\">")
26282650 if templ_7745c5c3_Err != nil {
26292651 return templ_7745c5c3_Err
26302652 }
2631- var templ_7745c5c3_Var135 string
2632- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
2653+ var templ_7745c5c3_Var136 string
2654+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
26332655 if templ_7745c5c3_Err != nil {
2634- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 16}
2656+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 16}
26352657 }
2636- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))
2658+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
26372659 if templ_7745c5c3_Err != nil {
26382660 return templ_7745c5c3_Err
26392661 }
2640- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, " ")
2662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, " ")
26412663 if templ_7745c5c3_Err != nil {
26422664 return templ_7745c5c3_Err
26432665 }
26442666 if c.WhenAt != "" {
2645- var templ_7745c5c3_Var136 string
2646- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
2667+ var templ_7745c5c3_Var137 string
2668+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
26472669 if templ_7745c5c3_Err != nil {
2648- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 15}
2670+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 15}
26492671 }
2650- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
2672+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
26512673 if templ_7745c5c3_Err != nil {
26522674 return templ_7745c5c3_Err
26532675 }
2654- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "<time datetime=\"")
2676+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<time datetime=\"")
26552677 if templ_7745c5c3_Err != nil {
26562678 return templ_7745c5c3_Err
26572679 }
2658- var templ_7745c5c3_Var137 string
2659- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
2680+ var templ_7745c5c3_Var138 string
2681+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
26602682 if templ_7745c5c3_Err != nil {
2661- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 42}
2683+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 42}
26622684 }
2663- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137)
2685+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
26642686 if templ_7745c5c3_Err != nil {
26652687 return templ_7745c5c3_Err
26662688 }
2667- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "\">")
2689+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "\">")
26682690 if templ_7745c5c3_Err != nil {
26692691 return templ_7745c5c3_Err
26702692 }
2671- var templ_7745c5c3_Var138 string
2672- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
2693+ var templ_7745c5c3_Var139 string
2694+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
26732695 if templ_7745c5c3_Err != nil {
2674- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 53}
2696+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 53}
26752697 }
2676- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
2698+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
26772699 if templ_7745c5c3_Err != nil {
26782700 return templ_7745c5c3_Err
26792701 }
2680- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</time>")
2702+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</time>")
26812703 if templ_7745c5c3_Err != nil {
26822704 return templ_7745c5c3_Err
26832705 }
26842706 }
2685- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</span></li>")
2707+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></li>")
26862708 if templ_7745c5c3_Err != nil {
26872709 return templ_7745c5c3_Err
26882710 }
26892711 }
2690- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</ul>")
2712+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</ul>")
26912713 if templ_7745c5c3_Err != nil {
26922714 return templ_7745c5c3_Err
26932715 }
26942716 if len(p.Ahead) == 0 {
2695- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<p class=\"dim\">no commit ahead of ")
2717+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<p class=\"dim\">no commit ahead of ")
26962718 if templ_7745c5c3_Err != nil {
26972719 return templ_7745c5c3_Err
26982720 }
2699- var templ_7745c5c3_Var139 string
2700- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2721+ var templ_7745c5c3_Var140 string
2722+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
27012723 if templ_7745c5c3_Err != nil {
2702- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 47}
2724+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 498, Col: 47}
27032725 }
2704- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
2726+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
27052727 if templ_7745c5c3_Err != nil {
27062728 return templ_7745c5c3_Err
27072729 }
2708- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, ".</p>")
2730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, ".</p>")
27092731 if templ_7745c5c3_Err != nil {
27102732 return templ_7745c5c3_Err
27112733 }
27122734 }
2713- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</section>")
2735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section>")
27142736 if templ_7745c5c3_Err != nil {
27152737 return templ_7745c5c3_Err
27162738 }
27172739 if p.HasDiff {
2718- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
2740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
27192741 if templ_7745c5c3_Err != nil {
27202742 return templ_7745c5c3_Err
27212743 }
2722- var templ_7745c5c3_Var140 string
2723- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
2744+ var templ_7745c5c3_Var141 string
2745+ templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
27242746 if templ_7745c5c3_Err != nil {
2725- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 66}
2747+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 503, Col: 66}
27262748 }
2727- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2749+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
27282750 if templ_7745c5c3_Err != nil {
27292751 return templ_7745c5c3_Err
27302752 }
2731- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</span></h2>")
2753+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</span></h2>")
27322754 if templ_7745c5c3_Err != nil {
27332755 return templ_7745c5c3_Err
27342756 }
@@ -2736,12 +2758,12 @@ func compareRail(p ComparePage) templ.Component {
27362758 if templ_7745c5c3_Err != nil {
27372759 return templ_7745c5c3_Err
27382760 }
2739- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section>")
2761+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "</section>")
27402762 if templ_7745c5c3_Err != nil {
27412763 return templ_7745c5c3_Err
27422764 }
27432765 }
2744- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
2766+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
27452767 if templ_7745c5c3_Err != nil {
27462768 return templ_7745c5c3_Err
27472769 }
@@ -2749,7 +2771,7 @@ func compareRail(p ComparePage) templ.Component {
27492771 if templ_7745c5c3_Err != nil {
27502772 return templ_7745c5c3_Err
27512773 }
2752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</section>")
2774+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "</section>")
27532775 if templ_7745c5c3_Err != nil {
27542776 return templ_7745c5c3_Err
27552777 }
@@ -1995,152 +1995,170 @@ func mergeBox(p MRPage) templ.Component {
1995 if templ_7745c5c3_Err != nil {1995 if templ_7745c5c3_Err != nil {
1996 return templ_7745c5c3_Err1996 return templ_7745c5c3_Err
1997 }1997 }
1998- if !p.Box.HeadGone {1998+ if p.Box.ReadyAction != "" {
1999- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<form class=\"d-acts\" method=\"post\" action=\"")1999+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<p class=\"d-draft\">this is a draft: it cannot be merged until it is ready for review.</p><form class=\"d-btns\" method=\"post\" action=\"")
2000 if templ_7745c5c3_Err != nil {2000 if templ_7745c5c3_Err != nil {
2001 return templ_7745c5c3_Err2001 return templ_7745c5c3_Err
2002 }2002 }
2003 var templ_7745c5c3_Var101 templ.SafeURL2003 var templ_7745c5c3_Var101 templ.SafeURL
2004- templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))2004+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
2005 if templ_7745c5c3_Err != nil {2005 if templ_7745c5c3_Err != nil {
2006- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 372, Col: 79}2006+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 79}
2007 }2007 }
2008 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))2008 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
2009 if templ_7745c5c3_Err != nil {2009 if templ_7745c5c3_Err != nil {
2010 return templ_7745c5c3_Err2010 return templ_7745c5c3_Err
2011 }2011 }
2012- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"><input type=\"hidden\" name=\"head_id\" value=\"")2012+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"><button type=\"submit\" class=\"btn primary\" title=\"remove the draft prefix from the title\">Ready for review</button></form>")
2013 if templ_7745c5c3_Err != nil {2013 if templ_7745c5c3_Err != nil {
2014 return templ_7745c5c3_Err2014 return templ_7745c5c3_Err
2015 }2015 }
2016- var templ_7745c5c3_Var102 string2016+ } else if !p.Box.HeadGone {
2017- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)2017+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<form class=\"d-acts\" method=\"post\" action=\"")
2018 if templ_7745c5c3_Err != nil {2018 if templ_7745c5c3_Err != nil {
2019- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 58}2019+ return templ_7745c5c3_Err
2020+ }
2021+ var templ_7745c5c3_Var102 templ.SafeURL
2022+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2023+ if templ_7745c5c3_Err != nil {
2024+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 79}
2025+ }
2026+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
2027+ if templ_7745c5c3_Err != nil {
2028+ return templ_7745c5c3_Err
2029+ }
2030+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2031+ if templ_7745c5c3_Err != nil {
2032+ return templ_7745c5c3_Err
2033+ }
2034+ var templ_7745c5c3_Var103 string
2035+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2036+ if templ_7745c5c3_Err != nil {
2037+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 378, Col: 58}
2020 }2038 }
2021- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var102)2039+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var103)
2022 if templ_7745c5c3_Err != nil {2040 if templ_7745c5c3_Err != nil {
2023 return templ_7745c5c3_Err2041 return templ_7745c5c3_Err
2024 }2042 }
2025- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "\"> <label class=\"sr\" for=\"merge-mode\">merge mode</label> <select id=\"merge-mode\" name=\"mode\">")2043+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\"> <label class=\"sr\" for=\"merge-mode\">merge mode</label> <select id=\"merge-mode\" name=\"mode\">")
2026 if templ_7745c5c3_Err != nil {2044 if templ_7745c5c3_Err != nil {
2027 return templ_7745c5c3_Err2045 return templ_7745c5c3_Err
2028 }2046 }
2029 for _, m := range p.Box.Modes {2047 for _, m := range p.Box.Modes {
2030- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<option value=\"")2048+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<option value=\"")
2031 if templ_7745c5c3_Err != nil {2049 if templ_7745c5c3_Err != nil {
2032 return templ_7745c5c3_Err2050 return templ_7745c5c3_Err
2033 }2051 }
2034- var templ_7745c5c3_Var103 string2052+ var templ_7745c5c3_Var104 string
2035- templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)2053+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2036 if templ_7745c5c3_Err != nil {2054 if templ_7745c5c3_Err != nil {
2037- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 29}2055+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 29}
2038 }2056 }
2039- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var103)2057+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var104)
2040 if templ_7745c5c3_Err != nil {2058 if templ_7745c5c3_Err != nil {
2041 return templ_7745c5c3_Err2059 return templ_7745c5c3_Err
2042 }2060 }
2043- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\">")2061+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\">")
2044 if templ_7745c5c3_Err != nil {2062 if templ_7745c5c3_Err != nil {
2045 return templ_7745c5c3_Err2063 return templ_7745c5c3_Err
2046 }2064 }
2047- var templ_7745c5c3_Var104 string2065+ var templ_7745c5c3_Var105 string
2048- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)2066+ templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2049 if templ_7745c5c3_Err != nil {2067 if templ_7745c5c3_Err != nil {
2050- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 41}2068+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 41}
2051 }2069 }
2052- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))2070+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
2053 if templ_7745c5c3_Err != nil {2071 if templ_7745c5c3_Err != nil {
2054 return templ_7745c5c3_Err2072 return templ_7745c5c3_Err
2055 }2073 }
2056- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "</option>")2074+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "</option>")
2057 if templ_7745c5c3_Err != nil {2075 if templ_7745c5c3_Err != nil {
2058 return templ_7745c5c3_Err2076 return templ_7745c5c3_Err
2059 }2077 }
2060 }2078 }
2061- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</select> ")2079+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "</select> ")
2062 if templ_7745c5c3_Err != nil {2080 if templ_7745c5c3_Err != nil {
2063 return templ_7745c5c3_Err2081 return templ_7745c5c3_Err
2064 }2082 }
2065 if p.Box.CanDeleteBranch {2083 if p.Box.CanDeleteBranch {
2066- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")2084+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2067 if templ_7745c5c3_Err != nil {2085 if templ_7745c5c3_Err != nil {
2068 return templ_7745c5c3_Err2086 return templ_7745c5c3_Err
2069 }2087 }
2070- var templ_7745c5c3_Var105 string2088+ var templ_7745c5c3_Var106 string
2071- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)2089+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2072 if templ_7745c5c3_Err != nil {2090 if templ_7745c5c3_Err != nil {
2073- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 381, Col: 101}2091+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 101}
2074 }2092 }
2075- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))2093+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
2076 if templ_7745c5c3_Err != nil {2094 if templ_7745c5c3_Err != nil {
2077 return templ_7745c5c3_Err2095 return templ_7745c5c3_Err
2078 }2096 }
2079- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, " after the merge</label>")2097+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, " after the merge</label>")
2080 if templ_7745c5c3_Err != nil {2098 if templ_7745c5c3_Err != nil {
2081 return templ_7745c5c3_Err2099 return templ_7745c5c3_Err
2082 }2100 }
2083 }2101 }
2084- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\">Merge</button></div></form>")2102+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\">Merge</button></div></form>")
2085 if templ_7745c5c3_Err != nil {2103 if templ_7745c5c3_Err != nil {
2086 return templ_7745c5c3_Err2104 return templ_7745c5c3_Err
2087 }2105 }
2088 }2106 }
2089- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<div class=\"d-btns\">")2107+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "<div class=\"d-btns\">")
2090 if templ_7745c5c3_Err != nil {2108 if templ_7745c5c3_Err != nil {
2091 return templ_7745c5c3_Err2109 return templ_7745c5c3_Err
2092 }2110 }
2093 if p.Box.CanMarkRead {2111 if p.Box.CanMarkRead {
2094- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"")2112+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<form method=\"post\" action=\"")
2095 if templ_7745c5c3_Err != nil {2113 if templ_7745c5c3_Err != nil {
2096 return templ_7745c5c3_Err2114 return templ_7745c5c3_Err
2097 }2115 }
2098- var templ_7745c5c3_Var106 templ.SafeURL2116+ var templ_7745c5c3_Var107 templ.SafeURL
2099- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))2117+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
2100 if templ_7745c5c3_Err != nil {2118 if templ_7745c5c3_Err != nil {
2101- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 390, Col: 64}2119+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64}
2102 }2120 }
2103- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))2121+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
2104 if templ_7745c5c3_Err != nil {2122 if templ_7745c5c3_Err != nil {
2105 return templ_7745c5c3_Err2123 return templ_7745c5c3_Err
2106 }2124 }
2107- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><input type=\"hidden\" name=\"head_id\" value=\"")2125+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2108 if templ_7745c5c3_Err != nil {2126 if templ_7745c5c3_Err != nil {
2109 return templ_7745c5c3_Err2127 return templ_7745c5c3_Err
2110 }2128 }
2111- var templ_7745c5c3_Var107 string2129+ var templ_7745c5c3_Var108 string
2112- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)2130+ templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2113 if templ_7745c5c3_Err != nil {2131 if templ_7745c5c3_Err != nil {
2114- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 391, Col: 59}2132+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 59}
2115 }2133 }
2116- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107)2134+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var108)
2117 if templ_7745c5c3_Err != nil {2135 if templ_7745c5c3_Err != nil {
2118 return templ_7745c5c3_Err2136 return templ_7745c5c3_Err
2119 }2137 }
2120- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\">Mark read</button></form>")2138+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\">Mark read</button></form>")
2121 if templ_7745c5c3_Err != nil {2139 if templ_7745c5c3_Err != nil {
2122 return templ_7745c5c3_Err2140 return templ_7745c5c3_Err
2123 }2141 }
2124 }2142 }
2125- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<form method=\"post\" action=\"")2143+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<form method=\"post\" action=\"")
2126 if templ_7745c5c3_Err != nil {2144 if templ_7745c5c3_Err != nil {
2127 return templ_7745c5c3_Err2145 return templ_7745c5c3_Err
2128 }2146 }
2129- var templ_7745c5c3_Var108 templ.SafeURL2147+ var templ_7745c5c3_Var109 templ.SafeURL
2130- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))2148+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
2131 if templ_7745c5c3_Err != nil {2149 if templ_7745c5c3_Err != nil {
2132- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64}2150+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 64}
2133 }2151 }
2134- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))2152+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
2135 if templ_7745c5c3_Err != nil {2153 if templ_7745c5c3_Err != nil {
2136 return templ_7745c5c3_Err2154 return templ_7745c5c3_Err
2137 }2155 }
2138- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form></div>")2156+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form></div>")
2139 if templ_7745c5c3_Err != nil {2157 if templ_7745c5c3_Err != nil {
2140 return templ_7745c5c3_Err2158 return templ_7745c5c3_Err
2141 }2159 }
2142- if !p.Box.HeadGone {2160+ if !p.Box.HeadGone && p.Box.ReadyAction == "" {
2143- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<p class=\"mute d-or\">or from a terminal</p>")2161+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
2144 if templ_7745c5c3_Err != nil {2162 if templ_7745c5c3_Err != nil {
2145 return templ_7745c5c3_Err2163 return templ_7745c5c3_Err
2146 }2164 }
@@ -2148,8 +2166,12 @@ func mergeBox(p MRPage) templ.Component {
2148 if templ_7745c5c3_Err != nil {2166 if templ_7745c5c3_Err != nil {
2149 return templ_7745c5c3_Err2167 return templ_7745c5c3_Err
2150 }2168 }
2169+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "</details>")
2170+ if templ_7745c5c3_Err != nil {
2171+ return templ_7745c5c3_Err
2172+ }
2151 }2173 }
2152- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</section>")2174+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</section>")
2153 if templ_7745c5c3_Err != nil {2175 if templ_7745c5c3_Err != nil {
2154 return templ_7745c5c3_Err2176 return templ_7745c5c3_Err
2155 }2177 }
@@ -2175,12 +2197,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
2175 }()2197 }()
2176 }2198 }
2177 ctx = templ.InitializeContext(ctx)2199 ctx = templ.InitializeContext(ctx)
2178- templ_7745c5c3_Var109 := templ.GetChildren(ctx)2200+ templ_7745c5c3_Var110 := templ.GetChildren(ctx)
2179- if templ_7745c5c3_Var109 == nil {2201+ if templ_7745c5c3_Var110 == nil {
2180- templ_7745c5c3_Var109 = templ.NopComponent2202+ templ_7745c5c3_Var110 = templ.NopComponent
2181 }2203 }
2182 ctx = templ.ClearChildren(ctx)2204 ctx = templ.ClearChildren(ctx)
2183- templ_7745c5c3_Var110 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {2205+ templ_7745c5c3_Var111 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2184 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context2206 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
2185 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)2207 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
2186 if !templ_7745c5c3_IsBuffer {2208 if !templ_7745c5c3_IsBuffer {
@@ -2192,263 +2214,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
2192 }()2214 }()
2193 }2215 }
2194 ctx = templ.InitializeContext(ctx)2216 ctx = templ.InitializeContext(ctx)
2195- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<header class=\"show mr-h\"><h1>")2217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "<header class=\"show mr-h\"><h1>")
2196- if templ_7745c5c3_Err != nil {
2197- return templ_7745c5c3_Err
2198- }
2199- var templ_7745c5c3_Var111 string
2200- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2201- if templ_7745c5c3_Err != nil {
2202- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 17}
2203- }
2204- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2205- if templ_7745c5c3_Err != nil {
2206- return templ_7745c5c3_Err
2207- }
2208- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, " → ")
2209 if templ_7745c5c3_Err != nil {2218 if templ_7745c5c3_Err != nil {
2210 return templ_7745c5c3_Err2219 return templ_7745c5c3_Err
2211 }2220 }
2212 var templ_7745c5c3_Var112 string2221 var templ_7745c5c3_Var112 string
2213- templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2222+ templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2214 if templ_7745c5c3_Err != nil {2223 if templ_7745c5c3_Err != nil {
2215- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 34}2224+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 17}
2216 }2225 }
2217 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))2226 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
2218 if templ_7745c5c3_Err != nil {2227 if templ_7745c5c3_Err != nil {
2219 return templ_7745c5c3_Err2228 return templ_7745c5c3_Err
2220 }2229 }
2221- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")2230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, " → ")
2222 if templ_7745c5c3_Err != nil {2231 if templ_7745c5c3_Err != nil {
2223 return templ_7745c5c3_Err2232 return templ_7745c5c3_Err
2224 }2233 }
2225- var templ_7745c5c3_Var113 templ.SafeURL2234+ var templ_7745c5c3_Var113 string
2226- templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))2235+ templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2227 if templ_7745c5c3_Err != nil {2236 if templ_7745c5c3_Err != nil {
2228- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 414, Col: 45}2237+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 34}
2229 }2238 }
2230 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))2239 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
2231 if templ_7745c5c3_Err != nil {2240 if templ_7745c5c3_Err != nil {
2232 return templ_7745c5c3_Err2241 return templ_7745c5c3_Err
2233 }2242 }
2234- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\">")2243+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
2235 if templ_7745c5c3_Err != nil {2244 if templ_7745c5c3_Err != nil {
2236 return templ_7745c5c3_Err2245 return templ_7745c5c3_Err
2237 }2246 }
2238- var templ_7745c5c3_Var114 string2247+ var templ_7745c5c3_Var114 templ.SafeURL
2239- templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)2248+ templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
2240 if templ_7745c5c3_Err != nil {2249 if templ_7745c5c3_Err != nil {
2241- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 414, Col: 58}2250+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 45}
2242 }2251 }
2243 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))2252 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))
2244 if templ_7745c5c3_Err != nil {2253 if templ_7745c5c3_Err != nil {
2245 return templ_7745c5c3_Err2254 return templ_7745c5c3_Err
2246 }2255 }
2247- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</a></dd><dt>target</dt><dd><a href=\"")2256+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "\">")
2248 if templ_7745c5c3_Err != nil {2257 if templ_7745c5c3_Err != nil {
2249 return templ_7745c5c3_Err2258 return templ_7745c5c3_Err
2250 }2259 }
2251- var templ_7745c5c3_Var115 templ.SafeURL2260+ var templ_7745c5c3_Var115 string
2252- templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))2261+ templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2253 if templ_7745c5c3_Err != nil {2262 if templ_7745c5c3_Err != nil {
2254- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 45}2263+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 58}
2255 }2264 }
2256 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))2265 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
2257 if templ_7745c5c3_Err != nil {2266 if templ_7745c5c3_Err != nil {
2258 return templ_7745c5c3_Err2267 return templ_7745c5c3_Err
2259 }2268 }
2260- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "\">")2269+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</a></dd><dt>target</dt><dd><a href=\"")
2261 if templ_7745c5c3_Err != nil {2270 if templ_7745c5c3_Err != nil {
2262 return templ_7745c5c3_Err2271 return templ_7745c5c3_Err
2263 }2272 }
2264- var templ_7745c5c3_Var116 string2273+ var templ_7745c5c3_Var116 templ.SafeURL
2265- templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2274+ templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
2266 if templ_7745c5c3_Err != nil {2275 if templ_7745c5c3_Err != nil {
2267- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 58}2276+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 45}
2268 }2277 }
2269 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))2278 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
2270 if templ_7745c5c3_Err != nil {2279 if templ_7745c5c3_Err != nil {
2271 return templ_7745c5c3_Err2280 return templ_7745c5c3_Err
2272 }2281 }
2273- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</a></dd><dt>ahead</dt><dd>")2282+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "\">")
2274 if templ_7745c5c3_Err != nil {2283 if templ_7745c5c3_Err != nil {
2275 return templ_7745c5c3_Err2284 return templ_7745c5c3_Err
2276 }2285 }
2277 var templ_7745c5c3_Var117 string2286 var templ_7745c5c3_Var117 string
2278- templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)2287+ templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2279 if templ_7745c5c3_Err != nil {2288 if templ_7745c5c3_Err != nil {
2280- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 21}2289+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 58}
2281 }2290 }
2282 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))2291 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
2283 if templ_7745c5c3_Err != nil {2292 if templ_7745c5c3_Err != nil {
2284 return templ_7745c5c3_Err2293 return templ_7745c5c3_Err
2285 }2294 }
2286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</dd></dl></header>")2295+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</a></dd><dt>ahead</dt><dd>")
2296+ if templ_7745c5c3_Err != nil {
2297+ return templ_7745c5c3_Err
2298+ }
2299+ var templ_7745c5c3_Var118 string
2300+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
2301+ if templ_7745c5c3_Err != nil {
2302+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 21}
2303+ }
2304+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2305+ if templ_7745c5c3_Err != nil {
2306+ return templ_7745c5c3_Err
2307+ }
2308+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</dd></dl></header>")
2287 if templ_7745c5c3_Err != nil {2309 if templ_7745c5c3_Err != nil {
2288 return templ_7745c5c3_Err2310 return templ_7745c5c3_Err
2289 }2311 }
2290 if p.Note != "" {2312 if p.Note != "" {
2291- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"notice\">")2313+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"notice\">")
2292 if templ_7745c5c3_Err != nil {2314 if templ_7745c5c3_Err != nil {
2293 return templ_7745c5c3_Err2315 return templ_7745c5c3_Err
2294 }2316 }
2295- var templ_7745c5c3_Var118 string2317+ var templ_7745c5c3_Var119 string
2296- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)2318+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
2297 if templ_7745c5c3_Err != nil {2319 if templ_7745c5c3_Err != nil {
2298- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 422, Col: 29}2320+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 29}
2299 }2321 }
2300- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))2322+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
2301 if templ_7745c5c3_Err != nil {2323 if templ_7745c5c3_Err != nil {
2302 return templ_7745c5c3_Err2324 return templ_7745c5c3_Err
2303 }2325 }
2304- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</p>")2326+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "</p>")
2305 if templ_7745c5c3_Err != nil {2327 if templ_7745c5c3_Err != nil {
2306 return templ_7745c5c3_Err2328 return templ_7745c5c3_Err
2307 }2329 }
2308 }2330 }
2309- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, " ")2331+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, " ")
2310 if templ_7745c5c3_Err != nil {2332 if templ_7745c5c3_Err != nil {
2311 return templ_7745c5c3_Err2333 return templ_7745c5c3_Err
2312 }2334 }
2313 if p.Existing != nil {2335 if p.Existing != nil {
2314- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")2336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
2315 if templ_7745c5c3_Err != nil {2337 if templ_7745c5c3_Err != nil {
2316 return templ_7745c5c3_Err2338 return templ_7745c5c3_Err
2317 }2339 }
2318- var templ_7745c5c3_Var119 templ.SafeURL2340+ var templ_7745c5c3_Var120 templ.SafeURL
2319- templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))2341+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
2320 if templ_7745c5c3_Err != nil {2342 if templ_7745c5c3_Err != nil {
2321- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 114}2343+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 114}
2322 }2344 }
2323- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))2345+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2324 if templ_7745c5c3_Err != nil {2346 if templ_7745c5c3_Err != nil {
2325 return templ_7745c5c3_Err2347 return templ_7745c5c3_Err
2326 }2348 }
2327- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "\">")2349+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\">")
2328 if templ_7745c5c3_Err != nil {2350 if templ_7745c5c3_Err != nil {
2329 return templ_7745c5c3_Err2351 return templ_7745c5c3_Err
2330 }2352 }
2331- var templ_7745c5c3_Var120 string2353+ var templ_7745c5c3_Var121 string
2332- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)2354+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
2333 if templ_7745c5c3_Err != nil {2355 if templ_7745c5c3_Err != nil {
2334- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 135}2356+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 135}
2335 }2357 }
2336- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))2358+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2337 if templ_7745c5c3_Err != nil {2359 if templ_7745c5c3_Err != nil {
2338 return templ_7745c5c3_Err2360 return templ_7745c5c3_Err
2339 }2361 }
2340- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</a></p>")2362+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "</a></p>")
2341 if templ_7745c5c3_Err != nil {2363 if templ_7745c5c3_Err != nil {
2342 return templ_7745c5c3_Err2364 return templ_7745c5c3_Err
2343 }2365 }
2344 }2366 }
2345- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, " ")2367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, " ")
2346 if templ_7745c5c3_Err != nil {2368 if templ_7745c5c3_Err != nil {
2347 return templ_7745c5c3_Err2369 return templ_7745c5c3_Err
2348 }2370 }
2349 if p.Create != nil {2371 if p.Create != nil {
2350- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<form class=\"cform create\" method=\"post\" action=\"")2372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<form class=\"cform create\" method=\"post\" action=\"")
2351 if templ_7745c5c3_Err != nil {2373 if templ_7745c5c3_Err != nil {
2352 return templ_7745c5c3_Err2374 return templ_7745c5c3_Err
2353 }2375 }
2354- var templ_7745c5c3_Var121 templ.SafeURL2376+ var templ_7745c5c3_Var122 templ.SafeURL
2355- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))2377+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
2356 if templ_7745c5c3_Err != nil {2378 if templ_7745c5c3_Err != nil {
2357- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 428, Col: 83}2379+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 83}
2358 }2380 }
2359- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))2381+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2360 if templ_7745c5c3_Err != nil {2382 if templ_7745c5c3_Err != nil {
2361 return templ_7745c5c3_Err2383 return templ_7745c5c3_Err
2362 }2384 }
2363- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\"><input type=\"hidden\" name=\"source\" value=\"")2385+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\"><input type=\"hidden\" name=\"source\" value=\"")
2364 if templ_7745c5c3_Err != nil {2386 if templ_7745c5c3_Err != nil {
2365 return templ_7745c5c3_Err2387 return templ_7745c5c3_Err
2366 }2388 }
2367- var templ_7745c5c3_Var122 string2389+ var templ_7745c5c3_Var123 string
2368- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)2390+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
2369 if templ_7745c5c3_Err != nil {2391 if templ_7745c5c3_Err != nil {
2370- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 62}2392+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 436, Col: 62}
2371 }2393 }
2372- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122)2394+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
2373 if templ_7745c5c3_Err != nil {2395 if templ_7745c5c3_Err != nil {
2374 return templ_7745c5c3_Err2396 return templ_7745c5c3_Err
2375 }2397 }
2376- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "\"> <input type=\"hidden\" name=\"target\" value=\"")2398+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "\"> <input type=\"hidden\" name=\"target\" value=\"")
2377 if templ_7745c5c3_Err != nil {2399 if templ_7745c5c3_Err != nil {
2378 return templ_7745c5c3_Err2400 return templ_7745c5c3_Err
2379 }2401 }
2380- var templ_7745c5c3_Var123 string2402+ var templ_7745c5c3_Var124 string
2381- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)2403+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
2382 if templ_7745c5c3_Err != nil {2404 if templ_7745c5c3_Err != nil {
2383- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 62}2405+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 437, Col: 62}
2384 }2406 }
2385- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)2407+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
2386 if templ_7745c5c3_Err != nil {2408 if templ_7745c5c3_Err != nil {
2387 return templ_7745c5c3_Err2409 return templ_7745c5c3_Err
2388 }2410 }
2389- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")2411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
2390 if templ_7745c5c3_Err != nil {2412 if templ_7745c5c3_Err != nil {
2391 return templ_7745c5c3_Err2413 return templ_7745c5c3_Err
2392 }2414 }
2393- var templ_7745c5c3_Var124 string2415+ var templ_7745c5c3_Var125 string
2394- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)2416+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
2395 if templ_7745c5c3_Err != nil {2417 if templ_7745c5c3_Err != nil {
2396- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 97}2418+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 439, Col: 97}
2397 }2419 }
2398- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)2420+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var125)
2399 if templ_7745c5c3_Err != nil {2421 if templ_7745c5c3_Err != nil {
2400 return templ_7745c5c3_Err2422 return templ_7745c5c3_Err
2401 }2423 }
2402- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")2424+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")
2403 if templ_7745c5c3_Err != nil {2425 if templ_7745c5c3_Err != nil {
2404 return templ_7745c5c3_Err2426 return templ_7745c5c3_Err
2405 }2427 }
2406 }2428 }
2407- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " ")2429+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, " ")
2408 if templ_7745c5c3_Err != nil {2430 if templ_7745c5c3_Err != nil {
2409 return templ_7745c5c3_Err2431 return templ_7745c5c3_Err
2410 }2432 }
2411 if p.HasDiff {2433 if p.HasDiff {
2412- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">")2434+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">")
2413 if templ_7745c5c3_Err != nil {2435 if templ_7745c5c3_Err != nil {
2414 return templ_7745c5c3_Err2436 return templ_7745c5c3_Err
2415 }2437 }
2416- var templ_7745c5c3_Var125 string2438+ var templ_7745c5c3_Var126 string
2417- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))2439+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
2418 if templ_7745c5c3_Err != nil {2440 if templ_7745c5c3_Err != nil {
2419- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 442, Col: 57}2441+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 57}
2420 }2442 }
2421- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))2443+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2422 if templ_7745c5c3_Err != nil {2444 if templ_7745c5c3_Err != nil {
2423 return templ_7745c5c3_Err2445 return templ_7745c5c3_Err
2424 }2446 }
2425- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, " · <span class=\"plus\">+")2447+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " · <span class=\"plus\">+")
2426 if templ_7745c5c3_Err != nil {2448 if templ_7745c5c3_Err != nil {
2427 return templ_7745c5c3_Err2449 return templ_7745c5c3_Err
2428 }2450 }
2429- var templ_7745c5c3_Var126 string2451+ var templ_7745c5c3_Var127 string
2430- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))2452+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
2431 if templ_7745c5c3_Err != nil {2453 if templ_7745c5c3_Err != nil {
2432- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 442, Col: 97}2454+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 97}
2433 }2455 }
2434- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))2456+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2435 if templ_7745c5c3_Err != nil {2457 if templ_7745c5c3_Err != nil {
2436 return templ_7745c5c3_Err2458 return templ_7745c5c3_Err
2437 }2459 }
2438- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</span> <span class=\"minus\">-")2460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "</span> <span class=\"minus\">-")
2439 if templ_7745c5c3_Err != nil {2461 if templ_7745c5c3_Err != nil {
2440 return templ_7745c5c3_Err2462 return templ_7745c5c3_Err
2441 }2463 }
2442- var templ_7745c5c3_Var127 string2464+ var templ_7745c5c3_Var128 string
2443- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))2465+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
2444 if templ_7745c5c3_Err != nil {2466 if templ_7745c5c3_Err != nil {
2445- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 442, Col: 142}2467+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 142}
2446 }2468 }
2447- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))2469+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2448 if templ_7745c5c3_Err != nil {2470 if templ_7745c5c3_Err != nil {
2449 return templ_7745c5c3_Err2471 return templ_7745c5c3_Err
2450 }2472 }
2451- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "</span></span> <span class=\"grow\"></span>")2473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</span></span> <span class=\"grow\"></span>")
2452 if templ_7745c5c3_Err != nil {2474 if templ_7745c5c3_Err != nil {
2453 return templ_7745c5c3_Err2475 return templ_7745c5c3_Err
2454 }2476 }
@@ -2464,64 +2486,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
2464 if templ_7745c5c3_Err != nil {2486 if templ_7745c5c3_Err != nil {
2465 return templ_7745c5c3_Err2487 return templ_7745c5c3_Err
2466 }2488 }
2467- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "</div>")2489+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</div>")
2468 if templ_7745c5c3_Err != nil {2490 if templ_7745c5c3_Err != nil {
2469 return templ_7745c5c3_Err2491 return templ_7745c5c3_Err
2470 }2492 }
2471 if p.Invisible {2493 if p.Invisible {
2472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\">&lt;U+XXXX&gt;</span> markers, never applied.</p>")2494+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\">&lt;U+XXXX&gt;</span> markers, never applied.</p>")
2473 if templ_7745c5c3_Err != nil {2495 if templ_7745c5c3_Err != nil {
2474 return templ_7745c5c3_Err2496 return templ_7745c5c3_Err
2475 }2497 }
2476 }2498 }
2477- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " <div id=\"diffs\"")2499+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " <div id=\"diffs\"")
2478 if templ_7745c5c3_Err != nil {2500 if templ_7745c5c3_Err != nil {
2479 return templ_7745c5c3_Err2501 return templ_7745c5c3_Err
2480 }2502 }
2481 if p.Both {2503 if p.Both {
2482- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, " data-diff=\"")2504+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, " data-diff=\"")
2483 if templ_7745c5c3_Err != nil {2505 if templ_7745c5c3_Err != nil {
2484 return templ_7745c5c3_Err2506 return templ_7745c5c3_Err
2485 }2507 }
2486- var templ_7745c5c3_Var128 string2508+ var templ_7745c5c3_Var129 string
2487- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)2509+ templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
2488 if templ_7745c5c3_Err != nil {2510 if templ_7745c5c3_Err != nil {
2489- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 457, Col: 23}2511+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 464, Col: 23}
2490 }2512 }
2491- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128)2513+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)
2492 if templ_7745c5c3_Err != nil {2514 if templ_7745c5c3_Err != nil {
2493 return templ_7745c5c3_Err2515 return templ_7745c5c3_Err
2494 }2516 }
2495- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "\"")2517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "\"")
2496 if templ_7745c5c3_Err != nil {2518 if templ_7745c5c3_Err != nil {
2497 return templ_7745c5c3_Err2519 return templ_7745c5c3_Err
2498 }2520 }
2499 } else {2521 } else {
2500- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " data-view=\"")2522+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, " data-view=\"")
2501 if templ_7745c5c3_Err != nil {2523 if templ_7745c5c3_Err != nil {
2502 return templ_7745c5c3_Err2524 return templ_7745c5c3_Err
2503 }2525 }
2504- var templ_7745c5c3_Var129 string2526+ var templ_7745c5c3_Var130 string
2505- templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)2527+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
2506 if templ_7745c5c3_Err != nil {2528 if templ_7745c5c3_Err != nil {
2507- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 23}2529+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 466, Col: 23}
2508 }2530 }
2509- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)2531+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
2510 if templ_7745c5c3_Err != nil {2532 if templ_7745c5c3_Err != nil {
2511 return templ_7745c5c3_Err2533 return templ_7745c5c3_Err
2512 }2534 }
2513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "\"")2535+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "\"")
2514 if templ_7745c5c3_Err != nil {2536 if templ_7745c5c3_Err != nil {
2515 return templ_7745c5c3_Err2537 return templ_7745c5c3_Err
2516 }2538 }
2517 }2539 }
2518 if p.Both && !p.Pinned {2540 if p.Both && !p.Pinned {
2519- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")2541+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
2520 if templ_7745c5c3_Err != nil {2542 if templ_7745c5c3_Err != nil {
2521 return templ_7745c5c3_Err2543 return templ_7745c5c3_Err
2522 }2544 }
2523 }2545 }
2524- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, ">")2546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, ">")
2525 if templ_7745c5c3_Err != nil {2547 if templ_7745c5c3_Err != nil {
2526 return templ_7745c5c3_Err2548 return templ_7745c5c3_Err
2527 }2549 }
@@ -2531,14 +2553,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
2531 return templ_7745c5c3_Err2553 return templ_7745c5c3_Err
2532 }2554 }
2533 }2555 }
2534- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "</div>")2556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</div>")
2535 if templ_7745c5c3_Err != nil {2557 if templ_7745c5c3_Err != nil {
2536 return templ_7745c5c3_Err2558 return templ_7745c5c3_Err
2537 }2559 }
2538 }2560 }
2539 return nil2561 return nil
2540 })2562 })
2541- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var110), templ_7745c5c3_Buffer)2563+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var111), templ_7745c5c3_Buffer)
2542 if templ_7745c5c3_Err != nil {2564 if templ_7745c5c3_Err != nil {
2543 return templ_7745c5c3_Err2565 return templ_7745c5c3_Err
2544 }2566 }
@@ -2562,173 +2584,173 @@ func compareRail(p ComparePage) templ.Component {
2562 }()2584 }()
2563 }2585 }
2564 ctx = templ.InitializeContext(ctx)2586 ctx = templ.InitializeContext(ctx)
2565- templ_7745c5c3_Var130 := templ.GetChildren(ctx)2587+ templ_7745c5c3_Var131 := templ.GetChildren(ctx)
2566- if templ_7745c5c3_Var130 == nil {2588+ if templ_7745c5c3_Var131 == nil {
2567- templ_7745c5c3_Var130 = templ.NopComponent2589+ templ_7745c5c3_Var131 = templ.NopComponent
2568 }2590 }
2569 ctx = templ.ClearChildren(ctx)2591 ctx = templ.ClearChildren(ctx)
2570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")2592+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
2571 if templ_7745c5c3_Err != nil {2593 if templ_7745c5c3_Err != nil {
2572 return templ_7745c5c3_Err2594 return templ_7745c5c3_Err
2573 }2595 }
2574- var templ_7745c5c3_Var131 string2596+ var templ_7745c5c3_Var132 string
2575- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))2597+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
2576 if templ_7745c5c3_Err != nil {2598 if templ_7745c5c3_Err != nil {
2577- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 475, Col: 67}2599+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 67}
2578 }2600 }
2579- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))2601+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
2580 if templ_7745c5c3_Err != nil {2602 if templ_7745c5c3_Err != nil {
2581 return templ_7745c5c3_Err2603 return templ_7745c5c3_Err
2582 }2604 }
2583- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</span></h2><ul class=\"pcommits\">")2605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</span></h2><ul class=\"pcommits\">")
2584 if templ_7745c5c3_Err != nil {2606 if templ_7745c5c3_Err != nil {
2585 return templ_7745c5c3_Err2607 return templ_7745c5c3_Err
2586 }2608 }
2587 for _, c := range p.Ahead {2609 for _, c := range p.Ahead {
2588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "<li><a href=\"")2610+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "<li><a href=\"")
2589 if templ_7745c5c3_Err != nil {2611 if templ_7745c5c3_Err != nil {
2590 return templ_7745c5c3_Err2612 return templ_7745c5c3_Err
2591 }2613 }
2592- var templ_7745c5c3_Var132 templ.SafeURL2614+ var templ_7745c5c3_Var133 templ.SafeURL
2593- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))2615+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
2594 if templ_7745c5c3_Err != nil {2616 if templ_7745c5c3_Err != nil {
2595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 37}2617+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 37}
2596 }2618 }
2597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))2619+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
2598 if templ_7745c5c3_Err != nil {2620 if templ_7745c5c3_Err != nil {
2599 return templ_7745c5c3_Err2621 return templ_7745c5c3_Err
2600 }2622 }
2601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "\">")2623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "\">")
2602 if templ_7745c5c3_Err != nil {2624 if templ_7745c5c3_Err != nil {
2603 return templ_7745c5c3_Err2625 return templ_7745c5c3_Err
2604 }2626 }
2605- var templ_7745c5c3_Var133 string2627+ var templ_7745c5c3_Var134 string
2606- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)2628+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
2607 if templ_7745c5c3_Err != nil {2629 if templ_7745c5c3_Err != nil {
2608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 49}2630+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 49}
2609 }2631 }
2610- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))2632+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
2611 if templ_7745c5c3_Err != nil {2633 if templ_7745c5c3_Err != nil {
2612 return templ_7745c5c3_Err2634 return templ_7745c5c3_Err
2613 }2635 }
2614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</a> <span class=\"s\">")2636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</a> <span class=\"s\">")
2615 if templ_7745c5c3_Err != nil {2637 if templ_7745c5c3_Err != nil {
2616 return templ_7745c5c3_Err2638 return templ_7745c5c3_Err
2617 }2639 }
2618- var templ_7745c5c3_Var134 string2640+ var templ_7745c5c3_Var135 string
2619- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)2641+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
2620 if templ_7745c5c3_Err != nil {2642 if templ_7745c5c3_Err != nil {
2621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 32}2643+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 32}
2622 }2644 }
2623- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))2645+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))
2624 if templ_7745c5c3_Err != nil {2646 if templ_7745c5c3_Err != nil {
2625 return templ_7745c5c3_Err2647 return templ_7745c5c3_Err
2626 }2648 }
2627- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "</span> <span class=\"mute\">")2649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "</span> <span class=\"mute\">")
2628 if templ_7745c5c3_Err != nil {2650 if templ_7745c5c3_Err != nil {
2629 return templ_7745c5c3_Err2651 return templ_7745c5c3_Err
2630 }2652 }
2631- var templ_7745c5c3_Var135 string2653+ var templ_7745c5c3_Var136 string
2632- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)2654+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
2633 if templ_7745c5c3_Err != nil {2655 if templ_7745c5c3_Err != nil {
2634- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 16}2656+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 16}
2635 }2657 }
2636- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))2658+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
2637 if templ_7745c5c3_Err != nil {2659 if templ_7745c5c3_Err != nil {
2638 return templ_7745c5c3_Err2660 return templ_7745c5c3_Err
2639 }2661 }
2640- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, " ")2662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, " ")
2641 if templ_7745c5c3_Err != nil {2663 if templ_7745c5c3_Err != nil {
2642 return templ_7745c5c3_Err2664 return templ_7745c5c3_Err
2643 }2665 }
2644 if c.WhenAt != "" {2666 if c.WhenAt != "" {
2645- var templ_7745c5c3_Var136 string2667+ var templ_7745c5c3_Var137 string
2646- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")2668+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
2647 if templ_7745c5c3_Err != nil {2669 if templ_7745c5c3_Err != nil {
2648- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 15}2670+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 15}
2649 }2671 }
2650- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))2672+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
2651 if templ_7745c5c3_Err != nil {2673 if templ_7745c5c3_Err != nil {
2652 return templ_7745c5c3_Err2674 return templ_7745c5c3_Err
2653 }2675 }
2654- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "<time datetime=\"")2676+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<time datetime=\"")
2655 if templ_7745c5c3_Err != nil {2677 if templ_7745c5c3_Err != nil {
2656 return templ_7745c5c3_Err2678 return templ_7745c5c3_Err
2657 }2679 }
2658- var templ_7745c5c3_Var137 string2680+ var templ_7745c5c3_Var138 string
2659- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)2681+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
2660 if templ_7745c5c3_Err != nil {2682 if templ_7745c5c3_Err != nil {
2661- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 42}2683+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 42}
2662 }2684 }
2663- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137)2685+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2664 if templ_7745c5c3_Err != nil {2686 if templ_7745c5c3_Err != nil {
2665 return templ_7745c5c3_Err2687 return templ_7745c5c3_Err
2666 }2688 }
2667- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "\">")2689+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "\">")
2668 if templ_7745c5c3_Err != nil {2690 if templ_7745c5c3_Err != nil {
2669 return templ_7745c5c3_Err2691 return templ_7745c5c3_Err
2670 }2692 }
2671- var templ_7745c5c3_Var138 string2693+ var templ_7745c5c3_Var139 string
2672- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)2694+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
2673 if templ_7745c5c3_Err != nil {2695 if templ_7745c5c3_Err != nil {
2674- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 53}2696+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 53}
2675 }2697 }
2676- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))2698+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
2677 if templ_7745c5c3_Err != nil {2699 if templ_7745c5c3_Err != nil {
2678 return templ_7745c5c3_Err2700 return templ_7745c5c3_Err
2679 }2701 }
2680- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</time>")2702+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</time>")
2681 if templ_7745c5c3_Err != nil {2703 if templ_7745c5c3_Err != nil {
2682 return templ_7745c5c3_Err2704 return templ_7745c5c3_Err
2683 }2705 }
2684 }2706 }
2685- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</span></li>")2707+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></li>")
2686 if templ_7745c5c3_Err != nil {2708 if templ_7745c5c3_Err != nil {
2687 return templ_7745c5c3_Err2709 return templ_7745c5c3_Err
2688 }2710 }
2689 }2711 }
2690- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</ul>")2712+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</ul>")
2691 if templ_7745c5c3_Err != nil {2713 if templ_7745c5c3_Err != nil {
2692 return templ_7745c5c3_Err2714 return templ_7745c5c3_Err
2693 }2715 }
2694 if len(p.Ahead) == 0 {2716 if len(p.Ahead) == 0 {
2695- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<p class=\"dim\">no commit ahead of ")2717+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<p class=\"dim\">no commit ahead of ")
2696 if templ_7745c5c3_Err != nil {2718 if templ_7745c5c3_Err != nil {
2697 return templ_7745c5c3_Err2719 return templ_7745c5c3_Err
2698 }2720 }
2699- var templ_7745c5c3_Var139 string2721+ var templ_7745c5c3_Var140 string
2700- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2722+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2701 if templ_7745c5c3_Err != nil {2723 if templ_7745c5c3_Err != nil {
2702- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 47}2724+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 498, Col: 47}
2703 }2725 }
2704- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))2726+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2705 if templ_7745c5c3_Err != nil {2727 if templ_7745c5c3_Err != nil {
2706 return templ_7745c5c3_Err2728 return templ_7745c5c3_Err
2707 }2729 }
2708- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, ".</p>")2730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, ".</p>")
2709 if templ_7745c5c3_Err != nil {2731 if templ_7745c5c3_Err != nil {
2710 return templ_7745c5c3_Err2732 return templ_7745c5c3_Err
2711 }2733 }
2712 }2734 }
2713- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</section>")2735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section>")
2714 if templ_7745c5c3_Err != nil {2736 if templ_7745c5c3_Err != nil {
2715 return templ_7745c5c3_Err2737 return templ_7745c5c3_Err
2716 }2738 }
2717 if p.HasDiff {2739 if p.HasDiff {
2718- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")2740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
2719 if templ_7745c5c3_Err != nil {2741 if templ_7745c5c3_Err != nil {
2720 return templ_7745c5c3_Err2742 return templ_7745c5c3_Err
2721 }2743 }
2722- var templ_7745c5c3_Var140 string2744+ var templ_7745c5c3_Var141 string
2723- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))2745+ templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
2724 if templ_7745c5c3_Err != nil {2746 if templ_7745c5c3_Err != nil {
2725- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 66}2747+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 503, Col: 66}
2726 }2748 }
2727- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))2749+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
2728 if templ_7745c5c3_Err != nil {2750 if templ_7745c5c3_Err != nil {
2729 return templ_7745c5c3_Err2751 return templ_7745c5c3_Err
2730 }2752 }
2731- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</span></h2>")2753+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</span></h2>")
2732 if templ_7745c5c3_Err != nil {2754 if templ_7745c5c3_Err != nil {
2733 return templ_7745c5c3_Err2755 return templ_7745c5c3_Err
2734 }2756 }
@@ -2736,12 +2758,12 @@ func compareRail(p ComparePage) templ.Component {
2736 if templ_7745c5c3_Err != nil {2758 if templ_7745c5c3_Err != nil {
2737 return templ_7745c5c3_Err2759 return templ_7745c5c3_Err
2738 }2760 }
2739- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section>")2761+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "</section>")
2740 if templ_7745c5c3_Err != nil {2762 if templ_7745c5c3_Err != nil {
2741 return templ_7745c5c3_Err2763 return templ_7745c5c3_Err
2742 }2764 }
2743 }2765 }
2744- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")2766+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
2745 if templ_7745c5c3_Err != nil {2767 if templ_7745c5c3_Err != nil {
2746 return templ_7745c5c3_Err2768 return templ_7745c5c3_Err
2747 }2769 }
@@ -2749,7 +2771,7 @@ func compareRail(p ComparePage) templ.Component {
2749 if templ_7745c5c3_Err != nil {2771 if templ_7745c5c3_Err != nil {
2750 return templ_7745c5c3_Err2772 return templ_7745c5c3_Err
2751 }2773 }
2752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</section>")2774+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "</section>")
2753 if templ_7745c5c3_Err != nil {2775 if templ_7745c5c3_Err != nil {
2754 return templ_7745c5c3_Err2776 return templ_7745c5c3_Err
2755 }2777 }
M internal/adapters/web/repo.go +27 -7
@@ -35,18 +35,26 @@ type repoData struct {
3535 // the Git state of its summary. A missing or private repository is
3636 // domain.ErrNotFound.
3737 func (s *Server) repoView(ctx context.Context, name domain.RepoName) (ports.RepoView, error) {
38- repo, err := s.cfg.Repos.Get(ctx, principal(ctx), name)
38+ repo, err := s.repoRecord(ctx, name)
3939 if err != nil {
4040 return ports.RepoView{}, err
4141 }
42- view, err := s.cfg.Repos.View(ctx, repo)
42+ return s.cfg.Repos.View(ctx, repo)
43+}
44+
45+// repoRecord reads the repository record only, without Git: its name,
46+// visibility and settings (the defaults when it has none).
47+//
48+//hexcheck:ignore suspicious-business-logic-in-adapter reads the repository from the use case; filling in the default settings for display decides no rule
49+func (s *Server) repoRecord(ctx context.Context, name domain.RepoName) (domain.Repo, error) {
50+ repo, err := s.cfg.Repos.Get(ctx, principal(ctx), name)
4351 if err != nil {
44- return ports.RepoView{}, err
52+ return domain.Repo{}, err
4553 }
46- if view.Settings.IsZero() {
47- view.Settings = domain.DefaultRepoSettings()
54+ if repo.Settings.IsZero() {
55+ repo.Settings = domain.DefaultRepoSettings()
4856 }
49- return view, nil
57+ return repo, nil
5058 }
5159
5260 // loadRepo reads a repository and its refs. A missing, private or invalidly
@@ -110,6 +118,16 @@ func (d *repoData) allRefs() []domain.Ref {
110118 // name is the repository name.
111119 func (d *repoData) name() string { return d.repo.Name.String() }
112120
121+// openMRCount is the number of open merge requests shown on the tab, empty
122+// when there are none or they cannot be counted (the tab still works).
123+func (s *Server) openMRCount(ctx context.Context, name domain.RepoName) string {
124+ n, err := s.cfg.MergeRequests.CountOpen(ctx, principal(ctx), name)
125+ if err != nil || n == 0 {
126+ return ""
127+ }
128+ return itoa(n)
129+}
130+
113131 // layout is the Layout of the pages of a repository: the sections, the ref
114132 // switcher on the head ref and the ":" targets. The caller sets the title,
115133 // the crumbs, the status and the rail.
@@ -133,7 +151,9 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La
133151 mk("r", "Branches & tags", repoHref(d.name(), "refs"), "refs"),
134152 }
135153 if s.cfg.MergeRequests != nil {
136- l.Sections = append(l.Sections, mk("m", "Merge requests", mrBase(d.name()), "merge-requests"))
154+ mrs := mk("m", "Merge requests", mrBase(d.name()), "merge-requests")
155+ mrs.Count = s.openMRCount(ctx, d.repo.Name)
156+ l.Sections = append(l.Sections, mrs)
137157 }
138158 l.Sections = s.withActions(l.Sections, d.name(), section)
139159 l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}}
@@ -35,18 +35,26 @@ type repoData struct {
35 // the Git state of its summary. A missing or private repository is35 // the Git state of its summary. A missing or private repository is
36 // domain.ErrNotFound.36 // domain.ErrNotFound.
37 func (s *Server) repoView(ctx context.Context, name domain.RepoName) (ports.RepoView, error) {37 func (s *Server) repoView(ctx context.Context, name domain.RepoName) (ports.RepoView, error) {
38- repo, err := s.cfg.Repos.Get(ctx, principal(ctx), name)38+ repo, err := s.repoRecord(ctx, name)
39 if err != nil {39 if err != nil {
40 return ports.RepoView{}, err40 return ports.RepoView{}, err
41 }41 }
42- view, err := s.cfg.Repos.View(ctx, repo)42+ return s.cfg.Repos.View(ctx, repo)
43+}
44+
45+// repoRecord reads the repository record only, without Git: its name,
46+// visibility and settings (the defaults when it has none).
47+//
48+//hexcheck:ignore suspicious-business-logic-in-adapter reads the repository from the use case; filling in the default settings for display decides no rule
49+func (s *Server) repoRecord(ctx context.Context, name domain.RepoName) (domain.Repo, error) {
50+ repo, err := s.cfg.Repos.Get(ctx, principal(ctx), name)
43 if err != nil {51 if err != nil {
44- return ports.RepoView{}, err52+ return domain.Repo{}, err
45 }53 }
46- if view.Settings.IsZero() {54+ if repo.Settings.IsZero() {
47- view.Settings = domain.DefaultRepoSettings()55+ repo.Settings = domain.DefaultRepoSettings()
48 }56 }
49- return view, nil57+ return repo, nil
50 }58 }
51 59
52 // loadRepo reads a repository and its refs. A missing, private or invalidly60 // loadRepo reads a repository and its refs. A missing, private or invalidly
@@ -110,6 +118,16 @@ func (d *repoData) allRefs() []domain.Ref {
110 // name is the repository name.118 // name is the repository name.
111 func (d *repoData) name() string { return d.repo.Name.String() }119 func (d *repoData) name() string { return d.repo.Name.String() }
112 120
121+// openMRCount is the number of open merge requests shown on the tab, empty
122+// when there are none or they cannot be counted (the tab still works).
123+func (s *Server) openMRCount(ctx context.Context, name domain.RepoName) string {
124+ n, err := s.cfg.MergeRequests.CountOpen(ctx, principal(ctx), name)
125+ if err != nil || n == 0 {
126+ return ""
127+ }
128+ return itoa(n)
129+}
130+
113 // layout is the Layout of the pages of a repository: the sections, the ref131 // layout is the Layout of the pages of a repository: the sections, the ref
114 // switcher on the head ref and the ":" targets. The caller sets the title,132 // switcher on the head ref and the ":" targets. The caller sets the title,
115 // the crumbs, the status and the rail.133 // the crumbs, the status and the rail.
@@ -133,7 +151,9 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La
133 mk("r", "Branches & tags", repoHref(d.name(), "refs"), "refs"),151 mk("r", "Branches & tags", repoHref(d.name(), "refs"), "refs"),
134 }152 }
135 if s.cfg.MergeRequests != nil {153 if s.cfg.MergeRequests != nil {
136- l.Sections = append(l.Sections, mk("m", "Merge requests", mrBase(d.name()), "merge-requests"))154+ mrs := mk("m", "Merge requests", mrBase(d.name()), "merge-requests")
155+ mrs.Count = s.openMRCount(ctx, d.repo.Name)
156+ l.Sections = append(l.Sections, mrs)
137 }157 }
138 l.Sections = s.withActions(l.Sections, d.name(), section)158 l.Sections = s.withActions(l.Sections, d.name(), section)
139 l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}}159 l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}}
M internal/adapters/web/shell.templ +7 -1
@@ -150,7 +150,13 @@ templ Header(l Layout) {
150150 if s.Current {
151151 aria-current="page"
152152 }
153- >{ s.Label }<kbd aria-hidden="true">{ s.Key }</kbd></a>
153+ >
154+ { s.Label }
155+ if s.Count != "" {
156+ <span class="count">{ s.Count }<span class="sr">{ " open" }</span></span>
157+ }
158+ <kbd aria-hidden="true">{ s.Key }</kbd>
159+ </a>
154160 </li>
155161 }
156162 </ul>
@@ -150,7 +150,13 @@ templ Header(l Layout) {
150 if s.Current {150 if s.Current {
151 aria-current="page"151 aria-current="page"
152 }152 }
153- >{ s.Label }<kbd aria-hidden="true">{ s.Key }</kbd></a>153+ >
154+ { s.Label }
155+ if s.Count != "" {
156+ <span class="count">{ s.Count }<span class="sr">{ " open" }</span></span>
157+ }
158+ <kbd aria-hidden="true">{ s.Key }</kbd>
159+ </a>
154 </li>160 </li>
155 }161 }
156 </ul>162 </ul>
M internal/adapters/web/shell_templ.go +64 -28 generated file: collapsed
M internal/adapters/web/static/css/60-mr.css +21 -0
@@ -5,6 +5,7 @@
55 .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); }
66 .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); }
77 .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); }
8+ .mr-state[data-state="draft"]::before { content: "◌ "; color: var(--ink-dim); }
89 .mr-desc { max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); }
910 .mr-desc a, .c-text a { text-decoration: underline; }
1011
@@ -29,11 +30,31 @@
2930 & .d-check input { accent-color: var(--ink-hi); margin: 0; }
3031 & .d-btns { display: flex; gap: var(--s2); margin-top: var(--s3); flex-wrap: wrap; }
3132 & .d-btns .primary { flex: 1; justify-content: center; }
33+ /* the terminal lines fold away: a reviewer who merges in the page does
34+ not need them in sight */
35+ & .d-draft { font-size: var(--fs-s); color: var(--ink-dim); }
3236 & .d-or { font-size: var(--fs-s); }
37+ & .d-or > summary { cursor: pointer; list-style: none; }
38+ & .d-or > summary::-webkit-details-marker { display: none; }
39+ & .d-or > summary::before { content: "▸ "; }
40+ & .d-or[open] > summary::before { content: "▾ "; }
41+ & .d-or > summary:hover { color: var(--ink-hi); }
42+ & .d-or[open] > .clone { margin-top: var(--s2); }
3343 & .clone { margin-top: 0; }
3444 & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; }
3545 }
3646
47+ /* the rail of a merge request follows the review: the merge block and the
48+ checks stay in reach at the end of a long diff. It scrolls on its own
49+ when it is taller than the window. Only on a two-column layout. */
50+ @media (min-width: 1081px) {
51+ .work.has-rail:has(#mr-checks) .rail {
52+ position: sticky; top: calc(var(--hdr-h, 0px) + var(--s4));
53+ max-height: calc(100dvh - var(--hdr-h, 0px) - var(--s4) * 2);
54+ overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
55+ }
56+ }
57+
3758 .pcommits { font-size: var(--fs-s); display: grid; gap: var(--s1); }
3859 .pcommits li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 2px var(--s2); margin: 0 calc(-1 * var(--s2)); border-radius: 4px; }
3960 .pcommits li a { font-family: var(--mono); }
@@ -5,6 +5,7 @@
5 .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); }5 .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); }
6 .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); }6 .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); }
7 .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); }7 .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); }
8+ .mr-state[data-state="draft"]::before { content: "◌ "; color: var(--ink-dim); }
8 .mr-desc { max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); }9 .mr-desc { max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; padding: var(--s4) var(--s5); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); }
9 .mr-desc a, .c-text a { text-decoration: underline; }10 .mr-desc a, .c-text a { text-decoration: underline; }
10 11
@@ -29,11 +30,31 @@
29 & .d-check input { accent-color: var(--ink-hi); margin: 0; }30 & .d-check input { accent-color: var(--ink-hi); margin: 0; }
30 & .d-btns { display: flex; gap: var(--s2); margin-top: var(--s3); flex-wrap: wrap; }31 & .d-btns { display: flex; gap: var(--s2); margin-top: var(--s3); flex-wrap: wrap; }
31 & .d-btns .primary { flex: 1; justify-content: center; }32 & .d-btns .primary { flex: 1; justify-content: center; }
33+ /* the terminal lines fold away: a reviewer who merges in the page does
34+ not need them in sight */
35+ & .d-draft { font-size: var(--fs-s); color: var(--ink-dim); }
32 & .d-or { font-size: var(--fs-s); }36 & .d-or { font-size: var(--fs-s); }
37+ & .d-or > summary { cursor: pointer; list-style: none; }
38+ & .d-or > summary::-webkit-details-marker { display: none; }
39+ & .d-or > summary::before { content: "▸ "; }
40+ & .d-or[open] > summary::before { content: "▾ "; }
41+ & .d-or > summary:hover { color: var(--ink-hi); }
42+ & .d-or[open] > .clone { margin-top: var(--s2); }
33 & .clone { margin-top: 0; }43 & .clone { margin-top: 0; }
34 & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; }44 & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; }
35 }45 }
36 46
47+ /* the rail of a merge request follows the review: the merge block and the
48+ checks stay in reach at the end of a long diff. It scrolls on its own
49+ when it is taller than the window. Only on a two-column layout. */
50+ @media (min-width: 1081px) {
51+ .work.has-rail:has(#mr-checks) .rail {
52+ position: sticky; top: calc(var(--hdr-h, 0px) + var(--s4));
53+ max-height: calc(100dvh - var(--hdr-h, 0px) - var(--s4) * 2);
54+ overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
55+ }
56+ }
57+
37 .pcommits { font-size: var(--fs-s); display: grid; gap: var(--s1); }58 .pcommits { font-size: var(--fs-s); display: grid; gap: var(--s1); }
38 .pcommits li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 2px var(--s2); margin: 0 calc(-1 * var(--s2)); border-radius: 4px; }59 .pcommits li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 2px var(--s2); margin: 0 calc(-1 * var(--s2)); border-radius: 4px; }
39 .pcommits li a { font-family: var(--mono); }60 .pcommits li a { font-family: var(--mono); }
M internal/adapters/web/static/css/65-ci.css +6 -1
@@ -50,7 +50,12 @@
5050 .jobs > li { padding: var(--s3) var(--s4); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); }
5151 .job-h { display: flex; align-items: center; gap: var(--s2); font-weight: 700; }
5252 .steps { display: grid; gap: 2px; padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }
53- .steps li { display: flex; align-items: center; gap: var(--s1); }
53+ /* a long step name (a whole shell command) wraps inside the rail; the icon
54+ stays on its first line and the duration at the right */
55+ .steps li { display: flex; align-items: flex-start; gap: var(--s1); min-width: 0; }
56+ .steps li > :is(a, span:not(.dim)) { flex: 1; min-width: 0; overflow-wrap: anywhere; }
57+ .steps li > .dim { flex: none; margin-left: auto; }
58+ .steps li > :first-child { margin-top: 3px; flex: none; }
5459 .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }
5560 .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); }
5661 .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }
@@ -50,7 +50,12 @@
50 .jobs > li { padding: var(--s3) var(--s4); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); }50 .jobs > li { padding: var(--s3) var(--s4); background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--r); }
51 .job-h { display: flex; align-items: center; gap: var(--s2); font-weight: 700; }51 .job-h { display: flex; align-items: center; gap: var(--s2); font-weight: 700; }
52 .steps { display: grid; gap: 2px; padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }52 .steps { display: grid; gap: 2px; padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }
53- .steps li { display: flex; align-items: center; gap: var(--s1); }53+ /* a long step name (a whole shell command) wraps inside the rail; the icon
54+ stays on its first line and the duration at the right */
55+ .steps li { display: flex; align-items: flex-start; gap: var(--s1); min-width: 0; }
56+ .steps li > :is(a, span:not(.dim)) { flex: 1; min-width: 0; overflow-wrap: anywhere; }
57+ .steps li > .dim { flex: none; margin-left: auto; }
58+ .steps li > :first-child { margin-top: 3px; flex: none; }
54 .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }59 .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }
55 .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); }60 .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); }
56 .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }61 .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); }
M internal/adapters/web/text.go +16 -0
@@ -26,6 +26,22 @@ func invisible(r rune) bool {
2626 // marker is the visible form of an invisible character.
2727 func marker(r rune) string { return fmt.Sprintf("<U+%04X>", r) }
2828
29+// who is the name shown for the author of a merge request or a comment, or the
30+// actor of a run. ermite has one user: the owner's web session, root and every
31+// stored token act for the owner, so they show the owner's name (ERMITE_OWNER),
32+// not the label of the token, which names a device ("laptop"). Only CI is
33+// someone else, shown as stored: a job token ("ci job") and the workflows a job
34+// dispatched ("github-actions[bot]").
35+func (s *Server) who(stored string) string {
36+ if strings.HasPrefix(stored, "ci ") || strings.HasSuffix(stored, "[bot]") {
37+ return visibleText(stored)
38+ }
39+ if s.cfg.Owner != "" {
40+ return s.cfg.Owner
41+ }
42+ return "owner"
43+}
44+
2945 // visibleText is s with every invisible character replaced by its marker,
3046 // for the places that show text as a plain string: paths, names, subjects.
3147 func visibleText(s string) string {
@@ -26,6 +26,22 @@ func invisible(r rune) bool {
26 // marker is the visible form of an invisible character.26 // marker is the visible form of an invisible character.
27 func marker(r rune) string { return fmt.Sprintf("<U+%04X>", r) }27 func marker(r rune) string { return fmt.Sprintf("<U+%04X>", r) }
28 28
29+// who is the name shown for the author of a merge request or a comment, or the
30+// actor of a run. ermite has one user: the owner's web session, root and every
31+// stored token act for the owner, so they show the owner's name (ERMITE_OWNER),
32+// not the label of the token, which names a device ("laptop"). Only CI is
33+// someone else, shown as stored: a job token ("ci job") and the workflows a job
34+// dispatched ("github-actions[bot]").
35+func (s *Server) who(stored string) string {
36+ if strings.HasPrefix(stored, "ci ") || strings.HasSuffix(stored, "[bot]") {
37+ return visibleText(stored)
38+ }
39+ if s.cfg.Owner != "" {
40+ return s.cfg.Owner
41+ }
42+ return "owner"
43+}
44+
29 // visibleText is s with every invisible character replaced by its marker,45 // visibleText is s with every invisible character replaced by its marker,
30 // for the places that show text as a plain string: paths, names, subjects.46 // for the places that show text as a plain string: paths, names, subjects.
31 func visibleText(s string) string {47 func visibleText(s string) string {
M internal/adapters/web/text_test.go +12 -0
@@ -8,6 +8,18 @@ import (
88 "github.com/stretchr/testify/assert"
99 )
1010
11+// People show as the owner's name, whatever token they used; CI as stored.
12+func TestWho(t *testing.T) {
13+ s := &Server{cfg: Config{Owner: "alice"}}
14+ for stored, want := range map[string]string{
15+ "owner": "alice", "root": "alice", "laptop": "alice", "": "alice",
16+ "ci job": "ci job", "github-actions[bot]": "github-actions[bot]",
17+ } {
18+ assert.Equal(t, want, s.who(stored), stored)
19+ }
20+ assert.Equal(t, "owner", (&Server{}).who("laptop"), "no owner configured")
21+}
22+
1123 // Invisible characters in names and subjects show as markers.
1224 func TestVisibleText(t *testing.T) {
1325 t.Parallel()
@@ -8,6 +8,18 @@ import (
8 "github.com/stretchr/testify/assert"8 "github.com/stretchr/testify/assert"
9 )9 )
10 10
11+// People show as the owner's name, whatever token they used; CI as stored.
12+func TestWho(t *testing.T) {
13+ s := &Server{cfg: Config{Owner: "alice"}}
14+ for stored, want := range map[string]string{
15+ "owner": "alice", "root": "alice", "laptop": "alice", "": "alice",
16+ "ci job": "ci job", "github-actions[bot]": "github-actions[bot]",
17+ } {
18+ assert.Equal(t, want, s.who(stored), stored)
19+ }
20+ assert.Equal(t, "owner", (&Server{}).who("laptop"), "no owner configured")
21+}
22+
11 // Invisible characters in names and subjects show as markers.23 // Invisible characters in names and subjects show as markers.
12 func TestVisibleText(t *testing.T) {24 func TestVisibleText(t *testing.T) {
13 t.Parallel()25 t.Parallel()
M internal/adapters/web/view.go +3 -0
@@ -20,6 +20,9 @@ type Section struct {
2020 Label string
2121 Href string
2222 Current bool
23+ // Count is shown after the label when it is not empty: the open merge
24+ // requests.
25+ Count string
2326 }
2427
2528 // RefItem is a branch or tag in the ref switcher.
@@ -20,6 +20,9 @@ type Section struct {
20 Label string20 Label string
21 Href string21 Href string
22 Current bool22 Current bool
23+ // Count is shown after the label when it is not empty: the open merge
24+ // requests.
25+ Count string
23 }26 }
24 27
25 // RefItem is a branch or tag in the ref switcher.28 // RefItem is a branch or tag in the ref switcher.
M internal/adapters/web/web.go +1 -0
@@ -184,6 +184,7 @@ func (s *Server) routes() *http.ServeMux {
184184 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/resolve", s.mrAction(s.mrResolve))
185185 mux.HandleFunc("POST /{repo}/merge-requests/{n}/merge", s.mrAction(s.mrMerge))
186186 mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose))
187+ mux.HandleFunc("POST /{repo}/merge-requests/{n}/ready", s.mrAction(s.mrReady))
187188 mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead))
188189 }
189190 if s.cfg.Runs != nil {
@@ -184,6 +184,7 @@ func (s *Server) routes() *http.ServeMux {
184 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/resolve", s.mrAction(s.mrResolve))184 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/resolve", s.mrAction(s.mrResolve))
185 mux.HandleFunc("POST /{repo}/merge-requests/{n}/merge", s.mrAction(s.mrMerge))185 mux.HandleFunc("POST /{repo}/merge-requests/{n}/merge", s.mrAction(s.mrMerge))
186 mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose))186 mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose))
187+ mux.HandleFunc("POST /{repo}/merge-requests/{n}/ready", s.mrAction(s.mrReady))
187 mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead))188 mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead))
188 }189 }
189 if s.cfg.Runs != nil {190 if s.cfg.Runs != nil {
M internal/app/ci_pull_target_test.go +1 -1
@@ -355,7 +355,7 @@ func TestWebShowsTheMergeRequestOfAPullRequestTargetRun(t *testing.T) {
355355 res := b.get(page, nil)
356356 require.Equal(t, 200, res.status, res.body)
357357 assert.Contains(t, res.body, fmt.Sprintf(`href="/proj/merge-requests/%d"`, n), page)
358- assert.Contains(t, res.body, fmt.Sprintf("!%d", n), page)
358+ assert.Contains(t, res.body, fmt.Sprintf(">#%d<", n), page)
359359 }
360360 }
361361
@@ -355,7 +355,7 @@ func TestWebShowsTheMergeRequestOfAPullRequestTargetRun(t *testing.T) {
355 res := b.get(page, nil)355 res := b.get(page, nil)
356 require.Equal(t, 200, res.status, res.body)356 require.Equal(t, 200, res.status, res.body)
357 assert.Contains(t, res.body, fmt.Sprintf(`href="/proj/merge-requests/%d"`, n), page)357 assert.Contains(t, res.body, fmt.Sprintf(`href="/proj/merge-requests/%d"`, n), page)
358- assert.Contains(t, res.body, fmt.Sprintf("!%d", n), page)358+ assert.Contains(t, res.body, fmt.Sprintf(">#%d<", n), page)
359 }359 }
360 }360 }
361 361
M internal/app/ci_web_test.go +13 -5
@@ -40,7 +40,7 @@ func TestWebActionsPages(t *testing.T) {
4040 assert.Contains(t, list.body, `href="/proj/actions/runs/1"`)
4141 assert.Contains(t, list.body, `aria-label="queued"`)
4242 assert.Contains(t, list.body, "/-/events?topic=repo%3Aproj&amp;view=actions", "the page follows the repository stream")
43- assert.Contains(t, list.body, ">Actions<", "the tab")
43+ assert.Contains(t, list.body, `aria-current="page">Actions <kbd`, "the tab")
4444
4545 // The MR page says "waiting for a runner" only when no runner matches.
4646 mr, err := ci.root.mr.CreateMergeRequest(ci.ctx, connect.NewRequest(&pb.CreateMergeRequestRequest{Repo: "proj", Title: "Add CI", SourceBranch: "feat"}))
@@ -134,7 +134,8 @@ func TestWebActionsPages(t *testing.T) {
134134 }
135135
136136 // The live updates of the pages: the run page gets the body of the run when a
137-// job changes, and the job page its state, never a line of the log.
137+// job changes, the job page its state, never a line of the log, and the page
138+// of a merge request the checks of the head it shows (and only of that head).
138139 func TestWebActionsLiveFragments(t *testing.T) {
139140 ci := newCIHarness(t, nil)
140141 c := &cliHarness{harness: ci.harness, url: ci.srv.URL}
@@ -151,12 +152,15 @@ func TestWebActionsLiveFragments(t *testing.T) {
151152 list := ci.sse("topic=repo:proj&view=actions", "")
152153 job := runner.await()
153154 jobStream := ci.sse("topic=repo:proj&view=job&job="+job.GetJobId(), "")
155+ sha := ci.runs()[0].GetSha()
156+ mrPage := ci.sse("topic=repo:proj&view=mr&n=1&ref=main&head="+sha, "")
157+ otherHead := ci.sse("topic=repo:proj&view=mr&n=2&ref=x&head="+strings.Repeat("0", 40), "")
154158 _ = token
155159
156160 runner.runJob(job, pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "a secret value in the log")
157161 deadline := time.After(10 * time.Second)
158- var sawRun, sawList, sawJob bool
159- for !sawRun || !sawList || !sawJob {
162+ var sawRun, sawList, sawJob, sawChecks bool
163+ for !sawRun || !sawList || !sawJob || !sawChecks {
160164 select {
161165 case ev := <-run.events:
162166 sawRun = sawRun || (ev.name == "ci-run" && strings.Contains(ev.data, `id="run-body"`))
@@ -166,8 +170,12 @@ func TestWebActionsLiveFragments(t *testing.T) {
166170 case ev := <-jobStream.events:
167171 sawJob = sawJob || (ev.name == "ci-job" && strings.Contains(ev.data, `id="job-head"`))
168172 assert.NotContains(t, ev.data, "a secret value in the log", "the stream of states never carries the log")
173+ case ev := <-mrPage.events:
174+ sawChecks = sawChecks || (ev.name == "ci-run" && strings.Contains(ev.data, `id="mr-checks"`) && strings.Contains(ev.data, "success"))
175+ case ev := <-otherHead.events:
176+ assert.NotEqual(t, "ci-run", ev.name, "the checks of another head are not sent")
169177 case <-deadline:
170- t.Fatalf("fragments: run %v, list %v, job %v", sawRun, sawList, sawJob)
178+ t.Fatalf("fragments: run %v, list %v, job %v, checks %v", sawRun, sawList, sawJob, sawChecks)
171179 }
172180 }
173181 _ = domain.CISuccess
@@ -40,7 +40,7 @@ func TestWebActionsPages(t *testing.T) {
40 assert.Contains(t, list.body, `href="/proj/actions/runs/1"`)40 assert.Contains(t, list.body, `href="/proj/actions/runs/1"`)
41 assert.Contains(t, list.body, `aria-label="queued"`)41 assert.Contains(t, list.body, `aria-label="queued"`)
42 assert.Contains(t, list.body, "/-/events?topic=repo%3Aproj&amp;view=actions", "the page follows the repository stream")42 assert.Contains(t, list.body, "/-/events?topic=repo%3Aproj&amp;view=actions", "the page follows the repository stream")
43- assert.Contains(t, list.body, ">Actions<", "the tab")43+ assert.Contains(t, list.body, `aria-current="page">Actions <kbd`, "the tab")
44 44
45 // The MR page says "waiting for a runner" only when no runner matches.45 // The MR page says "waiting for a runner" only when no runner matches.
46 mr, err := ci.root.mr.CreateMergeRequest(ci.ctx, connect.NewRequest(&pb.CreateMergeRequestRequest{Repo: "proj", Title: "Add CI", SourceBranch: "feat"}))46 mr, err := ci.root.mr.CreateMergeRequest(ci.ctx, connect.NewRequest(&pb.CreateMergeRequestRequest{Repo: "proj", Title: "Add CI", SourceBranch: "feat"}))
@@ -134,7 +134,8 @@ func TestWebActionsPages(t *testing.T) {
134 }134 }
135 135
136 // The live updates of the pages: the run page gets the body of the run when a136 // The live updates of the pages: the run page gets the body of the run when a
137-// job changes, and the job page its state, never a line of the log.137+// job changes, the job page its state, never a line of the log, and the page
138+// of a merge request the checks of the head it shows (and only of that head).
138 func TestWebActionsLiveFragments(t *testing.T) {139 func TestWebActionsLiveFragments(t *testing.T) {
139 ci := newCIHarness(t, nil)140 ci := newCIHarness(t, nil)
140 c := &cliHarness{harness: ci.harness, url: ci.srv.URL}141 c := &cliHarness{harness: ci.harness, url: ci.srv.URL}
@@ -151,12 +152,15 @@ func TestWebActionsLiveFragments(t *testing.T) {
151 list := ci.sse("topic=repo:proj&view=actions", "")152 list := ci.sse("topic=repo:proj&view=actions", "")
152 job := runner.await()153 job := runner.await()
153 jobStream := ci.sse("topic=repo:proj&view=job&job="+job.GetJobId(), "")154 jobStream := ci.sse("topic=repo:proj&view=job&job="+job.GetJobId(), "")
155+ sha := ci.runs()[0].GetSha()
156+ mrPage := ci.sse("topic=repo:proj&view=mr&n=1&ref=main&head="+sha, "")
157+ otherHead := ci.sse("topic=repo:proj&view=mr&n=2&ref=x&head="+strings.Repeat("0", 40), "")
154 _ = token158 _ = token
155 159
156 runner.runJob(job, pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "a secret value in the log")160 runner.runJob(job, pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "a secret value in the log")
157 deadline := time.After(10 * time.Second)161 deadline := time.After(10 * time.Second)
158- var sawRun, sawList, sawJob bool162+ var sawRun, sawList, sawJob, sawChecks bool
159- for !sawRun || !sawList || !sawJob {163+ for !sawRun || !sawList || !sawJob || !sawChecks {
160 select {164 select {
161 case ev := <-run.events:165 case ev := <-run.events:
162 sawRun = sawRun || (ev.name == "ci-run" && strings.Contains(ev.data, `id="run-body"`))166 sawRun = sawRun || (ev.name == "ci-run" && strings.Contains(ev.data, `id="run-body"`))
@@ -166,8 +170,12 @@ func TestWebActionsLiveFragments(t *testing.T) {
166 case ev := <-jobStream.events:170 case ev := <-jobStream.events:
167 sawJob = sawJob || (ev.name == "ci-job" && strings.Contains(ev.data, `id="job-head"`))171 sawJob = sawJob || (ev.name == "ci-job" && strings.Contains(ev.data, `id="job-head"`))
168 assert.NotContains(t, ev.data, "a secret value in the log", "the stream of states never carries the log")172 assert.NotContains(t, ev.data, "a secret value in the log", "the stream of states never carries the log")
173+ case ev := <-mrPage.events:
174+ sawChecks = sawChecks || (ev.name == "ci-run" && strings.Contains(ev.data, `id="mr-checks"`) && strings.Contains(ev.data, "success"))
175+ case ev := <-otherHead.events:
176+ assert.NotEqual(t, "ci-run", ev.name, "the checks of another head are not sent")
169 case <-deadline:177 case <-deadline:
170- t.Fatalf("fragments: run %v, list %v, job %v", sawRun, sawList, sawJob)178+ t.Fatalf("fragments: run %v, list %v, job %v, checks %v", sawRun, sawList, sawJob, sawChecks)
171 }179 }
172 }180 }
173 _ = domain.CISuccess181 _ = domain.CISuccess
M internal/app/testdata/web/blob-binary.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/blob-code.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/blob-empty.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/blob-image.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/blob-markdown.html +6 -6
@@ -74,27 +74,27 @@
7474 <nav class="tabs" aria-label="Sections">
7575 <ul>
7676 <li>
77-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
77+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7878 </a>
7979 </li>
8080 <li>
81-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
81+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
8282 </a>
8383 </li>
8484 <li>
85-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
85+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8686 </a>
8787 </li>
8888 <li>
89-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
89+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
9090 </a>
9191 </li>
9292 <li>
93-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
93+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9494 </a>
9595 </li>
9696 <li>
97-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
97+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9898 </a>
9999 </li>
100100 </ul>
@@ -74,27 +74,27 @@
74 <nav class="tabs" aria-label="Sections">74 <nav class="tabs" aria-label="Sections">
75 <ul>75 <ul>
76 <li>76 <li>
77-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>77+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
78 </a>78 </a>
79 </li>79 </li>
80 <li>80 <li>
81-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>81+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
82 </a>82 </a>
83 </li>83 </li>
84 <li>84 <li>
85-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>85+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
86 </a>86 </a>
87 </li>87 </li>
88 <li>88 <li>
89-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>89+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
90 </a>90 </a>
91 </li>91 </li>
92 <li>92 <li>
93-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>93+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
94 </a>94 </a>
95 </li>95 </li>
96 <li>96 <li>
97-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>97+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
98 </a>98 </a>
99 </li>99 </li>
100 </ul>100 </ul>
M internal/app/testdata/web/blob-svg.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/blob-text.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/blob-too-large.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/blob-trojan.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/commit-big.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commit-binary.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commit-merge.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commit-modify-split.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commit-modify.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commit-rename.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commit-root.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commit-trojan.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commits-page2.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commits-path.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>78+<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/commits-root.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/long" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/long" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/long/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/long/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/long/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/long/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/commits-small.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/diffs" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>75+<a href="/diffs/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/diffs/commits/main" data-key="c" aria-keyshortcuts="c" aria-current="page">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/diffs/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/refs.html +6 -6
@@ -65,27 +65,27 @@
6565 <nav class="tabs" aria-label="Sections">
6666 <ul>
6767 <li>
68-<a href="/demo" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
68+<a href="/demo" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
6969 </a>
7070 </li>
7171 <li>
72-<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
72+<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7373 </a>
7474 </li>
7575 <li>
76-<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
76+<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
7777 </a>
7878 </li>
7979 <li>
80-<a href="/demo/refs" data-key="r" aria-keyshortcuts="r" aria-current="page">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
80+<a href="/demo/refs" data-key="r" aria-keyshortcuts="r" aria-current="page">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8181 </a>
8282 </li>
8383 <li>
84-<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
84+<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8585 </a>
8686 </li>
8787 <li>
88-<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
88+<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
8989 </a>
9090 </li>
9191 </ul>
@@ -65,27 +65,27 @@
65 <nav class="tabs" aria-label="Sections">65 <nav class="tabs" aria-label="Sections">
66 <ul>66 <ul>
67 <li>67 <li>
68-<a href="/demo" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>68+<a href="/demo" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
69 </a>69 </a>
70 </li>70 </li>
71 <li>71 <li>
72-<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>72+<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
73 </a>73 </a>
74 </li>74 </li>
75 <li>75 <li>
76-<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>76+<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
77 </a>77 </a>
78 </li>78 </li>
79 <li>79 <li>
80-<a href="/demo/refs" data-key="r" aria-keyshortcuts="r" aria-current="page">Branches &amp; tags<kbd aria-hidden="true">r</kbd>80+<a href="/demo/refs" data-key="r" aria-keyshortcuts="r" aria-current="page">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
81 </a>81 </a>
82 </li>82 </li>
83 <li>83 <li>
84-<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>84+<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
85 </a>85 </a>
86 </li>86 </li>
87 <li>87 <li>
88-<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>88+<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
89 </a>89 </a>
90 </li>90 </li>
91 </ul>91 </ul>
M internal/app/testdata/web/summary.html +6 -6
@@ -62,27 +62,27 @@
6262 <nav class="tabs" aria-label="Sections">
6363 <ul>
6464 <li>
65-<a href="/demo" data-key="s" aria-keyshortcuts="s" aria-current="page">Summary<kbd aria-hidden="true">s</kbd>
65+<a href="/demo" data-key="s" aria-keyshortcuts="s" aria-current="page">Summary <kbd aria-hidden="true">s</kbd>
6666 </a>
6767 </li>
6868 <li>
69-<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>
69+<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
7070 </a>
7171 </li>
7272 <li>
73-<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
73+<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
7474 </a>
7575 </li>
7676 <li>
77-<a href="/demo/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
77+<a href="/demo/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
7878 </a>
7979 </li>
8080 <li>
81-<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
81+<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8282 </a>
8383 </li>
8484 <li>
85-<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
85+<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
8686 </a>
8787 </li>
8888 </ul>
@@ -62,27 +62,27 @@
62 <nav class="tabs" aria-label="Sections">62 <nav class="tabs" aria-label="Sections">
63 <ul>63 <ul>
64 <li>64 <li>
65-<a href="/demo" data-key="s" aria-keyshortcuts="s" aria-current="page">Summary<kbd aria-hidden="true">s</kbd>65+<a href="/demo" data-key="s" aria-keyshortcuts="s" aria-current="page">Summary <kbd aria-hidden="true">s</kbd>
66 </a>66 </a>
67 </li>67 </li>
68 <li>68 <li>
69-<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files<kbd aria-hidden="true">t</kbd>69+<a href="/demo/tree/main" data-key="t" aria-keyshortcuts="t">Files <kbd aria-hidden="true">t</kbd>
70 </a>70 </a>
71 </li>71 </li>
72 <li>72 <li>
73-<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>73+<a href="/demo/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
74 </a>74 </a>
75 </li>75 </li>
76 <li>76 <li>
77-<a href="/demo/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>77+<a href="/demo/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
78 </a>78 </a>
79 </li>79 </li>
80 <li>80 <li>
81-<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>81+<a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
82 </a>82 </a>
83 </li>83 </li>
84 <li>84 <li>
85-<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>85+<a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
86 </a>86 </a>
87 </li>87 </li>
88 </ul>88 </ul>
M internal/app/testdata/web/tree-nested.html +6 -6
@@ -74,27 +74,27 @@
7474 <nav class="tabs" aria-label="Sections">
7575 <ul>
7676 <li>
77-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
77+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7878 </a>
7979 </li>
8080 <li>
81-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
81+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
8282 </a>
8383 </li>
8484 <li>
85-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
85+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8686 </a>
8787 </li>
8888 <li>
89-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
89+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
9090 </a>
9191 </li>
9292 <li>
93-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
93+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9494 </a>
9595 </li>
9696 <li>
97-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
97+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9898 </a>
9999 </li>
100100 </ul>
@@ -74,27 +74,27 @@
74 <nav class="tabs" aria-label="Sections">74 <nav class="tabs" aria-label="Sections">
75 <ul>75 <ul>
76 <li>76 <li>
77-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>77+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
78 </a>78 </a>
79 </li>79 </li>
80 <li>80 <li>
81-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>81+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
82 </a>82 </a>
83 </li>83 </li>
84 <li>84 <li>
85-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>85+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
86 </a>86 </a>
87 </li>87 </li>
88 <li>88 <li>
89-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>89+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
90 </a>90 </a>
91 </li>91 </li>
92 <li>92 <li>
93-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>93+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
94 </a>94 </a>
95 </li>95 </li>
96 <li>96 <li>
97-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>97+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
98 </a>98 </a>
99 </li>99 </li>
100 </ul>100 </ul>
M internal/app/testdata/web/tree-root.html +6 -6
@@ -68,27 +68,27 @@
6868 <nav class="tabs" aria-label="Sections">
6969 <ul>
7070 <li>
71-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
71+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7272 </a>
7373 </li>
7474 <li>
75-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
75+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7676 </a>
7777 </li>
7878 <li>
79-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
79+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8080 </a>
8181 </li>
8282 <li>
83-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
83+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8484 </a>
8585 </li>
8686 <li>
87-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
87+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
8888 </a>
8989 </li>
9090 <li>
91-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
91+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9292 </a>
9393 </li>
9494 </ul>
@@ -68,27 +68,27 @@
68 <nav class="tabs" aria-label="Sections">68 <nav class="tabs" aria-label="Sections">
69 <ul>69 <ul>
70 <li>70 <li>
71-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>71+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
72 </a>72 </a>
73 </li>73 </li>
74 <li>74 <li>
75-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>75+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
76 </a>76 </a>
77 </li>77 </li>
78 <li>78 <li>
79-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>79+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
80 </a>80 </a>
81 </li>81 </li>
82 <li>82 <li>
83-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>83+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
84 </a>84 </a>
85 </li>85 </li>
86 <li>86 <li>
87-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>87+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
88 </a>88 </a>
89 </li>89 </li>
90 <li>90 <li>
91-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>91+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
92 </a>92 </a>
93 </li>93 </li>
94 </ul>94 </ul>
M internal/app/testdata/web/tree-slash-ref.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/testdata/web/tree-subdir.html +6 -6
@@ -71,27 +71,27 @@
7171 <nav class="tabs" aria-label="Sections">
7272 <ul>
7373 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>
74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
7575 </a>
7676 </li>
7777 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>
78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
7979 </a>
8080 </li>
8181 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>
82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
8383 </a>
8484 </li>
8585 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>
86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
8787 </a>
8888 </li>
8989 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>
90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
9191 </a>
9292 </li>
9393 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>
94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
9595 </a>
9696 </li>
9797 </ul>
@@ -71,27 +71,27 @@
71 <nav class="tabs" aria-label="Sections">71 <nav class="tabs" aria-label="Sections">
72 <ul>72 <ul>
73 <li>73 <li>
74-<a href="/files" data-key="s" aria-keyshortcuts="s">Summary<kbd aria-hidden="true">s</kbd>74+<a href="/files" data-key="s" aria-keyshortcuts="s">Summary <kbd aria-hidden="true">s</kbd>
75 </a>75 </a>
76 </li>76 </li>
77 <li>77 <li>
78-<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files<kbd aria-hidden="true">t</kbd>78+<a href="/files/tree/main" data-key="t" aria-keyshortcuts="t" aria-current="page">Files <kbd aria-hidden="true">t</kbd>
79 </a>79 </a>
80 </li>80 </li>
81 <li>81 <li>
82-<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits<kbd aria-hidden="true">c</kbd>82+<a href="/files/commits/main" data-key="c" aria-keyshortcuts="c">Commits <kbd aria-hidden="true">c</kbd>
83 </a>83 </a>
84 </li>84 </li>
85 <li>85 <li>
86-<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags<kbd aria-hidden="true">r</kbd>86+<a href="/files/refs" data-key="r" aria-keyshortcuts="r">Branches &amp; tags <kbd aria-hidden="true">r</kbd>
87 </a>87 </a>
88 </li>88 </li>
89 <li>89 <li>
90-<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests<kbd aria-hidden="true">m</kbd>90+<a href="/files/merge-requests" data-key="m" aria-keyshortcuts="m">Merge requests <kbd aria-hidden="true">m</kbd>
91 </a>91 </a>
92 </li>92 </li>
93 <li>93 <li>
94-<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions<kbd aria-hidden="true">a</kbd>94+<a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd>
95 </a>95 </a>
96 </li>96 </li>
97 </ul>97 </ul>
M internal/app/web_mr_test.go +51 -2
@@ -130,7 +130,7 @@ func TestWebMergeRequestPagesRender(t *testing.T) {
130130 require.Equal(t, 200, mr.status, mr.body)
131131 assert.Equal(t, "no-cache", mr.hdr.Get("Cache-Control"))
132132 assert.Equal(t, []string{"Accept-Encoding"}, mr.hdr.Values("Vary"))
133- assert.Contains(t, mr.body, "<h1><span class=\"mute\">!1</span> Add &lt;b&gt;search&lt;/b&gt;</h1>")
133+ assert.Contains(t, mr.body, "<h1><span class=\"mute\">#1</span> Add &lt;b&gt;search&lt;/b&gt;</h1>")
134134 assert.NotContains(t, mr.body, "<b>search</b>")
135135 assert.Contains(t, mr.body, "feat/search")
136136 assert.Contains(t, mr.body, "package search</td>")
@@ -218,6 +218,7 @@ func TestWebMergeRequestWritesNeedSessionAndOrigin(t *testing.T) {
218218 "/proj/merge-requests/1/merge": "mode=fast-forward&head_id=" + head,
219219 "/proj/merge-requests/1/close": "",
220220 "/proj/merge-requests/1/read": "head_id=" + head,
221+ "/proj/merge-requests/1/ready": "",
221222 }
222223 for path, body := range paths {
223224 noOrigin := b.do("POST", path, as(token), body)
@@ -430,6 +431,54 @@ func TestWebMergeRequestSquashCloseAndList(t *testing.T) {
430431 assert.Contains(t, stale.body, "has moved on")
431432 }
432433
434+// A "WIP:" request is a draft: the list and the page say so, the box offers
435+// "Ready for review" instead of the merge form and the terminal lines, the
436+// merge is refused; ready drops the prefix and the merge form comes back. The
437+// tab counts the open requests.
438+func TestWebMergeRequestDraft(t *testing.T) {
439+ r := newMRRepo(t, domain.VisibilityPublic)
440+ b := browser{r.harness}
441+ token := r.signIn(t)
442+ head := r.headOf("feat/search")
443+ r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "WIP: search").ok(t)
444+
445+ page := b.get("/proj/merge-requests/1", as(token)).body
446+ assert.Contains(t, page, `data-state="draft"`)
447+ assert.Contains(t, page, `action="/proj/merge-requests/1/ready"`)
448+ assert.NotContains(t, page, `action="/proj/merge-requests/1/merge"`)
449+ assert.NotContains(t, page, "ermite mr merge", "no terminal line for a draft")
450+ assert.Contains(t, page, `<span class="count">1<span class="sr"> open</span></span>`, "the tab counts the open requests")
451+ assert.Contains(t, b.get("/proj/merge-requests", nil).body, `data-state="draft"`)
452+
453+ res := b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"fast-forward"}, "head_id": {head}})
454+ assert.Equal(t, http.StatusConflict, res.status, "a draft is not merged")
455+
456+ res = b.form("/proj/merge-requests/1/ready", token, nil)
457+ require.Equal(t, http.StatusSeeOther, res.status, res.body)
458+ page = b.get("/proj/merge-requests/1", as(token)).body
459+ assert.Contains(t, page, "> search</h1>", "the prefix is gone")
460+ assert.Contains(t, page, `data-state="open"`)
461+ assert.Contains(t, page, `action="/proj/merge-requests/1/merge"`)
462+ assert.Contains(t, page, `<details class="d-or">`, "the terminal lines fold away")
463+ assert.Equal(t, http.StatusConflict, b.form("/proj/merge-requests/1/ready", token, nil).status, "not a draft any more")
464+}
465+
466+// A request opened with a token shows the owner's name, not the label of the
467+// token, which names a device.
468+func TestWebMergeRequestAuthorIsTheOwner(t *testing.T) {
469+ r := newMRRepo(t, domain.VisibilityPublic)
470+ out, errOut, code := r.bin(t, adminToken, "token", "create", "laptop", "--preset", "cli")
471+ require.Equal(t, 0, code, errOut)
472+ r.cli(t, strings.TrimSpace(out), "mr", "create", "proj", "--source", "feat/search", "--title", "search").ok(t)
473+
474+ b := browser{r.harness}
475+ for _, path := range []string{"/proj/merge-requests/1", "/proj/merge-requests"} {
476+ body := b.get(path, nil).body
477+ assert.NotContains(t, body, "laptop", path)
478+ }
479+ assert.Contains(t, b.get("/proj/merge-requests", nil).body, `<td class="c-who dim">owner</td>`)
480+}
481+
433482 // The box offers no deletion for the default branch; a fast-forward with the
434483 // box checked deletes the source.
435484 func TestWebMergeDeleteBranchBox(t *testing.T) {
@@ -543,7 +592,7 @@ func TestWebMergeRequestEvents(t *testing.T) {
543592 drained = true
544593 }
545594 }
546- assert.Contains(t, r.get1("/proj/merge-requests/1"), `hx-sse:connect="/-/events?topic=repo%3Aproj&amp;view=mr&amp;n=1&amp;ref=feat%2Fsearch"`)
595+ assert.Contains(t, r.get1("/proj/merge-requests/1"), `hx-sse:connect="/-/events?topic=repo%3Aproj&amp;view=mr&amp;n=1&amp;ref=feat%2Fsearch&amp;head=`+r.headOf("feat/search")+`"`)
547596 }
548597
549598 func (r *mrRepo) get1(path string) string {
@@ -130,7 +130,7 @@ func TestWebMergeRequestPagesRender(t *testing.T) {
130 require.Equal(t, 200, mr.status, mr.body)130 require.Equal(t, 200, mr.status, mr.body)
131 assert.Equal(t, "no-cache", mr.hdr.Get("Cache-Control"))131 assert.Equal(t, "no-cache", mr.hdr.Get("Cache-Control"))
132 assert.Equal(t, []string{"Accept-Encoding"}, mr.hdr.Values("Vary"))132 assert.Equal(t, []string{"Accept-Encoding"}, mr.hdr.Values("Vary"))
133- assert.Contains(t, mr.body, "<h1><span class=\"mute\">!1</span> Add &lt;b&gt;search&lt;/b&gt;</h1>")133+ assert.Contains(t, mr.body, "<h1><span class=\"mute\">#1</span> Add &lt;b&gt;search&lt;/b&gt;</h1>")
134 assert.NotContains(t, mr.body, "<b>search</b>")134 assert.NotContains(t, mr.body, "<b>search</b>")
135 assert.Contains(t, mr.body, "feat/search")135 assert.Contains(t, mr.body, "feat/search")
136 assert.Contains(t, mr.body, "package search</td>")136 assert.Contains(t, mr.body, "package search</td>")
@@ -218,6 +218,7 @@ func TestWebMergeRequestWritesNeedSessionAndOrigin(t *testing.T) {
218 "/proj/merge-requests/1/merge": "mode=fast-forward&head_id=" + head,218 "/proj/merge-requests/1/merge": "mode=fast-forward&head_id=" + head,
219 "/proj/merge-requests/1/close": "",219 "/proj/merge-requests/1/close": "",
220 "/proj/merge-requests/1/read": "head_id=" + head,220 "/proj/merge-requests/1/read": "head_id=" + head,
221+ "/proj/merge-requests/1/ready": "",
221 }222 }
222 for path, body := range paths {223 for path, body := range paths {
223 noOrigin := b.do("POST", path, as(token), body)224 noOrigin := b.do("POST", path, as(token), body)
@@ -430,6 +431,54 @@ func TestWebMergeRequestSquashCloseAndList(t *testing.T) {
430 assert.Contains(t, stale.body, "has moved on")431 assert.Contains(t, stale.body, "has moved on")
431 }432 }
432 433
434+// A "WIP:" request is a draft: the list and the page say so, the box offers
435+// "Ready for review" instead of the merge form and the terminal lines, the
436+// merge is refused; ready drops the prefix and the merge form comes back. The
437+// tab counts the open requests.
438+func TestWebMergeRequestDraft(t *testing.T) {
439+ r := newMRRepo(t, domain.VisibilityPublic)
440+ b := browser{r.harness}
441+ token := r.signIn(t)
442+ head := r.headOf("feat/search")
443+ r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "WIP: search").ok(t)
444+
445+ page := b.get("/proj/merge-requests/1", as(token)).body
446+ assert.Contains(t, page, `data-state="draft"`)
447+ assert.Contains(t, page, `action="/proj/merge-requests/1/ready"`)
448+ assert.NotContains(t, page, `action="/proj/merge-requests/1/merge"`)
449+ assert.NotContains(t, page, "ermite mr merge", "no terminal line for a draft")
450+ assert.Contains(t, page, `<span class="count">1<span class="sr"> open</span></span>`, "the tab counts the open requests")
451+ assert.Contains(t, b.get("/proj/merge-requests", nil).body, `data-state="draft"`)
452+
453+ res := b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"fast-forward"}, "head_id": {head}})
454+ assert.Equal(t, http.StatusConflict, res.status, "a draft is not merged")
455+
456+ res = b.form("/proj/merge-requests/1/ready", token, nil)
457+ require.Equal(t, http.StatusSeeOther, res.status, res.body)
458+ page = b.get("/proj/merge-requests/1", as(token)).body
459+ assert.Contains(t, page, "> search</h1>", "the prefix is gone")
460+ assert.Contains(t, page, `data-state="open"`)
461+ assert.Contains(t, page, `action="/proj/merge-requests/1/merge"`)
462+ assert.Contains(t, page, `<details class="d-or">`, "the terminal lines fold away")
463+ assert.Equal(t, http.StatusConflict, b.form("/proj/merge-requests/1/ready", token, nil).status, "not a draft any more")
464+}
465+
466+// A request opened with a token shows the owner's name, not the label of the
467+// token, which names a device.
468+func TestWebMergeRequestAuthorIsTheOwner(t *testing.T) {
469+ r := newMRRepo(t, domain.VisibilityPublic)
470+ out, errOut, code := r.bin(t, adminToken, "token", "create", "laptop", "--preset", "cli")
471+ require.Equal(t, 0, code, errOut)
472+ r.cli(t, strings.TrimSpace(out), "mr", "create", "proj", "--source", "feat/search", "--title", "search").ok(t)
473+
474+ b := browser{r.harness}
475+ for _, path := range []string{"/proj/merge-requests/1", "/proj/merge-requests"} {
476+ body := b.get(path, nil).body
477+ assert.NotContains(t, body, "laptop", path)
478+ }
479+ assert.Contains(t, b.get("/proj/merge-requests", nil).body, `<td class="c-who dim">owner</td>`)
480+}
481+
433 // The box offers no deletion for the default branch; a fast-forward with the482 // The box offers no deletion for the default branch; a fast-forward with the
434 // box checked deletes the source.483 // box checked deletes the source.
435 func TestWebMergeDeleteBranchBox(t *testing.T) {484 func TestWebMergeDeleteBranchBox(t *testing.T) {
@@ -543,7 +592,7 @@ func TestWebMergeRequestEvents(t *testing.T) {
543 drained = true592 drained = true
544 }593 }
545 }594 }
546- assert.Contains(t, r.get1("/proj/merge-requests/1"), `hx-sse:connect="/-/events?topic=repo%3Aproj&amp;view=mr&amp;n=1&amp;ref=feat%2Fsearch"`)595+ assert.Contains(t, r.get1("/proj/merge-requests/1"), `hx-sse:connect="/-/events?topic=repo%3Aproj&amp;view=mr&amp;n=1&amp;ref=feat%2Fsearch&amp;head=`+r.headOf("feat/search")+`"`)
547 }596 }
548 597
549 func (r *mrRepo) get1(path string) string {598 func (r *mrRepo) get1(path string) string {
M internal/domain/event.go +12 -0
@@ -148,6 +148,18 @@ type MergeRequestClosed struct {
148148 // EventKind implements Event.
149149 func (MergeRequestClosed) EventKind() string { return "mr.closed" }
150150
151+// MergeRequestReady is published when a draft merge request was marked ready
152+// for review: its title lost the draft prefix.
153+type MergeRequestReady struct {
154+ MergeRequest MergeRequest
155+ At time.Time
156+ Visibility Visibility
157+ CreatedAt time.Time
158+}
159+
160+// EventKind implements Event.
161+func (MergeRequestReady) EventKind() string { return "mr.ready" }
162+
151163 // CommentAdded is published when a comment was added to a merge request.
152164 type CommentAdded struct {
153165 Repo RepoName
@@ -148,6 +148,18 @@ type MergeRequestClosed struct {
148 // EventKind implements Event.148 // EventKind implements Event.
149 func (MergeRequestClosed) EventKind() string { return "mr.closed" }149 func (MergeRequestClosed) EventKind() string { return "mr.closed" }
150 150
151+// MergeRequestReady is published when a draft merge request was marked ready
152+// for review: its title lost the draft prefix.
153+type MergeRequestReady struct {
154+ MergeRequest MergeRequest
155+ At time.Time
156+ Visibility Visibility
157+ CreatedAt time.Time
158+}
159+
160+// EventKind implements Event.
161+func (MergeRequestReady) EventKind() string { return "mr.ready" }
162+
151 // CommentAdded is published when a comment was added to a merge request.163 // CommentAdded is published when a comment was added to a merge request.
152 type CommentAdded struct {164 type CommentAdded struct {
153 Repo RepoName165 Repo RepoName
M internal/domain/mergerequest.go +44 -0
@@ -137,6 +137,50 @@ type MergeRequest struct {
137137 Version uint64
138138 }
139139
140+// draftPrefixes mark a draft at the start of a title, compared without regard
141+// to case, as Forgejo and Gitea do.
142+var draftPrefixes = []string{"wip:", "[wip]"}
143+
144+// IsDraft reports whether the title marks the request as a draft: it starts
145+// with "WIP:" or "[WIP]". A draft cannot be merged.
146+func (mr MergeRequest) IsDraft() bool {
147+ _, ok := draftRest(mr.Title.String())
148+ return ok
149+}
150+
151+// ReadyTitle is the title without its draft prefixes (all of them, so that
152+// "WIP: [WIP] x" is no longer a draft), and false when the title has none or
153+// nothing would be left of it.
154+func ReadyTitle(t MRTitle) (MRTitle, bool) {
155+ rest, ok := draftRest(t.String())
156+ if !ok {
157+ return MRTitle{}, false
158+ }
159+ for {
160+ next, more := draftRest(rest)
161+ if !more {
162+ break
163+ }
164+ rest = next
165+ }
166+ ready, err := ParseMRTitle(rest)
167+ if err != nil {
168+ return MRTitle{}, false
169+ }
170+ return ready, true
171+}
172+
173+// draftRest is the title after its draft prefix and the spaces that follow.
174+func draftRest(title string) (string, bool) {
175+ t := strings.TrimLeft(title, " ")
176+ for _, p := range draftPrefixes {
177+ if len(t) >= len(p) && strings.EqualFold(t[:len(p)], p) {
178+ return strings.TrimLeft(t[len(p):], " "), true
179+ }
180+ }
181+ return "", false
182+}
183+
140184 // IsOpen reports whether the request awaits review.
141185 func (m MergeRequest) IsOpen() bool { return m.State == MRStateOpen }
142186
@@ -137,6 +137,50 @@ type MergeRequest struct {
137 Version uint64137 Version uint64
138 }138 }
139 139
140+// draftPrefixes mark a draft at the start of a title, compared without regard
141+// to case, as Forgejo and Gitea do.
142+var draftPrefixes = []string{"wip:", "[wip]"}
143+
144+// IsDraft reports whether the title marks the request as a draft: it starts
145+// with "WIP:" or "[WIP]". A draft cannot be merged.
146+func (mr MergeRequest) IsDraft() bool {
147+ _, ok := draftRest(mr.Title.String())
148+ return ok
149+}
150+
151+// ReadyTitle is the title without its draft prefixes (all of them, so that
152+// "WIP: [WIP] x" is no longer a draft), and false when the title has none or
153+// nothing would be left of it.
154+func ReadyTitle(t MRTitle) (MRTitle, bool) {
155+ rest, ok := draftRest(t.String())
156+ if !ok {
157+ return MRTitle{}, false
158+ }
159+ for {
160+ next, more := draftRest(rest)
161+ if !more {
162+ break
163+ }
164+ rest = next
165+ }
166+ ready, err := ParseMRTitle(rest)
167+ if err != nil {
168+ return MRTitle{}, false
169+ }
170+ return ready, true
171+}
172+
173+// draftRest is the title after its draft prefix and the spaces that follow.
174+func draftRest(title string) (string, bool) {
175+ t := strings.TrimLeft(title, " ")
176+ for _, p := range draftPrefixes {
177+ if len(t) >= len(p) && strings.EqualFold(t[:len(p)], p) {
178+ return strings.TrimLeft(t[len(p):], " "), true
179+ }
180+ }
181+ return "", false
182+}
183+
140 // IsOpen reports whether the request awaits review.184 // IsOpen reports whether the request awaits review.
141 func (m MergeRequest) IsOpen() bool { return m.State == MRStateOpen }185 func (m MergeRequest) IsOpen() bool { return m.State == MRStateOpen }
142 186
M internal/domain/mergerequest_test.go +24 -0
@@ -211,3 +211,27 @@ func TestParseBranchName(t *testing.T) {
211211 assert.ErrorIs(t, err, domain.ErrInvalidRevision, "%q", bad)
212212 }
213213 }
214+
215+// A title that starts with "WIP:" or "[WIP]", in any case, is a draft; ready
216+// drops the prefix and the spaces after it.
217+func TestMergeRequestDraft(t *testing.T) {
218+ for _, tc := range []struct {
219+ title, ready string
220+ draft bool
221+ }{
222+ {"WIP: feat: drag and drop", "feat: drag and drop", true},
223+ {"wip:fix", "fix", true},
224+ {" [WIP] tidy", "tidy", true},
225+ {"WIP: [wip] both", "both", true}, // every prefix goes
226+ {"feat: wip: later", "", false},
227+ {"WIPE the cache", "", false},
228+ {"WIP:", "", true}, // a draft, but nothing would be left
229+ } {
230+ title, err := domain.ParseMRTitle(tc.title)
231+ require.NoError(t, err)
232+ assert.Equal(t, tc.draft, domain.MergeRequest{Title: title}.IsDraft(), tc.title)
233+ ready, ok := domain.ReadyTitle(title)
234+ assert.Equal(t, tc.ready != "", ok, tc.title)
235+ assert.Equal(t, tc.ready, ready.String(), tc.title)
236+ }
237+}
@@ -211,3 +211,27 @@ func TestParseBranchName(t *testing.T) {
211 assert.ErrorIs(t, err, domain.ErrInvalidRevision, "%q", bad)211 assert.ErrorIs(t, err, domain.ErrInvalidRevision, "%q", bad)
212 }212 }
213 }213 }
214+
215+// A title that starts with "WIP:" or "[WIP]", in any case, is a draft; ready
216+// drops the prefix and the spaces after it.
217+func TestMergeRequestDraft(t *testing.T) {
218+ for _, tc := range []struct {
219+ title, ready string
220+ draft bool
221+ }{
222+ {"WIP: feat: drag and drop", "feat: drag and drop", true},
223+ {"wip:fix", "fix", true},
224+ {" [WIP] tidy", "tidy", true},
225+ {"WIP: [wip] both", "both", true}, // every prefix goes
226+ {"feat: wip: later", "", false},
227+ {"WIPE the cache", "", false},
228+ {"WIP:", "", true}, // a draft, but nothing would be left
229+ } {
230+ title, err := domain.ParseMRTitle(tc.title)
231+ require.NoError(t, err)
232+ assert.Equal(t, tc.draft, domain.MergeRequest{Title: title}.IsDraft(), tc.title)
233+ ready, ok := domain.ReadyTitle(title)
234+ assert.Equal(t, tc.ready != "", ok, tc.title)
235+ assert.Equal(t, tc.ready, ready.String(), tc.title)
236+ }
237+}
M internal/ports/dto.go +1 -1
@@ -141,7 +141,7 @@ type WatchEvent struct {
141141 Gap bool
142142 // Event is a domain.RepoCreated, RepoUpdated, RepoDeleted, PushEvent,
143143 // MergeRequestOpened, MergeRequestMerged, MergeRequestClosed,
144- // CommentAdded, RunUpdated or JobUpdated.
144+ // MergeRequestReady, CommentAdded, RunUpdated or JobUpdated.
145145 Event domain.Event
146146 // Repo is the state of the repository at delivery, for RepoCreated and
147147 // RepoUpdated.
@@ -141,7 +141,7 @@ type WatchEvent struct {
141 Gap bool141 Gap bool
142 // Event is a domain.RepoCreated, RepoUpdated, RepoDeleted, PushEvent,142 // Event is a domain.RepoCreated, RepoUpdated, RepoDeleted, PushEvent,
143 // MergeRequestOpened, MergeRequestMerged, MergeRequestClosed,143 // MergeRequestOpened, MergeRequestMerged, MergeRequestClosed,
144- // CommentAdded, RunUpdated or JobUpdated.144+ // MergeRequestReady, CommentAdded, RunUpdated or JobUpdated.
145 Event domain.Event145 Event domain.Event
146 // Repo is the state of the repository at delivery, for RepoCreated and146 // Repo is the state of the repository at delivery, for RepoCreated and
147 // RepoUpdated.147 // RepoUpdated.
M internal/ports/mocks/MergeRequestService.go +150 -0
@@ -210,6 +210,78 @@ func (_c *MergeRequestService_Close_Call) RunAndReturn(run func(ctx context.Cont
210210 return _c
211211 }
212212
213+// CountOpen provides a mock function for the type MergeRequestService
214+func (_mock *MergeRequestService) CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error) {
215+ ret := _mock.Called(ctx, p, name)
216+
217+ if len(ret) == 0 {
218+ panic("no return value specified for CountOpen")
219+ }
220+
221+ var r0 int
222+ var r1 error
223+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) (int, error)); ok {
224+ return returnFunc(ctx, p, name)
225+ }
226+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) int); ok {
227+ r0 = returnFunc(ctx, p, name)
228+ } else {
229+ r0 = ret.Get(0).(int)
230+ }
231+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName) error); ok {
232+ r1 = returnFunc(ctx, p, name)
233+ } else {
234+ r1 = ret.Error(1)
235+ }
236+ return r0, r1
237+}
238+
239+// MergeRequestService_CountOpen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountOpen'
240+type MergeRequestService_CountOpen_Call struct {
241+ *mock.Call
242+}
243+
244+// CountOpen is a helper method to define mock.On call
245+// - ctx context.Context
246+// - p domain.Principal
247+// - name domain.RepoName
248+func (_e *MergeRequestService_Expecter) CountOpen(ctx any, p any, name any) *MergeRequestService_CountOpen_Call {
249+ return &MergeRequestService_CountOpen_Call{Call: _e.mock.On("CountOpen", ctx, p, name)}
250+}
251+
252+func (_c *MergeRequestService_CountOpen_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName)) *MergeRequestService_CountOpen_Call {
253+ _c.Call.Run(func(args mock.Arguments) {
254+ var arg0 context.Context
255+ if args[0] != nil {
256+ arg0 = args[0].(context.Context)
257+ }
258+ var arg1 domain.Principal
259+ if args[1] != nil {
260+ arg1 = args[1].(domain.Principal)
261+ }
262+ var arg2 domain.RepoName
263+ if args[2] != nil {
264+ arg2 = args[2].(domain.RepoName)
265+ }
266+ run(
267+ arg0,
268+ arg1,
269+ arg2,
270+ )
271+ })
272+ return _c
273+}
274+
275+func (_c *MergeRequestService_CountOpen_Call) Return(n int, err error) *MergeRequestService_CountOpen_Call {
276+ _c.Call.Return(n, err)
277+ return _c
278+}
279+
280+func (_c *MergeRequestService_CountOpen_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error)) *MergeRequestService_CountOpen_Call {
281+ _c.Call.Return(run)
282+ return _c
283+}
284+
213285 // Create provides a mock function for the type MergeRequestService
214286 func (_mock *MergeRequestService) Create(ctx context.Context, p domain.Principal, name domain.RepoName, params ports.CreateMergeRequestParams) (domain.MergeRequest, error) {
215287 ret := _mock.Called(ctx, p, name, params)
@@ -534,6 +606,84 @@ func (_c *MergeRequestService_MarkRead_Call) RunAndReturn(run func(ctx context.C
534606 return _c
535607 }
536608
609+// MarkReady provides a mock function for the type MergeRequestService
610+func (_mock *MergeRequestService) MarkReady(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error) {
611+ ret := _mock.Called(ctx, p, name, number)
612+
613+ if len(ret) == 0 {
614+ panic("no return value specified for MarkReady")
615+ }
616+
617+ var r0 domain.MergeRequest
618+ var r1 error
619+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) (domain.MergeRequest, error)); ok {
620+ return returnFunc(ctx, p, name, number)
621+ }
622+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) domain.MergeRequest); ok {
623+ r0 = returnFunc(ctx, p, name, number)
624+ } else {
625+ r0 = ret.Get(0).(domain.MergeRequest)
626+ }
627+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
628+ r1 = returnFunc(ctx, p, name, number)
629+ } else {
630+ r1 = ret.Error(1)
631+ }
632+ return r0, r1
633+}
634+
635+// MergeRequestService_MarkReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkReady'
636+type MergeRequestService_MarkReady_Call struct {
637+ *mock.Call
638+}
639+
640+// MarkReady is a helper method to define mock.On call
641+// - ctx context.Context
642+// - p domain.Principal
643+// - name domain.RepoName
644+// - number int64
645+func (_e *MergeRequestService_Expecter) MarkReady(ctx any, p any, name any, number any) *MergeRequestService_MarkReady_Call {
646+ return &MergeRequestService_MarkReady_Call{Call: _e.mock.On("MarkReady", ctx, p, name, number)}
647+}
648+
649+func (_c *MergeRequestService_MarkReady_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64)) *MergeRequestService_MarkReady_Call {
650+ _c.Call.Run(func(args mock.Arguments) {
651+ var arg0 context.Context
652+ if args[0] != nil {
653+ arg0 = args[0].(context.Context)
654+ }
655+ var arg1 domain.Principal
656+ if args[1] != nil {
657+ arg1 = args[1].(domain.Principal)
658+ }
659+ var arg2 domain.RepoName
660+ if args[2] != nil {
661+ arg2 = args[2].(domain.RepoName)
662+ }
663+ var arg3 int64
664+ if args[3] != nil {
665+ arg3 = args[3].(int64)
666+ }
667+ run(
668+ arg0,
669+ arg1,
670+ arg2,
671+ arg3,
672+ )
673+ })
674+ return _c
675+}
676+
677+func (_c *MergeRequestService_MarkReady_Call) Return(mergeRequest domain.MergeRequest, err error) *MergeRequestService_MarkReady_Call {
678+ _c.Call.Return(mergeRequest, err)
679+ return _c
680+}
681+
682+func (_c *MergeRequestService_MarkReady_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)) *MergeRequestService_MarkReady_Call {
683+ _c.Call.Return(run)
684+ return _c
685+}
686+
537687 // Merge provides a mock function for the type MergeRequestService
538688 func (_mock *MergeRequestService) Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams) (ports.MergeResult, error) {
539689 ret := _mock.Called(ctx, p, name, number, params)
@@ -210,6 +210,78 @@ func (_c *MergeRequestService_Close_Call) RunAndReturn(run func(ctx context.Cont
210 return _c210 return _c
211 }211 }
212 212
213+// CountOpen provides a mock function for the type MergeRequestService
214+func (_mock *MergeRequestService) CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error) {
215+ ret := _mock.Called(ctx, p, name)
216+
217+ if len(ret) == 0 {
218+ panic("no return value specified for CountOpen")
219+ }
220+
221+ var r0 int
222+ var r1 error
223+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) (int, error)); ok {
224+ return returnFunc(ctx, p, name)
225+ }
226+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) int); ok {
227+ r0 = returnFunc(ctx, p, name)
228+ } else {
229+ r0 = ret.Get(0).(int)
230+ }
231+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName) error); ok {
232+ r1 = returnFunc(ctx, p, name)
233+ } else {
234+ r1 = ret.Error(1)
235+ }
236+ return r0, r1
237+}
238+
239+// MergeRequestService_CountOpen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountOpen'
240+type MergeRequestService_CountOpen_Call struct {
241+ *mock.Call
242+}
243+
244+// CountOpen is a helper method to define mock.On call
245+// - ctx context.Context
246+// - p domain.Principal
247+// - name domain.RepoName
248+func (_e *MergeRequestService_Expecter) CountOpen(ctx any, p any, name any) *MergeRequestService_CountOpen_Call {
249+ return &MergeRequestService_CountOpen_Call{Call: _e.mock.On("CountOpen", ctx, p, name)}
250+}
251+
252+func (_c *MergeRequestService_CountOpen_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName)) *MergeRequestService_CountOpen_Call {
253+ _c.Call.Run(func(args mock.Arguments) {
254+ var arg0 context.Context
255+ if args[0] != nil {
256+ arg0 = args[0].(context.Context)
257+ }
258+ var arg1 domain.Principal
259+ if args[1] != nil {
260+ arg1 = args[1].(domain.Principal)
261+ }
262+ var arg2 domain.RepoName
263+ if args[2] != nil {
264+ arg2 = args[2].(domain.RepoName)
265+ }
266+ run(
267+ arg0,
268+ arg1,
269+ arg2,
270+ )
271+ })
272+ return _c
273+}
274+
275+func (_c *MergeRequestService_CountOpen_Call) Return(n int, err error) *MergeRequestService_CountOpen_Call {
276+ _c.Call.Return(n, err)
277+ return _c
278+}
279+
280+func (_c *MergeRequestService_CountOpen_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error)) *MergeRequestService_CountOpen_Call {
281+ _c.Call.Return(run)
282+ return _c
283+}
284+
213 // Create provides a mock function for the type MergeRequestService285 // Create provides a mock function for the type MergeRequestService
214 func (_mock *MergeRequestService) Create(ctx context.Context, p domain.Principal, name domain.RepoName, params ports.CreateMergeRequestParams) (domain.MergeRequest, error) {286 func (_mock *MergeRequestService) Create(ctx context.Context, p domain.Principal, name domain.RepoName, params ports.CreateMergeRequestParams) (domain.MergeRequest, error) {
215 ret := _mock.Called(ctx, p, name, params)287 ret := _mock.Called(ctx, p, name, params)
@@ -534,6 +606,84 @@ func (_c *MergeRequestService_MarkRead_Call) RunAndReturn(run func(ctx context.C
534 return _c606 return _c
535 }607 }
536 608
609+// MarkReady provides a mock function for the type MergeRequestService
610+func (_mock *MergeRequestService) MarkReady(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error) {
611+ ret := _mock.Called(ctx, p, name, number)
612+
613+ if len(ret) == 0 {
614+ panic("no return value specified for MarkReady")
615+ }
616+
617+ var r0 domain.MergeRequest
618+ var r1 error
619+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) (domain.MergeRequest, error)); ok {
620+ return returnFunc(ctx, p, name, number)
621+ }
622+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) domain.MergeRequest); ok {
623+ r0 = returnFunc(ctx, p, name, number)
624+ } else {
625+ r0 = ret.Get(0).(domain.MergeRequest)
626+ }
627+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
628+ r1 = returnFunc(ctx, p, name, number)
629+ } else {
630+ r1 = ret.Error(1)
631+ }
632+ return r0, r1
633+}
634+
635+// MergeRequestService_MarkReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkReady'
636+type MergeRequestService_MarkReady_Call struct {
637+ *mock.Call
638+}
639+
640+// MarkReady is a helper method to define mock.On call
641+// - ctx context.Context
642+// - p domain.Principal
643+// - name domain.RepoName
644+// - number int64
645+func (_e *MergeRequestService_Expecter) MarkReady(ctx any, p any, name any, number any) *MergeRequestService_MarkReady_Call {
646+ return &MergeRequestService_MarkReady_Call{Call: _e.mock.On("MarkReady", ctx, p, name, number)}
647+}
648+
649+func (_c *MergeRequestService_MarkReady_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64)) *MergeRequestService_MarkReady_Call {
650+ _c.Call.Run(func(args mock.Arguments) {
651+ var arg0 context.Context
652+ if args[0] != nil {
653+ arg0 = args[0].(context.Context)
654+ }
655+ var arg1 domain.Principal
656+ if args[1] != nil {
657+ arg1 = args[1].(domain.Principal)
658+ }
659+ var arg2 domain.RepoName
660+ if args[2] != nil {
661+ arg2 = args[2].(domain.RepoName)
662+ }
663+ var arg3 int64
664+ if args[3] != nil {
665+ arg3 = args[3].(int64)
666+ }
667+ run(
668+ arg0,
669+ arg1,
670+ arg2,
671+ arg3,
672+ )
673+ })
674+ return _c
675+}
676+
677+func (_c *MergeRequestService_MarkReady_Call) Return(mergeRequest domain.MergeRequest, err error) *MergeRequestService_MarkReady_Call {
678+ _c.Call.Return(mergeRequest, err)
679+ return _c
680+}
681+
682+func (_c *MergeRequestService_MarkReady_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)) *MergeRequestService_MarkReady_Call {
683+ _c.Call.Return(run)
684+ return _c
685+}
686+
537 // Merge provides a mock function for the type MergeRequestService687 // Merge provides a mock function for the type MergeRequestService
538 func (_mock *MergeRequestService) Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams) (ports.MergeResult, error) {688 func (_mock *MergeRequestService) Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams) (ports.MergeResult, error) {
539 ret := _mock.Called(ctx, p, name, number, params)689 ret := _mock.Called(ctx, p, name, number, params)
M internal/ports/mocks/MergeRequestStore.go +66 -0
@@ -47,6 +47,72 @@ func (_m *MergeRequestStore) EXPECT() *MergeRequestStore_Expecter {
4747 return &MergeRequestStore_Expecter{mock: &_m.Mock}
4848 }
4949
50+// CountOpen provides a mock function for the type MergeRequestStore
51+func (_mock *MergeRequestStore) CountOpen(ctx context.Context, repo domain.RepoName) (int, error) {
52+ ret := _mock.Called(ctx, repo)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for CountOpen")
56+ }
57+
58+ var r0 int
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) (int, error)); ok {
61+ return returnFunc(ctx, repo)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) int); ok {
64+ r0 = returnFunc(ctx, repo)
65+ } else {
66+ r0 = ret.Get(0).(int)
67+ }
68+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
69+ r1 = returnFunc(ctx, repo)
70+ } else {
71+ r1 = ret.Error(1)
72+ }
73+ return r0, r1
74+}
75+
76+// MergeRequestStore_CountOpen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountOpen'
77+type MergeRequestStore_CountOpen_Call struct {
78+ *mock.Call
79+}
80+
81+// CountOpen is a helper method to define mock.On call
82+// - ctx context.Context
83+// - repo domain.RepoName
84+func (_e *MergeRequestStore_Expecter) CountOpen(ctx any, repo any) *MergeRequestStore_CountOpen_Call {
85+ return &MergeRequestStore_CountOpen_Call{Call: _e.mock.On("CountOpen", ctx, repo)}
86+}
87+
88+func (_c *MergeRequestStore_CountOpen_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *MergeRequestStore_CountOpen_Call {
89+ _c.Call.Run(func(args mock.Arguments) {
90+ var arg0 context.Context
91+ if args[0] != nil {
92+ arg0 = args[0].(context.Context)
93+ }
94+ var arg1 domain.RepoName
95+ if args[1] != nil {
96+ arg1 = args[1].(domain.RepoName)
97+ }
98+ run(
99+ arg0,
100+ arg1,
101+ )
102+ })
103+ return _c
104+}
105+
106+func (_c *MergeRequestStore_CountOpen_Call) Return(n int, err error) *MergeRequestStore_CountOpen_Call {
107+ _c.Call.Return(n, err)
108+ return _c
109+}
110+
111+func (_c *MergeRequestStore_CountOpen_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) (int, error)) *MergeRequestStore_CountOpen_Call {
112+ _c.Call.Return(run)
113+ return _c
114+}
115+
50116 // Create provides a mock function for the type MergeRequestStore
51117 func (_mock *MergeRequestStore) Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error) {
52118 ret := _mock.Called(ctx, mr)
@@ -47,6 +47,72 @@ func (_m *MergeRequestStore) EXPECT() *MergeRequestStore_Expecter {
47 return &MergeRequestStore_Expecter{mock: &_m.Mock}47 return &MergeRequestStore_Expecter{mock: &_m.Mock}
48 }48 }
49 49
50+// CountOpen provides a mock function for the type MergeRequestStore
51+func (_mock *MergeRequestStore) CountOpen(ctx context.Context, repo domain.RepoName) (int, error) {
52+ ret := _mock.Called(ctx, repo)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for CountOpen")
56+ }
57+
58+ var r0 int
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) (int, error)); ok {
61+ return returnFunc(ctx, repo)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) int); ok {
64+ r0 = returnFunc(ctx, repo)
65+ } else {
66+ r0 = ret.Get(0).(int)
67+ }
68+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
69+ r1 = returnFunc(ctx, repo)
70+ } else {
71+ r1 = ret.Error(1)
72+ }
73+ return r0, r1
74+}
75+
76+// MergeRequestStore_CountOpen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountOpen'
77+type MergeRequestStore_CountOpen_Call struct {
78+ *mock.Call
79+}
80+
81+// CountOpen is a helper method to define mock.On call
82+// - ctx context.Context
83+// - repo domain.RepoName
84+func (_e *MergeRequestStore_Expecter) CountOpen(ctx any, repo any) *MergeRequestStore_CountOpen_Call {
85+ return &MergeRequestStore_CountOpen_Call{Call: _e.mock.On("CountOpen", ctx, repo)}
86+}
87+
88+func (_c *MergeRequestStore_CountOpen_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *MergeRequestStore_CountOpen_Call {
89+ _c.Call.Run(func(args mock.Arguments) {
90+ var arg0 context.Context
91+ if args[0] != nil {
92+ arg0 = args[0].(context.Context)
93+ }
94+ var arg1 domain.RepoName
95+ if args[1] != nil {
96+ arg1 = args[1].(domain.RepoName)
97+ }
98+ run(
99+ arg0,
100+ arg1,
101+ )
102+ })
103+ return _c
104+}
105+
106+func (_c *MergeRequestStore_CountOpen_Call) Return(n int, err error) *MergeRequestStore_CountOpen_Call {
107+ _c.Call.Return(n, err)
108+ return _c
109+}
110+
111+func (_c *MergeRequestStore_CountOpen_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) (int, error)) *MergeRequestStore_CountOpen_Call {
112+ _c.Call.Return(run)
113+ return _c
114+}
115+
50 // Create provides a mock function for the type MergeRequestStore116 // Create provides a mock function for the type MergeRequestStore
51 func (_mock *MergeRequestStore) Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error) {117 func (_mock *MergeRequestStore) Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error) {
52 ret := _mock.Called(ctx, mr)118 ret := _mock.Called(ctx, mr)
M internal/ports/ports.go +3 -0
@@ -248,6 +248,9 @@ type MergeRequestStore interface {
248248 // ListOpen returns the open merge requests of the repository, highest
249249 // number first, reading only the open ones (through an index).
250250 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
251+ // CountOpen returns the number of open merge requests of the repository
252+ // from the index of keys: no request is read.
253+ CountOpen(ctx context.Context, repo domain.RepoName) (int, error)
251254 // OpenBySource returns the numbers of the open merge requests whose source
252255 // branch is one of branches (short names), from an index of keys: a push
253256 // looks up the branches it moved, whatever the number of merge requests the
@@ -248,6 +248,9 @@ type MergeRequestStore interface {
248 // ListOpen returns the open merge requests of the repository, highest248 // ListOpen returns the open merge requests of the repository, highest
249 // number first, reading only the open ones (through an index).249 // number first, reading only the open ones (through an index).
250 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)250 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
251+ // CountOpen returns the number of open merge requests of the repository
252+ // from the index of keys: no request is read.
253+ CountOpen(ctx context.Context, repo domain.RepoName) (int, error)
251 // OpenBySource returns the numbers of the open merge requests whose source254 // OpenBySource returns the numbers of the open merge requests whose source
252 // branch is one of branches (short names), from an index of keys: a push255 // branch is one of branches (short names), from an index of keys: a push
253 // looks up the branches it moved, whatever the number of merge requests the256 // looks up the branches it moved, whatever the number of merge requests the
M internal/ports/services.go +5 -0
@@ -114,6 +114,11 @@ type MergeRequestService interface {
114114 Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)
115115 MarkRead(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, head domain.ObjectID) (domain.ObjectID, error)
116116 Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (MergeResult, error)
117+ // MarkReady takes a draft out of draft: its title loses its "WIP:" prefix.
118+ MarkReady(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)
119+ // CountOpen is the number of open merge requests of a repository the
120+ // principal can read. It reads no Git: the tab of every page shows it.
121+ CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error)
117122 }
118123
119124 // SigningKeyService tells which key signs the commits of the forge.
@@ -114,6 +114,11 @@ type MergeRequestService interface {
114 Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)114 Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)
115 MarkRead(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, head domain.ObjectID) (domain.ObjectID, error)115 MarkRead(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, head domain.ObjectID) (domain.ObjectID, error)
116 Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (MergeResult, error)116 Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (MergeResult, error)
117+ // MarkReady takes a draft out of draft: its title loses its "WIP:" prefix.
118+ MarkReady(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error)
119+ // CountOpen is the number of open merge requests of a repository the
120+ // principal can read. It reads no Git: the tab of every page shows it.
121+ CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error)
117 }122 }
118 123
119 // SigningKeyService tells which key signs the commits of the forge.124 // SigningKeyService tells which key signs the commits of the forge.
M internal/usecase/events.go +4 -0
@@ -174,6 +174,8 @@ func (e *Events) snapshot(ctx context.Context, ev domain.Event) bufferedEvent {
174174 return repoSnapshot(ev.Visibility, ev.CreatedAt)
175175 case domain.MergeRequestClosed:
176176 return repoSnapshot(ev.Visibility, ev.CreatedAt)
177+ case domain.MergeRequestReady:
178+ return repoSnapshot(ev.Visibility, ev.CreatedAt)
177179 case domain.CommentAdded:
178180 return repoSnapshot(ev.Visibility, ev.CreatedAt)
179181 case domain.RunUpdated:
@@ -481,6 +483,8 @@ func (e *Events) deliver(ctx context.Context, w *watch, be bufferedEvent, send f
481483 return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)
482484 case domain.MergeRequestClosed:
483485 return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)
486+ case domain.MergeRequestReady:
487+ return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)
484488 case domain.CommentAdded:
485489 return e.deliverRepo(ctx, w, ev.Repo, false, be, out, send)
486490 case domain.RunUpdated:
@@ -174,6 +174,8 @@ func (e *Events) snapshot(ctx context.Context, ev domain.Event) bufferedEvent {
174 return repoSnapshot(ev.Visibility, ev.CreatedAt)174 return repoSnapshot(ev.Visibility, ev.CreatedAt)
175 case domain.MergeRequestClosed:175 case domain.MergeRequestClosed:
176 return repoSnapshot(ev.Visibility, ev.CreatedAt)176 return repoSnapshot(ev.Visibility, ev.CreatedAt)
177+ case domain.MergeRequestReady:
178+ return repoSnapshot(ev.Visibility, ev.CreatedAt)
177 case domain.CommentAdded:179 case domain.CommentAdded:
178 return repoSnapshot(ev.Visibility, ev.CreatedAt)180 return repoSnapshot(ev.Visibility, ev.CreatedAt)
179 case domain.RunUpdated:181 case domain.RunUpdated:
@@ -481,6 +483,8 @@ func (e *Events) deliver(ctx context.Context, w *watch, be bufferedEvent, send f
481 return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)483 return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)
482 case domain.MergeRequestClosed:484 case domain.MergeRequestClosed:
483 return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)485 return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)
486+ case domain.MergeRequestReady:
487+ return e.deliverRepo(ctx, w, ev.MergeRequest.Repo, false, be, out, send)
484 case domain.CommentAdded:488 case domain.CommentAdded:
485 return e.deliverRepo(ctx, w, ev.Repo, false, be, out, send)489 return e.deliverRepo(ctx, w, ev.Repo, false, be, out, send)
486 case domain.RunUpdated:490 case domain.RunUpdated:
M internal/usecase/mergerequests.go +55 -0
@@ -333,6 +333,58 @@ func (m *MergeRequests) List(ctx context.Context, p domain.Principal, name domai
333333 return page, nil
334334 }
335335
336+// MarkReady implements ports.MergeRequestService: the title of an open draft
337+// loses its draft prefix, so the request can be merged. The principal needs
338+// mr:write. A request that is not open, or not a draft, gives
339+// domain.ErrPreconditionFailed.
340+func (m *MergeRequests) MarkReady(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error) {
341+ repo, err := m.writable(ctx, p, name, domain.ScopeMRWrite)
342+ if err != nil {
343+ return domain.MergeRequest{}, err
344+ }
345+ unlock, err := m.lockRepo(ctx, name) // a merge sees the request as it is after
346+ if err != nil {
347+ return domain.MergeRequest{}, err
348+ }
349+ defer unlock()
350+ for range maxWriteRetries {
351+ mr, err := m.store.Get(ctx, name, number)
352+ if err != nil {
353+ return domain.MergeRequest{}, err
354+ }
355+ if !mr.IsOpen() {
356+ return domain.MergeRequest{}, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State)
357+ }
358+ title, ok := domain.ReadyTitle(mr.Title)
359+ if !ok {
360+ return domain.MergeRequest{}, fmt.Errorf("%w: !%d is not a draft", domain.ErrPreconditionFailed, number)
361+ }
362+ mr.Title, mr.UpdatedAt = title, m.clock.Now().UTC()
363+ err = m.store.Update(ctx, mr)
364+ if errors.Is(err, domain.ErrConflict) {
365+ continue
366+ }
367+ if err != nil {
368+ return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, err)
369+ }
370+ m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
371+ return m.withHead(ctx, mr)
372+ }
373+ return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, domain.ErrConflict)
374+}
375+
376+// CountOpen implements ports.MergeRequestService.
377+func (m *MergeRequests) CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error) {
378+ if _, err := m.readable(ctx, p, name); err != nil {
379+ return 0, err
380+ }
381+ n, err := m.store.CountOpen(ctx, name)
382+ if err != nil {
383+ return 0, fmt.Errorf("count open merge requests of %q: %w", name, err)
384+ }
385+ return n, nil
386+}
387+
336388 func encodeMRToken(last domain.MergeRequest) string {
337389 return base64.RawURLEncoding.EncodeToString([]byte(mrTokenPrefix + strconv.FormatInt(last.Number, 10)))
338390 }
@@ -750,6 +802,9 @@ func (m *MergeRequests) merge(ctx context.Context, p domain.Principal, name doma
750802 if !mr.IsOpen() {
751803 return domain.MergeRequest{}, false, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State)
752804 }
805+ if mr.IsDraft() {
806+ return domain.MergeRequest{}, false, fmt.Errorf("%w: !%d is a draft: mark it ready for review first", domain.ErrPreconditionFailed, number)
807+ }
753808 // The settings are read again under the lock: a change applies at once.
754809 repo, err = m.meta.Get(ctx, name)
755810 if err != nil {
@@ -333,6 +333,58 @@ func (m *MergeRequests) List(ctx context.Context, p domain.Principal, name domai
333 return page, nil333 return page, nil
334 }334 }
335 335
336+// MarkReady implements ports.MergeRequestService: the title of an open draft
337+// loses its draft prefix, so the request can be merged. The principal needs
338+// mr:write. A request that is not open, or not a draft, gives
339+// domain.ErrPreconditionFailed.
340+func (m *MergeRequests) MarkReady(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.MergeRequest, error) {
341+ repo, err := m.writable(ctx, p, name, domain.ScopeMRWrite)
342+ if err != nil {
343+ return domain.MergeRequest{}, err
344+ }
345+ unlock, err := m.lockRepo(ctx, name) // a merge sees the request as it is after
346+ if err != nil {
347+ return domain.MergeRequest{}, err
348+ }
349+ defer unlock()
350+ for range maxWriteRetries {
351+ mr, err := m.store.Get(ctx, name, number)
352+ if err != nil {
353+ return domain.MergeRequest{}, err
354+ }
355+ if !mr.IsOpen() {
356+ return domain.MergeRequest{}, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State)
357+ }
358+ title, ok := domain.ReadyTitle(mr.Title)
359+ if !ok {
360+ return domain.MergeRequest{}, fmt.Errorf("%w: !%d is not a draft", domain.ErrPreconditionFailed, number)
361+ }
362+ mr.Title, mr.UpdatedAt = title, m.clock.Now().UTC()
363+ err = m.store.Update(ctx, mr)
364+ if errors.Is(err, domain.ErrConflict) {
365+ continue
366+ }
367+ if err != nil {
368+ return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, err)
369+ }
370+ m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
371+ return m.withHead(ctx, mr)
372+ }
373+ return domain.MergeRequest{}, fmt.Errorf("mark !%d of %q ready: %w", number, name, domain.ErrConflict)
374+}
375+
376+// CountOpen implements ports.MergeRequestService.
377+func (m *MergeRequests) CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error) {
378+ if _, err := m.readable(ctx, p, name); err != nil {
379+ return 0, err
380+ }
381+ n, err := m.store.CountOpen(ctx, name)
382+ if err != nil {
383+ return 0, fmt.Errorf("count open merge requests of %q: %w", name, err)
384+ }
385+ return n, nil
386+}
387+
336 func encodeMRToken(last domain.MergeRequest) string {388 func encodeMRToken(last domain.MergeRequest) string {
337 return base64.RawURLEncoding.EncodeToString([]byte(mrTokenPrefix + strconv.FormatInt(last.Number, 10)))389 return base64.RawURLEncoding.EncodeToString([]byte(mrTokenPrefix + strconv.FormatInt(last.Number, 10)))
338 }390 }
@@ -750,6 +802,9 @@ func (m *MergeRequests) merge(ctx context.Context, p domain.Principal, name doma
750 if !mr.IsOpen() {802 if !mr.IsOpen() {
751 return domain.MergeRequest{}, false, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State)803 return domain.MergeRequest{}, false, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State)
752 }804 }
805+ if mr.IsDraft() {
806+ return domain.MergeRequest{}, false, fmt.Errorf("%w: !%d is a draft: mark it ready for review first", domain.ErrPreconditionFailed, number)
807+ }
753 // The settings are read again under the lock: a change applies at once.808 // The settings are read again under the lock: a change applies at once.
754 repo, err = m.meta.Get(ctx, name)809 repo, err = m.meta.Get(ctx, name)
755 if err != nil {810 if err != nil {
M internal/usecase/mergerequests_integration_test.go +37 -0
@@ -889,6 +889,43 @@ func TestMergeFastForward(t *testing.T) {
889889 assert.ErrorIs(t, err, domain.ErrPreconditionFailed)
890890 }
891891
892+// A draft is not merged until it is marked ready; ready drops the prefix of
893+// the title, and only for an open draft. CountOpen counts drafts as open.
894+func TestDraftMergeRequest(t *testing.T) {
895+ r := newMRRig(t)
896+ r.seed()
897+ head := r.feature("feat")
898+ mr, err := r.mrs.Create(r.ctx, root, r.repo, ports.CreateMergeRequestParams{Title: "WIP: add feat", Source: "feat"})
899+ require.NoError(t, err)
900+
901+ _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, ports.MergeParams{HeadID: head})
902+ require.ErrorIs(t, err, domain.ErrPreconditionFailed)
903+ assert.Contains(t, err.Error(), "draft")
904+
905+ n, err := r.mrs.CountOpen(r.ctx, reader, r.repo)
906+ require.NoError(t, err)
907+ assert.Equal(t, 1, n)
908+
909+ _, err = r.mrs.MarkReady(r.ctx, reader, r.repo, mr.Number)
910+ require.Error(t, err, "mr:write is needed")
911+ r.drain()
912+ ready, err := r.mrs.MarkReady(r.ctx, mrW, r.repo, mr.Number)
913+ require.NoError(t, err)
914+ assert.Equal(t, "add feat", ready.Title.String())
915+ assert.False(t, ready.IsDraft())
916+ evs := r.drain()
917+ require.Len(t, evs, 1, "live pages learn the request is ready")
918+ assert.Equal(t, "add feat", evs[0].(domain.MergeRequestReady).MergeRequest.Title.String())
919+ _, err = r.mrs.MarkReady(r.ctx, mrW, r.repo, mr.Number)
920+ require.ErrorIs(t, err, domain.ErrPreconditionFailed, "not a draft any more")
921+
922+ _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, ports.MergeParams{HeadID: head})
923+ require.NoError(t, err)
924+ n, err = r.mrs.CountOpen(r.ctx, reader, r.repo)
925+ require.NoError(t, err)
926+ assert.Zero(t, n)
927+}
928+
892929 // AC4: the checks of a merge.
893930 func TestMergeRefusals(t *testing.T) {
894931 t.Run("a branch that moved since the review", func(t *testing.T) {
@@ -889,6 +889,43 @@ func TestMergeFastForward(t *testing.T) {
889 assert.ErrorIs(t, err, domain.ErrPreconditionFailed)889 assert.ErrorIs(t, err, domain.ErrPreconditionFailed)
890 }890 }
891 891
892+// A draft is not merged until it is marked ready; ready drops the prefix of
893+// the title, and only for an open draft. CountOpen counts drafts as open.
894+func TestDraftMergeRequest(t *testing.T) {
895+ r := newMRRig(t)
896+ r.seed()
897+ head := r.feature("feat")
898+ mr, err := r.mrs.Create(r.ctx, root, r.repo, ports.CreateMergeRequestParams{Title: "WIP: add feat", Source: "feat"})
899+ require.NoError(t, err)
900+
901+ _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, ports.MergeParams{HeadID: head})
902+ require.ErrorIs(t, err, domain.ErrPreconditionFailed)
903+ assert.Contains(t, err.Error(), "draft")
904+
905+ n, err := r.mrs.CountOpen(r.ctx, reader, r.repo)
906+ require.NoError(t, err)
907+ assert.Equal(t, 1, n)
908+
909+ _, err = r.mrs.MarkReady(r.ctx, reader, r.repo, mr.Number)
910+ require.Error(t, err, "mr:write is needed")
911+ r.drain()
912+ ready, err := r.mrs.MarkReady(r.ctx, mrW, r.repo, mr.Number)
913+ require.NoError(t, err)
914+ assert.Equal(t, "add feat", ready.Title.String())
915+ assert.False(t, ready.IsDraft())
916+ evs := r.drain()
917+ require.Len(t, evs, 1, "live pages learn the request is ready")
918+ assert.Equal(t, "add feat", evs[0].(domain.MergeRequestReady).MergeRequest.Title.String())
919+ _, err = r.mrs.MarkReady(r.ctx, mrW, r.repo, mr.Number)
920+ require.ErrorIs(t, err, domain.ErrPreconditionFailed, "not a draft any more")
921+
922+ _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, ports.MergeParams{HeadID: head})
923+ require.NoError(t, err)
924+ n, err = r.mrs.CountOpen(r.ctx, reader, r.repo)
925+ require.NoError(t, err)
926+ assert.Zero(t, n)
927+}
928+
892 // AC4: the checks of a merge.929 // AC4: the checks of a merge.
893 func TestMergeRefusals(t *testing.T) {930 func TestMergeRefusals(t *testing.T) {
894 t.Run("a branch that moved since the review", func(t *testing.T) {931 t.Run("a branch that moved since the review", func(t *testing.T) {

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 42

↑↓ moveenter openesc close