detached b5b4bf9

feat(web,cli): live mergeability in the merge box, merge modes and inline merge errors

commit
b5b4bf99dbfc084d3ae3141928c279c9e43dd162
author
brice · · 2026-10-19 14:23 UTC
committer
brice · · 2026-10-19 18:30 UTC
parent
fc9623b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
37 files · +2392 -849
M README.md +1 -1
@@ -122,7 +122,7 @@ ermite mr show myrepo 1 --since-last-read --mark-read
122122 **5. You merge** (or click Merge in the web interface):
123123
124124 ```sh
125-ermite mr merge myrepo 1 --head <head id> --squash --delete-branch
125+ermite mr merge myrepo 1 --head <head id> --mode squash --delete-branch
126126 ```
127127
128128 Give the agent the [`skills/ermite`](skills/ermite/SKILL.md) skill so it knows the CLI. Rules of agent tokens: [docs/tokens.md](docs/tokens.md).
M docs/cli.md +5 -4
@@ -54,10 +54,10 @@ ermite repo delete <name> [--yes]
5454 ermite token create <label> [--preset cli|agent|ci|runner] [--scope …] [--repo …] [--branch-prefix …] [--expires 30d]
5555 ermite token list | revoke <id>
5656
57-ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff]
57+ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff] # show prints the mergeability: up to date, behind, conflict
5858 ermite mr comment <repo> <n> -m … [--path … --line N|A-B --side old|new] [--reply <id>] [--approve|--request-changes]
5959 ermite mr resolve <repo> <n> <comment> | edit-comment <repo> <n> <comment> -m … | delete-comment <repo> <n> <comment>
60-ermite mr merge <repo> <n> --head <id> [--squash] [--delete-branch] | close <repo> <n>
60+ermite mr merge <repo> <n> --head <id> [--mode fast-forward|squash|merge] [--delete-branch] | close <repo> <n>
6161
6262 ermite issue create <repo> --title … [--body … | --body-file …] | list <repo> [--state open|closed|all]
6363 ermite issue show <repo> <n> | comment <repo> <n> -m … | close <repo> <n> | reopen <repo> <n>
@@ -78,7 +78,7 @@ ermite environment create|list|delete … --repo <repo>
7878 ermite web login # one-time sign-in link (60 s)
7979 ermite web logout --all # sign every browser out
8080 ermite web passkey list | remove <id>
81-ermite signing-key show # public key of squash commits
81+ermite signing-key show # public key of squash and merge commits (server-signed)
8282 ermite oidc rotate-key
8383 ermite watch [repo] [--json] # follow live events
8484 ermite version # client and server versions
@@ -90,7 +90,8 @@ Issues need a server with the `issues` feature and `ermite repo edit <repo> --is
9090 ## Output
9191
9292 - `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list` and `version`.
93-- `watch --json` prints one object per line.
93+- `watch --json` prints one object per line. `merge_request_mergeability` events carry `mergeability` (`state`, `behind`, `conflict_paths`…) and print as `repo!n mergeability: behind by 2`.
94+- `mr merge --mode squash|merge` creates a commit signed by the server's own key (generated on first start); the output names its fingerprint, `ermite signing-key show` prints the key. Every mode is allowed by default; `repo edit --merge-modes fast-forward,squash` disables `merge`.
9495 - Errors are one line on stderr (`ermite: …`) with exit status 1.
9596 - Shell completion completes repository names from the server.
9697
M docs/commit-signatures.md +1 -1
@@ -36,5 +36,5 @@ ermite signing-key trust remove <id>
3636
3737 - A key may sign for 1 to 10 emails; up to 100 keys.
3838 - The id is the fingerprint: `SHA256:…` for SSH, the primary key fingerprint for OpenPGP.
39-- The server's own key (`ermite signing-key show`), which signs squash merges, is always trusted for the committer email and cannot be removed.
39+- The server's own key (`ermite signing-key show`), which signs squash and merge commits (generated automatically on first start), is always trusted for the committer email and cannot be removed.
4040 - Private keys are refused.
M docs/configuration.md +2 -2
@@ -34,8 +34,8 @@ ermite serve
3434 |---|---|---|
3535 | `ERMITE_GIT` | `git` | git 2.45 or newer |
3636 | `ERMITE_GIT_READER` | `ermite-git` | `ermite-git` reads with the [ermite-git](https://github.com/bnema/ermite-git) library where it is faster, and falls back to `git`. `cli` uses `git` for every read: set it and restart if a read looks wrong |
37-| `ERMITE_COMMITTER_NAME` | `ermite` | committer of squash merges |
38-| `ERMITE_COMMITTER_EMAIL` | `ermite@<public host>` | committer email of squash merges |
37+| `ERMITE_COMMITTER_NAME` | `ermite` | committer of squash and merge commits |
38+| `ERMITE_COMMITTER_EMAIL` | `ermite@<public host>` | committer email of squash and merge commits |
3939
4040 ## CI
4141
M docs/merge-requests.md +45 -7
@@ -5,26 +5,64 @@ A merge request proposes a branch for a target (the default branch unless set).
55 ```sh
66 ermite mr create myrepo --source feature --title "Add feature"
77 ermite mr show myrepo 1 --diff
8-ermite mr merge myrepo 1 --head <commit> [--squash] [--delete-branch]
8+ermite mr merge myrepo 1 --head <commit> [--mode fast-forward|squash|merge] [--delete-branch]
99 ```
1010
1111 `ermite mr create` and `ermite mr close` print a confirmation followed by the full
1212 merge request web URL on its own line. With `--json`, merge request objects
1313 (create, show, list, close and merge) include `url` alongside the other fields.
1414
15-- **Fast-forward** moves the target. **Squash** writes one commit signed by the server's SSH key.
16-- The merge is refused when the branch moved since the reviewed `--head`, or when the target is not an ancestor of it.
15+## Merge modes
16+
17+| Mode | Result | Needs |
18+|---|---|---|
19+| `fast-forward` (default) | the target moves to the head, no commit | the target is an ancestor of the head |
20+| `squash` | one new commit with the merged tree, the target tip as only parent | the branch merges without conflicts |
21+| `merge` | a merge commit with two parents, the target tip and the head | the branch merges without conflicts |
22+
23+- The server never rebases. A branch behind its target merges with `squash` or `merge`, or is rebased and pushed again for a fast-forward.
24+- The server checks the mode again when it merges, whatever the badge showed, and refuses with the reason (the target moved, a conflict with the paths).
25+- The merge is also refused when the branch moved since the reviewed `--head`.
1726 - With `failing_checks_block` on (default), the merge is refused while a check of the head failed or is still running.
18-- `--delete-branch` deletes the source branch, unless it is the default branch or another open request uses it.
19-- `ermite repo edit --merge-modes` limits the modes per repository.
27+- The merge answers as soon as the target has moved. `--delete-branch` then deletes the source branch right after, in the background (unless it is the default branch or another open request uses it); if the branch moved meanwhile, it is kept.
28+- Every mode is allowed by default, including in repositories that had only `fast-forward` and `squash` before `merge` existed: those gain `merge`. Disable a mode per repository with `ermite repo edit <repo> --merge-modes fast-forward,squash` (list the modes to keep).
2029 - Scopes: `mr:write` to open and comment, `mr:review` to give a verdict, `mr:merge` to merge.
2130
22-Verify squash commits with the `allowed_signers` line from `ermite signing-key show`:
31+## Signed commits
32+
33+Squash and merge commits are signed by ermite's own key. The key is generated automatically the first time the server starts, belongs to the server, and its committer identity is `ERMITE_COMMITTER_NAME` / `ERMITE_COMMITTER_EMAIL`. `ermite signing-key show` prints its fingerprint and the `allowed_signers` line; use it to verify locally, or register the public key on GitHub or Forgejo.
34+
35+```sh
36+ermite signing-key show --allowed-signers > allowed_signers
37+git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=allowed_signers verify-commit <commit>
38+```
39+
40+The merge box of the web page and the output of `mr merge --mode squash|merge` repeat this with the fingerprint.
41+
42+## Mergeability
43+
44+The server checks in the background whether each open request merges with its target, and again whenever the source or the target moves. `mr show` prints it; the web merge box shows it as a live badge; `ermite watch` reports each change.
45+
46+| State | Meaning | Modes possible |
47+|---|---|---|
48+| `checking` | no result yet for the current branches | none (wait) |
49+| `up_to_date` | the target is an ancestor of the head | all |
50+| `behind` | the head lacks `behind` commits of the target (`behind_capped`: at least 1000) but merges without conflicts | `squash`, `merge` |
51+| `conflict` | the merge conflicts; up to 20 `conflict_paths` are listed (`conflict_paths_truncated` when there are more); no paths means no common history | none (rebase) |
2352
2453 ```sh
25-git verify-commit <commit>
54+ermite mr show myrepo 1
55+# mergeability behind by 2, merges without conflicts (squash or merge commit; fast-forward needs a rebase)
2656 ```
2757
58+In `mr show --json`, `mergeability` holds `state`, `behind`, `behind_capped`, `conflict_paths`, `conflict_paths_truncated`, `target_id` and `head_id`; it is `null` unless the request is open. `ermite watch` prints `merge_request_mergeability` events when the state changes (not on every push):
59+
60+```text
61+2026-10-19T14:00:00Z merge_request_mergeability myrepo!1 mergeability: behind by 2
62+```
63+
64+In the web merge box a mode that is not possible is disabled with its reason, and the Merge button too when no mode is possible. A merge the server refuses (target moved, conflict, branch moved) shows the page again with the reason in the box.
65+
2866 ## Notes
2967
3068 A note is the only review object. It is published as soon as it is written: there is no draft and no submit step.
M internal/adapters/web/events.go +6 -2
@@ -31,7 +31,7 @@ import (
3131 // GET /-/events?topic=repo:<name>&view=summary|refs
3232 // GET /-/events?topic=repo:<name>&view=commits&ref=<branch>&head=<sha>
3333 // GET /-/events?topic=repo:<name>&view=mrs
34-// GET /-/events?topic=repo:<name>&view=mr&n=<number>&ref=<source branch>
34+// GET /-/events?topic=repo:<name>&view=mr&n=<number>&ref=<source branch>&target=<target branch>
3535 // GET /-/events?topic=repo:<name>&view=issues
3636 // GET /-/events?topic=repo:<name>&view=issue&n=<number>
3737 // GET /-/events?topic=repo:<name>&view=actions
@@ -56,6 +56,10 @@ import (
5656 // refs-list data: <tbody id="ref-rows">…</tbody>, and again with
5757 // <dl id="refs-kv">…</dl>, the facts of the rail
5858 // commits-head data: <div id="commits-head">N new commits — reload</div>
59+// mr-mergeability data: <div id="mr-mergeability">…</div>, on the merge request pages:
60+// the badge of the merge box (a slot like mr-notice), when the
61+// mergeability of the request shown settles or a push moves its
62+// source or target
5963 // mr-notice data: <div id="mr-notice">…</div>, on the merge request and issue pages:
6064 // something changed, with the link that reloads the page
6165 // ci-run data: <tr id="run-<id>">…</tr> (the list of runs),
@@ -254,7 +258,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
254258 return
255259 }
256260
257- st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n"), job: q.Get("job"), sha: sha}
261+ st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n"), target: q.Get("target"), job: q.Get("job"), sha: sha}
258262 if topic == ownerTopic {
259263 st.filter = q.Get("repo")
260264 }
M internal/adapters/web/fragments.go +58 -8
@@ -28,6 +28,7 @@ type liveStream struct {
2828 head string // commits: the commit listed first
2929 n string // mr: the number of the merge request shown; run: the run id
3030 job string // job: the id of the job shown
31+ target string // mr: the target branch of the merge request shown
3132 // sha is the commit of the run of the job shown, read once when the stream
3233 // is set up: the annotations link to files at it, and it never changes.
3334 sha string
@@ -60,7 +61,7 @@ func liveEvent(we ports.WatchEvent) bool {
6061 }
6162 switch we.Event.(type) {
6263 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
63- domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
64+ domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
6465 domain.RunUpdated, domain.JobUpdated,
6566 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved:
6667 return true
@@ -188,6 +189,11 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
188189 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
189190 case domain.MergeRequestReady:
190191 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")
192+ case domain.MergeRequestMergeability:
193+ if l.view != "mr" || strconv.FormatInt(ev.Number, 10) != l.n {
194+ return nil, nil
195+ }
196+ return l.mrMergeability(ctx)
191197 case domain.CommentEdited:
192198 return l.mrNotice(ctx, ev.Number, "a note was edited — reload")
193199 case domain.CommentDeleted:
@@ -454,21 +460,65 @@ func (l *liveStream) issueNotice(ctx context.Context, number int64, text string)
454460 return nil, nil
455461 }
456462
457-// mrPushed announces a push to the source branch of the merge request shown.
463+// mrPushed announces a push to the source branch of the merge request shown,
464+// and has the badge follow a push to its source or its target.
458465 func (l *liveStream) mrPushed(ctx context.Context, p domain.PushEvent) ([]fragment, error) {
459- if l.ref == "" {
466+ n, err := strconv.ParseInt(l.n, 10, 64)
467+ if err != nil {
460468 return nil, nil
461469 }
470+ var out []fragment
471+ moved := false
462472 for _, u := range p.Updates {
463- if u.Ref == "refs/heads/"+l.ref {
464- n, err := strconv.ParseInt(l.n, 10, 64)
473+ switch {
474+ case l.ref != "" && u.Ref == "refs/heads/"+l.ref:
475+ notice, err := l.notice(ctx, "the branch changed — reload", mrHref(l.repo, n))
465476 if err != nil {
466- return nil, nil
477+ return nil, err
467478 }
468- return l.notice(ctx, "the branch changed — reload", mrHref(l.repo, n))
479+ out = append(out, notice...)
480+ moved = true
481+ case l.target != "" && u.Ref == "refs/heads/"+l.target:
482+ moved = true
483+ }
484+ if moved && len(out) > 0 {
485+ break
469486 }
470487 }
471- return nil, nil
488+ if !moved {
489+ return nil, nil
490+ }
491+ badge, err := l.mrMergeability(ctx)
492+ return append(out, badge...), err
493+}
494+
495+// mrMergeability is the badge of the merge box, read again: the result for the
496+// tips as they are now, "checking" until the server has one (it then sends the
497+// MergeRequestMergeability that turns the badge). Nothing for a request that
498+// is not open.
499+func (l *liveStream) mrMergeability(ctx context.Context) ([]fragment, error) {
500+ if !SignedIn(ctx) || l.s.cfg.MergeRequests == nil {
501+ return nil, nil // the box is the owner's
502+ }
503+ name, err := domain.ParseRepoName(l.repo)
504+ if err != nil {
505+ return nil, nil
506+ }
507+ n, err := strconv.ParseInt(l.n, 10, 64)
508+ if err != nil {
509+ return nil, nil
510+ }
511+ m, err := l.s.cfg.MergeRequests.Mergeability(ctx, principal(ctx), name, n)
512+ switch {
513+ case errors.Is(err, domain.ErrNotFound):
514+ return gap, nil
515+ case err != nil:
516+ return nil, err
517+ case m.State == "":
518+ return nil, nil
519+ }
520+ f, err := renderFragment(ctx, "mr-mergeability", MRMergeabilityFragment(mergeabilityView(m, l.target)))
521+ return []fragment{f}, err
472522 }
473523
474524 func (l *liveStream) notice(ctx context.Context, text, href string) ([]fragment, error) {
M internal/adapters/web/fragments.templ +1 -1
@@ -102,7 +102,7 @@ templ Live(l Layout) {
102102 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
103103 htmx:sse:error -> ermiteLive.error(event.detail.status);
104104 htmx:sse:before:connection -> ermiteLive.gate(event);
105- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
105+ home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
106106 gap -> ermiteLive.reload()"
107107 ></span>
108108 }
M internal/adapters/web/fragments_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/fragments_test.go +1 -0
@@ -33,6 +33,7 @@ func TestLiveSlotsAreStableStatusRegions(t *testing.T) {
3333 }{
3434 {"empty commits slot", LiveSlot("commits-head"), "commits-head", ""},
3535 {"empty mr slot", LiveSlot("mr-notice"), "mr-notice", ""},
36+ {"mergeability slot, as sent by the stream", MRMergeabilityFragment(MergeabilityView{State: "up_to_date", Label: "Ready to merge", FFOk: true, MergeOk: true}), "mr-mergeability", "Ready to merge"},
3637 {"commits banner", CommitsBanner("2 new commits — reload", "/x/commits/main"), "commits-head", "2 new commits — reload"},
3738 {"mr notice", MRNotice("a comment was added — reload", "/x/merge-requests/1"), "mr-notice", "a comment was added — reload"},
3839 } {
M internal/adapters/web/mr.go +149 -18
@@ -196,10 +196,38 @@ type MergeBox struct {
196196 // CanDeleteBranch offers to delete the source after the merge: it is not
197197 // the default branch. The server checks again, with the open requests.
198198 CanDeleteBranch bool
199+ // Mergeability is the live badge of the box and what the modes depend on.
200+ Mergeability MergeabilityView
201+ // Signed is true when a mode that creates a commit (squash, merge commit)
202+ // is offered; Fingerprint is then the one of the key that signs it, empty
203+ // when the server does not tell.
204+ Signed bool
205+ Fingerprint string
206+ // Error is why the last merge was refused, shown in the box.
207+ Error string
199208 }
200209
201-// ModeOption is a merge mode the repository allows.
202-type ModeOption struct{ Value, Label string }
210+// ModeOption is a merge mode the repository allows. Disabled says that the
211+// mode is not possible for the current state of the request, Reason why.
212+type ModeOption struct {
213+ Value, Label string
214+ Disabled bool
215+ Reason string
216+}
217+
218+// MergeabilityView is the mergeability of an open request, ready to show: the
219+// badge, and what the merge modes depend on (the flags are what the controls
220+// of the box read, live).
221+type MergeabilityView struct {
222+ State, Label, Detail string
223+ Paths []string
224+ // PathsNote says that more paths conflict than are listed.
225+ PathsNote string
226+ // FFOk and MergeOk say whether fast-forward, squash and merge commit are
227+ // possible; the reasons say why not.
228+ FFOk, MergeOk bool
229+ FFReason, MergeReason string
230+}
203231
204232 // SinceNav switches between the whole diff and the diff since the last read.
205233 type SinceNav struct {
@@ -233,6 +261,12 @@ type MRPage struct {
233261 MergedShort string
234262 MergedHref string
235263 MergeMode string
264+ // Deleting is the source branch that a merge just asked to delete: the
265+ // deletion follows the answer.
266+ Deleting string
267+ // MergeError is why a merge was refused when the page has no merge box
268+ // to show it in (the request is not open any more).
269+ MergeError string
236270
237271 Since *SinceNav
238272 SinceNote string
@@ -346,10 +380,59 @@ func modeName(m domain.MergeMode) string {
346380 return "fast-forward"
347381 case domain.MergeModeSquash:
348382 return "squash"
383+ case domain.MergeModeMerge:
384+ return "merge commit"
349385 }
350386 return ""
351387 }
352388
389+// mergeabilityView converts the mergeability of a request into the badge and
390+// the flags of the controls of the merge box. A request without result is
391+// being checked.
392+func mergeabilityView(m domain.Mergeability, target string) MergeabilityView {
393+ v := MergeabilityView{State: string(domain.MergeabilityChecking), Label: "Checking…"}
394+ v.FFOk, v.FFReason = m.Allows(domain.MergeModeFastForward)
395+ v.MergeOk, v.MergeReason = m.Allows(domain.MergeModeSquash)
396+ target = visibleText(target)
397+ switch m.State {
398+ case domain.MergeabilityUpToDate:
399+ v.State, v.Label = string(m.State), "Ready to merge"
400+ case domain.MergeabilityBehind:
401+ v.State, v.Label, v.Detail = string(m.State), "Behind "+target+" by "+behindText(m), "merges without conflicts"
402+ case domain.MergeabilityConflict:
403+ v.State, v.Label, v.Detail = string(m.State), "Conflicts with "+target, "rebase needed"
404+ v.Paths, v.PathsNote = conflictList(m)
405+ }
406+ return v
407+}
408+
409+// behindText is "2 commits", "1 commit" or "1000+ commits".
410+func behindText(m domain.Mergeability) string {
411+ if m.Behind == 1 && !m.BehindCapped {
412+ return "1 commit"
413+ }
414+ n := itoa(m.Behind)
415+ if m.BehindCapped {
416+ n += "+"
417+ }
418+ return n + " commits"
419+}
420+
421+// conflictList is the conflicting paths to show, and the note that says what
422+// the list lacks.
423+func conflictList(m domain.Mergeability) (paths []string, note string) {
424+ for _, p := range m.ConflictPaths {
425+ paths = append(paths, visibleText(p))
426+ }
427+ switch {
428+ case len(paths) == 0:
429+ note = "no common history."
430+ case m.ConflictPathsTruncated:
431+ note = "more files conflict than are listed."
432+ }
433+ return paths, note
434+}
435+
353436 // mrRow converts a merge request of a listing.
354437 func (s *Server) mrRow(repo string, m domain.MergeRequest) MRRow {
355438 row := MRRow{
@@ -540,6 +623,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
540623 p.MergedHref = repoHref(d.name(), "commit", m.MergedCommitID.String())
541624 p.MergeMode = modeName(m.MergeMode)
542625 }
626+ if signed && m.State == domain.MRStateMerged && r.URL.Query().Get("deleting") == "1" && d.hasBranch(m.Source) {
627+ p.Deleting = visibleText(m.Source)
628+ }
543629 for _, mode := range d.repo.Settings.AllowedMergeModes {
544630 if name := modeName(mode); name != "" {
545631 p.Modes = append(p.Modes, name)
@@ -624,11 +710,15 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
624710 p.Invisible = invisible
625711 p.DiffView = newDiffView(r, lines, withFrom(self+"?view=unified", from), withFrom(self+"?view=split", from))
626712 if signed && open {
627- p.Box = s.mergeBox(d, n, m.Source, head, lastRead)
713+ p.Box = s.mergeBox(d, n, m.Source, head, lastRead, mergeabilityView(res.Mergeability, m.Target))
714+ p.Box.Error = mergeErrorOf(ctx)
628715 if m.IsDraft() {
629716 p.Box.ReadyAction = mrHref(d.name(), n, "ready")
630717 }
631718 }
719+ if p.Box == nil {
720+ p.MergeError = mergeErrorOf(ctx) // the request is not open any more
721+ }
632722 if canWrite {
633723 p.GeneralForm = &CommentForm{
634724 ID: "cf-general", Action: mrHref(d.name(), n, "comments"), Title: "comment on the merge request",
@@ -652,7 +742,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
652742 l.Crumbs = append(l.Crumbs, Crumb{Label: "since", Current: true})
653743 }
654744 p.Live = true
655- 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))
745+ 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)+"&target="+url.QueryEscape(m.Target))
656746 l.HasRail = true
657747 l.MR = true
658748 return MergeRequest(l, p), http.StatusOK, nil
@@ -669,37 +759,78 @@ func (d *repoData) hasBranch(name string) bool {
669759 }
670760
671761 // mergeBox is what the owner can do with an open request.
672-func (s *Server) mergeBox(d *repoData, n int64, source, head, lastRead string) *MergeBox {
762+func (s *Server) mergeBox(d *repoData, n int64, source, head, lastRead string, mg MergeabilityView) *MergeBox {
673763 b := &MergeBox{
674764 MergeAction: mrHref(d.name(), n, "merge"), CloseAction: mrHref(d.name(), n, "close"),
675765 ReadAction: mrHref(d.name(), n, "read"), Head: head, HeadGone: head == "",
676766 CanMarkRead: head != "" && head != lastRead,
677767 CanDeleteBranch: source != d.repo.DefaultBranch,
768+ Mergeability: mg,
769+ }
770+ var lines []string
771+ cmd := ""
772+ if head != "" {
773+ cmd = "ermite mr merge " + shellWord(d.name()) + " " + strconv.FormatInt(n, 10) + " --head " + head
678774 }
679- ff, sq := false, false
680775 for _, mode := range d.repo.Settings.AllowedMergeModes {
681776 switch mode {
682777 case domain.MergeModeFastForward:
683- ff = true
684- b.Modes = append(b.Modes, ModeOption{Value: "fast-forward", Label: "fast-forward"})
778+ b.Modes = append(b.Modes, ModeOption{Value: "fast-forward", Label: "fast-forward", Disabled: !mg.FFOk, Reason: reasonIf(!mg.FFOk, mg.FFReason)})
779+ lines = append(lines, cmd)
685780 case domain.MergeModeSquash:
686- sq = true
687- b.Modes = append(b.Modes, ModeOption{Value: "squash", Label: "squash (one signed commit)"})
781+ b.Signed = true
782+ b.Modes = append(b.Modes, ModeOption{Value: "squash", Label: "squash (one signed commit)", Disabled: !mg.MergeOk, Reason: reasonIf(!mg.MergeOk, mg.MergeReason)})
783+ lines = append(lines, cmd+" --mode squash")
784+ case domain.MergeModeMerge:
785+ b.Signed = true
786+ b.Modes = append(b.Modes, ModeOption{Value: "merge", Label: "merge commit (signed)", Disabled: !mg.MergeOk, Reason: reasonIf(!mg.MergeOk, mg.MergeReason)})
787+ lines = append(lines, cmd+" --mode merge")
688788 }
689789 }
790+ if b.Signed && s.cfg.SigningKey != nil {
791+ b.Fingerprint = s.cfg.SigningKey.Info().Key.Fingerprint
792+ }
690793 if head != "" {
691- cmd := "ermite mr merge " + shellWord(d.name()) + " " + strconv.FormatInt(n, 10) + " --head " + head
692- b.CLI = Clone{ID: "cli-merge"}
693- if ff {
694- b.CLI.Lines = append(b.CLI.Lines, cmd)
695- }
696- if sq {
697- b.CLI.Lines = append(b.CLI.Lines, cmd+" --squash")
698- }
794+ b.CLI = Clone{ID: "cli-merge", Lines: lines}
699795 }
700796 return b
701797 }
702798
799+func reasonIf(cond bool, reason string) string {
800+ if cond {
801+ return reason
802+ }
803+ return ""
804+}
805+
806+// firstModeOK is the index of the first mode that is possible, -1 for none.
807+func (b *MergeBox) firstModeOK() int {
808+ for i, m := range b.Modes {
809+ if !m.Disabled {
810+ return i
811+ }
812+ }
813+ return -1
814+}
815+
816+// Checked is the mode the form has checked: the first that is possible, else
817+// the first (it is disabled, and a live update may enable it).
818+func (b *MergeBox) Checked() int { return max(b.firstModeOK(), 0) }
819+
820+// CanMerge tells whether some mode is possible: the button is enabled.
821+func (b *MergeBox) CanMerge() bool { return b.firstModeOK() >= 0 }
822+
823+// MergeTitle is why the button is disabled, empty when it is not.
824+func (b *MergeBox) MergeTitle() string {
825+ if b.CanMerge() {
826+ return ""
827+ }
828+ if len(b.Modes) == 0 {
829+ return "the repository allows no merge mode"
830+ }
831+ return b.Modes[0].Reason
832+}
833+
703834 // shellWord quotes s for a shell unless it is made of safe characters, which
704835 // a repository name always is.
705836 func shellWord(s string) string {
M internal/adapters/web/mr.templ +89 -4
@@ -2,7 +2,10 @@
22
33 package web
44
5-import "strings"
5+import (
6+ "strconv"
7+ "strings"
8+)
69
710 // MRList is the list of the merge requests of a repository: the open ones,
811 // then the merged and closed ones.
@@ -145,6 +148,12 @@ templ MergeRequest(l Layout, p MRPage) {
145148 </article>
146149 }
147150 </section>
151+ if p.Deleting != "" {
152+ <p class="note" role="status">merged. the branch { p.Deleting } is being deleted.</p>
153+ }
154+ if p.MergeError != "" {
155+ <p class="d-err" role="alert">{ p.MergeError }</p>
156+ }
148157 @LiveSlot("mr-notice")
149158 if p.Since != nil {
150159 <nav class="since" aria-label="Diff range">
@@ -446,8 +455,13 @@ templ mrBar(p MRPage) {
446455 // mergeMenu is the merge of an open request, for the owner: the form (it
447456 // carries the head that was read), or "Ready for review" for a draft. The
448457 // mode and the branch deletion have keys (1-3, D) while it is open.
458+//
459+// The badge is a live slot (mr-mergeability); the modes and the button read
460+// its data-* attributes (hx-live bindings), so that they follow the slot when
461+// the stream replaces its content. The server renders the same state in the
462+// attributes, for a page without scripts.
449463 templ mergeMenu(p MRPage) {
450- <details class="menu mmenu" id="merge-menu" data-state="open">
464+ <details class="menu mmenu" id="merge-menu" data-state="open" open?={ p.Box.Error != "" }>
451465 <summary class="btn primary" aria-keyshortcuts="Shift+M">
452466 if p.Box.ReadyAction != "" {
453467 Draft
@@ -469,19 +483,32 @@ templ mergeMenu(p MRPage) {
469483 <dt>into</dt>
470484 <dd>{ p.Target }</dd>
471485 </dl>
486+ if p.Box.Error != "" {
487+ <p class="d-err" role="alert">{ p.Box.Error }</p>
488+ }
472489 if p.Box.ReadyAction != "" {
473490 <p class="d-draft">this is a draft: it cannot be merged until it is ready for review.</p>
474491 <form class="d-btns" method="post" action={ templ.SafeURL(p.Box.ReadyAction) }>
475492 <button type="submit" class="btn primary" data-default title="remove the draft prefix from the title">Ready for review</button>
476493 </form>
477494 } else if !p.Box.HeadGone {
495+ @mrMergeabilitySlot(p.Box.Mergeability)
478496 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>
479497 <input type="hidden" name="head_id" value={ p.Box.Head }/>
480498 <fieldset class="d-modes">
481499 <legend class="sr">merge mode</legend>
482500 for i, m := range p.Box.Modes {
483501 <label>
484- <input type="radio" name="mode" value={ m.Value } checked?={ i == 0 }/>
502+ <input
503+ type="radio"
504+ name="mode"
505+ value={ m.Value }
506+ checked?={ i == p.Box.Checked() }
507+ disabled?={ m.Disabled }
508+ title={ m.Reason }
509+ :disabled="this.value === 'fast-forward' ? q('[data-mergeability]').data.ffOk !== true : q('[data-mergeability]').data.mergeOk !== true"
510+ :title="this.value === 'fast-forward' ? q('[data-mergeability]').data.ffReason : q('[data-mergeability]').data.mergeReason"
511+ />
485512 { m.Label }
486513 if i < 9 {
487514 <kbd aria-hidden="true">{ itoa(i + 1) }</kbd>
@@ -489,11 +516,28 @@ templ mergeMenu(p MRPage) {
489516 </label>
490517 }
491518 </fieldset>
519+ if p.Box.Signed {
520+ <p class="d-note" id="mr-signing">
521+ squash and merge commits are signed with ermite's own key, generated automatically on first start.
522+ if p.Box.Fingerprint != "" {
523+ { " " }key <code>{ p.Box.Fingerprint }</code>.
524+ }
525+ { " " }<code>ermite signing-key show</code> prints the public key to verify them.
526+ </p>
527+ }
492528 if p.Box.CanDeleteBranch {
493529 <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge <kbd aria-hidden="true">D</kbd></label>
494530 }
495531 <div class="d-btns">
496- <button type="submit" class="btn primary" data-default>Merge into { p.Target }</button>
532+ <button
533+ type="submit"
534+ class="btn primary"
535+ data-default
536+ disabled?={ !p.Box.CanMerge() }
537+ title={ p.Box.MergeTitle() }
538+ :disabled="!this.form.querySelector('input[name=mode]:checked:not(:disabled)')"
539+ :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''"
540+ >Merge into { p.Target }</button>
497541 </div>
498542 </form>
499543 }
@@ -501,6 +545,47 @@ templ mergeMenu(p MRPage) {
501545 </details>
502546 }
503547
548+// MRMergeability is the content of the mergeability slot: the badge, and the
549+// state the controls of the merge box read.
550+templ MRMergeability(v MergeabilityView) {
551+ <span
552+ data-mergeability
553+ data-state={ v.State }
554+ data-ff-ok={ strconv.FormatBool(v.FFOk) }
555+ data-merge-ok={ strconv.FormatBool(v.MergeOk) }
556+ data-ff-reason={ v.FFReason }
557+ data-merge-reason={ v.MergeReason }
558+ >
559+ <span class="badge" data-state={ v.State }>{ v.Label }</span>
560+ if v.Detail != "" {
561+ { " " }<span class="dim">{ v.Detail }</span>
562+ }
563+ </span>
564+ if len(v.Paths) > 0 {
565+ <ul class="mg-paths" aria-label="conflicting files">
566+ for _, path := range v.Paths {
567+ <li><code>{ path }</code></li>
568+ }
569+ </ul>
570+ }
571+ if v.PathsNote != "" {
572+ <p class="dim">{ v.PathsNote }</p>
573+ }
574+}
575+
576+// mrMergeabilitySlot is the status region of the badge, with its content: the
577+// stream replaces the children, never the element (see LiveSlot).
578+templ mrMergeabilitySlot(v MergeabilityView) {
579+ <div id="mr-mergeability" class="live-slot" role="status" data-slot>
580+ @MRMergeability(v)
581+ </div>
582+}
583+
584+// MRMergeabilityFragment is the slot as the stream sends it.
585+templ MRMergeabilityFragment(v MergeabilityView) {
586+ @mrMergeabilitySlot(v)
587+}
588+
504589 // moreMenu is what else the owner does with an open request: mark the head
505590 // read, close it, merge it from a terminal.
506591 templ moreMenu(p MRPage) {
M internal/adapters/web/mr_post.go +55 -3
@@ -60,6 +60,10 @@ func (s *Server) mrAction(h func(ctx context.Context, r *http.Request) (to strin
6060 return
6161 }
6262 to, back, err := h(ctx, r)
63+ var re *rerender
64+ if errors.As(err, &re) && s.rerenderMR(ctx, w, r, re) {
65+ return
66+ }
6367 if err != nil {
6468 s.actionFailed(ctx, w, r, err, back)
6569 return
@@ -68,6 +72,43 @@ func (s *Server) mrAction(h func(ctx context.Context, r *http.Request) (to strin
6872 }
6973 }
7074
75+// rerender is the error of a merge the use case refused for a reason the
76+// owner reads on the page of the request: the page is shown again, with the
77+// status, and the reason in its merge box. Any other error keeps actionFailed.
78+type rerender struct {
79+ status int
80+ msg string
81+ err error
82+}
83+
84+func (e *rerender) Error() string { return e.err.Error() }
85+func (e *rerender) Unwrap() error { return e.err }
86+
87+type mergeErrorKey struct{}
88+
89+// withMergeError carries the reason of a refused merge to the page.
90+func withMergeError(ctx context.Context, msg string) context.Context {
91+ return context.WithValue(ctx, mergeErrorKey{}, msg)
92+}
93+
94+func mergeErrorOf(ctx context.Context) string {
95+ msg, _ := ctx.Value(mergeErrorKey{}).(string)
96+ return msg
97+}
98+
99+// rerenderMR writes the page of the request with the reason of the refusal.
100+// It reports false when the page cannot be built; the caller then answers as
101+// for any refused action.
102+func (s *Server) rerenderMR(ctx context.Context, w http.ResponseWriter, r *http.Request, re *rerender) bool {
103+ c, _, err := s.mrPage(withMergeError(ctx, re.msg), r, false)
104+ if err != nil {
105+ return false
106+ }
107+ w.Header().Set("Cache-Control", "no-store")
108+ s.write(ctx, w, r, re.status, c)
109+ return true
110+}
111+
71112 // actionFailed answers a refused action.
72113 func (s *Server) actionFailed(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, back string) {
73114 // The words of the use case, for the rules the owner can fix.
@@ -322,8 +363,11 @@ func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string,
322363 mode = domain.MergeModeFastForward
323364 case "squash":
324365 mode = domain.MergeModeSquash
366+ case "merge":
367+ mode = domain.MergeModeMerge
325368 default:
326- return "", back, formError("mode must be fast-forward or squash")
369+ err := formError("mode must be fast-forward, squash or merge")
370+ return "", back, &rerender{status: http.StatusBadRequest, msg: err.Error(), err: err}
327371 }
328372 name, err := domain.ParseRepoName(repo)
329373 if err != nil {
@@ -333,11 +377,19 @@ func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string,
333377 if err != nil {
334378 return "", back, err
335379 }
336- if _, err := s.cfg.MergeRequests.Merge(ctx, principal(ctx), name, n, ports.MergeParams{
380+ res, err := s.cfg.MergeRequests.Merge(ctx, principal(ctx), name, n, ports.MergeParams{
337381 Mode: mode, HeadID: head, DeleteSource: r.PostFormValue("delete_branch") == "1",
338- }); err != nil {
382+ })
383+ switch {
384+ case errors.Is(err, domain.ErrPreconditionFailed), errors.Is(err, domain.ErrConflict):
385+ return "", back, &rerender{status: http.StatusConflict, msg: rule(err.Error(), "the merge request is not in a state that allows this."), err: err}
386+ case err != nil:
339387 return "", back, err
340388 }
389+ if res.SourceDeleted {
390+ // The deletion follows the answer: the page says it is being done.
391+ return back + "?deleting=1", back, nil
392+ }
341393 return back, back, nil
342394 }
343395
M internal/adapters/web/mr_templ.go +1184 -737
@@ -10,7 +10,10 @@ package web
1010 import "github.com/a-h/templ"
1111 import templruntime "github.com/a-h/templ/runtime"
1212
13-import "strings"
13+import (
14+ "strconv"
15+ "strings"
16+)
1417
1518 // MRList is the list of the merge requests of a repository: the open ones,
1619 // then the merged and closed ones.
@@ -65,7 +68,7 @@ func MRList(l Layout, p MRListPage) templ.Component {
6568 var templ_7745c5c3_Var3 string
6669 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Open)))
6770 if templ_7745c5c3_Err != nil {
68- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 26, Col: 78}
71+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 29, Col: 78}
6972 }
7073 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
7174 if templ_7745c5c3_Err != nil {
@@ -101,7 +104,7 @@ func MRList(l Layout, p MRListPage) templ.Component {
101104 var templ_7745c5c3_Var4 string
102105 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p.OpenNote)
103106 if templ_7745c5c3_Err != nil {
104- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 32, Col: 32}
107+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 35, Col: 32}
105108 }
106109 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
107110 if templ_7745c5c3_Err != nil {
@@ -120,7 +123,7 @@ func MRList(l Layout, p MRListPage) templ.Component {
120123 var templ_7745c5c3_Var5 string
121124 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Done)))
122125 if templ_7745c5c3_Err != nil {
123- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 35, Col: 90}
126+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 38, Col: 90}
124127 }
125128 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
126129 if templ_7745c5c3_Err != nil {
@@ -161,7 +164,7 @@ func MRList(l Layout, p MRListPage) templ.Component {
161164 var templ_7745c5c3_Var6 templ.SafeURL
162165 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest))
163166 if templ_7745c5c3_Err != nil {
164- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 43, Col: 50}
167+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 46, Col: 50}
165168 }
166169 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
167170 if templ_7745c5c3_Err != nil {
@@ -185,7 +188,7 @@ func MRList(l Layout, p MRListPage) templ.Component {
185188 var templ_7745c5c3_Var7 templ.SafeURL
186189 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older))
187190 if templ_7745c5c3_Err != nil {
188- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 48, Col: 49}
191+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 51, Col: 49}
189192 }
190193 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
191194 if templ_7745c5c3_Err != nil {
@@ -245,7 +248,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
245248 var templ_7745c5c3_Var9 string
246249 templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(label)
247250 if templ_7745c5c3_Err != nil {
248- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 59, Col: 44}
251+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 62, Col: 44}
249252 }
250253 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
251254 if templ_7745c5c3_Err != nil {
@@ -273,7 +276,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
273276 var templ_7745c5c3_Var10 string
274277 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.State)
275278 if templ_7745c5c3_Err != nil {
276- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 62, Col: 55}
279+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 55}
277280 }
278281 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
279282 if templ_7745c5c3_Err != nil {
@@ -286,7 +289,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
286289 var templ_7745c5c3_Var11 string
287290 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number)
288291 if templ_7745c5c3_Err != nil {
289- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 63, Col: 37}
292+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 37}
290293 }
291294 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
292295 if templ_7745c5c3_Err != nil {
@@ -299,7 +302,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
299302 var templ_7745c5c3_Var12 string
300303 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(m.State)
301304 if templ_7745c5c3_Err != nil {
302- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 64, Col: 35}
305+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 35}
303306 }
304307 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
305308 if templ_7745c5c3_Err != nil {
@@ -312,7 +315,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
312315 var templ_7745c5c3_Var13 templ.SafeURL
313316 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href))
314317 if templ_7745c5c3_Err != nil {
315- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 59}
318+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 59}
316319 }
317320 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
318321 if templ_7745c5c3_Err != nil {
@@ -325,7 +328,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
325328 var templ_7745c5c3_Var14 string
326329 templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
327330 if templ_7745c5c3_Err != nil {
328- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 71}
331+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 71}
329332 }
330333 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
331334 if templ_7745c5c3_Err != nil {
@@ -338,7 +341,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
338341 var templ_7745c5c3_Var15 string
339342 templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(m.Branches)
340343 if templ_7745c5c3_Err != nil {
341- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 109}
344+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 109}
342345 }
343346 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
344347 if templ_7745c5c3_Err != nil {
@@ -351,7 +354,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
351354 var templ_7745c5c3_Var16 string
352355 templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author)
353356 if templ_7745c5c3_Err != nil {
354- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 38}
357+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 38}
355358 }
356359 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
357360 if templ_7745c5c3_Err != nil {
@@ -369,7 +372,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
369372 var templ_7745c5c3_Var17 string
370373 templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
371374 if templ_7745c5c3_Err != nil {
372- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 33}
375+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 72, Col: 33}
373376 }
374377 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
375378 if templ_7745c5c3_Err != nil {
@@ -382,7 +385,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
382385 var templ_7745c5c3_Var18 string
383386 templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
384387 if templ_7745c5c3_Err != nil {
385- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 52}
388+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 72, Col: 52}
386389 }
387390 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
388391 if templ_7745c5c3_Err != nil {
@@ -395,7 +398,7 @@ func mrRows(rows []MRRow, label string) templ.Component {
395398 var templ_7745c5c3_Var19 string
396399 templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
397400 if templ_7745c5c3_Err != nil {
398- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 63}
401+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 72, Col: 63}
399402 }
400403 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
401404 if templ_7745c5c3_Err != nil {
@@ -453,7 +456,7 @@ func mrListRail(p MRListPage) templ.Component {
453456 var templ_7745c5c3_Var21 string
454457 templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(p.Default)
455458 if templ_7745c5c3_Err != nil {
456- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 94, Col: 51}
459+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 97, Col: 51}
457460 }
458461 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
459462 if templ_7745c5c3_Err != nil {
@@ -471,7 +474,7 @@ func mrListRail(p MRListPage) templ.Component {
471474 var templ_7745c5c3_Var22 templ.SafeURL
472475 templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
473476 if templ_7745c5c3_Err != nil {
474- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 97, Col: 36}
477+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 100, Col: 36}
475478 }
476479 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
477480 if templ_7745c5c3_Err != nil {
@@ -484,7 +487,7 @@ func mrListRail(p MRListPage) templ.Component {
484487 var templ_7745c5c3_Var23 string
485488 templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(b.Name)
486489 if templ_7745c5c3_Err != nil {
487- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 97, Col: 47}
490+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 100, Col: 47}
488491 }
489492 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
490493 if templ_7745c5c3_Err != nil {
@@ -560,7 +563,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
560563 var templ_7745c5c3_Var26 string
561564 templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
562565 if templ_7745c5c3_Err != nil {
563- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 37}
566+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 37}
564567 }
565568 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
566569 if templ_7745c5c3_Err != nil {
@@ -573,7 +576,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
573576 var templ_7745c5c3_Var27 string
574577 templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
575578 if templ_7745c5c3_Err != nil {
576- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 114, Col: 56}
579+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 56}
577580 }
578581 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
579582 if templ_7745c5c3_Err != nil {
@@ -586,7 +589,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
586589 var templ_7745c5c3_Var28 string
587590 templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)
588591 if templ_7745c5c3_Err != nil {
589- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 46}
592+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 46}
590593 }
591594 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
592595 if templ_7745c5c3_Err != nil {
@@ -599,7 +602,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
599602 var templ_7745c5c3_Var29 string
600603 templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
601604 if templ_7745c5c3_Err != nil {
602- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 118, Col: 15}
605+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 15}
603606 }
604607 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
605608 if templ_7745c5c3_Err != nil {
@@ -613,7 +616,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
613616 var templ_7745c5c3_Var30 string
614617 templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
615618 if templ_7745c5c3_Err != nil {
616- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 15}
619+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 15}
617620 }
618621 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
619622 if templ_7745c5c3_Err != nil {
@@ -626,7 +629,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
626629 var templ_7745c5c3_Var31 templ.SafeURL
627630 templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.MergedHref))
628631 if templ_7745c5c3_Err != nil {
629- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 54}
632+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 54}
630633 }
631634 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
632635 if templ_7745c5c3_Err != nil {
@@ -639,7 +642,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
639642 var templ_7745c5c3_Var32 string
640643 templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergedShort)
641644 if templ_7745c5c3_Err != nil {
642- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 72}
645+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 72}
643646 }
644647 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
645648 if templ_7745c5c3_Err != nil {
@@ -652,7 +655,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
652655 var templ_7745c5c3_Var33 string
653656 templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + p.MergeMode)
654657 if templ_7745c5c3_Err != nil {
655- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 100}
658+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 100}
656659 }
657660 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
658661 if templ_7745c5c3_Err != nil {
@@ -670,7 +673,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
670673 var templ_7745c5c3_Var34 string
671674 templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" → ")
672675 if templ_7745c5c3_Err != nil {
673- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 126, Col: 15}
676+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 129, Col: 15}
674677 }
675678 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
676679 if templ_7745c5c3_Err != nil {
@@ -687,7 +690,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
687690 var templ_7745c5c3_Var35 string
688691 templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)
689692 if templ_7745c5c3_Err != nil {
690- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 131, Col: 21}
693+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 21}
691694 }
692695 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
693696 if templ_7745c5c3_Err != nil {
@@ -701,7 +704,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
701704 var templ_7745c5c3_Var36 string
702705 templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
703706 if templ_7745c5c3_Err != nil {
704- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 15}
707+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 15}
705708 }
706709 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
707710 if templ_7745c5c3_Err != nil {
@@ -714,7 +717,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
714717 var templ_7745c5c3_Var37 string
715718 templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)
716719 if templ_7745c5c3_Err != nil {
717- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 49}
720+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 49}
718721 }
719722 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
720723 if templ_7745c5c3_Err != nil {
@@ -727,7 +730,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
727730 var templ_7745c5c3_Var38 string
728731 templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)
729732 if templ_7745c5c3_Err != nil {
730- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 72}
733+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 72}
731734 }
732735 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
733736 if templ_7745c5c3_Err != nil {
@@ -740,7 +743,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
740743 var templ_7745c5c3_Var39 string
741744 templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)
742745 if templ_7745c5c3_Err != nil {
743- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 133, Col: 90}
746+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 90}
744747 }
745748 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
746749 if templ_7745c5c3_Err != nil {
@@ -763,7 +766,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
763766 var templ_7745c5c3_Var40 string
764767 templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)
765768 if templ_7745c5c3_Err != nil {
766- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 38}
769+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 141, Col: 38}
767770 }
768771 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
769772 if templ_7745c5c3_Err != nil {
@@ -776,7 +779,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
776779 var templ_7745c5c3_Var41 string
777780 templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)
778781 if templ_7745c5c3_Err != nil {
779- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 52}
782+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 141, Col: 52}
780783 }
781784 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
782785 if templ_7745c5c3_Err != nil {
@@ -809,129 +812,175 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
809812 if templ_7745c5c3_Err != nil {
810813 return templ_7745c5c3_Err
811814 }
815+ if p.Deleting != "" {
816+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<p class=\"note\" role=\"status\">merged. the branch ")
817+ if templ_7745c5c3_Err != nil {
818+ return templ_7745c5c3_Err
819+ }
820+ var templ_7745c5c3_Var42 string
821+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(p.Deleting)
822+ if templ_7745c5c3_Err != nil {
823+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 152, Col: 64}
824+ }
825+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
826+ if templ_7745c5c3_Err != nil {
827+ return templ_7745c5c3_Err
828+ }
829+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " is being deleted.</p>")
830+ if templ_7745c5c3_Err != nil {
831+ return templ_7745c5c3_Err
832+ }
833+ }
834+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, " ")
835+ if templ_7745c5c3_Err != nil {
836+ return templ_7745c5c3_Err
837+ }
838+ if p.MergeError != "" {
839+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"d-err\" role=\"alert\">")
840+ if templ_7745c5c3_Err != nil {
841+ return templ_7745c5c3_Err
842+ }
843+ var templ_7745c5c3_Var43 string
844+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergeError)
845+ if templ_7745c5c3_Err != nil {
846+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 155, Col: 47}
847+ }
848+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
849+ if templ_7745c5c3_Err != nil {
850+ return templ_7745c5c3_Err
851+ }
852+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</p>")
853+ if templ_7745c5c3_Err != nil {
854+ return templ_7745c5c3_Err
855+ }
856+ }
857+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ")
858+ if templ_7745c5c3_Err != nil {
859+ return templ_7745c5c3_Err
860+ }
812861 templ_7745c5c3_Err = LiveSlot("mr-notice").Render(ctx, templ_7745c5c3_Buffer)
813862 if templ_7745c5c3_Err != nil {
814863 return templ_7745c5c3_Err
815864 }
816- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ")
865+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " ")
817866 if templ_7745c5c3_Err != nil {
818867 return templ_7745c5c3_Err
819868 }
820869 if p.Since != nil {
821- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<nav class=\"since\" aria-label=\"Diff range\"><a href=\"")
870+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<nav class=\"since\" aria-label=\"Diff range\"><a href=\"")
822871 if templ_7745c5c3_Err != nil {
823872 return templ_7745c5c3_Err
824873 }
825- var templ_7745c5c3_Var42 templ.SafeURL
826- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref))
874+ var templ_7745c5c3_Var44 templ.SafeURL
875+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref))
827876 if templ_7745c5c3_Err != nil {
828- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 152, Col: 42}
877+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 161, Col: 42}
829878 }
830- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
879+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
831880 if templ_7745c5c3_Err != nil {
832881 return templ_7745c5c3_Err
833882 }
834- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\"")
883+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\"")
835884 if templ_7745c5c3_Err != nil {
836885 return templ_7745c5c3_Err
837886 }
838887 if !p.Since.Current {
839- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " aria-current=\"page\"")
888+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " aria-current=\"page\"")
840889 if templ_7745c5c3_Err != nil {
841890 return templ_7745c5c3_Err
842891 }
843892 }
844- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, ">all changes <span class=\"mute\">vs ")
893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, ">all changes <span class=\"mute\">vs ")
845894 if templ_7745c5c3_Err != nil {
846895 return templ_7745c5c3_Err
847896 }
848- var templ_7745c5c3_Var43 string
849- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
897+ var templ_7745c5c3_Var45 string
898+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
850899 if templ_7745c5c3_Err != nil {
851- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 156, Col: 49}
900+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 165, Col: 49}
852901 }
853- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
902+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
854903 if templ_7745c5c3_Err != nil {
855904 return templ_7745c5c3_Err
856905 }
857- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span></a> <a href=\"")
906+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</span></a> <a href=\"")
858907 if templ_7745c5c3_Err != nil {
859908 return templ_7745c5c3_Err
860909 }
861- var templ_7745c5c3_Var44 templ.SafeURL
862- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref))
910+ var templ_7745c5c3_Var46 templ.SafeURL
911+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref))
863912 if templ_7745c5c3_Err != nil {
864- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 158, Col: 44}
913+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 44}
865914 }
866- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
915+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
867916 if templ_7745c5c3_Err != nil {
868917 return templ_7745c5c3_Err
869918 }
870- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\"")
919+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\"")
871920 if templ_7745c5c3_Err != nil {
872921 return templ_7745c5c3_Err
873922 }
874923 if p.Since.Current {
875- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " aria-current=\"page\"")
924+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, " aria-current=\"page\"")
876925 if templ_7745c5c3_Err != nil {
877926 return templ_7745c5c3_Err
878927 }
879928 }
880- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, ">since your last read <span class=\"mute\">")
929+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, ">since your last read <span class=\"mute\">")
881930 if templ_7745c5c3_Err != nil {
882931 return templ_7745c5c3_Err
883932 }
884- var templ_7745c5c3_Var45 string
885- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort)
933+ var templ_7745c5c3_Var47 string
934+ templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort)
886935 if templ_7745c5c3_Err != nil {
887- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 162, Col: 64}
936+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 171, Col: 64}
888937 }
889- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
938+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
890939 if templ_7745c5c3_Err != nil {
891940 return templ_7745c5c3_Err
892941 }
893- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " → ")
942+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " → ")
894943 if templ_7745c5c3_Err != nil {
895944 return templ_7745c5c3_Err
896945 }
897- var templ_7745c5c3_Var46 string
898- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort)
946+ var templ_7745c5c3_Var48 string
947+ templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort)
899948 if templ_7745c5c3_Err != nil {
900- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 162, Col: 90}
949+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 171, Col: 90}
901950 }
902- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
951+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
903952 if templ_7745c5c3_Err != nil {
904953 return templ_7745c5c3_Err
905954 }
906- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span></a></nav>")
955+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span></a></nav>")
907956 if templ_7745c5c3_Err != nil {
908957 return templ_7745c5c3_Err
909958 }
910959 }
911- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, " ")
960+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, " ")
912961 if templ_7745c5c3_Err != nil {
913962 return templ_7745c5c3_Err
914963 }
915964 if p.SinceNote != "" {
916- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "<p class=\"notice\">")
965+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<p class=\"notice\">")
917966 if templ_7745c5c3_Err != nil {
918967 return templ_7745c5c3_Err
919968 }
920- var templ_7745c5c3_Var47 string
921- templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote)
969+ var templ_7745c5c3_Var49 string
970+ templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote)
922971 if templ_7745c5c3_Err != nil {
923- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 34}
972+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 175, Col: 34}
924973 }
925- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
974+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
926975 if templ_7745c5c3_Err != nil {
927976 return templ_7745c5c3_Err
928977 }
929- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</p>")
978+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</p>")
930979 if templ_7745c5c3_Err != nil {
931980 return templ_7745c5c3_Err
932981 }
933982 }
934- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, " ")
983+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " ")
935984 if templ_7745c5c3_Err != nil {
936985 return templ_7745c5c3_Err
937986 }
@@ -939,87 +988,87 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
939988 if templ_7745c5c3_Err != nil {
940989 return templ_7745c5c3_Err
941990 }
942- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " ")
991+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " ")
943992 if templ_7745c5c3_Err != nil {
944993 return templ_7745c5c3_Err
945994 }
946995 if p.Invisible {
947- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<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>")
996+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "<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>")
948997 if templ_7745c5c3_Err != nil {
949998 return templ_7745c5c3_Err
950999 }
9511000 }
952- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, " ")
1001+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " ")
9531002 if templ_7745c5c3_Err != nil {
9541003 return templ_7745c5c3_Err
9551004 }
9561005 if p.Note != "" {
957- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<p class=\"notice\">")
1006+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<p class=\"notice\">")
9581007 if templ_7745c5c3_Err != nil {
9591008 return templ_7745c5c3_Err
9601009 }
961- var templ_7745c5c3_Var48 string
962- templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
1010+ var templ_7745c5c3_Var50 string
1011+ templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
9631012 if templ_7745c5c3_Err != nil {
964- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 176, Col: 29}
1013+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 185, Col: 29}
9651014 }
966- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
1015+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
9671016 if templ_7745c5c3_Err != nil {
9681017 return templ_7745c5c3_Err
9691018 }
970- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</p>")
1019+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "</p>")
9711020 if templ_7745c5c3_Err != nil {
9721021 return templ_7745c5c3_Err
9731022 }
9741023 }
975- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " <div id=\"diffs\" data-mr")
1024+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, " <div id=\"diffs\" data-mr")
9761025 if templ_7745c5c3_Err != nil {
9771026 return templ_7745c5c3_Err
9781027 }
9791028 if p.Both {
980- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " data-diff=\"")
1029+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, " data-diff=\"")
9811030 if templ_7745c5c3_Err != nil {
9821031 return templ_7745c5c3_Err
9831032 }
984- var templ_7745c5c3_Var49 string
985- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
1033+ var templ_7745c5c3_Var51 string
1034+ templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
9861035 if templ_7745c5c3_Err != nil {
987- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 182, Col: 22}
1036+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 191, Col: 22}
9881037 }
989- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49)
1038+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
9901039 if templ_7745c5c3_Err != nil {
9911040 return templ_7745c5c3_Err
9921041 }
993- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\"")
1042+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\"")
9941043 if templ_7745c5c3_Err != nil {
9951044 return templ_7745c5c3_Err
9961045 }
9971046 } else {
998- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " data-view=\"")
1047+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " data-view=\"")
9991048 if templ_7745c5c3_Err != nil {
10001049 return templ_7745c5c3_Err
10011050 }
1002- var templ_7745c5c3_Var50 string
1003- templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
1051+ var templ_7745c5c3_Var52 string
1052+ templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
10041053 if templ_7745c5c3_Err != nil {
1005- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 184, Col: 22}
1054+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 193, Col: 22}
10061055 }
1007- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
1056+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52)
10081057 if templ_7745c5c3_Err != nil {
10091058 return templ_7745c5c3_Err
10101059 }
1011- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\"")
1060+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\"")
10121061 if templ_7745c5c3_Err != nil {
10131062 return templ_7745c5c3_Err
10141063 }
10151064 }
10161065 if p.Both && !p.Pinned {
1017- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
1066+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
10181067 if templ_7745c5c3_Err != nil {
10191068 return templ_7745c5c3_Err
10201069 }
10211070 }
1022- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, ">")
1071+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, ">")
10231072 if templ_7745c5c3_Err != nil {
10241073 return templ_7745c5c3_Err
10251074 }
@@ -1030,38 +1079,38 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
10301079 }
10311080 }
10321081 if len(p.Files) == 0 {
1033- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<p class=\"note\">no changed files.</p>")
1082+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<p class=\"note\">no changed files.</p>")
10341083 if templ_7745c5c3_Err != nil {
10351084 return templ_7745c5c3_Err
10361085 }
10371086 }
1038- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</div><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
1087+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "</div><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
10391088 if templ_7745c5c3_Err != nil {
10401089 return templ_7745c5c3_Err
10411090 }
1042- var templ_7745c5c3_Var51 string
1043- templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved))
1091+ var templ_7745c5c3_Var53 string
1092+ templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved))
10441093 if templ_7745c5c3_Err != nil {
1045- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 97}
1094+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 206, Col: 97}
10461095 }
1047- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
1096+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
10481097 if templ_7745c5c3_Err != nil {
10491098 return templ_7745c5c3_Err
10501099 }
1051- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " open / ")
1100+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, " open / ")
10521101 if templ_7745c5c3_Err != nil {
10531102 return templ_7745c5c3_Err
10541103 }
1055- var templ_7745c5c3_Var52 string
1056- templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments))
1104+ var templ_7745c5c3_Var54 string
1105+ templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments))
10571106 if templ_7745c5c3_Err != nil {
1058- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 126}
1107+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 206, Col: 126}
10591108 }
1060- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
1109+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
10611110 if templ_7745c5c3_Err != nil {
10621111 return templ_7745c5c3_Err
10631112 }
1064- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</span></h2>")
1113+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</span></h2>")
10651114 if templ_7745c5c3_Err != nil {
10661115 return templ_7745c5c3_Err
10671116 }
@@ -1077,17 +1126,17 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
10771126 return templ_7745c5c3_Err
10781127 }
10791128 }
1080- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " ")
1129+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, " ")
10811130 if templ_7745c5c3_Err != nil {
10821131 return templ_7745c5c3_Err
10831132 }
10841133 if p.NComments == 0 {
1085- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<p class=\"dim\">no comments yet.</p>")
1134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<p class=\"dim\">no comments yet.</p>")
10861135 if templ_7745c5c3_Err != nil {
10871136 return templ_7745c5c3_Err
10881137 }
10891138 }
1090- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " ")
1139+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, " ")
10911140 if templ_7745c5c3_Err != nil {
10921141 return templ_7745c5c3_Err
10931142 }
@@ -1123,49 +1172,49 @@ func branchLink(name, href string) templ.Component {
11231172 }()
11241173 }
11251174 ctx = templ.InitializeContext(ctx)
1126- templ_7745c5c3_Var53 := templ.GetChildren(ctx)
1127- if templ_7745c5c3_Var53 == nil {
1128- templ_7745c5c3_Var53 = templ.NopComponent
1175+ templ_7745c5c3_Var55 := templ.GetChildren(ctx)
1176+ if templ_7745c5c3_Var55 == nil {
1177+ templ_7745c5c3_Var55 = templ.NopComponent
11291178 }
11301179 ctx = templ.ClearChildren(ctx)
11311180 if href != "" {
1132- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "<a href=\"")
1181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<a href=\"")
11331182 if templ_7745c5c3_Err != nil {
11341183 return templ_7745c5c3_Err
11351184 }
1136- var templ_7745c5c3_Var54 templ.SafeURL
1137- templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
1185+ var templ_7745c5c3_Var56 templ.SafeURL
1186+ templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
11381187 if templ_7745c5c3_Err != nil {
1139- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 31}
1188+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 224, Col: 31}
11401189 }
1141- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
1190+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
11421191 if templ_7745c5c3_Err != nil {
11431192 return templ_7745c5c3_Err
11441193 }
1145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "\">")
1194+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\">")
11461195 if templ_7745c5c3_Err != nil {
11471196 return templ_7745c5c3_Err
11481197 }
1149- var templ_7745c5c3_Var55 string
1150- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(name)
1198+ var templ_7745c5c3_Var57 string
1199+ templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(name)
11511200 if templ_7745c5c3_Err != nil {
1152- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 40}
1201+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 224, Col: 40}
11531202 }
1154- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
1203+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
11551204 if templ_7745c5c3_Err != nil {
11561205 return templ_7745c5c3_Err
11571206 }
1158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</a>")
1207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</a>")
11591208 if templ_7745c5c3_Err != nil {
11601209 return templ_7745c5c3_Err
11611210 }
11621211 } else {
1163- var templ_7745c5c3_Var56 string
1164- templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(name)
1212+ var templ_7745c5c3_Var58 string
1213+ templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(name)
11651214 if templ_7745c5c3_Err != nil {
1166- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 8}
1215+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 226, Col: 8}
11671216 }
1168- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
1217+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
11691218 if templ_7745c5c3_Err != nil {
11701219 return templ_7745c5c3_Err
11711220 }
@@ -1193,9 +1242,9 @@ func lineNotes(notes []CommentView, form *CommentForm, suffix string) templ.Comp
11931242 }()
11941243 }
11951244 ctx = templ.InitializeContext(ctx)
1196- templ_7745c5c3_Var57 := templ.GetChildren(ctx)
1197- if templ_7745c5c3_Var57 == nil {
1198- templ_7745c5c3_Var57 = templ.NopComponent
1245+ templ_7745c5c3_Var59 := templ.GetChildren(ctx)
1246+ if templ_7745c5c3_Var59 == nil {
1247+ templ_7745c5c3_Var59 = templ.NopComponent
11991248 }
12001249 ctx = templ.ClearChildren(ctx)
12011250 for _, c := range notes {
@@ -1232,182 +1281,182 @@ func commentView(c CommentView, suffix string) templ.Component {
12321281 }()
12331282 }
12341283 ctx = templ.InitializeContext(ctx)
1235- templ_7745c5c3_Var58 := templ.GetChildren(ctx)
1236- if templ_7745c5c3_Var58 == nil {
1237- templ_7745c5c3_Var58 = templ.NopComponent
1284+ templ_7745c5c3_Var60 := templ.GetChildren(ctx)
1285+ if templ_7745c5c3_Var60 == nil {
1286+ templ_7745c5c3_Var60 = templ.NopComponent
12381287 }
12391288 ctx = templ.ClearChildren(ctx)
1240- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<div class=\"cmt\" id=\"")
1289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<div class=\"cmt\" id=\"")
12411290 if templ_7745c5c3_Err != nil {
12421291 return templ_7745c5c3_Err
12431292 }
1244- var templ_7745c5c3_Var59 string
1245- templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)
1293+ var templ_7745c5c3_Var61 string
1294+ templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)
12461295 if templ_7745c5c3_Err != nil {
1247- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 42}
1296+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 245, Col: 42}
12481297 }
1249- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
1298+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var61)
12501299 if templ_7745c5c3_Err != nil {
12511300 return templ_7745c5c3_Err
12521301 }
1253- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\" data-resolved=\"")
1302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "\" data-resolved=\"")
12541303 if templ_7745c5c3_Err != nil {
12551304 return templ_7745c5c3_Err
12561305 }
1257- var templ_7745c5c3_Var60 string
1258- templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))
1306+ var templ_7745c5c3_Var62 string
1307+ templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))
12591308 if templ_7745c5c3_Err != nil {
1260- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 81}
1309+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 245, Col: 81}
12611310 }
1262- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
1311+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
12631312 if templ_7745c5c3_Err != nil {
12641313 return templ_7745c5c3_Err
12651314 }
1266- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"")
1315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\"")
12671316 if templ_7745c5c3_Err != nil {
12681317 return templ_7745c5c3_Err
12691318 }
12701319 if c.Reply != nil {
1271- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " data-thread")
1320+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, " data-thread")
12721321 if templ_7745c5c3_Err != nil {
12731322 return templ_7745c5c3_Err
12741323 }
12751324 }
1276- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "><p class=\"c-head\"><span class=\"c-who\">")
1325+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "><p class=\"c-head\"><span class=\"c-who\">")
12771326 if templ_7745c5c3_Err != nil {
12781327 return templ_7745c5c3_Err
12791328 }
1280- var templ_7745c5c3_Var61 string
1281- templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
1329+ var templ_7745c5c3_Var63 string
1330+ templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
12821331 if templ_7745c5c3_Err != nil {
1283- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 33}
1332+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 33}
12841333 }
1285- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
1334+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
12861335 if templ_7745c5c3_Err != nil {
12871336 return templ_7745c5c3_Err
12881337 }
1289- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span> ")
1338+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</span> ")
12901339 if templ_7745c5c3_Err != nil {
12911340 return templ_7745c5c3_Err
12921341 }
12931342 if c.WhenAt != "" {
1294- var templ_7745c5c3_Var62 string
1295- templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1343+ var templ_7745c5c3_Var64 string
1344+ templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
12961345 if templ_7745c5c3_Err != nil {
1297- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 9}
1346+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 9}
12981347 }
1299- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
1348+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
13001349 if templ_7745c5c3_Err != nil {
13011350 return templ_7745c5c3_Err
13021351 }
1303- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<time class=\"mute\" datetime=\"")
1352+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<time class=\"mute\" datetime=\"")
13041353 if templ_7745c5c3_Err != nil {
13051354 return templ_7745c5c3_Err
13061355 }
1307- var templ_7745c5c3_Var63 string
1308- templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
1356+ var templ_7745c5c3_Var65 string
1357+ templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
13091358 if templ_7745c5c3_Err != nil {
1310- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 49}
1359+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 49}
13111360 }
1312- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
1361+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var65)
13131362 if templ_7745c5c3_Err != nil {
13141363 return templ_7745c5c3_Err
13151364 }
1316- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "\" title=\"")
1365+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\" title=\"")
13171366 if templ_7745c5c3_Err != nil {
13181367 return templ_7745c5c3_Err
13191368 }
1320- var templ_7745c5c3_Var64 string
1321- templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)
1369+ var templ_7745c5c3_Var66 string
1370+ templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)
13221371 if templ_7745c5c3_Err != nil {
1323- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 65}
1372+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 65}
13241373 }
1325- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
1374+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
13261375 if templ_7745c5c3_Err != nil {
13271376 return templ_7745c5c3_Err
13281377 }
1329- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\">")
1378+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\">")
13301379 if templ_7745c5c3_Err != nil {
13311380 return templ_7745c5c3_Err
13321381 }
1333- var templ_7745c5c3_Var65 string
1334- templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
1382+ var templ_7745c5c3_Var67 string
1383+ templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
13351384 if templ_7745c5c3_Err != nil {
1336- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 76}
1385+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 76}
13371386 }
1338- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
1387+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
13391388 if templ_7745c5c3_Err != nil {
13401389 return templ_7745c5c3_Err
13411390 }
1342- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "</time> ")
1391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</time> ")
13431392 if templ_7745c5c3_Err != nil {
13441393 return templ_7745c5c3_Err
13451394 }
13461395 }
13471396 if c.Resolved {
1348- var templ_7745c5c3_Var66 string
1349- templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1397+ var templ_7745c5c3_Var68 string
1398+ templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13501399 if templ_7745c5c3_Err != nil {
1351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 9}
1400+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 9}
13521401 }
1353- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
1402+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
13541403 if templ_7745c5c3_Err != nil {
13551404 return templ_7745c5c3_Err
13561405 }
1357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<span class=\"c-state\">resolved</span> ")
1406+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<span class=\"c-state\">resolved</span> ")
13581407 if templ_7745c5c3_Err != nil {
13591408 return templ_7745c5c3_Err
13601409 }
13611410 }
13621411 if c.Outdated {
1363- var templ_7745c5c3_Var67 string
1364- templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1412+ var templ_7745c5c3_Var69 string
1413+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13651414 if templ_7745c5c3_Err != nil {
1366- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 246, Col: 9}
1415+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 255, Col: 9}
13671416 }
1368- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
1417+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
13691418 if templ_7745c5c3_Err != nil {
13701419 return templ_7745c5c3_Err
13711420 }
1372- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")
1421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")
13731422 if templ_7745c5c3_Err != nil {
13741423 return templ_7745c5c3_Err
13751424 }
13761425 }
13771426 if c.Where != "" {
1378- var templ_7745c5c3_Var68 string
1379- templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1427+ var templ_7745c5c3_Var70 string
1428+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13801429 if templ_7745c5c3_Err != nil {
1381- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 9}
1430+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 9}
13821431 }
1383- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
1432+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
13841433 if templ_7745c5c3_Err != nil {
13851434 return templ_7745c5c3_Err
13861435 }
1387- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"c-where mute\">")
1436+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<span class=\"c-where mute\">")
13881437 if templ_7745c5c3_Err != nil {
13891438 return templ_7745c5c3_Err
13901439 }
1391- var templ_7745c5c3_Var69 string
1392- templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)
1440+ var templ_7745c5c3_Var71 string
1441+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)
13931442 if templ_7745c5c3_Err != nil {
1394- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 47}
1443+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 47}
13951444 }
1396- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
1445+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
13971446 if templ_7745c5c3_Err != nil {
13981447 return templ_7745c5c3_Err
13991448 }
1400- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</span>")
1449+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "</span>")
14011450 if templ_7745c5c3_Err != nil {
14021451 return templ_7745c5c3_Err
14031452 }
14041453 }
1405- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</p>")
1454+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</p>")
14061455 if templ_7745c5c3_Err != nil {
14071456 return templ_7745c5c3_Err
14081457 }
14091458 if len(c.Parts) > 0 {
1410- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"c-text\">")
1459+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<p class=\"c-text\">")
14111460 if templ_7745c5c3_Err != nil {
14121461 return templ_7745c5c3_Err
14131462 }
@@ -1415,24 +1464,24 @@ func commentView(c CommentView, suffix string) templ.Component {
14151464 if templ_7745c5c3_Err != nil {
14161465 return templ_7745c5c3_Err
14171466 }
1418- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</p>")
1467+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "</p>")
14191468 if templ_7745c5c3_Err != nil {
14201469 return templ_7745c5c3_Err
14211470 }
14221471 } else if c.Deleted {
14231472 if c.Verdict != "" {
1424- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<p class=\"mute\">text deleted</p>")
1473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "<p class=\"mute\">text deleted</p>")
14251474 if templ_7745c5c3_Err != nil {
14261475 return templ_7745c5c3_Err
14271476 }
14281477 } else {
1429- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<p class=\"mute\">deleted note</p>")
1478+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "<p class=\"mute\">deleted note</p>")
14301479 if templ_7745c5c3_Err != nil {
14311480 return templ_7745c5c3_Err
14321481 }
14331482 }
14341483 } else {
1435- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<div class=\"c-body prose\">")
1484+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "<div class=\"c-body prose\">")
14361485 if templ_7745c5c3_Err != nil {
14371486 return templ_7745c5c3_Err
14381487 }
@@ -1447,147 +1496,147 @@ func commentView(c CommentView, suffix string) templ.Component {
14471496 return templ_7745c5c3_Err
14481497 }
14491498 }
1450- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</div>")
1499+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "</div>")
14511500 if templ_7745c5c3_Err != nil {
14521501 return templ_7745c5c3_Err
14531502 }
14541503 }
14551504 if c.Edited {
1456- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<span class=\"mute\">edited</span> ")
1505+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "<span class=\"mute\">edited</span> ")
14571506 if templ_7745c5c3_Err != nil {
14581507 return templ_7745c5c3_Err
14591508 }
14601509 }
14611510 if c.Verdict != "" {
1462- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<span class=\"c-state\">")
1511+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "<span class=\"c-state\">")
14631512 if templ_7745c5c3_Err != nil {
14641513 return templ_7745c5c3_Err
14651514 }
1466- var templ_7745c5c3_Var70 string
1467- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict)
1515+ var templ_7745c5c3_Var72 string
1516+ templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict)
14681517 if templ_7745c5c3_Err != nil {
1469- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 36}
1518+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 36}
14701519 }
1471- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
1520+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72))
14721521 if templ_7745c5c3_Err != nil {
14731522 return templ_7745c5c3_Err
14741523 }
1475- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</span> ")
1524+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "</span> ")
14761525 if templ_7745c5c3_Err != nil {
14771526 return templ_7745c5c3_Err
14781527 }
14791528 }
14801529 if c.Range != "" {
1481- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "<span class=\"mute\">")
1530+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<span class=\"mute\">")
14821531 if templ_7745c5c3_Err != nil {
14831532 return templ_7745c5c3_Err
14841533 }
1485- var templ_7745c5c3_Var71 string
1486- templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range)
1534+ var templ_7745c5c3_Var73 string
1535+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range)
14871536 if templ_7745c5c3_Err != nil {
1488- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 31}
1537+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 31}
14891538 }
1490- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
1539+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
14911540 if templ_7745c5c3_Err != nil {
14921541 return templ_7745c5c3_Err
14931542 }
1494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</span> ")
1543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</span> ")
14951544 if templ_7745c5c3_Err != nil {
14961545 return templ_7745c5c3_Err
14971546 }
14981547 }
14991548 if c.Quote != "" {
1500- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
1549+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
15011550 if templ_7745c5c3_Err != nil {
15021551 return templ_7745c5c3_Err
15031552 }
1504- var templ_7745c5c3_Var72 string
1505- templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote)
1553+ var templ_7745c5c3_Var74 string
1554+ templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote)
15061555 if templ_7745c5c3_Err != nil {
1507- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 279, Col: 65}
1556+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 65}
15081557 }
1509- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
1558+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)
15101559 if templ_7745c5c3_Err != nil {
15111560 return templ_7745c5c3_Err
15121561 }
1513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "\">Quote</button> ")
1562+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "\">Quote</button> ")
15141563 if templ_7745c5c3_Err != nil {
15151564 return templ_7745c5c3_Err
15161565 }
15171566 }
15181567 if c.Edit != nil {
1519- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "<details><summary>Edit</summary><form method=\"post\" action=\"")
1568+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "<details><summary>Edit</summary><form method=\"post\" action=\"")
15201569 if templ_7745c5c3_Err != nil {
15211570 return templ_7745c5c3_Err
15221571 }
1523- var templ_7745c5c3_Var73 templ.SafeURL
1524- templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action))
1572+ var templ_7745c5c3_Var75 templ.SafeURL
1573+ templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action))
15251574 if templ_7745c5c3_Err != nil {
1526- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 92}
1575+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 92}
15271576 }
1528- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1577+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
15291578 if templ_7745c5c3_Err != nil {
15301579 return templ_7745c5c3_Err
15311580 }
1532- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "\"><input type=\"hidden\" name=\"from\" value=\"")
1581+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "\"><input type=\"hidden\" name=\"from\" value=\"")
15331582 if templ_7745c5c3_Err != nil {
15341583 return templ_7745c5c3_Err
15351584 }
1536- var templ_7745c5c3_Var74 string
1537- templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From))
1585+ var templ_7745c5c3_Var76 string
1586+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From))
15381587 if templ_7745c5c3_Err != nil {
1539- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 153}
1588+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 153}
15401589 }
1541- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)
1590+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76)
15421591 if templ_7745c5c3_Err != nil {
15431592 return templ_7745c5c3_Err
15441593 }
1545- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "\"><textarea name=\"text\" maxlength=\"8000\">")
1594+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "\"><textarea name=\"text\" maxlength=\"8000\">")
15461595 if templ_7745c5c3_Err != nil {
15471596 return templ_7745c5c3_Err
15481597 }
1549- var templ_7745c5c3_Var75 string
1550- templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text)
1598+ var templ_7745c5c3_Var77 string
1599+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text)
15511600 if templ_7745c5c3_Err != nil {
1552- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 209}
1601+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 209}
15531602 }
1554- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
1603+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77))
15551604 if templ_7745c5c3_Err != nil {
15561605 return templ_7745c5c3_Err
15571606 }
1558- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
1607+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
15591608 if templ_7745c5c3_Err != nil {
15601609 return templ_7745c5c3_Err
15611610 }
15621611 }
15631612 if c.Delete != nil {
1564- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<details><summary>Delete</summary><form method=\"post\" action=\"")
1613+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "<details><summary>Delete</summary><form method=\"post\" action=\"")
15651614 if templ_7745c5c3_Err != nil {
15661615 return templ_7745c5c3_Err
15671616 }
1568- var templ_7745c5c3_Var76 templ.SafeURL
1569- templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action))
1617+ var templ_7745c5c3_Var78 templ.SafeURL
1618+ templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action))
15701619 if templ_7745c5c3_Err != nil {
1571- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 96}
1620+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 96}
15721621 }
1573- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
1622+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
15741623 if templ_7745c5c3_Err != nil {
15751624 return templ_7745c5c3_Err
15761625 }
1577- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "\"><input type=\"hidden\" name=\"from\" value=\"")
1626+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "\"><input type=\"hidden\" name=\"from\" value=\"")
15781627 if templ_7745c5c3_Err != nil {
15791628 return templ_7745c5c3_Err
15801629 }
1581- var templ_7745c5c3_Var77 string
1582- templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From))
1630+ var templ_7745c5c3_Var79 string
1631+ templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From))
15831632 if templ_7745c5c3_Err != nil {
1584- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 159}
1633+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 159}
15851634 }
1586- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
1635+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79)
15871636 if templ_7745c5c3_Err != nil {
15881637 return templ_7745c5c3_Err
15891638 }
1590- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "\"><p>Delete this note?</p><button class=\"btn\" type=\"submit\">Confirm delete</button></form></details> ")
1639+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "\"><p>Delete this note?</p><button class=\"btn\" type=\"submit\">Confirm delete</button></form></details> ")
15911640 if templ_7745c5c3_Err != nil {
15921641 return templ_7745c5c3_Err
15931642 }
@@ -1599,7 +1648,7 @@ func commentView(c CommentView, suffix string) templ.Component {
15991648 }
16001649 }
16011650 if c.Reply != nil {
1602- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<details data-reply><summary>Reply</summary>")
1651+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "<details data-reply><summary>Reply</summary>")
16031652 if templ_7745c5c3_Err != nil {
16041653 return templ_7745c5c3_Err
16051654 }
@@ -1607,80 +1656,80 @@ func commentView(c CommentView, suffix string) templ.Component {
16071656 if templ_7745c5c3_Err != nil {
16081657 return templ_7745c5c3_Err
16091658 }
1610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "</details> ")
1659+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</details> ")
16111660 if templ_7745c5c3_Err != nil {
16121661 return templ_7745c5c3_Err
16131662 }
16141663 }
16151664 if c.Resolve != nil {
1616- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "<form class=\"c-act\" method=\"post\" action=\"")
1665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<form class=\"c-act\" method=\"post\" action=\"")
16171666 if templ_7745c5c3_Err != nil {
16181667 return templ_7745c5c3_Err
16191668 }
1620- var templ_7745c5c3_Var78 templ.SafeURL
1621- templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
1669+ var templ_7745c5c3_Var80 templ.SafeURL
1670+ templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
16221671 if templ_7745c5c3_Err != nil {
1623- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 296, Col: 77}
1672+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 305, Col: 77}
16241673 }
1625- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
1674+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80))
16261675 if templ_7745c5c3_Err != nil {
16271676 return templ_7745c5c3_Err
16281677 }
1629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
1678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
16301679 if templ_7745c5c3_Err != nil {
16311680 return templ_7745c5c3_Err
16321681 }
1633- var templ_7745c5c3_Var79 string
1634- templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
1682+ var templ_7745c5c3_Var81 string
1683+ templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
16351684 if templ_7745c5c3_Err != nil {
1636- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 74}
1685+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 306, Col: 74}
16371686 }
1638- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79)
1687+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var81)
16391688 if templ_7745c5c3_Err != nil {
16401689 return templ_7745c5c3_Err
16411690 }
1642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "\"> ")
1691+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "\"> ")
16431692 if templ_7745c5c3_Err != nil {
16441693 return templ_7745c5c3_Err
16451694 }
16461695 if c.Resolve.From > 0 {
1647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<input type=\"hidden\" name=\"from\" value=\"")
1696+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "<input type=\"hidden\" name=\"from\" value=\"")
16481697 if templ_7745c5c3_Err != nil {
16491698 return templ_7745c5c3_Err
16501699 }
1651- var templ_7745c5c3_Var80 string
1652- templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
1700+ var templ_7745c5c3_Var82 string
1701+ templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
16531702 if templ_7745c5c3_Err != nil {
1654- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 299, Col: 66}
1703+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 308, Col: 66}
16551704 }
1656- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
1705+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)
16571706 if templ_7745c5c3_Err != nil {
16581707 return templ_7745c5c3_Err
16591708 }
1660- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "\"> ")
1709+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\"> ")
16611710 if templ_7745c5c3_Err != nil {
16621711 return templ_7745c5c3_Err
16631712 }
16641713 }
1665- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "<button type=\"submit\" class=\"btn\">")
1714+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "<button type=\"submit\" class=\"btn\">")
16661715 if templ_7745c5c3_Err != nil {
16671716 return templ_7745c5c3_Err
16681717 }
1669- var templ_7745c5c3_Var81 string
1670- templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
1718+ var templ_7745c5c3_Var83 string
1719+ templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
16711720 if templ_7745c5c3_Err != nil {
1672- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 301, Col: 55}
1721+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 310, Col: 55}
16731722 }
1674- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
1723+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
16751724 if templ_7745c5c3_Err != nil {
16761725 return templ_7745c5c3_Err
16771726 }
1678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "</button></form>")
1727+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</button></form>")
16791728 if templ_7745c5c3_Err != nil {
16801729 return templ_7745c5c3_Err
16811730 }
16821731 }
1683- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "</div>")
1732+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "</div>")
16841733 if templ_7745c5c3_Err != nil {
16851734 return templ_7745c5c3_Err
16861735 }
@@ -1712,214 +1761,214 @@ func commentForm(f CommentForm, suffix string) templ.Component {
17121761 }()
17131762 }
17141763 ctx = templ.InitializeContext(ctx)
1715- templ_7745c5c3_Var82 := templ.GetChildren(ctx)
1716- if templ_7745c5c3_Var82 == nil {
1717- templ_7745c5c3_Var82 = templ.NopComponent
1764+ templ_7745c5c3_Var84 := templ.GetChildren(ctx)
1765+ if templ_7745c5c3_Var84 == nil {
1766+ templ_7745c5c3_Var84 = templ.NopComponent
17181767 }
17191768 ctx = templ.ClearChildren(ctx)
1720- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "<form class=\"cform\" method=\"post\" action=\"")
1769+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "<form class=\"cform\" method=\"post\" action=\"")
17211770 if templ_7745c5c3_Err != nil {
17221771 return templ_7745c5c3_Err
17231772 }
1724- var templ_7745c5c3_Var83 templ.SafeURL
1725- templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
1773+ var templ_7745c5c3_Var85 templ.SafeURL
1774+ templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
17261775 if templ_7745c5c3_Err != nil {
1727- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 316, Col: 67}
1776+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 325, Col: 67}
17281777 }
1729- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
1778+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
17301779 if templ_7745c5c3_Err != nil {
17311780 return templ_7745c5c3_Err
17321781 }
1733- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\"><label for=\"")
1782+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "\"><label for=\"")
17341783 if templ_7745c5c3_Err != nil {
17351784 return templ_7745c5c3_Err
17361785 }
1737- var templ_7745c5c3_Var84 string
1738- templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1786+ var templ_7745c5c3_Var86 string
1787+ templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
17391788 if templ_7745c5c3_Err != nil {
1740- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 28}
1789+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 326, Col: 28}
17411790 }
1742- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84)
1791+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86)
17431792 if templ_7745c5c3_Err != nil {
17441793 return templ_7745c5c3_Err
17451794 }
1746- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\">")
1795+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "\">")
17471796 if templ_7745c5c3_Err != nil {
17481797 return templ_7745c5c3_Err
17491798 }
1750- var templ_7745c5c3_Var85 string
1751- templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
1799+ var templ_7745c5c3_Var87 string
1800+ templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
17521801 if templ_7745c5c3_Err != nil {
1753- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 40}
1802+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 326, Col: 40}
17541803 }
1755- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
1804+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87))
17561805 if templ_7745c5c3_Err != nil {
17571806 return templ_7745c5c3_Err
17581807 }
1759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "</label> <textarea id=\"")
1808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</label> <textarea id=\"")
17601809 if templ_7745c5c3_Err != nil {
17611810 return templ_7745c5c3_Err
17621811 }
1763- var templ_7745c5c3_Var86 string
1764- templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1812+ var templ_7745c5c3_Var88 string
1813+ templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
17651814 if templ_7745c5c3_Err != nil {
1766- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 21}
1815+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 328, Col: 21}
17671816 }
1768- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86)
1817+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88)
17691818 if templ_7745c5c3_Err != nil {
17701819 return templ_7745c5c3_Err
17711820 }
1772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "\" name=\"text\" rows=\"3\" maxlength=\"8000\"")
1821+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "\" name=\"text\" rows=\"3\" maxlength=\"8000\"")
17731822 if templ_7745c5c3_Err != nil {
17741823 return templ_7745c5c3_Err
17751824 }
17761825 if f.Plain {
1777- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, " required placeholder=\"plain text, 8000 characters at most\"")
1826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, " required placeholder=\"plain text, 8000 characters at most\"")
17781827 if templ_7745c5c3_Err != nil {
17791828 return templ_7745c5c3_Err
17801829 }
17811830 } else {
1782- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, " placeholder=\"Markdown, 8000 characters at most\"")
1831+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, " placeholder=\"Markdown, 8000 characters at most\"")
17831832 if templ_7745c5c3_Err != nil {
17841833 return templ_7745c5c3_Err
17851834 }
17861835 }
17871836 if f.Focus {
1788- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, " autofocus")
1837+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, " autofocus")
17891838 if templ_7745c5c3_Err != nil {
17901839 return templ_7745c5c3_Err
17911840 }
17921841 }
1793- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "></textarea> ")
1842+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "></textarea> ")
17941843 if templ_7745c5c3_Err != nil {
17951844 return templ_7745c5c3_Err
17961845 }
17971846 if f.Path != "" {
1798- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "<input type=\"hidden\" name=\"path\" value=\"")
1847+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "<input type=\"hidden\" name=\"path\" value=\"")
17991848 if templ_7745c5c3_Err != nil {
18001849 return templ_7745c5c3_Err
18011850 }
1802- var templ_7745c5c3_Var87 string
1803- templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
1851+ var templ_7745c5c3_Var89 string
1852+ templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
18041853 if templ_7745c5c3_Err != nil {
1805- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 50}
1854+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 343, Col: 50}
18061855 }
1807- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87)
1856+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89)
18081857 if templ_7745c5c3_Err != nil {
18091858 return templ_7745c5c3_Err
18101859 }
1811- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "\"> <input type=\"hidden\" name=\"line\" value=\"")
1860+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "\"> <input type=\"hidden\" name=\"line\" value=\"")
18121861 if templ_7745c5c3_Err != nil {
18131862 return templ_7745c5c3_Err
18141863 }
1815- var templ_7745c5c3_Var88 string
1816- templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
1864+ var templ_7745c5c3_Var90 string
1865+ templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
18171866 if templ_7745c5c3_Err != nil {
1818- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 50}
1867+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 344, Col: 50}
18191868 }
1820- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88)
1869+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
18211870 if templ_7745c5c3_Err != nil {
18221871 return templ_7745c5c3_Err
18231872 }
1824- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "\"> <input type=\"hidden\" name=\"side\" value=\"")
1873+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "\"> <input type=\"hidden\" name=\"side\" value=\"")
18251874 if templ_7745c5c3_Err != nil {
18261875 return templ_7745c5c3_Err
18271876 }
1828- var templ_7745c5c3_Var89 string
1829- templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
1877+ var templ_7745c5c3_Var91 string
1878+ templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
18301879 if templ_7745c5c3_Err != nil {
1831- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 50}
1880+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 345, Col: 50}
18321881 }
1833- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89)
1882+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91)
18341883 if templ_7745c5c3_Err != nil {
18351884 return templ_7745c5c3_Err
18361885 }
1837- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
1886+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
18381887 if templ_7745c5c3_Err != nil {
18391888 return templ_7745c5c3_Err
18401889 }
1841- var templ_7745c5c3_Var90 string
1842- templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine)
1890+ var templ_7745c5c3_Var92 string
1891+ templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine)
18431892 if templ_7745c5c3_Err != nil {
1844- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 337, Col: 86}
1893+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 346, Col: 86}
18451894 }
1846- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
1895+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
18471896 if templ_7745c5c3_Err != nil {
18481897 return templ_7745c5c3_Err
18491898 }
1850- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "\"></label> ")
1899+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "\"></label> ")
18511900 if templ_7745c5c3_Err != nil {
18521901 return templ_7745c5c3_Err
18531902 }
18541903 }
18551904 if f.ReplyTo != "" {
1856- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "<input type=\"hidden\" name=\"reply_to\" value=\"")
1905+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<input type=\"hidden\" name=\"reply_to\" value=\"")
18571906 if templ_7745c5c3_Err != nil {
18581907 return templ_7745c5c3_Err
18591908 }
1860- var templ_7745c5c3_Var91 string
1861- templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo)
1909+ var templ_7745c5c3_Var93 string
1910+ templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo)
18621911 if templ_7745c5c3_Err != nil {
1863- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 340, Col: 57}
1912+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 349, Col: 57}
18641913 }
1865- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91)
1914+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var93)
18661915 if templ_7745c5c3_Err != nil {
18671916 return templ_7745c5c3_Err
18681917 }
1869- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "\"> ")
1918+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"> ")
18701919 if templ_7745c5c3_Err != nil {
18711920 return templ_7745c5c3_Err
18721921 }
18731922 }
18741923 if f.Path == "" && f.ReplyTo == "" && !f.Plain {
1875- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "<fieldset><legend>Verdict</legend><label><input type=\"radio\" name=\"verdict\" value=\"\" checked>Note</label><label><input type=\"radio\" name=\"verdict\" value=\"approve\">Approve</label><label><input type=\"radio\" name=\"verdict\" value=\"changes_requested\">Request changes</label></fieldset>")
1924+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<fieldset><legend>Verdict</legend><label><input type=\"radio\" name=\"verdict\" value=\"\" checked>Note</label><label><input type=\"radio\" name=\"verdict\" value=\"approve\">Approve</label><label><input type=\"radio\" name=\"verdict\" value=\"changes_requested\">Request changes</label></fieldset>")
18761925 if templ_7745c5c3_Err != nil {
18771926 return templ_7745c5c3_Err
18781927 }
18791928 }
18801929 if f.From > 0 {
1881- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "<input type=\"hidden\" name=\"from\" value=\"")
1930+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<input type=\"hidden\" name=\"from\" value=\"")
18821931 if templ_7745c5c3_Err != nil {
18831932 return templ_7745c5c3_Err
18841933 }
1885- var templ_7745c5c3_Var92 string
1886- templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
1934+ var templ_7745c5c3_Var94 string
1935+ templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
18871936 if templ_7745c5c3_Err != nil {
1888- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 346, Col: 56}
1937+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 355, Col: 56}
18891938 }
1890- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
1939+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var94)
18911940 if templ_7745c5c3_Err != nil {
18921941 return templ_7745c5c3_Err
18931942 }
1894- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "\">")
1943+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\">")
18951944 if templ_7745c5c3_Err != nil {
18961945 return templ_7745c5c3_Err
18971946 }
18981947 }
1899- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
1948+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
19001949 if templ_7745c5c3_Err != nil {
19011950 return templ_7745c5c3_Err
19021951 }
19031952 if f.Cancel != "" {
1904- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<a class=\"btn\" href=\"")
1953+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "<a class=\"btn\" href=\"")
19051954 if templ_7745c5c3_Err != nil {
19061955 return templ_7745c5c3_Err
19071956 }
1908- var templ_7745c5c3_Var93 templ.SafeURL
1909- templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
1957+ var templ_7745c5c3_Var95 templ.SafeURL
1958+ templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
19101959 if templ_7745c5c3_Err != nil {
1911- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 351, Col: 49}
1960+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 360, Col: 49}
19121961 }
1913- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))
1962+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))
19141963 if templ_7745c5c3_Err != nil {
19151964 return templ_7745c5c3_Err
19161965 }
1917- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\">Cancel</a>")
1966+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "\">Cancel</a>")
19181967 if templ_7745c5c3_Err != nil {
19191968 return templ_7745c5c3_Err
19201969 }
19211970 }
1922- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "</div></form>")
1971+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "</div></form>")
19231972 if templ_7745c5c3_Err != nil {
19241973 return templ_7745c5c3_Err
19251974 }
@@ -1943,25 +1992,25 @@ func mrRail(p MRPage) templ.Component {
19431992 }()
19441993 }
19451994 ctx = templ.InitializeContext(ctx)
1946- templ_7745c5c3_Var94 := templ.GetChildren(ctx)
1947- if templ_7745c5c3_Var94 == nil {
1948- templ_7745c5c3_Var94 = templ.NopComponent
1995+ templ_7745c5c3_Var96 := templ.GetChildren(ctx)
1996+ if templ_7745c5c3_Var96 == nil {
1997+ templ_7745c5c3_Var96 = templ.NopComponent
19491998 }
19501999 ctx = templ.ClearChildren(ctx)
1951- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
2000+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
19522001 if templ_7745c5c3_Err != nil {
19532002 return templ_7745c5c3_Err
19542003 }
1955- var templ_7745c5c3_Var95 string
1956- templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
2004+ var templ_7745c5c3_Var97 string
2005+ templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
19572006 if templ_7745c5c3_Err != nil {
1958- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 360, Col: 49}
2007+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 369, Col: 49}
19592008 }
1960- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))
2009+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97))
19612010 if templ_7745c5c3_Err != nil {
19622011 return templ_7745c5c3_Err
19632012 }
1964- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "</span><kbd aria-hidden=\"true\">g f</kbd> <button type=\"button\" class=\"btn icon fold\" data-js aria-expanded=\"true\" title=\"fold the list of files\" hx-on:click=\"ermiteMR.foldFiles()\"><span class=\"ico i-chevron\" aria-hidden=\"true\"></span><span class=\"sr\">fold the list of files</span></button></h2>")
2013+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</span><kbd aria-hidden=\"true\">g f</kbd> <button type=\"button\" class=\"btn icon fold\" data-js aria-expanded=\"true\" title=\"fold the list of files\" hx-on:click=\"ermiteMR.foldFiles()\"><span class=\"ico i-chevron\" aria-hidden=\"true\"></span><span class=\"sr\">fold the list of files</span></button></h2>")
19652014 if templ_7745c5c3_Err != nil {
19662015 return templ_7745c5c3_Err
19672016 }
@@ -1969,71 +2018,71 @@ func mrRail(p MRPage) templ.Component {
19692018 if templ_7745c5c3_Err != nil {
19702019 return templ_7745c5c3_Err
19712020 }
1972- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "</section>")
2021+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "</section>")
19732022 if templ_7745c5c3_Err != nil {
19742023 return templ_7745c5c3_Err
19752024 }
19762025 if len(p.Verdicts) > 0 {
1977- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "<section><h2>Verdicts</h2><ul>")
2026+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "<section><h2>Verdicts</h2><ul>")
19782027 if templ_7745c5c3_Err != nil {
19792028 return templ_7745c5c3_Err
19802029 }
19812030 for _, v := range p.Verdicts {
1982- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<li><a href=\"")
2031+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<li><a href=\"")
19832032 if templ_7745c5c3_Err != nil {
19842033 return templ_7745c5c3_Err
19852034 }
1986- var templ_7745c5c3_Var96 templ.SafeURL
1987- templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
2035+ var templ_7745c5c3_Var98 templ.SafeURL
2036+ templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
19882037 if templ_7745c5c3_Err != nil {
1989- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 37}
2038+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 37}
19902039 }
1991- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
2040+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
19922041 if templ_7745c5c3_Err != nil {
19932042 return templ_7745c5c3_Err
19942043 }
1995- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "\">")
2044+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "\">")
19962045 if templ_7745c5c3_Err != nil {
19972046 return templ_7745c5c3_Err
19982047 }
1999- var templ_7745c5c3_Var97 string
2000- templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
2048+ var templ_7745c5c3_Var99 string
2049+ templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
20012050 if templ_7745c5c3_Err != nil {
2002- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 50}
2051+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 50}
20032052 }
2004- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97))
2053+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
20052054 if templ_7745c5c3_Err != nil {
20062055 return templ_7745c5c3_Err
20072056 }
2008- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, ": ")
2057+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, ": ")
20092058 if templ_7745c5c3_Err != nil {
20102059 return templ_7745c5c3_Err
20112060 }
2012- var templ_7745c5c3_Var98 string
2013- templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2061+ var templ_7745c5c3_Var100 string
2062+ templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
20142063 if templ_7745c5c3_Err != nil {
2015- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 63}
2064+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 63}
20162065 }
2017- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
2066+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
20182067 if templ_7745c5c3_Err != nil {
20192068 return templ_7745c5c3_Err
20202069 }
2021- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</a> ")
2070+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "</a> ")
20222071 if templ_7745c5c3_Err != nil {
20232072 return templ_7745c5c3_Err
20242073 }
20252074 if v.Stale {
2026- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<span class=\"mute\">(stale)</span>")
2075+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "<span class=\"mute\">(stale)</span>")
20272076 if templ_7745c5c3_Err != nil {
20282077 return templ_7745c5c3_Err
20292078 }
20302079 }
2031- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "</li>")
2080+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "</li>")
20322081 if templ_7745c5c3_Err != nil {
20332082 return templ_7745c5c3_Err
20342083 }
20352084 }
2036- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</ul></section>")
2085+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "</ul></section>")
20372086 if templ_7745c5c3_Err != nil {
20382087 return templ_7745c5c3_Err
20392088 }
@@ -2042,74 +2091,74 @@ func mrRail(p MRPage) templ.Component {
20422091 if templ_7745c5c3_Err != nil {
20432092 return templ_7745c5c3_Err
20442093 }
2045- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
2094+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
20462095 if templ_7745c5c3_Err != nil {
20472096 return templ_7745c5c3_Err
20482097 }
2049- var templ_7745c5c3_Var99 string
2050- templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
2098+ var templ_7745c5c3_Var101 string
2099+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
20512100 if templ_7745c5c3_Err != nil {
2052- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 384, Col: 71}
2101+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 393, Col: 71}
20532102 }
2054- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
2103+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
20552104 if templ_7745c5c3_Err != nil {
20562105 return templ_7745c5c3_Err
20572106 }
2058- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "</span></h2><ul class=\"pcommits\">")
2107+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</span></h2><ul class=\"pcommits\">")
20592108 if templ_7745c5c3_Err != nil {
20602109 return templ_7745c5c3_Err
20612110 }
20622111 for _, c := range p.Commits {
2063- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<li")
2112+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "<li")
20642113 if templ_7745c5c3_Err != nil {
20652114 return templ_7745c5c3_Err
20662115 }
20672116 if c.New {
2068- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, " data-new")
2117+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, " data-new")
20692118 if templ_7745c5c3_Err != nil {
20702119 return templ_7745c5c3_Err
20712120 }
20722121 }
2073- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "><a href=\"")
2122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "><a href=\"")
20742123 if templ_7745c5c3_Err != nil {
20752124 return templ_7745c5c3_Err
20762125 }
2077- var templ_7745c5c3_Var100 templ.SafeURL
2078- templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
2126+ var templ_7745c5c3_Var102 templ.SafeURL
2127+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
20792128 if templ_7745c5c3_Err != nil {
2080- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 36}
2129+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 397, Col: 36}
20812130 }
2082- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
2131+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
20832132 if templ_7745c5c3_Err != nil {
20842133 return templ_7745c5c3_Err
20852134 }
2086- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "\">")
2135+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "\">")
20872136 if templ_7745c5c3_Err != nil {
20882137 return templ_7745c5c3_Err
20892138 }
2090- var templ_7745c5c3_Var101 string
2091- templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
2139+ var templ_7745c5c3_Var103 string
2140+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
20922141 if templ_7745c5c3_Err != nil {
2093- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 48}
2142+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 397, Col: 48}
20942143 }
2095- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
2144+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
20962145 if templ_7745c5c3_Err != nil {
20972146 return templ_7745c5c3_Err
20982147 }
2099- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</a> <span class=\"s\">")
2148+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "</a> <span class=\"s\">")
21002149 if templ_7745c5c3_Err != nil {
21012150 return templ_7745c5c3_Err
21022151 }
2103- var templ_7745c5c3_Var102 string
2104- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
2152+ var templ_7745c5c3_Var104 string
2153+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
21052154 if templ_7745c5c3_Err != nil {
2106- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 389, Col: 32}
2155+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 398, Col: 32}
21072156 }
2108- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
2157+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
21092158 if templ_7745c5c3_Err != nil {
21102159 return templ_7745c5c3_Err
21112160 }
2112- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</span> ")
2161+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</span> ")
21132162 if templ_7745c5c3_Err != nil {
21142163 return templ_7745c5c3_Err
21152164 }
@@ -2119,138 +2168,138 @@ func mrRail(p MRPage) templ.Component {
21192168 return templ_7745c5c3_Err
21202169 }
21212170 }
2122- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "<span class=\"mute\">")
2171+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "<span class=\"mute\">")
21232172 if templ_7745c5c3_Err != nil {
21242173 return templ_7745c5c3_Err
21252174 }
2126- var templ_7745c5c3_Var103 string
2127- templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
2175+ var templ_7745c5c3_Var105 string
2176+ templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
21282177 if templ_7745c5c3_Err != nil {
2129- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 16}
2178+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 403, Col: 16}
21302179 }
2131- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
2180+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
21322181 if templ_7745c5c3_Err != nil {
21332182 return templ_7745c5c3_Err
21342183 }
2135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, " ")
2184+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, " ")
21362185 if templ_7745c5c3_Err != nil {
21372186 return templ_7745c5c3_Err
21382187 }
21392188 if c.WhenAt != "" {
2140- var templ_7745c5c3_Var104 string
2141- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
2189+ var templ_7745c5c3_Var106 string
2190+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
21422191 if templ_7745c5c3_Err != nil {
2143- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 15}
2192+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 405, Col: 15}
21442193 }
2145- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
2194+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
21462195 if templ_7745c5c3_Err != nil {
21472196 return templ_7745c5c3_Err
21482197 }
2149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "<time datetime=\"")
2198+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<time datetime=\"")
21502199 if templ_7745c5c3_Err != nil {
21512200 return templ_7745c5c3_Err
21522201 }
2153- var templ_7745c5c3_Var105 string
2154- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
2202+ var templ_7745c5c3_Var107 string
2203+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
21552204 if templ_7745c5c3_Err != nil {
2156- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 42}
2205+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 405, Col: 42}
21572206 }
2158- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105)
2207+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107)
21592208 if templ_7745c5c3_Err != nil {
21602209 return templ_7745c5c3_Err
21612210 }
2162- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "\">")
2211+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "\">")
21632212 if templ_7745c5c3_Err != nil {
21642213 return templ_7745c5c3_Err
21652214 }
2166- var templ_7745c5c3_Var106 string
2167- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
2215+ var templ_7745c5c3_Var108 string
2216+ templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
21682217 if templ_7745c5c3_Err != nil {
2169- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 53}
2218+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 405, Col: 53}
21702219 }
2171- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
2220+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
21722221 if templ_7745c5c3_Err != nil {
21732222 return templ_7745c5c3_Err
21742223 }
2175- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "</time> ")
2224+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</time> ")
21762225 if templ_7745c5c3_Err != nil {
21772226 return templ_7745c5c3_Err
21782227 }
21792228 }
21802229 if c.New {
2181- var templ_7745c5c3_Var107 string
2182- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
2230+ var templ_7745c5c3_Var109 string
2231+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
21832232 if templ_7745c5c3_Err != nil {
2184- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 399, Col: 18}
2233+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 18}
21852234 }
2186- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
2235+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
21872236 if templ_7745c5c3_Err != nil {
21882237 return templ_7745c5c3_Err
21892238 }
21902239 }
2191- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</span></li>")
2240+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "</span></li>")
21922241 if templ_7745c5c3_Err != nil {
21932242 return templ_7745c5c3_Err
21942243 }
21952244 }
2196- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</ul>")
2245+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "</ul>")
21972246 if templ_7745c5c3_Err != nil {
21982247 return templ_7745c5c3_Err
21992248 }
22002249 if len(p.Commits) == 0 {
2201- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"dim\">no commit to merge.</p>")
2250+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "<p class=\"dim\">no commit to merge.</p>")
22022251 if templ_7745c5c3_Err != nil {
22032252 return templ_7745c5c3_Err
22042253 }
22052254 }
22062255 if p.CommitsNote != "" {
2207- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<p class=\"note\">")
2256+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<p class=\"note\">")
22082257 if templ_7745c5c3_Err != nil {
22092258 return templ_7745c5c3_Err
22102259 }
2211- var templ_7745c5c3_Var108 string
2212- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
2260+ var templ_7745c5c3_Var110 string
2261+ templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
22132262 if templ_7745c5c3_Err != nil {
2214- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 409, Col: 34}
2263+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 34}
22152264 }
2216- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
2265+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))
22172266 if templ_7745c5c3_Err != nil {
22182267 return templ_7745c5c3_Err
22192268 }
2220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</p>")
2269+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "</p>")
22212270 if templ_7745c5c3_Err != nil {
22222271 return templ_7745c5c3_Err
22232272 }
22242273 }
2225- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
2274+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
22262275 if templ_7745c5c3_Err != nil {
22272276 return templ_7745c5c3_Err
22282277 }
2229- var templ_7745c5c3_Var109 string
2230- templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
2278+ var templ_7745c5c3_Var111 string
2279+ templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
22312280 if templ_7745c5c3_Err != nil {
2232- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 36}
2281+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 425, Col: 36}
22332282 }
2234- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
2283+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
22352284 if templ_7745c5c3_Err != nil {
22362285 return templ_7745c5c3_Err
22372286 }
2238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "</dd><dt>checks</dt><dd>")
2287+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</dd><dt>checks</dt><dd>")
22392288 if templ_7745c5c3_Err != nil {
22402289 return templ_7745c5c3_Err
22412290 }
22422291 if p.ChecksBlock {
2243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "failing ones block the merge")
2292+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "failing ones block the merge")
22442293 if templ_7745c5c3_Err != nil {
22452294 return templ_7745c5c3_Err
22462295 }
22472296 } else {
2248- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "do not block the merge")
2297+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "do not block the merge")
22492298 if templ_7745c5c3_Err != nil {
22502299 return templ_7745c5c3_Err
22512300 }
22522301 }
2253- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "</dd></dl></section>")
2302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "</dd></dl></section>")
22542303 if templ_7745c5c3_Err != nil {
22552304 return templ_7745c5c3_Err
22562305 }
@@ -2278,51 +2327,51 @@ func mrBar(p MRPage) templ.Component {
22782327 }()
22792328 }
22802329 ctx = templ.InitializeContext(ctx)
2281- templ_7745c5c3_Var110 := templ.GetChildren(ctx)
2282- if templ_7745c5c3_Var110 == nil {
2283- templ_7745c5c3_Var110 = templ.NopComponent
2330+ templ_7745c5c3_Var112 := templ.GetChildren(ctx)
2331+ if templ_7745c5c3_Var112 == nil {
2332+ templ_7745c5c3_Var112 = templ.NopComponent
22842333 }
22852334 ctx = templ.ClearChildren(ctx)
2286- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")
2335+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")
22872336 if templ_7745c5c3_Err != nil {
22882337 return templ_7745c5c3_Err
22892338 }
2290- var templ_7745c5c3_Var111 string
2291- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
2339+ var templ_7745c5c3_Var113 string
2340+ templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
22922341 if templ_7745c5c3_Err != nil {
2293- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 55}
2342+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 55}
22942343 }
2295- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2344+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
22962345 if templ_7745c5c3_Err != nil {
22972346 return templ_7745c5c3_Err
22982347 }
2299- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " · <span class=\"plus\">+")
2348+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, " · <span class=\"plus\">+")
23002349 if templ_7745c5c3_Err != nil {
23012350 return templ_7745c5c3_Err
23022351 }
2303- var templ_7745c5c3_Var112 string
2304- templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
2352+ var templ_7745c5c3_Var114 string
2353+ templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
23052354 if templ_7745c5c3_Err != nil {
2306- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 95}
2355+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 95}
23072356 }
2308- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
2357+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))
23092358 if templ_7745c5c3_Err != nil {
23102359 return templ_7745c5c3_Err
23112360 }
2312- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</span> <span class=\"minus\">-")
2361+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</span> <span class=\"minus\">-")
23132362 if templ_7745c5c3_Err != nil {
23142363 return templ_7745c5c3_Err
23152364 }
2316- var templ_7745c5c3_Var113 string
2317- templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
2365+ var templ_7745c5c3_Var115 string
2366+ templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
23182367 if templ_7745c5c3_Err != nil {
2319- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 140}
2368+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 140}
23202369 }
2321- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
2370+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
23222371 if templ_7745c5c3_Err != nil {
23232372 return templ_7745c5c3_Err
23242373 }
2325- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "</span></span>")
2374+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</span></span>")
23262375 if templ_7745c5c3_Err != nil {
23272376 return templ_7745c5c3_Err
23282377 }
@@ -2330,7 +2379,7 @@ func mrBar(p MRPage) templ.Component {
23302379 if templ_7745c5c3_Err != nil {
23312380 return templ_7745c5c3_Err
23322381 }
2333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "<span class=\"grow\"></span>")
2382+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "<span class=\"grow\"></span>")
23342383 if templ_7745c5c3_Err != nil {
23352384 return templ_7745c5c3_Err
23362385 }
@@ -2343,7 +2392,7 @@ func mrBar(p MRPage) templ.Component {
23432392 if templ_7745c5c3_Err != nil {
23442393 return templ_7745c5c3_Err
23452394 }
2346- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " ")
2395+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " ")
23472396 if templ_7745c5c3_Err != nil {
23482397 return templ_7745c5c3_Err
23492398 }
@@ -2352,7 +2401,7 @@ func mrBar(p MRPage) templ.Component {
23522401 return templ_7745c5c3_Err
23532402 }
23542403 }
2355- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "</div>")
2404+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</div>")
23562405 if templ_7745c5c3_Err != nil {
23572406 return templ_7745c5c3_Err
23582407 }
@@ -2363,6 +2412,11 @@ func mrBar(p MRPage) templ.Component {
23632412 // mergeMenu is the merge of an open request, for the owner: the form (it
23642413 // carries the head that was read), or "Ready for review" for a draft. The
23652414 // mode and the branch deletion have keys (1-3, D) while it is open.
2415+//
2416+// The badge is a live slot (mr-mergeability); the modes and the button read
2417+// its data-* attributes (hx-live bindings), so that they follow the slot when
2418+// the stream replaces its content. The server renders the same state in the
2419+// attributes, for a page without scripts.
23662420 func mergeMenu(p MRPage) templ.Component {
23672421 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
23682422 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -2379,219 +2433,612 @@ func mergeMenu(p MRPage) templ.Component {
23792433 }()
23802434 }
23812435 ctx = templ.InitializeContext(ctx)
2382- templ_7745c5c3_Var114 := templ.GetChildren(ctx)
2383- if templ_7745c5c3_Var114 == nil {
2384- templ_7745c5c3_Var114 = templ.NopComponent
2436+ templ_7745c5c3_Var116 := templ.GetChildren(ctx)
2437+ if templ_7745c5c3_Var116 == nil {
2438+ templ_7745c5c3_Var116 = templ.NopComponent
23852439 }
23862440 ctx = templ.ClearChildren(ctx)
2387- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
2441+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"")
2442+ if templ_7745c5c3_Err != nil {
2443+ return templ_7745c5c3_Err
2444+ }
2445+ if p.Box.Error != "" {
2446+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, " open")
2447+ if templ_7745c5c3_Err != nil {
2448+ return templ_7745c5c3_Err
2449+ }
2450+ }
2451+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
23882452 if templ_7745c5c3_Err != nil {
23892453 return templ_7745c5c3_Err
23902454 }
23912455 if p.Box.ReadyAction != "" {
2392- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "Draft ")
2456+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "Draft ")
23932457 if templ_7745c5c3_Err != nil {
23942458 return templ_7745c5c3_Err
23952459 }
23962460 } else {
2397- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "Merge ")
2461+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "Merge ")
23982462 if templ_7745c5c3_Err != nil {
23992463 return templ_7745c5c3_Err
24002464 }
24012465 }
2402- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>")
2466+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>")
24032467 if templ_7745c5c3_Err != nil {
24042468 return templ_7745c5c3_Err
24052469 }
24062470 if p.Box.HeadGone {
2407- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "gone: the source branch was deleted")
2471+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "gone: the source branch was deleted")
24082472 if templ_7745c5c3_Err != nil {
24092473 return templ_7745c5c3_Err
24102474 }
24112475 } else {
2412- var templ_7745c5c3_Var115 string
2413- templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
2476+ var templ_7745c5c3_Var117 string
2477+ templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
24142478 if templ_7745c5c3_Err != nil {
2415- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 466, Col: 19}
2479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 19}
24162480 }
2417- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
2481+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
24182482 if templ_7745c5c3_Err != nil {
24192483 return templ_7745c5c3_Err
24202484 }
24212485 }
2422- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</dd><dt>into</dt><dd>")
2486+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</dd><dt>into</dt><dd>")
24232487 if templ_7745c5c3_Err != nil {
24242488 return templ_7745c5c3_Err
24252489 }
2426- var templ_7745c5c3_Var116 string
2427- templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2490+ var templ_7745c5c3_Var118 string
2491+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
24282492 if templ_7745c5c3_Err != nil {
2429- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 18}
2493+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 18}
24302494 }
2431- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
2495+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
24322496 if templ_7745c5c3_Err != nil {
24332497 return templ_7745c5c3_Err
24342498 }
2435- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "</dd></dl>")
2499+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "</dd></dl>")
24362500 if templ_7745c5c3_Err != nil {
24372501 return templ_7745c5c3_Err
24382502 }
2503+ if p.Box.Error != "" {
2504+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "<p class=\"d-err\" role=\"alert\">")
2505+ if templ_7745c5c3_Err != nil {
2506+ return templ_7745c5c3_Err
2507+ }
2508+ var templ_7745c5c3_Var119 string
2509+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Error)
2510+ if templ_7745c5c3_Err != nil {
2511+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 47}
2512+ }
2513+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
2514+ if templ_7745c5c3_Err != nil {
2515+ return templ_7745c5c3_Err
2516+ }
2517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</p>")
2518+ if templ_7745c5c3_Err != nil {
2519+ return templ_7745c5c3_Err
2520+ }
2521+ }
24392522 if p.Box.ReadyAction != "" {
2440- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "<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=\"")
2523+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<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=\"")
24412524 if templ_7745c5c3_Err != nil {
24422525 return templ_7745c5c3_Err
24432526 }
2444- var templ_7745c5c3_Var117 templ.SafeURL
2445- templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
2527+ var templ_7745c5c3_Var120 templ.SafeURL
2528+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
24462529 if templ_7745c5c3_Err != nil {
2447- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 474, Col: 80}
2530+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 491, Col: 80}
24482531 }
2449- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
2532+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
24502533 if templ_7745c5c3_Err != nil {
24512534 return templ_7745c5c3_Err
24522535 }
2453- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")
2536+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")
24542537 if templ_7745c5c3_Err != nil {
24552538 return templ_7745c5c3_Err
24562539 }
24572540 } else if !p.Box.HeadGone {
2458- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<form class=\"d-acts\" method=\"post\" action=\"")
2541+ templ_7745c5c3_Err = mrMergeabilitySlot(p.Box.Mergeability).Render(ctx, templ_7745c5c3_Buffer)
2542+ if templ_7745c5c3_Err != nil {
2543+ return templ_7745c5c3_Err
2544+ }
2545+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, " <form class=\"d-acts\" method=\"post\" action=\"")
24592546 if templ_7745c5c3_Err != nil {
24602547 return templ_7745c5c3_Err
24612548 }
2462- var templ_7745c5c3_Var118 templ.SafeURL
2463- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2549+ var templ_7745c5c3_Var121 templ.SafeURL
2550+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
24642551 if templ_7745c5c3_Err != nil {
2465- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 478, Col: 80}
2552+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 80}
24662553 }
2467- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2554+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
24682555 if templ_7745c5c3_Err != nil {
24692556 return templ_7745c5c3_Err
24702557 }
2471- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2558+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
24722559 if templ_7745c5c3_Err != nil {
24732560 return templ_7745c5c3_Err
24742561 }
2475- var templ_7745c5c3_Var119 string
2476- templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2562+ var templ_7745c5c3_Var122 string
2563+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
24772564 if templ_7745c5c3_Err != nil {
2478- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 59}
2565+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 497, Col: 59}
24792566 }
2480- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var119)
2567+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122)
24812568 if templ_7745c5c3_Err != nil {
24822569 return templ_7745c5c3_Err
24832570 }
2484- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
2571+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
24852572 if templ_7745c5c3_Err != nil {
24862573 return templ_7745c5c3_Err
24872574 }
24882575 for i, m := range p.Box.Modes {
2489- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "<label><input type=\"radio\" name=\"mode\" value=\"")
2576+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "<label><input type=\"radio\" name=\"mode\" value=\"")
24902577 if templ_7745c5c3_Err != nil {
24912578 return templ_7745c5c3_Err
24922579 }
2493- var templ_7745c5c3_Var120 string
2494- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2580+ var templ_7745c5c3_Var123 string
2581+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
24952582 if templ_7745c5c3_Err != nil {
2496- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 55}
2583+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 505, Col: 24}
24972584 }
2498- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var120)
2585+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
24992586 if templ_7745c5c3_Err != nil {
25002587 return templ_7745c5c3_Err
25012588 }
2502- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "\"")
2589+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "\"")
25032590 if templ_7745c5c3_Err != nil {
25042591 return templ_7745c5c3_Err
25052592 }
2506- if i == 0 {
2507- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, " checked")
2593+ if i == p.Box.Checked() {
2594+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, " checked")
2595+ if templ_7745c5c3_Err != nil {
2596+ return templ_7745c5c3_Err
2597+ }
2598+ }
2599+ if m.Disabled {
2600+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, " disabled")
25082601 if templ_7745c5c3_Err != nil {
25092602 return templ_7745c5c3_Err
25102603 }
25112604 }
2512- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "> ")
2605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, " title=\"")
25132606 if templ_7745c5c3_Err != nil {
25142607 return templ_7745c5c3_Err
25152608 }
2516- var templ_7745c5c3_Var121 string
2517- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2609+ var templ_7745c5c3_Var124 string
2610+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)
25182611 if templ_7745c5c3_Err != nil {
2519- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 485, Col: 17}
2612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 508, Col: 25}
25202613 }
2521- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
25222615 if templ_7745c5c3_Err != nil {
25232616 return templ_7745c5c3_Err
25242617 }
2525- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, " ")
2618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "\" :disabled=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffOk !== true : q('[data-mergeability]').data.mergeOk !== true\" :title=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffReason : q('[data-mergeability]').data.mergeReason\"> ")
2619+ if templ_7745c5c3_Err != nil {
2620+ return templ_7745c5c3_Err
2621+ }
2622+ var templ_7745c5c3_Var125 string
2623+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2624+ if templ_7745c5c3_Err != nil {
2625+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 17}
2626+ }
2627+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
2628+ if templ_7745c5c3_Err != nil {
2629+ return templ_7745c5c3_Err
2630+ }
2631+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " ")
25262632 if templ_7745c5c3_Err != nil {
25272633 return templ_7745c5c3_Err
25282634 }
25292635 if i < 9 {
2530- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<kbd aria-hidden=\"true\">")
2636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "<kbd aria-hidden=\"true\">")
25312637 if templ_7745c5c3_Err != nil {
25322638 return templ_7745c5c3_Err
25332639 }
2534- var templ_7745c5c3_Var122 string
2535- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
2640+ var templ_7745c5c3_Var126 string
2641+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
25362642 if templ_7745c5c3_Err != nil {
2537- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 46}
2643+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 514, Col: 46}
25382644 }
2539- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2645+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
25402646 if templ_7745c5c3_Err != nil {
25412647 return templ_7745c5c3_Err
25422648 }
2543- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</kbd>")
2649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "</kbd>")
25442650 if templ_7745c5c3_Err != nil {
25452651 return templ_7745c5c3_Err
25462652 }
25472653 }
2548- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</label>")
2654+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "</label>")
25492655 if templ_7745c5c3_Err != nil {
25502656 return templ_7745c5c3_Err
25512657 }
25522658 }
2553- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</fieldset>")
2659+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "</fieldset>")
25542660 if templ_7745c5c3_Err != nil {
25552661 return templ_7745c5c3_Err
25562662 }
2663+ if p.Box.Signed {
2664+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "<p class=\"d-note\" id=\"mr-signing\">squash and merge commits are signed with ermite's own key, generated automatically on first start. ")
2665+ if templ_7745c5c3_Err != nil {
2666+ return templ_7745c5c3_Err
2667+ }
2668+ if p.Box.Fingerprint != "" {
2669+ var templ_7745c5c3_Var127 string
2670+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2671+ if templ_7745c5c3_Err != nil {
2672+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 523, Col: 13}
2673+ }
2674+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2675+ if templ_7745c5c3_Err != nil {
2676+ return templ_7745c5c3_Err
2677+ }
2678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "key <code>")
2679+ if templ_7745c5c3_Err != nil {
2680+ return templ_7745c5c3_Err
2681+ }
2682+ var templ_7745c5c3_Var128 string
2683+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
2684+ if templ_7745c5c3_Err != nil {
2685+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 523, Col: 44}
2686+ }
2687+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2688+ if templ_7745c5c3_Err != nil {
2689+ return templ_7745c5c3_Err
2690+ }
2691+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "</code>. ")
2692+ if templ_7745c5c3_Err != nil {
2693+ return templ_7745c5c3_Err
2694+ }
2695+ }
2696+ var templ_7745c5c3_Var129 string
2697+ templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2698+ if templ_7745c5c3_Err != nil {
2699+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 12}
2700+ }
2701+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var129))
2702+ if templ_7745c5c3_Err != nil {
2703+ return templ_7745c5c3_Err
2704+ }
2705+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "<code>ermite signing-key show</code> prints the public key to verify them.</p>")
2706+ if templ_7745c5c3_Err != nil {
2707+ return templ_7745c5c3_Err
2708+ }
2709+ }
25572710 if p.Box.CanDeleteBranch {
2558- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2711+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
25592712 if templ_7745c5c3_Err != nil {
25602713 return templ_7745c5c3_Err
25612714 }
2562- var templ_7745c5c3_Var123 string
2563- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2715+ var templ_7745c5c3_Var130 string
2716+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2717+ if templ_7745c5c3_Err != nil {
2718+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 102}
2719+ }
2720+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var130))
2721+ if templ_7745c5c3_Err != nil {
2722+ return templ_7745c5c3_Err
2723+ }
2724+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2725+ if templ_7745c5c3_Err != nil {
2726+ return templ_7745c5c3_Err
2727+ }
2728+ }
2729+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")
2730+ if templ_7745c5c3_Err != nil {
2731+ return templ_7745c5c3_Err
2732+ }
2733+ if !p.Box.CanMerge() {
2734+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, " disabled")
2735+ if templ_7745c5c3_Err != nil {
2736+ return templ_7745c5c3_Err
2737+ }
2738+ }
2739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, " title=\"")
2740+ if templ_7745c5c3_Err != nil {
2741+ return templ_7745c5c3_Err
2742+ }
2743+ var templ_7745c5c3_Var131 string
2744+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
2745+ if templ_7745c5c3_Err != nil {
2746+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 537, Col: 33}
2747+ }
2748+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)
2749+ if templ_7745c5c3_Err != nil {
2750+ return templ_7745c5c3_Err
2751+ }
2752+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "\" :disabled=\"!this.form.querySelector('input[name=mode]:checked:not(:disabled)')\" :title=\"(this.form.querySelector('input[name=mode]:checked') || {}).title || ''\">Merge into ")
2753+ if templ_7745c5c3_Err != nil {
2754+ return templ_7745c5c3_Err
2755+ }
2756+ var templ_7745c5c3_Var132 string
2757+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2758+ if templ_7745c5c3_Err != nil {
2759+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 540, Col: 28}
2760+ }
2761+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
2762+ if templ_7745c5c3_Err != nil {
2763+ return templ_7745c5c3_Err
2764+ }
2765+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</button></div></form>")
2766+ if templ_7745c5c3_Err != nil {
2767+ return templ_7745c5c3_Err
2768+ }
2769+ }
2770+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</div></details>")
2771+ if templ_7745c5c3_Err != nil {
2772+ return templ_7745c5c3_Err
2773+ }
2774+ return nil
2775+ })
2776+}
2777+
2778+// MRMergeability is the content of the mergeability slot: the badge, and the
2779+// state the controls of the merge box read.
2780+func MRMergeability(v MergeabilityView) templ.Component {
2781+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2782+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
2783+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
2784+ return templ_7745c5c3_CtxErr
2785+ }
2786+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
2787+ if !templ_7745c5c3_IsBuffer {
2788+ defer func() {
2789+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
2790+ if templ_7745c5c3_Err == nil {
2791+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
2792+ }
2793+ }()
2794+ }
2795+ ctx = templ.InitializeContext(ctx)
2796+ templ_7745c5c3_Var133 := templ.GetChildren(ctx)
2797+ if templ_7745c5c3_Var133 == nil {
2798+ templ_7745c5c3_Var133 = templ.NopComponent
2799+ }
2800+ ctx = templ.ClearChildren(ctx)
2801+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "<span data-mergeability data-state=\"")
2802+ if templ_7745c5c3_Err != nil {
2803+ return templ_7745c5c3_Err
2804+ }
2805+ var templ_7745c5c3_Var134 string
2806+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2807+ if templ_7745c5c3_Err != nil {
2808+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 553, Col: 22}
2809+ }
2810+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var134)
2811+ if templ_7745c5c3_Err != nil {
2812+ return templ_7745c5c3_Err
2813+ }
2814+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "\" data-ff-ok=\"")
2815+ if templ_7745c5c3_Err != nil {
2816+ return templ_7745c5c3_Err
2817+ }
2818+ var templ_7745c5c3_Var135 string
2819+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
2820+ if templ_7745c5c3_Err != nil {
2821+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 554, Col: 41}
2822+ }
2823+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
2824+ if templ_7745c5c3_Err != nil {
2825+ return templ_7745c5c3_Err
2826+ }
2827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\" data-merge-ok=\"")
2828+ if templ_7745c5c3_Err != nil {
2829+ return templ_7745c5c3_Err
2830+ }
2831+ var templ_7745c5c3_Var136 string
2832+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
2833+ if templ_7745c5c3_Err != nil {
2834+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 555, Col: 47}
2835+ }
2836+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136)
2837+ if templ_7745c5c3_Err != nil {
2838+ return templ_7745c5c3_Err
2839+ }
2840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\" data-ff-reason=\"")
2841+ if templ_7745c5c3_Err != nil {
2842+ return templ_7745c5c3_Err
2843+ }
2844+ var templ_7745c5c3_Var137 string
2845+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
2846+ if templ_7745c5c3_Err != nil {
2847+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 29}
2848+ }
2849+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137)
2850+ if templ_7745c5c3_Err != nil {
2851+ return templ_7745c5c3_Err
2852+ }
2853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\" data-merge-reason=\"")
2854+ if templ_7745c5c3_Err != nil {
2855+ return templ_7745c5c3_Err
2856+ }
2857+ var templ_7745c5c3_Var138 string
2858+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
2859+ if templ_7745c5c3_Err != nil {
2860+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 557, Col: 35}
2861+ }
2862+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2863+ if templ_7745c5c3_Err != nil {
2864+ return templ_7745c5c3_Err
2865+ }
2866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\"><span class=\"badge\" data-state=\"")
2867+ if templ_7745c5c3_Err != nil {
2868+ return templ_7745c5c3_Err
2869+ }
2870+ var templ_7745c5c3_Var139 string
2871+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2872+ if templ_7745c5c3_Err != nil {
2873+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 559, Col: 42}
2874+ }
2875+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var139)
2876+ if templ_7745c5c3_Err != nil {
2877+ return templ_7745c5c3_Err
2878+ }
2879+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "\">")
2880+ if templ_7745c5c3_Err != nil {
2881+ return templ_7745c5c3_Err
2882+ }
2883+ var templ_7745c5c3_Var140 string
2884+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2885+ if templ_7745c5c3_Err != nil {
2886+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 559, Col: 54}
2887+ }
2888+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2889+ if templ_7745c5c3_Err != nil {
2890+ return templ_7745c5c3_Err
2891+ }
2892+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "</span> ")
2893+ if templ_7745c5c3_Err != nil {
2894+ return templ_7745c5c3_Err
2895+ }
2896+ if v.Detail != "" {
2897+ var templ_7745c5c3_Var141 string
2898+ templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2899+ if templ_7745c5c3_Err != nil {
2900+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 561, Col: 8}
2901+ }
2902+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
2903+ if templ_7745c5c3_Err != nil {
2904+ return templ_7745c5c3_Err
2905+ }
2906+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "<span class=\"dim\">")
2907+ if templ_7745c5c3_Err != nil {
2908+ return templ_7745c5c3_Err
2909+ }
2910+ var templ_7745c5c3_Var142 string
2911+ templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
2912+ if templ_7745c5c3_Err != nil {
2913+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 561, Col: 38}
2914+ }
2915+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142))
2916+ if templ_7745c5c3_Err != nil {
2917+ return templ_7745c5c3_Err
2918+ }
2919+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span>")
2920+ if templ_7745c5c3_Err != nil {
2921+ return templ_7745c5c3_Err
2922+ }
2923+ }
2924+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "</span> ")
2925+ if templ_7745c5c3_Err != nil {
2926+ return templ_7745c5c3_Err
2927+ }
2928+ if len(v.Paths) > 0 {
2929+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")
2930+ if templ_7745c5c3_Err != nil {
2931+ return templ_7745c5c3_Err
2932+ }
2933+ for _, path := range v.Paths {
2934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<li><code>")
25642935 if templ_7745c5c3_Err != nil {
2565- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 493, Col: 102}
2936+ return templ_7745c5c3_Err
2937+ }
2938+ var templ_7745c5c3_Var143 string
2939+ templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(path)
2940+ if templ_7745c5c3_Err != nil {
2941+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 567, Col: 20}
25662942 }
2567- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
2943+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143))
25682944 if templ_7745c5c3_Err != nil {
25692945 return templ_7745c5c3_Err
25702946 }
2571- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2947+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</code></li>")
25722948 if templ_7745c5c3_Err != nil {
25732949 return templ_7745c5c3_Err
25742950 }
25752951 }
2576- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default>Merge into ")
2952+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "</ul>")
25772953 if templ_7745c5c3_Err != nil {
25782954 return templ_7745c5c3_Err
25792955 }
2580- var templ_7745c5c3_Var124 string
2581- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2956+ }
2957+ if v.PathsNote != "" {
2958+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "<p class=\"dim\">")
25822959 if templ_7745c5c3_Err != nil {
2583- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 82}
2960+ return templ_7745c5c3_Err
25842961 }
2585- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
2962+ var templ_7745c5c3_Var144 string
2963+ templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
2964+ if templ_7745c5c3_Err != nil {
2965+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 572, Col: 30}
2966+ }
2967+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var144))
25862968 if templ_7745c5c3_Err != nil {
25872969 return templ_7745c5c3_Err
25882970 }
2589- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "</button></div></form>")
2971+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</p>")
25902972 if templ_7745c5c3_Err != nil {
25912973 return templ_7745c5c3_Err
25922974 }
25932975 }
2594- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</div></details>")
2976+ return nil
2977+ })
2978+}
2979+
2980+// mrMergeabilitySlot is the status region of the badge, with its content: the
2981+// stream replaces the children, never the element (see LiveSlot).
2982+func mrMergeabilitySlot(v MergeabilityView) templ.Component {
2983+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2984+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
2985+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
2986+ return templ_7745c5c3_CtxErr
2987+ }
2988+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
2989+ if !templ_7745c5c3_IsBuffer {
2990+ defer func() {
2991+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
2992+ if templ_7745c5c3_Err == nil {
2993+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
2994+ }
2995+ }()
2996+ }
2997+ ctx = templ.InitializeContext(ctx)
2998+ templ_7745c5c3_Var145 := templ.GetChildren(ctx)
2999+ if templ_7745c5c3_Var145 == nil {
3000+ templ_7745c5c3_Var145 = templ.NopComponent
3001+ }
3002+ ctx = templ.ClearChildren(ctx)
3003+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<div id=\"mr-mergeability\" class=\"live-slot\" role=\"status\" data-slot>")
3004+ if templ_7745c5c3_Err != nil {
3005+ return templ_7745c5c3_Err
3006+ }
3007+ templ_7745c5c3_Err = MRMergeability(v).Render(ctx, templ_7745c5c3_Buffer)
3008+ if templ_7745c5c3_Err != nil {
3009+ return templ_7745c5c3_Err
3010+ }
3011+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</div>")
3012+ if templ_7745c5c3_Err != nil {
3013+ return templ_7745c5c3_Err
3014+ }
3015+ return nil
3016+ })
3017+}
3018+
3019+// MRMergeabilityFragment is the slot as the stream sends it.
3020+func MRMergeabilityFragment(v MergeabilityView) templ.Component {
3021+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3022+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
3023+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
3024+ return templ_7745c5c3_CtxErr
3025+ }
3026+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
3027+ if !templ_7745c5c3_IsBuffer {
3028+ defer func() {
3029+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
3030+ if templ_7745c5c3_Err == nil {
3031+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
3032+ }
3033+ }()
3034+ }
3035+ ctx = templ.InitializeContext(ctx)
3036+ templ_7745c5c3_Var146 := templ.GetChildren(ctx)
3037+ if templ_7745c5c3_Var146 == nil {
3038+ templ_7745c5c3_Var146 = templ.NopComponent
3039+ }
3040+ ctx = templ.ClearChildren(ctx)
3041+ templ_7745c5c3_Err = mrMergeabilitySlot(v).Render(ctx, templ_7745c5c3_Buffer)
25953042 if templ_7745c5c3_Err != nil {
25963043 return templ_7745c5c3_Err
25973044 }
@@ -2617,66 +3064,66 @@ func moreMenu(p MRPage) templ.Component {
26173064 }()
26183065 }
26193066 ctx = templ.InitializeContext(ctx)
2620- templ_7745c5c3_Var125 := templ.GetChildren(ctx)
2621- if templ_7745c5c3_Var125 == nil {
2622- templ_7745c5c3_Var125 = templ.NopComponent
3067+ templ_7745c5c3_Var147 := templ.GetChildren(ctx)
3068+ if templ_7745c5c3_Var147 == nil {
3069+ templ_7745c5c3_Var147 = templ.NopComponent
26233070 }
26243071 ctx = templ.ClearChildren(ctx)
2625- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "<details class=\"menu omenu\"><summary class=\"btn icon\" aria-label=\"More actions\" title=\"more actions\">⋯</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"More actions\">")
3072+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "<details class=\"menu omenu\"><summary class=\"btn icon\" aria-label=\"More actions\" title=\"more actions\">⋯</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"More actions\">")
26263073 if templ_7745c5c3_Err != nil {
26273074 return templ_7745c5c3_Err
26283075 }
26293076 if p.Box.CanMarkRead {
2630- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<form method=\"post\" action=\"")
3077+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "<form method=\"post\" action=\"")
26313078 if templ_7745c5c3_Err != nil {
26323079 return templ_7745c5c3_Err
26333080 }
2634- var templ_7745c5c3_Var126 templ.SafeURL
2635- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
3081+ var templ_7745c5c3_Var148 templ.SafeURL
3082+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
26363083 if templ_7745c5c3_Err != nil {
2637- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 64}
3084+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 64}
26383085 }
2639- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
3086+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
26403087 if templ_7745c5c3_Err != nil {
26413088 return templ_7745c5c3_Err
26423089 }
2643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
3090+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
26443091 if templ_7745c5c3_Err != nil {
26453092 return templ_7745c5c3_Err
26463093 }
2647- var templ_7745c5c3_Var127 string
2648- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
3094+ var templ_7745c5c3_Var149 string
3095+ templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
26493096 if templ_7745c5c3_Err != nil {
2650- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 59}
3097+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 597, Col: 59}
26513098 }
2652- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127)
3099+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149)
26533100 if templ_7745c5c3_Err != nil {
26543101 return templ_7745c5c3_Err
26553102 }
2656- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\" aria-keyshortcuts=\"Shift+R\" data-read>Mark read <kbd aria-hidden=\"true\">R</kbd></button></form>")
3103+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\" aria-keyshortcuts=\"Shift+R\" data-read>Mark read <kbd aria-hidden=\"true\">R</kbd></button></form>")
26573104 if templ_7745c5c3_Err != nil {
26583105 return templ_7745c5c3_Err
26593106 }
26603107 }
2661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "<form method=\"post\" action=\"")
3108+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "<form method=\"post\" action=\"")
26623109 if templ_7745c5c3_Err != nil {
26633110 return templ_7745c5c3_Err
26643111 }
2665- var templ_7745c5c3_Var128 templ.SafeURL
2666- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
3112+ var templ_7745c5c3_Var150 templ.SafeURL
3113+ templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
26673114 if templ_7745c5c3_Err != nil {
2668- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 64}
3115+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 64}
26693116 }
2670- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
3117+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))
26713118 if templ_7745c5c3_Err != nil {
26723119 return templ_7745c5c3_Err
26733120 }
2674- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
3121+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
26753122 if templ_7745c5c3_Err != nil {
26763123 return templ_7745c5c3_Err
26773124 }
26783125 if !p.Box.HeadGone && p.Box.ReadyAction == "" {
2679- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
3126+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
26803127 if templ_7745c5c3_Err != nil {
26813128 return templ_7745c5c3_Err
26823129 }
@@ -2684,12 +3131,12 @@ func moreMenu(p MRPage) templ.Component {
26843131 if templ_7745c5c3_Err != nil {
26853132 return templ_7745c5c3_Err
26863133 }
2687- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</details>")
3134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "</details>")
26883135 if templ_7745c5c3_Err != nil {
26893136 return templ_7745c5c3_Err
26903137 }
26913138 }
2692- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</div></details>")
3139+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, "</div></details>")
26933140 if templ_7745c5c3_Err != nil {
26943141 return templ_7745c5c3_Err
26953142 }
@@ -2713,12 +3160,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
27133160 }()
27143161 }
27153162 ctx = templ.InitializeContext(ctx)
2716- templ_7745c5c3_Var129 := templ.GetChildren(ctx)
2717- if templ_7745c5c3_Var129 == nil {
2718- templ_7745c5c3_Var129 = templ.NopComponent
3163+ templ_7745c5c3_Var151 := templ.GetChildren(ctx)
3164+ if templ_7745c5c3_Var151 == nil {
3165+ templ_7745c5c3_Var151 = templ.NopComponent
27193166 }
27203167 ctx = templ.ClearChildren(ctx)
2721- templ_7745c5c3_Var130 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3168+ templ_7745c5c3_Var152 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
27223169 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
27233170 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
27243171 if !templ_7745c5c3_IsBuffer {
@@ -2730,263 +3177,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
27303177 }()
27313178 }
27323179 ctx = templ.InitializeContext(ctx)
2733- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "<header class=\"show mr-h\"><h1>")
3180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<header class=\"show mr-h\"><h1>")
27343181 if templ_7745c5c3_Err != nil {
27353182 return templ_7745c5c3_Err
27363183 }
2737- var templ_7745c5c3_Var131 string
2738- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3184+ var templ_7745c5c3_Var153 string
3185+ templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
27393186 if templ_7745c5c3_Err != nil {
2740- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 17}
3187+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 617, Col: 17}
27413188 }
2742- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
3189+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
27433190 if templ_7745c5c3_Err != nil {
27443191 return templ_7745c5c3_Err
27453192 }
2746- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, " → ")
3193+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, " → ")
27473194 if templ_7745c5c3_Err != nil {
27483195 return templ_7745c5c3_Err
27493196 }
2750- var templ_7745c5c3_Var132 string
2751- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3197+ var templ_7745c5c3_Var154 string
3198+ templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
27523199 if templ_7745c5c3_Err != nil {
2753- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 34}
3200+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 617, Col: 34}
27543201 }
2755- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
3202+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
27563203 if templ_7745c5c3_Err != nil {
27573204 return templ_7745c5c3_Err
27583205 }
2759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
3206+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
27603207 if templ_7745c5c3_Err != nil {
27613208 return templ_7745c5c3_Err
27623209 }
2763- var templ_7745c5c3_Var133 templ.SafeURL
2764- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
3210+ var templ_7745c5c3_Var155 templ.SafeURL
3211+ templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
27653212 if templ_7745c5c3_Err != nil {
2766- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 535, Col: 45}
3213+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 620, Col: 45}
27673214 }
2768- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
3215+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
27693216 if templ_7745c5c3_Err != nil {
27703217 return templ_7745c5c3_Err
27713218 }
2772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "\">")
3219+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, "\">")
27733220 if templ_7745c5c3_Err != nil {
27743221 return templ_7745c5c3_Err
27753222 }
2776- var templ_7745c5c3_Var134 string
2777- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3223+ var templ_7745c5c3_Var156 string
3224+ templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
27783225 if templ_7745c5c3_Err != nil {
2779- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 535, Col: 58}
3226+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 620, Col: 58}
27803227 }
2781- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
3228+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
27823229 if templ_7745c5c3_Err != nil {
27833230 return templ_7745c5c3_Err
27843231 }
2785- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "</a></dd><dt>target</dt><dd><a href=\"")
3232+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "</a></dd><dt>target</dt><dd><a href=\"")
27863233 if templ_7745c5c3_Err != nil {
27873234 return templ_7745c5c3_Err
27883235 }
2789- var templ_7745c5c3_Var135 templ.SafeURL
2790- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
3236+ var templ_7745c5c3_Var157 templ.SafeURL
3237+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
27913238 if templ_7745c5c3_Err != nil {
2792- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 537, Col: 45}
3239+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 622, Col: 45}
27933240 }
2794- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))
3241+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
27953242 if templ_7745c5c3_Err != nil {
27963243 return templ_7745c5c3_Err
27973244 }
2798- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "\">")
3245+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "\">")
27993246 if templ_7745c5c3_Err != nil {
28003247 return templ_7745c5c3_Err
28013248 }
2802- var templ_7745c5c3_Var136 string
2803- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3249+ var templ_7745c5c3_Var158 string
3250+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
28043251 if templ_7745c5c3_Err != nil {
2805- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 537, Col: 58}
3252+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 622, Col: 58}
28063253 }
2807- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
3254+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
28083255 if templ_7745c5c3_Err != nil {
28093256 return templ_7745c5c3_Err
28103257 }
2811- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "</a></dd><dt>ahead</dt><dd>")
3258+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</a></dd><dt>ahead</dt><dd>")
28123259 if templ_7745c5c3_Err != nil {
28133260 return templ_7745c5c3_Err
28143261 }
2815- var templ_7745c5c3_Var137 string
2816- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
3262+ var templ_7745c5c3_Var159 string
3263+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
28173264 if templ_7745c5c3_Err != nil {
2818- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 539, Col: 21}
3265+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 624, Col: 21}
28193266 }
2820- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
3267+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
28213268 if templ_7745c5c3_Err != nil {
28223269 return templ_7745c5c3_Err
28233270 }
2824- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</dd></dl></header>")
3271+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</dd></dl></header>")
28253272 if templ_7745c5c3_Err != nil {
28263273 return templ_7745c5c3_Err
28273274 }
28283275 if p.Note != "" {
2829- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "<p class=\"notice\">")
3276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "<p class=\"notice\">")
28303277 if templ_7745c5c3_Err != nil {
28313278 return templ_7745c5c3_Err
28323279 }
2833- var templ_7745c5c3_Var138 string
2834- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
3280+ var templ_7745c5c3_Var160 string
3281+ templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
28353282 if templ_7745c5c3_Err != nil {
2836- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 29}
3283+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 628, Col: 29}
28373284 }
2838- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
3285+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
28393286 if templ_7745c5c3_Err != nil {
28403287 return templ_7745c5c3_Err
28413288 }
2842- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</p>")
3289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "</p>")
28433290 if templ_7745c5c3_Err != nil {
28443291 return templ_7745c5c3_Err
28453292 }
28463293 }
2847- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, " ")
3294+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, " ")
28483295 if templ_7745c5c3_Err != nil {
28493296 return templ_7745c5c3_Err
28503297 }
28513298 if p.Existing != nil {
2852- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
3299+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
28533300 if templ_7745c5c3_Err != nil {
28543301 return templ_7745c5c3_Err
28553302 }
2856- var templ_7745c5c3_Var139 templ.SafeURL
2857- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
3303+ var templ_7745c5c3_Var161 templ.SafeURL
3304+ templ_7745c5c3_Var161, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
28583305 if templ_7745c5c3_Err != nil {
2859- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 114}
3306+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 631, Col: 114}
28603307 }
2861- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
3308+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var161))
28623309 if templ_7745c5c3_Err != nil {
28633310 return templ_7745c5c3_Err
28643311 }
2865- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\">")
3312+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "\">")
28663313 if templ_7745c5c3_Err != nil {
28673314 return templ_7745c5c3_Err
28683315 }
2869- var templ_7745c5c3_Var140 string
2870- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
3316+ var templ_7745c5c3_Var162 string
3317+ templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
28713318 if templ_7745c5c3_Err != nil {
2872- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 135}
3319+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 631, Col: 135}
28733320 }
2874- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
3321+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var162))
28753322 if templ_7745c5c3_Err != nil {
28763323 return templ_7745c5c3_Err
28773324 }
2878- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "</a></p>")
3325+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "</a></p>")
28793326 if templ_7745c5c3_Err != nil {
28803327 return templ_7745c5c3_Err
28813328 }
28823329 }
2883- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, " ")
3330+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, " ")
28843331 if templ_7745c5c3_Err != nil {
28853332 return templ_7745c5c3_Err
28863333 }
28873334 if p.Create != nil {
2888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "<form class=\"cform create\" method=\"post\" action=\"")
3335+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "<form class=\"cform create\" method=\"post\" action=\"")
28893336 if templ_7745c5c3_Err != nil {
28903337 return templ_7745c5c3_Err
28913338 }
2892- var templ_7745c5c3_Var141 templ.SafeURL
2893- templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
3339+ var templ_7745c5c3_Var163 templ.SafeURL
3340+ templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
28943341 if templ_7745c5c3_Err != nil {
2895- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 549, Col: 83}
3342+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 634, Col: 83}
28963343 }
2897- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
3344+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var163))
28983345 if templ_7745c5c3_Err != nil {
28993346 return templ_7745c5c3_Err
29003347 }
2901- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "\"><input type=\"hidden\" name=\"source\" value=\"")
3348+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "\"><input type=\"hidden\" name=\"source\" value=\"")
29023349 if templ_7745c5c3_Err != nil {
29033350 return templ_7745c5c3_Err
29043351 }
2905- var templ_7745c5c3_Var142 string
2906- templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
3352+ var templ_7745c5c3_Var164 string
3353+ templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
29073354 if templ_7745c5c3_Err != nil {
2908- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 550, Col: 62}
3355+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 635, Col: 62}
29093356 }
2910- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
3357+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var164)
29113358 if templ_7745c5c3_Err != nil {
29123359 return templ_7745c5c3_Err
29133360 }
2914- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\"> <input type=\"hidden\" name=\"target\" value=\"")
3361+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "\"> <input type=\"hidden\" name=\"target\" value=\"")
29153362 if templ_7745c5c3_Err != nil {
29163363 return templ_7745c5c3_Err
29173364 }
2918- var templ_7745c5c3_Var143 string
2919- templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
3365+ var templ_7745c5c3_Var165 string
3366+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
29203367 if templ_7745c5c3_Err != nil {
2921- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 551, Col: 62}
3368+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 636, Col: 62}
29223369 }
2923- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
3370+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var165)
29243371 if templ_7745c5c3_Err != nil {
29253372 return templ_7745c5c3_Err
29263373 }
2927- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
3374+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
29283375 if templ_7745c5c3_Err != nil {
29293376 return templ_7745c5c3_Err
29303377 }
2931- var templ_7745c5c3_Var144 string
2932- templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
3378+ var templ_7745c5c3_Var166 string
3379+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
29333380 if templ_7745c5c3_Err != nil {
2934- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 553, Col: 97}
3381+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 638, Col: 97}
29353382 }
2936- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
3383+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var166)
29373384 if templ_7745c5c3_Err != nil {
29383385 return templ_7745c5c3_Err
29393386 }
2940- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")
3387+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")
29413388 if templ_7745c5c3_Err != nil {
29423389 return templ_7745c5c3_Err
29433390 }
29443391 }
2945- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, " ")
3392+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 311, " ")
29463393 if templ_7745c5c3_Err != nil {
29473394 return templ_7745c5c3_Err
29483395 }
29493396 if p.HasDiff {
2950- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
3397+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 312, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
29513398 if templ_7745c5c3_Err != nil {
29523399 return templ_7745c5c3_Err
29533400 }
2954- var templ_7745c5c3_Var145 string
2955- templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
3401+ var templ_7745c5c3_Var167 string
3402+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
29563403 if templ_7745c5c3_Err != nil {
2957- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 57}
3404+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 648, Col: 57}
29583405 }
2959- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
3406+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
29603407 if templ_7745c5c3_Err != nil {
29613408 return templ_7745c5c3_Err
29623409 }
2963- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, " · <span class=\"plus\">+")
3410+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, " · <span class=\"plus\">+")
29643411 if templ_7745c5c3_Err != nil {
29653412 return templ_7745c5c3_Err
29663413 }
2967- var templ_7745c5c3_Var146 string
2968- templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
3414+ var templ_7745c5c3_Var168 string
3415+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
29693416 if templ_7745c5c3_Err != nil {
2970- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 97}
3417+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 648, Col: 97}
29713418 }
2972- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
3419+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
29733420 if templ_7745c5c3_Err != nil {
29743421 return templ_7745c5c3_Err
29753422 }
2976- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</span> <span class=\"minus\">-")
3423+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "</span> <span class=\"minus\">-")
29773424 if templ_7745c5c3_Err != nil {
29783425 return templ_7745c5c3_Err
29793426 }
2980- var templ_7745c5c3_Var147 string
2981- templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
3427+ var templ_7745c5c3_Var169 string
3428+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
29823429 if templ_7745c5c3_Err != nil {
2983- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 142}
3430+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 648, Col: 142}
29843431 }
2985- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
3432+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
29863433 if templ_7745c5c3_Err != nil {
29873434 return templ_7745c5c3_Err
29883435 }
2989- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "</span></span> <span class=\"grow\"></span>")
3436+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, "</span></span> <span class=\"grow\"></span>")
29903437 if templ_7745c5c3_Err != nil {
29913438 return templ_7745c5c3_Err
29923439 }
@@ -2994,64 +3441,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
29943441 if templ_7745c5c3_Err != nil {
29953442 return templ_7745c5c3_Err
29963443 }
2997- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</div>")
3444+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "</div>")
29983445 if templ_7745c5c3_Err != nil {
29993446 return templ_7745c5c3_Err
30003447 }
30013448 if p.Invisible {
3002- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "<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>")
3449+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "<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>")
30033450 if templ_7745c5c3_Err != nil {
30043451 return templ_7745c5c3_Err
30053452 }
30063453 }
3007- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, " <div id=\"diffs\"")
3454+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, " <div id=\"diffs\"")
30083455 if templ_7745c5c3_Err != nil {
30093456 return templ_7745c5c3_Err
30103457 }
30113458 if p.Both {
3012- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, " data-diff=\"")
3459+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 319, " data-diff=\"")
30133460 if templ_7745c5c3_Err != nil {
30143461 return templ_7745c5c3_Err
30153462 }
3016- var templ_7745c5c3_Var148 string
3017- templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3463+ var templ_7745c5c3_Var170 string
3464+ templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
30183465 if templ_7745c5c3_Err != nil {
3019- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 576, Col: 23}
3466+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 661, Col: 23}
30203467 }
3021- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148)
3468+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var170)
30223469 if templ_7745c5c3_Err != nil {
30233470 return templ_7745c5c3_Err
30243471 }
3025- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "\"")
3472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, "\"")
30263473 if templ_7745c5c3_Err != nil {
30273474 return templ_7745c5c3_Err
30283475 }
30293476 } else {
3030- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, " data-view=\"")
3477+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " data-view=\"")
30313478 if templ_7745c5c3_Err != nil {
30323479 return templ_7745c5c3_Err
30333480 }
3034- var templ_7745c5c3_Var149 string
3035- templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3481+ var templ_7745c5c3_Var171 string
3482+ templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
30363483 if templ_7745c5c3_Err != nil {
3037- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 23}
3484+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 663, Col: 23}
30383485 }
3039- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149)
3486+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var171)
30403487 if templ_7745c5c3_Err != nil {
30413488 return templ_7745c5c3_Err
30423489 }
3043- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "\"")
3490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "\"")
30443491 if templ_7745c5c3_Err != nil {
30453492 return templ_7745c5c3_Err
30463493 }
30473494 }
30483495 if p.Both && !p.Pinned {
3049- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
3496+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
30503497 if templ_7745c5c3_Err != nil {
30513498 return templ_7745c5c3_Err
30523499 }
30533500 }
3054- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, ">")
3501+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, ">")
30553502 if templ_7745c5c3_Err != nil {
30563503 return templ_7745c5c3_Err
30573504 }
@@ -3061,14 +3508,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
30613508 return templ_7745c5c3_Err
30623509 }
30633510 }
3064- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "</div>")
3511+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, "</div>")
30653512 if templ_7745c5c3_Err != nil {
30663513 return templ_7745c5c3_Err
30673514 }
30683515 }
30693516 return nil
30703517 })
3071- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var130), templ_7745c5c3_Buffer)
3518+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var152), templ_7745c5c3_Buffer)
30723519 if templ_7745c5c3_Err != nil {
30733520 return templ_7745c5c3_Err
30743521 }
@@ -3092,173 +3539,173 @@ func compareRail(p ComparePage) templ.Component {
30923539 }()
30933540 }
30943541 ctx = templ.InitializeContext(ctx)
3095- templ_7745c5c3_Var150 := templ.GetChildren(ctx)
3096- if templ_7745c5c3_Var150 == nil {
3097- templ_7745c5c3_Var150 = templ.NopComponent
3542+ templ_7745c5c3_Var172 := templ.GetChildren(ctx)
3543+ if templ_7745c5c3_Var172 == nil {
3544+ templ_7745c5c3_Var172 = templ.NopComponent
30983545 }
30993546 ctx = templ.ClearChildren(ctx)
3100- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
3547+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
31013548 if templ_7745c5c3_Err != nil {
31023549 return templ_7745c5c3_Err
31033550 }
3104- var templ_7745c5c3_Var151 string
3105- templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
3551+ var templ_7745c5c3_Var173 string
3552+ templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
31063553 if templ_7745c5c3_Err != nil {
3107- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 67}
3554+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 679, Col: 67}
31083555 }
3109- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
3556+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
31103557 if templ_7745c5c3_Err != nil {
31113558 return templ_7745c5c3_Err
31123559 }
3113- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "</span></h2><ul class=\"pcommits\">")
3560+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, "</span></h2><ul class=\"pcommits\">")
31143561 if templ_7745c5c3_Err != nil {
31153562 return templ_7745c5c3_Err
31163563 }
31173564 for _, c := range p.Ahead {
3118- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, "<li><a href=\"")
3565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "<li><a href=\"")
31193566 if templ_7745c5c3_Err != nil {
31203567 return templ_7745c5c3_Err
31213568 }
3122- var templ_7745c5c3_Var152 templ.SafeURL
3123- templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
3569+ var templ_7745c5c3_Var174 templ.SafeURL
3570+ templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
31243571 if templ_7745c5c3_Err != nil {
3125- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 37}
3572+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 683, Col: 37}
31263573 }
3127- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
3574+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var174))
31283575 if templ_7745c5c3_Err != nil {
31293576 return templ_7745c5c3_Err
31303577 }
3131- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "\">")
3578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, "\">")
31323579 if templ_7745c5c3_Err != nil {
31333580 return templ_7745c5c3_Err
31343581 }
3135- var templ_7745c5c3_Var153 string
3136- templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
3582+ var templ_7745c5c3_Var175 string
3583+ templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
31373584 if templ_7745c5c3_Err != nil {
3138- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 49}
3585+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 683, Col: 49}
31393586 }
3140- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
3587+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var175))
31413588 if templ_7745c5c3_Err != nil {
31423589 return templ_7745c5c3_Err
31433590 }
3144- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "</a> <span class=\"s\">")
3591+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, "</a> <span class=\"s\">")
31453592 if templ_7745c5c3_Err != nil {
31463593 return templ_7745c5c3_Err
31473594 }
3148- var templ_7745c5c3_Var154 string
3149- templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
3595+ var templ_7745c5c3_Var176 string
3596+ templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
31503597 if templ_7745c5c3_Err != nil {
3151- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 599, Col: 32}
3598+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 684, Col: 32}
31523599 }
3153- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
3600+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var176))
31543601 if templ_7745c5c3_Err != nil {
31553602 return templ_7745c5c3_Err
31563603 }
3157- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "</span> <span class=\"mute\">")
3604+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, "</span> <span class=\"mute\">")
31583605 if templ_7745c5c3_Err != nil {
31593606 return templ_7745c5c3_Err
31603607 }
3161- var templ_7745c5c3_Var155 string
3162- templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
3608+ var templ_7745c5c3_Var177 string
3609+ templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
31633610 if templ_7745c5c3_Err != nil {
3164- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 16}
3611+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 686, Col: 16}
31653612 }
3166- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
3613+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))
31673614 if templ_7745c5c3_Err != nil {
31683615 return templ_7745c5c3_Err
31693616 }
3170- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, " ")
3617+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, " ")
31713618 if templ_7745c5c3_Err != nil {
31723619 return templ_7745c5c3_Err
31733620 }
31743621 if c.WhenAt != "" {
3175- var templ_7745c5c3_Var156 string
3176- templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
3622+ var templ_7745c5c3_Var178 string
3623+ templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
31773624 if templ_7745c5c3_Err != nil {
3178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 15}
3625+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 688, Col: 15}
31793626 }
3180- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
3627+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))
31813628 if templ_7745c5c3_Err != nil {
31823629 return templ_7745c5c3_Err
31833630 }
3184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "<time datetime=\"")
3631+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "<time datetime=\"")
31853632 if templ_7745c5c3_Err != nil {
31863633 return templ_7745c5c3_Err
31873634 }
3188- var templ_7745c5c3_Var157 string
3189- templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
3635+ var templ_7745c5c3_Var179 string
3636+ templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
31903637 if templ_7745c5c3_Err != nil {
3191- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 42}
3638+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 688, Col: 42}
31923639 }
3193- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var157)
3640+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var179)
31943641 if templ_7745c5c3_Err != nil {
31953642 return templ_7745c5c3_Err
31963643 }
3197- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "\">")
3644+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, "\">")
31983645 if templ_7745c5c3_Err != nil {
31993646 return templ_7745c5c3_Err
32003647 }
3201- var templ_7745c5c3_Var158 string
3202- templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
3648+ var templ_7745c5c3_Var180 string
3649+ templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
32033650 if templ_7745c5c3_Err != nil {
3204- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 53}
3651+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 688, Col: 53}
32053652 }
3206- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
3653+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var180))
32073654 if templ_7745c5c3_Err != nil {
32083655 return templ_7745c5c3_Err
32093656 }
3210- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</time>")
3657+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "</time>")
32113658 if templ_7745c5c3_Err != nil {
32123659 return templ_7745c5c3_Err
32133660 }
32143661 }
3215- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</span></li>")
3662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "</span></li>")
32163663 if templ_7745c5c3_Err != nil {
32173664 return templ_7745c5c3_Err
32183665 }
32193666 }
3220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "</ul>")
3667+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "</ul>")
32213668 if templ_7745c5c3_Err != nil {
32223669 return templ_7745c5c3_Err
32233670 }
32243671 if len(p.Ahead) == 0 {
3225- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "<p class=\"dim\">no commit ahead of ")
3672+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "<p class=\"dim\">no commit ahead of ")
32263673 if templ_7745c5c3_Err != nil {
32273674 return templ_7745c5c3_Err
32283675 }
3229- var templ_7745c5c3_Var159 string
3230- templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3676+ var templ_7745c5c3_Var181 string
3677+ templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
32313678 if templ_7745c5c3_Err != nil {
3232- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 610, Col: 47}
3679+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 47}
32333680 }
3234- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
3681+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var181))
32353682 if templ_7745c5c3_Err != nil {
32363683 return templ_7745c5c3_Err
32373684 }
3238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, ".</p>")
3685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, ".</p>")
32393686 if templ_7745c5c3_Err != nil {
32403687 return templ_7745c5c3_Err
32413688 }
32423689 }
3243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "</section>")
3690+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, "</section>")
32443691 if templ_7745c5c3_Err != nil {
32453692 return templ_7745c5c3_Err
32463693 }
32473694 if p.HasDiff {
3248- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
3695+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
32493696 if templ_7745c5c3_Err != nil {
32503697 return templ_7745c5c3_Err
32513698 }
3252- var templ_7745c5c3_Var160 string
3253- templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
3699+ var templ_7745c5c3_Var182 string
3700+ templ_7745c5c3_Var182, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
32543701 if templ_7745c5c3_Err != nil {
3255- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 615, Col: 66}
3702+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 700, Col: 66}
32563703 }
3257- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
3704+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var182))
32583705 if templ_7745c5c3_Err != nil {
32593706 return templ_7745c5c3_Err
32603707 }
3261- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "</span></h2>")
3708+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, "</span></h2>")
32623709 if templ_7745c5c3_Err != nil {
32633710 return templ_7745c5c3_Err
32643711 }
@@ -3266,12 +3713,12 @@ func compareRail(p ComparePage) templ.Component {
32663713 if templ_7745c5c3_Err != nil {
32673714 return templ_7745c5c3_Err
32683715 }
3269- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "</section>")
3716+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "</section>")
32703717 if templ_7745c5c3_Err != nil {
32713718 return templ_7745c5c3_Err
32723719 }
32733720 }
3274- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
3721+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
32753722 if templ_7745c5c3_Err != nil {
32763723 return templ_7745c5c3_Err
32773724 }
@@ -3279,7 +3726,7 @@ func compareRail(p ComparePage) templ.Component {
32793726 if templ_7745c5c3_Err != nil {
32803727 return templ_7745c5c3_Err
32813728 }
3282- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "</section>")
3729+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</section>")
32833730 if templ_7745c5c3_Err != nil {
32843731 return templ_7745c5c3_Err
32853732 }
A internal/adapters/web/mr_test.go +130 -0
@@ -0,0 +1,130 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "context"
7+ "testing"
8+
9+ "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+
13+ "bnema.dev/ermite/internal/domain"
14+ "bnema.dev/ermite/internal/ports"
15+ "bnema.dev/ermite/internal/ports/mocks"
16+)
17+
18+func repoWithModes(modes ...domain.MergeMode) ports.RepoView {
19+ var v ports.RepoView
20+ v.Settings.AllowedMergeModes = modes
21+ return v
22+}
23+
24+func watchOf(ev domain.Event) ports.WatchEvent { return ports.WatchEvent{Event: ev} }
25+
26+func TestMergeabilityView(t *testing.T) {
27+ t.Parallel()
28+ for _, tt := range []struct {
29+ name string
30+ m domain.Mergeability
31+ label, detail string
32+ ffOK, mergeOK bool
33+ note string
34+ paths []string
35+ }{
36+ {"up to date", domain.Mergeability{State: domain.MergeabilityUpToDate}, "Ready to merge", "", true, true, "", nil},
37+ {"behind", domain.Mergeability{State: domain.MergeabilityBehind, Behind: 2}, "Behind main by 2 commits", "merges without conflicts", false, true, "", nil},
38+ {"behind by one", domain.Mergeability{State: domain.MergeabilityBehind, Behind: 1}, "Behind main by 1 commit", "merges without conflicts", false, true, "", nil},
39+ {"behind, capped", domain.Mergeability{State: domain.MergeabilityBehind, Behind: 1000, BehindCapped: true}, "Behind main by 1000+ commits", "merges without conflicts", false, true, "", nil},
40+ {"conflict", domain.Mergeability{State: domain.MergeabilityConflict, ConflictPaths: []string{"a.go"}}, "Conflicts with main", "rebase needed", false, false, "", []string{"a.go"}},
41+ {"conflict, truncated", domain.Mergeability{State: domain.MergeabilityConflict, ConflictPaths: []string{"a.go"}, ConflictPathsTruncated: true}, "Conflicts with main", "rebase needed", false, false, "more files conflict than are listed.", []string{"a.go"}},
42+ {"no common history", domain.Mergeability{State: domain.MergeabilityConflict}, "Conflicts with main", "rebase needed", false, false, "no common history.", nil},
43+ {"checking", domain.Mergeability{State: domain.MergeabilityChecking}, "Checking…", "", false, false, "", nil},
44+ {"none", domain.Mergeability{}, "Checking…", "", false, false, "", nil},
45+ } {
46+ t.Run(tt.name, func(t *testing.T) {
47+ t.Parallel()
48+ v := mergeabilityView(tt.m, "main")
49+ assert.Equal(t, tt.label, v.Label)
50+ assert.Equal(t, tt.detail, v.Detail)
51+ assert.Equal(t, tt.ffOK, v.FFOk)
52+ assert.Equal(t, tt.mergeOK, v.MergeOk)
53+ assert.Equal(t, tt.note, v.PathsNote)
54+ assert.Equal(t, tt.paths, v.Paths)
55+ assert.Equal(t, !tt.ffOK, v.FFReason != "", "a mode that is not possible says why")
56+ assert.Equal(t, !tt.mergeOK, v.MergeReason != "")
57+ })
58+ }
59+}
60+
61+func TestMergeBoxModes(t *testing.T) {
62+ t.Parallel()
63+ d := &repoData{repo: repoWithModes(domain.MergeModes()...)}
64+ s := &Server{}
65+
66+ behind := s.mergeBox(d, 1, "feat", "abc", "", mergeabilityView(domain.Mergeability{State: domain.MergeabilityBehind, Behind: 1}, "main"))
67+ require.Len(t, behind.Modes, 3)
68+ assert.True(t, behind.Modes[0].Disabled, "fast-forward is not possible when behind")
69+ assert.NotEmpty(t, behind.Modes[0].Reason)
70+ assert.False(t, behind.Modes[1].Disabled)
71+ assert.False(t, behind.Modes[2].Disabled)
72+ assert.Equal(t, 1, behind.Checked(), "the first possible mode is checked")
73+ assert.True(t, behind.CanMerge())
74+ assert.True(t, behind.Signed)
75+
76+ conflict := s.mergeBox(d, 1, "feat", "abc", "", mergeabilityView(domain.Mergeability{State: domain.MergeabilityConflict}, "main"))
77+ assert.False(t, conflict.CanMerge())
78+ assert.NotEmpty(t, conflict.MergeTitle(), "the button says why it is disabled")
79+ assert.Equal(t, 0, conflict.Checked())
80+ assert.Contains(t, conflict.CLI.Lines[2], "--mode merge")
81+ assert.NotContains(t, conflict.CLI.Lines[1], "--squash")
82+}
83+
84+func TestMergeBoxOnlyFastForwardIsNotSigned(t *testing.T) {
85+ t.Parallel()
86+ d := &repoData{repo: repoWithModes(domain.MergeModeFastForward)}
87+ b := (&Server{}).mergeBox(d, 1, "feat", "abc", "", mergeabilityView(domain.Mergeability{State: domain.MergeabilityUpToDate}, "main"))
88+ assert.False(t, b.Signed, "no commit is created, nothing to explain")
89+ assert.Len(t, b.Modes, 1)
90+}
91+
92+func TestMergeBoxNamesTheSigningKey(t *testing.T) {
93+ t.Parallel()
94+ key := mocks.NewSigningKeyService(t)
95+ key.EXPECT().Info().Return(domain.SigningKeyInfo{Key: domain.SigningPublicKey{Fingerprint: "SHA256:abc"}})
96+ d := &repoData{repo: repoWithModes(domain.MergeModes()...)}
97+ b := (&Server{cfg: Config{SigningKey: key}}).mergeBox(d, 1, "feat", "abc", "", mergeabilityView(domain.Mergeability{}, "main"))
98+ assert.Equal(t, "SHA256:abc", b.Fingerprint)
99+}
100+
101+func TestMergeErrorTravelsInTheContext(t *testing.T) {
102+ t.Parallel()
103+ assert.Empty(t, mergeErrorOf(context.Background()))
104+ assert.Equal(t, "boom", mergeErrorOf(withMergeError(context.Background(), "boom")))
105+}
106+
107+func TestMergeabilityLiveEvents(t *testing.T) {
108+ t.Parallel()
109+ assert.True(t, liveEvent(watchOf(domain.MergeRequestMergeability{Number: 1})))
110+ svc := mocks.NewMergeRequestService(t)
111+ svc.EXPECT().Mergeability(mock.Anything, mock.Anything, mock.Anything, int64(1)).
112+ Return(domain.Mergeability{State: domain.MergeabilityBehind, Behind: 3}, nil)
113+ l := liveStream{s: &Server{cfg: Config{MergeRequests: svc}}, repo: "demo", view: "mr", n: "1", target: "main"}
114+ ctx := context.WithValue(context.Background(), signedInKey{}, struct{}{})
115+
116+ fs, err := l.repoPage(ctx, watchOf(domain.MergeRequestMergeability{Number: 1}))
117+ require.NoError(t, err)
118+ require.Len(t, fs, 1)
119+ assert.Equal(t, "mr-mergeability", fs[0].event)
120+ assert.Contains(t, fs[0].html, "Behind main by 3 commits")
121+
122+ // Another request, or a page that is not one: nothing.
123+ fs, err = l.repoPage(ctx, watchOf(domain.MergeRequestMergeability{Number: 2}))
124+ require.NoError(t, err)
125+ assert.Empty(t, fs)
126+ // The box is the owner's.
127+ fs, err = l.repoPage(context.Background(), watchOf(domain.MergeRequestMergeability{Number: 1}))
128+ require.NoError(t, err)
129+ assert.Empty(t, fs)
130+}
M internal/adapters/web/static/css/60-mr.css +16 -1
@@ -78,7 +78,22 @@
7878 & .d-btns { display: flex; margin-top: var(--s1); }
7979 & .d-btns .btn { flex: 1; justify-content: center; }
8080 & .d-draft { color: var(--ink-dim); }
81- }
81+ & .d-note { margin: 0; color: var(--ink-dim); }
82+ & .d-err { margin: 0; padding: var(--s2) var(--s3); color: var(--ink-hi); border: 1px solid var(--red); border-left-width: 3px; border-radius: var(--r-s); overflow-wrap: anywhere; }
83+ /* the badge is a live slot, but not a banner */
84+ & #mr-mergeability:not(:empty) { padding: 0; font-weight: normal; background: none; border: 0; display: grid; gap: var(--s1); }
85+ & .mg-paths { margin: 0; padding-left: 2ch; max-height: 8em; overflow: auto; }
86+ & .mg-paths code { overflow-wrap: anywhere; }
87+ & .d-modes label:has(:disabled) { color: var(--ink-mute); cursor: not-allowed; }
88+ & .d-modes label:has(:disabled):hover { background: none; }
89+ & .btn:disabled { opacity: .5; cursor: not-allowed; }
90+ }
91+ .badge[data-state] { display: inline-block; padding: 0 var(--s2); border: 1px solid var(--border-strong); border-radius: var(--r-s); font-weight: 600; color: var(--ink-hi); }
92+ .badge[data-state="up_to_date"] { border-color: var(--teal); color: var(--teal); }
93+ .badge[data-state="behind"] { border-color: var(--teal-2); }
94+ .badge[data-state="conflict"] { border-color: var(--red); color: var(--red); }
95+ .badge[data-state="checking"] { color: var(--ink-dim); border-style: dashed; }
96+ .note[role="status"] { color: var(--ink-dim); }
8297 .omenu {
8398 & > summary { font-size: 18px; line-height: 1; }
8499 & .menu-pop { width: 280px; }
M internal/adapters/web/static/js/mr.js +1 -1
@@ -213,7 +213,7 @@ window.ermiteMR = (() => {
213213 const box = $('input[name="delete_branch"]', m);
214214 if (box) box.checked = !box.checked;
215215 } else {
216- radios[act.mode].checked = true;
216+ if (!radios[act.mode].disabled) radios[act.mode].checked = true;
217217 }
218218 $('[data-default]', m)?.focus();
219219 return true;
M internal/adapters/web/web.go +4 -0
@@ -76,6 +76,10 @@ type Config struct {
7676 // leads to it. Without it there is neither. Leave it unset rather than
7777 // assigning a typed nil pointer.
7878 Inbox ports.InboxService
79+ // SigningKey tells the key that signs the squash and merge commits, shown
80+ // in the merge box. Without it the box does not name the key. Leave it
81+ // unset rather than assigning a typed nil pointer.
82+ SigningKey ports.SigningKeyService
7983 // Runs serves the CI pages (/<repo>/actions) and the checks shown next to
8084 // commits and merge requests. Without it there are no such pages. Leave it
8185 // unset rather than assigning a typed nil pointer.
M internal/app/cli_mr.go +143 -19
@@ -8,6 +8,7 @@ import (
88 "fmt"
99 "io"
1010 "net/url"
11+ "slices"
1112 "strconv"
1213 "strings"
1314
@@ -122,6 +123,71 @@ type jsonMergeRequest struct {
122123 MergeMode string `json:"merge_mode"`
123124 }
124125
126+// jsonMergeability is the stable JSON shape of the mergeability of an open
127+// merge request.
128+type jsonMergeability struct {
129+ // State is "checking", "up_to_date", "behind" or "conflict".
130+ State string `json:"state"`
131+ // Behind counts the commits of the target the head lacks (behind only);
132+ // BehindCapped says it is a lower bound.
133+ Behind int32 `json:"behind"`
134+ BehindCapped bool `json:"behind_capped"`
135+ ConflictPaths []string `json:"conflict_paths"`
136+ // ConflictPathsTruncated says that more paths conflict than listed.
137+ ConflictPathsTruncated bool `json:"conflict_paths_truncated"`
138+ TargetID string `json:"target_id"`
139+ HeadID string `json:"head_id"`
140+}
141+
142+func mergeabilityName(s ermite.MergeabilityState) string {
143+ switch s {
144+ case ermite.MergeabilityState_MERGEABILITY_STATE_CHECKING:
145+ return "checking"
146+ case ermite.MergeabilityState_MERGEABILITY_STATE_UP_TO_DATE:
147+ return "up_to_date"
148+ case ermite.MergeabilityState_MERGEABILITY_STATE_BEHIND:
149+ return "behind"
150+ case ermite.MergeabilityState_MERGEABILITY_STATE_CONFLICT:
151+ return "conflict"
152+ }
153+ return ""
154+}
155+
156+// toJSONMergeability maps a mergeability; nil when the server sent none.
157+func toJSONMergeability(m *ermite.Mergeability) *jsonMergeability {
158+ if m == nil {
159+ return nil
160+ }
161+ return &jsonMergeability{
162+ State: mergeabilityName(m.GetState()), Behind: m.GetBehind(), BehindCapped: m.GetBehindCapped(),
163+ ConflictPaths: append([]string{}, m.GetConflictPaths()...), ConflictPathsTruncated: m.GetConflictPathsTruncated(),
164+ TargetID: m.GetTargetId(), HeadID: m.GetHeadId(),
165+ }
166+}
167+
168+// mergeabilityText is the human form of a mergeability: "up to date",
169+// "behind by 2", "conflict (a, b, ...)", "checking".
170+func mergeabilityText(m *jsonMergeability) string {
171+ switch m.State {
172+ case "up_to_date":
173+ return "up to date with the target, can be fast-forwarded"
174+ case "behind":
175+ return fmt.Sprintf("behind by %d%s, merges without conflicts (squash or merge commit; fast-forward needs a rebase)", m.Behind, plus(m.BehindCapped))
176+ case "conflict":
177+ if len(m.ConflictPaths) == 0 {
178+ return "conflict (no common history with the target), rebase needed"
179+ }
180+ more := ""
181+ if m.ConflictPathsTruncated {
182+ more = ", ..."
183+ }
184+ return "conflict (" + strings.Join(m.ConflictPaths, ", ") + more + "), rebase needed"
185+ case "checking":
186+ return "checking"
187+ }
188+ return m.State
189+}
190+
125191 // jsonMerged is the output of `mr merge --json`.
126192 type jsonMerged struct {
127193 jsonMergeRequest
@@ -178,9 +244,15 @@ const jsonMRHelp = `
178244 JSON fields of a merge request (--json): repo, url (full web URL), number, title, description, source_branch,
179245 target_branch, state ("open", "merged" or "closed"), author (token label), created_at and
180246 updated_at (RFC 3339 UTC), head_id (the commit the source branch points to; empty when the
181-branch is gone), merged_commit_id and merge_mode ("fast-forward" or "squash"; both empty
247+branch is gone), merged_commit_id and merge_mode ("fast-forward", "squash" or "merge"; both empty
182248 unless merged).`
183249
250+const jsonMergeabilityHelp = `
251+mr show --json adds mergeability, null unless the request is open: state ("checking", "up_to_date",
252+"behind" or "conflict"), behind and behind_capped (commits of the target the head lacks; behind
253+only), conflict_paths and conflict_paths_truncated (conflict only), target_id and head_id (the
254+commits the result is for).`
255+
184256 const jsonCommentHelp = `
185257 JSON fields of a comment: id, author, path, line and side ("old" or "new"; empty path, line 0
186258 and empty side for a general comment), commit_id (the head when it was written), text,
@@ -417,6 +489,8 @@ type jsonCommit struct {
417489 // jsonMRDetail is the stable JSON shape of `mr show --json`.
418490 type jsonMRDetail struct {
419491 jsonMergeRequest
492+ // Mergeability is null unless the request is open and the server computes it.
493+ Mergeability *jsonMergeability `json:"mergeability"`
420494 Commits []jsonCommit `json:"commits"`
421495 CommitsTruncated bool `json:"commits_truncated"`
422496 DiffBaseID string `json:"diff_base_id"`
@@ -449,6 +523,7 @@ func lineKindName(k ermite.LineKind) string {
449523 func toJSONDetail(d *ermite.GetMergeRequestResponse, withDiff bool, baseURL string) jsonMRDetail {
450524 out := jsonMRDetail{
451525 jsonMergeRequest: toJSONMergeRequest(d.GetMergeRequest(), baseURL),
526+ Mergeability: toJSONMergeability(d.GetMergeRequest().GetMergeability()),
452527 Commits: []jsonCommit{}, CommitsTruncated: d.GetCommitsTruncated(),
453528 DiffBaseID: d.GetDiffBaseId(),
454529 Stats: jsonStats{
@@ -501,7 +576,9 @@ func newMRShowCmd(env *cliEnv) *cobra.Command {
501576 "--since-last-read shows the diff since the head recorded by --mark-read (or by the web) instead of\n" +
502577 "the whole change; after a force-push it still works when the recorded commit exists, else the\n" +
503578 "whole change is shown and the output says so. --mark-read records the head shown as read; it needs\n" +
504- "the mr:write scope." + jsonMRHelp + jsonCommentHelp + "\n" +
579+ "the mr:write scope. The mergeability line says whether the branch merges with its target now: up to\n" +
580+ "date (can be fast-forwarded), behind by N commits (merges without conflicts, squash or merge commit),\n" +
581+ "or in conflict with the paths that conflict (rebase needed)." + jsonMRHelp + jsonMergeabilityHelp + jsonCommentHelp + "\n" +
505582 "--json adds to the merge request fields: commits (id, author, summary), commits_truncated,\n" +
506583 "diff_base_id (the commit the diff starts from), stats (files_changed, additions, deletions),\n" +
507584 "diff_truncated, files (old_path, new_path, status, additions, deletions, binary, truncated and,\n" +
@@ -574,6 +651,9 @@ func printMRDetail(w io.Writer, d *ermite.GetMergeRequestResponse, withDiff, sin
574651 if m.GetState() == ermite.MergeRequestState_MERGE_REQUEST_STATE_MERGED {
575652 pairs = append(pairs, [2]string{"merged", orDash(m.GetMergedCommitId()) + " (" + modeName(m.GetMergeMode()) + ")"})
576653 }
654+ if mg := toJSONMergeability(m.GetMergeability()); mg != nil && m.GetState() == ermite.MergeRequestState_MERGE_REQUEST_STATE_OPEN {
655+ pairs = append(pairs, [2]string{"mergeability", mergeabilityText(mg)})
656+ }
577657 if len(d.GetVerdicts()) > 0 {
578658 var verdicts []string
579659 for _, v := range d.GetVerdicts() {
@@ -962,37 +1042,52 @@ func newMRResolveCmd(env *cliEnv) *cobra.Command {
9621042 }
9631043
9641044 func newMRMergeCmd(env *cliEnv) *cobra.Command {
965- var squash, deleteBranch bool
1045+ var mode string
1046+ var deleteBranch bool
9661047 var head string
9671048 cmd := &cobra.Command{
9681049 Use: "merge <repo> <number>",
9691050 Short: "Merge a merge request",
970- Long: "Merge an open merge request. The default is a fast-forward: the target branch moves to the head\n" +
971- "and no commit is created. --squash makes one new commit with the tree of the head and the target\n" +
972- "as only parent, signed by the server's key (see `ermite signing-key show`). The target must be an\n" +
973- "ancestor of the head, else rebase the branch and push it again; the repository settings may allow\n" +
974- "only one of the modes.\n\n" +
1051+ Long: "Merge an open merge request with --mode (default fast-forward; the repository settings may allow\n" +
1052+ "only some modes, see `ermite repo edit --merge-modes`):\n" +
1053+ " fast-forward the target branch moves to the head and no commit is created. The target must be\n" +
1054+ " an ancestor of the head, else rebase the branch and push it again, or use another mode.\n" +
1055+ " squash one new commit with the tree of the merge of the head and the target, the target tip\n" +
1056+ " as only parent. The branch must merge without conflicts.\n" +
1057+ " merge a merge commit with two parents, the target tip and the head. The branch must merge\n" +
1058+ " without conflicts.\n" +
1059+ "A squash and a merge commit are signed with the server's own key, generated automatically when the\n" +
1060+ "server first starts; `ermite signing-key show` prints it. The server never rebases. `ermite mr show`\n" +
1061+ "prints the mergeability (up to date, behind, conflict); the server checks again when merging and\n" +
1062+ "refuses a merge that is not possible, saying why.\n\n" +
9751063 "The server merges only if the source branch still points to the head you reviewed, so --head is\n" +
9761064 "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" +
9771065 "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" +
9781066 "mr:merge scope.\n\n" +
979- "--delete-branch asks the server to delete the source branch right after the merge, in the\n" +
980- "background. The merge is refused when the branch is the default branch or used by another open\n" +
981- "merge request. If the deletion later fails (the branch moved, say), the merge stands and the\n" +
982- "branch is kept." + jsonMRHelp +
1067+ "The merge answers as soon as the target branch has moved. --delete-branch asks the server to delete\n" +
1068+ "the source branch right after, in the background. The merge is refused when the branch is the\n" +
1069+ "default branch or used by another open merge request. If the deletion later fails (the branch\n" +
1070+ "moved, say), the merge stands and the branch is kept." + jsonMRHelp +
9831071 "\n--json adds source_branch_deleted (true when the deletion was requested: the server deletes the branch right after the merge).",
9841072 Args: cobra.ExactArgs(2),
9851073 ValidArgsFunction: completeMRRepoThenNumber(env),
9861074 }
987- cmd.Flags().BoolVar(&squash, "squash", false, "squash the branch into one signed commit instead of a fast-forward")
1075+ cmd.Flags().StringVar(&mode, "mode", modeFastForward, "merge mode: fast-forward, squash or merge (a merge commit)")
9881076 cmd.Flags().StringVar(&head, "head", "", "full id of the head you reviewed, from `ermite mr show` (required)")
9891077 cmd.Flags().BoolVar(&deleteBranch, "delete-branch", false, "delete the source branch after the merge")
1078+ _ = cmd.RegisterFlagCompletionFunc("mode", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
1079+ return []string{modeFastForward, modeSquash, modeMerge}, cobra.ShellCompDirectiveNoFileComp
1080+ })
9901081 asJSON := addJSONFlag(cmd)
9911082 cmd.RunE = func(cmd *cobra.Command, args []string) error {
9921083 n, err := parseNumber(args[1])
9931084 if err != nil {
9941085 return err
9951086 }
1087+ pbMode, err := parseMode(mode)
1088+ if err != nil {
1089+ return err
1090+ }
9961091 head = strings.TrimSpace(head)
9971092 if head == "" {
9981093 return fmt.Errorf("--head is required: run `ermite mr show %s %d` and give the head id it prints, "+
@@ -1003,12 +1098,13 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
10031098 return err
10041099 }
10051100 ctx := env.ctx(cmd)
1006- mode := ermite.MergeMode_MERGE_MODE_FAST_FORWARD
1007- if squash {
1008- mode = ermite.MergeMode_MERGE_MODE_SQUASH
1101+ if pbMode == ermite.MergeMode_MERGE_MODE_MERGE_COMMIT {
1102+ if err := requireFeature(ctx, c, client.FeatureMergeability, "has no merge commit mode (--mode merge)"); err != nil {
1103+ return err
1104+ }
10091105 }
10101106 res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{
1011- Repo: args[0], Number: n, Mode: mode, HeadId: head, DeleteSourceBranch: deleteBranch,
1107+ Repo: args[0], Number: n, Mode: pbMode, HeadId: head, DeleteSourceBranch: deleteBranch,
10121108 }))
10131109 if err != nil {
10141110 return c.Describe(err)
@@ -1018,16 +1114,44 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
10181114 if *asJSON {
10191115 return writeJSON(cmd.OutOrStdout(), jsonMerged{jsonMergeRequest: toJSONMergeRequest(m, c.URL()), SourceBranchDeleted: deleted})
10201116 }
1021- _, err = fmt.Fprintf(cmd.OutOrStdout(), "merged %s!%d into %s (%s) at %s\n", clean(m.GetRepo()), m.GetNumber(),
1117+ out := cmd.OutOrStdout()
1118+ _, err = fmt.Fprintf(out, "merged %s!%d into %s (%s) at %s\n", clean(m.GetRepo()), m.GetNumber(),
10221119 clean(m.GetTargetBranch()), modeName(m.GetMergeMode()), shortID(m.GetMergedCommitId()))
1120+ if err == nil && pbMode != ermite.MergeMode_MERGE_MODE_FAST_FORWARD {
1121+ _, err = fmt.Fprintln(out, signedNote(ctx, c))
1122+ }
10231123 if err == nil && deleted {
1024- _, err = fmt.Fprintf(cmd.OutOrStdout(), "deleting branch %s (done by the server after the merge)\n", clean(m.GetSourceBranch()))
1124+ _, err = fmt.Fprintf(out, "deleting branch %s (done by the server after the merge)\n", clean(m.GetSourceBranch()))
10251125 }
10261126 return err
10271127 }
10281128 return cmd
10291129 }
10301130
1131+// signedNote says that a squash or merge commit is signed by the server's
1132+// key, with its fingerprint when the server tells it.
1133+func signedNote(ctx context.Context, c *client.Client) string {
1134+ const how = "; `ermite signing-key show` prints the key to verify it"
1135+ res, err := c.Server().GetSigningKey(ctx, connect.NewRequest(&ermite.GetSigningKeyRequest{}))
1136+ if err != nil || res.Msg.GetFingerprint() == "" {
1137+ return "the commit is signed by the server's key" + how
1138+ }
1139+ return "the commit is signed by the server's key " + clean(res.Msg.GetFingerprint()) + how
1140+}
1141+
1142+// requireFeature refuses to go on with a server that does not list the
1143+// feature. A server whose features cannot be read is refused too.
1144+func requireFeature(ctx context.Context, c *client.Client, feature, what string) error {
1145+ res, err := c.Server().GetVersion(ctx, connect.NewRequest(&ermite.GetVersionRequest{}))
1146+ if err != nil {
1147+ return c.Describe(err)
1148+ }
1149+ if !slices.Contains(res.Msg.GetFeatures(), feature) {
1150+ return fmt.Errorf("the server does not support %s, so it %s (upgrade the server, see `ermite version`)", feature, what)
1151+ }
1152+ return nil
1153+}
1154+
10311155 func newMRCloseCmd(env *cliEnv) *cobra.Command {
10321156 cmd := &cobra.Command{
10331157 Use: "close <repo> <number>",
M internal/app/cli_mr_e2e_test.go +5 -5
@@ -260,19 +260,19 @@ func TestMergeRequestsCLIEndToEnd(t *testing.T) {
260260 require.Error(t, res.err)
261261 assert.Contains(t, res.err.Error(), "precondition failed")
262262
263- // Squash, with --squash. Without --head the merge is refused before it reaches the server.
263+ // Squash, with --mode squash. Without --head the merge is refused before it reaches the server.
264264 c.mustGit(wt, "checkout", "-q", "-b", "feat/two", head3)
265265 c.mustGit(wt, "-c", "user.name=Cy", "-c", "user.email=cy@example.org", "commit", "-q", "--allow-empty", "-m", "empty by Cy")
266266 c.writeFile(wt, "two.txt", []byte("two\n"))
267267 c.mustGit(wt, "push", "-q", url, "feat/two")
268268 c.cli(t, secret, "mr", "create", "proj", "--source", "feat/two", "--title", "Second").ok(t)
269269 w.nextOfType(t, "merge_request_opened")
270- _, stderr, code = c.bin(t, secret, "mr", "merge", "proj", "2", "--squash")
270+ _, stderr, code = c.bin(t, secret, "mr", "merge", "proj", "2", "--mode", "squash")
271271 requireOneLineError(t, stderr, code, "--head is required", "ermite mr show proj 2")
272272 head4 := c.bareGit("proj", "rev-parse", "feat/two")
273- res = c.cli(t, secret, "mr", "merge", "proj", "2", "--squash", "--head", head4)
273+ res = c.cli(t, secret, "mr", "merge", "proj", "2", "--mode", "squash", "--head", head4)
274274 require.NoError(t, res.err, res.stderr)
275- assert.Regexp(t, `^merged proj!2 into main \(squash\) at [0-9a-f]{10}\n$`, res.stdout)
275+ assert.Regexp(t, `^merged proj!2 into main \(squash\) at [0-9a-f]{10}\nthe commit is signed by the server's key SHA256:\S+; .ermite signing-key show. prints the key to verify it\n$`, res.stdout)
276276 sq := c.bareGit("proj", "rev-parse", "main")
277277 assert.Equal(t, head3, c.bareGit("proj", "rev-parse", sq+"^"))
278278 assert.Equal(t, "Second (!2)", c.bareGit("proj", "log", "-1", "--format=%s", sq))
@@ -338,7 +338,7 @@ func TestSigningKeyShowVerifiesASquashCommit(t *testing.T) {
338338 c.writeFile(wt, "x.txt", []byte("x\n"))
339339 c.mustGit(wt, "push", "-q", url, "main", "feat")
340340 c.cli(t, adminToken, "mr", "create", "proj", "--source", "feat", "--title", "Add x").ok(t)
341- res := c.cli(t, adminToken, "mr", "merge", "proj", "1", "--squash", "--head", c.bareGit("proj", "rev-parse", "feat"))
341+ res := c.cli(t, adminToken, "mr", "merge", "proj", "1", "--mode", "squash", "--head", c.bareGit("proj", "rev-parse", "feat"))
342342 require.NoError(t, res.err, res.stderr)
343343 sq := c.bareGit("proj", "rev-parse", "main")
344344
M internal/app/cli_mr_test.go +80 -4
@@ -3,6 +3,8 @@
33 package app
44
55 import (
6+ "encoding/json"
7+ "regexp"
68 "strings"
79 "testing"
810
@@ -174,6 +176,53 @@ func TestMRShow(t *testing.T) {
174176 assert.Contains(t, res.stderr, "head you last read is gone")
175177 }
176178
179+// The mergeability of an open request is a line of `mr show` and a field of
180+// its JSON.
181+func TestMRShowMergeability(t *testing.T) {
182+ withState := func(mg *ermite.Mergeability) *ermite.GetMergeRequestResponse {
183+ m := proto.Clone(mrOne).(*ermite.MergeRequest)
184+ m.Mergeability = mg
185+ return &ermite.GetMergeRequestResponse{MergeRequest: m}
186+ }
187+ var cur *ermite.GetMergeRequestResponse
188+ f := &fakeAPI{getMR: func(*ermite.GetMergeRequestRequest) (*ermite.GetMergeRequestResponse, error) { return cur, nil }}
189+ url := f.serve(t)
190+ for _, tt := range []struct {
191+ mg *ermite.Mergeability
192+ line string
193+ }{
194+ {&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_UP_TO_DATE}, "up to date"},
195+ {&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_BEHIND, Behind: 1000, BehindCapped: true}, "behind by 1000+"},
196+ {&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_CONFLICT, ConflictPaths: []string{"a.txt", "b/c.go"}, ConflictPathsTruncated: true}, "conflict (a.txt, b/c.go, ...)"},
197+ {&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_CHECKING}, "checking"},
198+ } {
199+ cur = withState(tt.mg)
200+ res := runCLI(t, envFor(url), "", "mr", "show", "alpha", "1").ok(t)
201+ assert.Regexp(t, `(?m)^mergeability\s+.*`+regexp.QuoteMeta(tt.line), res.stdout)
202+ }
203+ cur = withState(&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_BEHIND, Behind: 2, TargetId: headA})
204+ res := runCLI(t, envFor(url), "", "mr", "show", "alpha", "1", "--json").ok(t)
205+ var got struct {
206+ Mergeability *struct {
207+ State string `json:"state"`
208+ Behind int `json:"behind"`
209+ TargetID string `json:"target_id"`
210+ } `json:"mergeability"`
211+ }
212+ require.NoError(t, json.Unmarshal([]byte(res.stdout), &got))
213+ require.NotNil(t, got.Mergeability)
214+ assert.Equal(t, "behind", got.Mergeability.State)
215+ assert.Equal(t, 2, got.Mergeability.Behind)
216+ assert.Equal(t, headA, got.Mergeability.TargetID)
217+
218+ // No mergeability (a merged request, or an older server): no line, null.
219+ cur = withState(nil)
220+ res = runCLI(t, envFor(url), "", "mr", "show", "alpha", "1").ok(t)
221+ assert.NotContains(t, res.stdout, "mergeability")
222+ res = runCLI(t, envFor(url), "", "mr", "show", "alpha", "1", "--json").ok(t)
223+ assert.Regexp(t, `"mergeability":\s*null`, res.stdout)
224+}
225+
177226 func TestMRShowMarkRead(t *testing.T) {
178227 f := &fakeAPI{
179228 getMR: func(*ermite.GetMergeRequestRequest) (*ermite.GetMergeRequestResponse, error) {
@@ -252,6 +301,12 @@ func TestMRResolveMergeAndClose(t *testing.T) {
252301 closeMR: func(*ermite.CloseMergeRequestRequest) (*ermite.CloseMergeRequestResponse, error) {
253302 return &ermite.CloseMergeRequestResponse{MergeRequest: closed}, nil
254303 },
304+ getSigningKey: func(*ermite.GetSigningKeyRequest) (*ermite.GetSigningKeyResponse, error) {
305+ return &ermite.GetSigningKeyResponse{Fingerprint: "SHA256:abc"}, nil
306+ },
307+ getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
308+ return &ermite.GetVersionResponse{Version: "test", Features: []string{"merge_requests.mergeability"}}, nil
309+ },
255310 }
256311 url := f.serve(t)
257312
@@ -265,16 +320,37 @@ func TestMRResolveMergeAndClose(t *testing.T) {
265320 assert.False(t, reqs[1].(*ermite.ResolveCommentRequest).GetResolved())
266321
267322 // Merge with an explicit head and mode.
268- res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--squash", "--head", headA).ok(t)
269- assert.Equal(t, "merged alpha!1 into main (squash) at dddddddddd\n", res.stdout)
323+ res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--mode", "squash", "--head", headA).ok(t)
324+ assert.Equal(t, "merged alpha!1 into main (squash) at dddddddddd\n"+
325+ "the commit is signed by the server's key SHA256:abc; `ermite signing-key show` prints the key to verify it\n", res.stdout)
270326 assert.Empty(t, res.stderr)
271327 calls, reqs := f.snapshot()
272- assert.Equal(t, []string{"ResolveComment", "ResolveComment", "MergeMergeRequest"}, calls, "an explicit head needs no read first")
328+ assert.Equal(t, []string{"ResolveComment", "ResolveComment", "MergeMergeRequest", "GetSigningKey"}, calls, "an explicit head needs no read first; the signing key is for the note")
273329 m := reqs[2].(*ermite.MergeMergeRequestRequest)
274330 assert.Equal(t, ermite.MergeMode_MERGE_MODE_SQUASH, m.GetMode())
275331 assert.Equal(t, headA, m.GetHeadId())
276332 assert.EqualValues(t, 1, m.GetNumber())
277333
334+ // A merge commit, and a mode that does not exist.
335+ merged.MergeMode = ermite.MergeMode_MERGE_MODE_MERGE_COMMIT
336+ res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--mode", "merge", "--head", headA).ok(t)
337+ assert.Contains(t, res.stdout, "merged alpha!1 into main (merge) at dddddddddd\n")
338+ assert.Contains(t, res.stdout, "signed by the server's key SHA256:abc")
339+ _, reqs = f.snapshot()
340+ var lastMerge *ermite.MergeMergeRequestRequest
341+ for _, r := range reqs {
342+ if mr, ok := r.(*ermite.MergeMergeRequestRequest); ok {
343+ lastMerge = mr
344+ }
345+ }
346+ require.NotNil(t, lastMerge)
347+ assert.Equal(t, ermite.MergeMode_MERGE_MODE_MERGE_COMMIT, lastMerge.GetMode())
348+ bad := runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--mode", "rebase", "--head", headA)
349+ require.Error(t, bad.err)
350+ assert.Contains(t, bad.err.Error(), "invalid merge mode")
351+ assert.Error(t, runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--squash", "--head", headA).err, "--squash is gone")
352+ merged.MergeMode = ermite.MergeMode_MERGE_MODE_SQUASH
353+
278354 // The default mode is a fast-forward.
279355 res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--json").ok(t)
280356 assert.Equal(t, "merged", decode[mrOut](t, res.stdout).State)
@@ -299,7 +375,7 @@ func TestMRMergeNeedsTheHead(t *testing.T) {
299375 url := f.serve(t)
300376 for _, args := range [][]string{
301377 {"mr", "merge", "alpha", "1"},
302- {"mr", "merge", "alpha", "1", "--squash"},
378+ {"mr", "merge", "alpha", "1", "--mode", "squash"},
303379 {"mr", "merge", "alpha", "1", "--head", " "},
304380 } {
305381 res := runCLI(t, envFor(url), "", args...)
M internal/app/cli_repo.go +9 -7
@@ -33,7 +33,7 @@ type jsonRepo struct {
3333 DefaultBranch string `json:"default_branch"`
3434 CreatedAt *string `json:"created_at"`
3535 PushedAt *string `json:"pushed_at"`
36- // MergeModes are the allowed merge modes ("fast-forward", "squash").
36+ // MergeModes are the allowed merge modes ("fast-forward", "squash", "merge").
3737 MergeModes []string `json:"merge_modes"`
3838 FailingChecksBlock bool `json:"failing_checks_block"`
3939 // Issues is the issue mode ("off", "owner" or "public").
@@ -114,8 +114,8 @@ func parseVisibility(s string) (ermite.Visibility, error) {
114114 const jsonRepoHelp = `
115115 JSON fields of a repository (--json): name, description, visibility ("public" or
116116 "private"), default_branch, created_at and pushed_at (RFC 3339 UTC; pushed_at is
117-null until the first push), merge_modes (array of the allowed merge modes, "fast-forward" and/or
118-"squash"), failing_checks_block (whether a failing check prevents a merge) and issues (the issue
117+null until the first push), merge_modes (array of the allowed merge modes, "fast-forward", "squash" and/or
118+"merge"), failing_checks_block (whether a failing check prevents a merge) and issues (the issue
119119 mode: "off", "owner" or "public").`
120120
121121 func newRepoCmd(env *cliEnv) *cobra.Command {
@@ -315,7 +315,9 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
315315 Short: "Change a repository's description, topics, visibility, default branch, merge settings or issues",
316316 Long: "Change the fields given as flags and leave the others alone; --description \"\" clears the\n" +
317317 "description and --topics \"\" clears the topics. The default branch must already exist. --merge-modes lists the modes a merge request\n" +
318- "may be merged with (fast-forward, squash; default for a new repository: both), and\n" +
318+ "may be merged with (fast-forward, squash, merge; every mode is allowed by default, so to disable one,\n" +
319+ "list the others: --merge-modes fast-forward,squash disables the merge commit; squash and merge\n" +
320+ "commits are signed with the server's key, see `ermite signing-key show`), and\n" +
319321 "--failing-checks-block says whether a failing check prevents a merge (a failed or unfinished CI check of the head; see README, CI).\n" +
320322 "--issues sets who may use issues: off (default), owner (the owner and tokens with issue:write) or\n" +
321323 "public (visitors of a public repository may also propose issues through a moderated form).\nNeeds a server with the issues feature.\n" +
@@ -327,7 +329,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
327329 cmd.Flags().StringSliceVar(&topics, "topics", nil, "new topics, comma-separated (empty clears them)")
328330 cmd.Flags().StringVar(&visibility, "visibility", "", "new visibility: public or private")
329331 cmd.Flags().StringVar(&defaultBranch, "default-branch", "", "short name of the new default branch")
330- cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash")
332+ cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash, merge (all by default; list the ones to keep)")
331333 cmd.Flags().BoolVar(&failingChecksBlock, "failing-checks-block", true, "a failing check prevents a merge (--failing-checks-block=false to allow it)")
332334 cmd.Flags().StringVar(&issues, "issues", "", "issue mode: off, owner or public")
333335 _ = cmd.RegisterFlagCompletionFunc("issues", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
@@ -335,7 +337,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
335337 })
336338 _ = cmd.RegisterFlagCompletionFunc("visibility", visibilityCompletion)
337339 _ = cmd.RegisterFlagCompletionFunc("merge-modes", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
338- return []string{modeFastForward, modeSquash}, cobra.ShellCompDirectiveNoFileComp
340+ return []string{modeFastForward, modeSquash, modeMerge}, cobra.ShellCompDirectiveNoFileComp
339341 })
340342 asJSON := addJSONFlag(cmd)
341343 cmd.RunE = func(cmd *cobra.Command, args []string) error {
@@ -400,7 +402,7 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri
400402 modes = append(modes, mode)
401403 }
402404 if len(modes) == 0 {
403- return nil, errors.New("--merge-modes must name at least one mode: fast-forward, squash")
405+ return nil, errors.New("--merge-modes must name at least one mode: fast-forward, squash, merge")
404406 }
405407 req.Settings = &ermite.RepoSettings{AllowedMergeModes: modes}
406408 paths = append(paths, "settings.allowed_merge_modes")
M internal/app/cli_watch.go +26 -4
@@ -29,8 +29,8 @@ type jsonEvent struct {
2929 MergeRequest *jsonMergeRequest `json:"merge_request,omitempty"`
3030 Number int64 `json:"number,omitempty"`
3131 Comment *jsonComment `json:"comment,omitempty"`
32- // Mergeability is the state of a merge_request_mergeability event.
33- Mergeability string `json:"mergeability,omitempty"`
32+ // Mergeability is set for a merge_request_mergeability event.
33+ Mergeability *jsonMergeability `json:"mergeability,omitempty"`
3434 }
3535
3636 type jsonRefUpdate struct {
@@ -99,7 +99,7 @@ func toJSONEvent(ev *ermite.Event, baseURL string) (jsonEvent, bool) {
9999 out.Type, out.Repo, out.Number, out.Comment = eventCommentResolved, k.CommentResolved.GetRepo(), k.CommentResolved.GetNumber(), &c
100100 case *ermite.Event_MergeRequestMergeability:
101101 out.Type, out.Repo, out.Number = eventMRMergeability, k.MergeRequestMergeability.GetRepo(), k.MergeRequestMergeability.GetNumber()
102- out.Mergeability = k.MergeRequestMergeability.GetMergeability().GetState().String()
102+ out.Mergeability = toJSONMergeability(k.MergeRequestMergeability.GetMergeability())
103103 case *ermite.Event_Gap:
104104 out.Type = eventGap
105105 default:
@@ -133,13 +133,35 @@ func eventLine(e jsonEvent) string {
133133 case eventCommentAdded, eventCommentEdited, eventCommentDeleted, eventCommentResolved:
134134 detail = fmt.Sprintf("%s!%d by %s", e.Repo, e.Number, e.Comment.Author)
135135 case eventMRMergeability:
136- detail = fmt.Sprintf("%s!%d %s", e.Repo, e.Number, e.Mergeability)
136+ detail = fmt.Sprintf("%s!%d mergeability: %s", e.Repo, e.Number, mergeabilityEventText(e.Mergeability))
137137 case eventGap:
138138 detail = "events were lost, reload what you display"
139139 }
140140 return clean(ts + " " + e.Type + " " + detail)
141141 }
142142
143+// mergeabilityEventText is the short form of a mergeability for a watch line:
144+// "up_to_date", "behind by 2", "conflict (a, b)", "checking".
145+func mergeabilityEventText(m *jsonMergeability) string {
146+ if m == nil {
147+ return "unknown"
148+ }
149+ switch m.State {
150+ case "behind":
151+ return fmt.Sprintf("behind by %d%s", m.Behind, plus(m.BehindCapped))
152+ case "conflict":
153+ if len(m.ConflictPaths) == 0 {
154+ return "conflict (no common history)"
155+ }
156+ more := ""
157+ if m.ConflictPathsTruncated {
158+ more = ", ..."
159+ }
160+ return "conflict (" + strings.Join(m.ConflictPaths, ", ") + more + ")"
161+ }
162+ return m.State
163+}
164+
143165 func refUpdateText(u jsonRefUpdate) string {
144166 short := func(id string) string {
145167 if len(id) > 8 {
M internal/app/cli_watch_test.go +43 -0
@@ -200,6 +200,49 @@ func TestWatchMergeRequestEvents(t *testing.T) {
200200 assert.Equal(t, "merge_request_closed", decode[eventOut](t, lines[3]).Type)
201201 }
202202
203+// A mergeability event shows the state, with how far behind or the paths.
204+func TestWatchMergeabilityEvents(t *testing.T) {
205+ mg := func(m *ermite.Mergeability) func(*ermite.Event) {
206+ return func(e *ermite.Event) {
207+ e.Kind = &ermite.Event_MergeRequestMergeability{MergeRequestMergeability: &ermite.MergeRequestMergeability{Repo: "alpha", Number: 1, Mergeability: m}}
208+ }
209+ }
210+ events := []*ermite.WatchResponse{
211+ evt("1", mg(&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_UP_TO_DATE})),
212+ evt("2", mg(&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_BEHIND, Behind: 2})),
213+ evt("3", mg(&ermite.Mergeability{State: ermite.MergeabilityState_MERGEABILITY_STATE_CONFLICT, ConflictPaths: []string{"a.txt", "b.txt"}})),
214+ }
215+ f := &fakeAPI{watch: finishing(events)}
216+ url := f.serve(t)
217+
218+ res := runCLI(t, envFor(url), "", "watch")
219+ require.Error(t, res.err)
220+ assert.Equal(t, ""+
221+ "2026-10-10T12:00:00Z merge_request_mergeability alpha!1 mergeability: up_to_date\n"+
222+ "2026-10-10T12:00:00Z merge_request_mergeability alpha!1 mergeability: behind by 2\n"+
223+ "2026-10-10T12:00:00Z merge_request_mergeability alpha!1 mergeability: conflict (a.txt, b.txt)\n", res.stdout)
224+
225+ res = runCLI(t, envFor(url), "", "watch", "--json")
226+ lines := strings.Split(strings.TrimSpace(res.stdout), "\n")
227+ require.Len(t, lines, 3)
228+ var ev struct {
229+ Type string `json:"type"`
230+ Number int64 `json:"number"`
231+ Mergeability struct {
232+ State string `json:"state"`
233+ Behind int `json:"behind"`
234+ ConflictPaths []string `json:"conflict_paths"`
235+ } `json:"mergeability"`
236+ }
237+ require.NoError(t, json.Unmarshal([]byte(lines[1]), &ev))
238+ assert.Equal(t, "merge_request_mergeability", ev.Type)
239+ assert.EqualValues(t, 1, ev.Number)
240+ assert.Equal(t, "behind", ev.Mergeability.State)
241+ assert.Equal(t, 2, ev.Mergeability.Behind)
242+ require.NoError(t, json.Unmarshal([]byte(lines[2]), &ev))
243+ assert.Equal(t, []string{"a.txt", "b.txt"}, ev.Mergeability.ConflictPaths)
244+}
245+
203246 func TestNoteWatchEvents(t *testing.T) {
204247 note := &ermite.Comment{Id: "4", ReplyTo: "2", StartLine: 10, Line: 24, Verdict: ermite.Verdict_VERDICT_APPROVE, EditedAt: ts("2026-10-10T12:00:00Z"), Deleted: true, Author: "owner"}
205248 events := []*ermite.Event{
M internal/app/testdata/web/commits-root.html +6 -0
@@ -972,7 +972,13 @@
972972 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
973973 htmx:sse:error -> ermiteLive.error(event.detail.status);
974974 htmx:sse:before:connection -> ermiteLive.gate(event);
975+<<<<<<< HEAD
975976 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
977+||||||| parent of 7eacfea (feat(web,cli): live mergeability in the merge box, merge modes and inline merge errors)
978+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
979+=======
980+ home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
981+>>>>>>> 7eacfea (feat(web,cli): live mergeability in the merge box, merge modes and inline merge errors)
976982 gap -> ermiteLive.reload()">
977983 </span>
978984 </div>
M internal/app/testdata/web/commits-small.html +6 -0
@@ -317,7 +317,13 @@
317317 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
318318 htmx:sse:error -> ermiteLive.error(event.detail.status);
319319 htmx:sse:before:connection -> ermiteLive.gate(event);
320+<<<<<<< HEAD
320321 home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
322+||||||| parent of 7eacfea (feat(web,cli): live mergeability in the merge box, merge modes and inline merge errors)
323+ home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
324+=======
325+ home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job -> ermiteLive.swap(event.detail.data);
326+>>>>>>> 7eacfea (feat(web,cli): live mergeability in the merge box, merge modes and inline merge errors)
321327 gap -> ermiteLive.reload()">
322328 </span>
323329 </div>
M internal/app/testdata/web/home.html +1 -1
@@ -193,7 +193,7 @@
193193 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
194194 htmx:sse:error -> ermiteLive.error(event.detail.status);
195195 htmx:sse:before:connection -> ermiteLive.gate(event);
196- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
196+ home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
197197 gap -> ermiteLive.reload()">
198198 </span>
199199 </div>
M internal/app/testdata/web/refs.html +1 -1
@@ -199,7 +199,7 @@
199199 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
200200 htmx:sse:error -> ermiteLive.error(event.detail.status);
201201 htmx:sse:before:connection -> ermiteLive.gate(event);
202- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
202+ home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
203203 gap -> ermiteLive.reload()">
204204 </span>
205205 </div>
M internal/app/testdata/web/summary.html +1 -1
@@ -225,7 +225,7 @@
225225 htmx:sse:close, htmx:sse:error -> ermiteLive.lost();
226226 htmx:sse:error -> ermiteLive.error(event.detail.status);
227227 htmx:sse:before:connection -> ermiteLive.gate(event);
228- home-list, repo-last-push, refs-list, commits-head, mr-notice, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
228+ home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
229229 gap -> ermiteLive.reload()">
230230 </span>
231231 </div>
M internal/app/web.go +5 -0
@@ -35,6 +35,10 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han
3535 if f.Passkeys != nil {
3636 passkeys = webPasskeys{f.Passkeys}
3737 }
38+ var signing ports.SigningKeyService
39+ if f.SigningKey != nil {
40+ signing = f.SigningKey
41+ }
3842 // Without artifacts (ERMITE_CI_ARTIFACTS=0) the run page has no section
3943 // for them and no download route.
4044 var artifacts ports.ArtifactService
@@ -50,6 +54,7 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han
5054 Inbox: f.Inbox,
5155 IssueForm: f.IssueSubmissions,
5256 Moderation: f.IssueModeration,
57+ SigningKey: signing,
5358 Runs: f.CI,
5459 Artifacts: artifacts,
5560 Events: f.EventLog,
M internal/app/web_events_test.go +9 -0
@@ -37,6 +37,12 @@ type sseStream struct {
3737 // background. It returns once the server answered, which it does when the
3838 // watch is registered: what happens after is not missed.
3939 func (h *harness) sse(query, lastID string) *sseStream {
40+ h.t.Helper()
41+ return h.sseAs(query, lastID, nil)
42+}
43+
44+// sseAs opens the stream with the headers given (the owner's cookie).
45+func (h *harness) sseAs(query, lastID string, headers map[string]string) *sseStream {
4046 h.t.Helper()
4147 ctx, cancel := context.WithCancel(h.ctx)
4248 h.t.Cleanup(cancel)
@@ -45,6 +51,9 @@ func (h *harness) sse(query, lastID string) *sseStream {
4551 if lastID != "" {
4652 req.Header.Set("Last-Event-ID", lastID)
4753 }
54+ for k, v := range headers {
55+ req.Header.Set(k, v)
56+ }
4857 resp, err := http.DefaultClient.Do(req)
4958 require.NoError(h.t, err)
5059 t := h.t
M internal/app/web_mr_test.go +197 -10
@@ -10,6 +10,7 @@ import (
1010 "regexp"
1111 "strings"
1212 "testing"
13+ "time"
1314
1415 "github.com/stretchr/testify/assert"
1516 "github.com/stretchr/testify/require"
@@ -165,12 +166,15 @@ func TestWebMergeRequestPagesRender(t *testing.T) {
165166 assert.Equal(t, head, formHead(t, own.body))
166167 assert.Contains(t, own.body, `action="/proj/merge-requests/1/merge"`)
167168 assert.Contains(t, own.body, `id="merge-menu"`, "the merge is a menu of the bar above the diff")
168- assert.Contains(t, own.body, `<input type="radio" name="mode" value="fast-forward" checked>`, "the first mode is the default")
169- assert.Contains(t, own.body, `<input type="radio" name="mode" value="squash">`)
169+ assert.Contains(t, own.body, `name="mode" value="fast-forward" checked`, "the first possible mode is the default")
170+ assert.Contains(t, own.body, `name="mode" value="squash"`)
171+ assert.Contains(t, own.body, `name="mode" value="merge"`, "every mode is allowed by default")
170172 assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head)
171173 assert.Contains(t, own.body, `name="delete_branch" value="1"> delete feat/search after the merge <kbd aria-hidden="true">D</kbd>`)
172174 assert.NotContains(t, own.body, `name="delete_branch" value="1" checked>`, "unchecked by default")
173- assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head+" --squash")
175+ assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head+" --mode squash")
176+ assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head+" --mode merge")
177+ assert.NotContains(t, own.body, "--squash")
174178 assert.Contains(t, own.body, `action="/proj/merge-requests/1/comments"`)
175179 assert.Contains(t, own.body, `action="/proj/merge-requests/1/close"`)
176180 assert.Contains(t, own.body, `href="/proj/merge-requests/1?on=new:1:search.go#f`, "line numbers link to their form")
@@ -573,8 +577,7 @@ func TestWebMergeRequestEvents(t *testing.T) {
573577 r := newMRRepo(t, domain.VisibilityPublic)
574578 list := r.sse("topic=repo:proj&view=mrs", "")
575579 r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Secret <title>").ok(t)
576- ev := list.next(t, watchWait)
577- assert.Equal(t, "mr-notice", ev.name)
580+ ev := nextNamed(t, list, "mr-notice")
578581 assert.Contains(t, ev.data, `<div id="mr-notice" class="live-slot" role="status" data-slot>`)
579582 assert.Contains(t, ev.data, `href="/proj/merge-requests"`)
580583 assert.NotContains(t, ev.data, "Secret")
@@ -582,19 +585,18 @@ func TestWebMergeRequestEvents(t *testing.T) {
582585 one := r.sse("topic=repo:proj&view=mr&n=1&ref=feat/search", "")
583586 other := r.sse("topic=repo:proj&view=mr&n=2&ref=feat/other", "")
584587 r.cli(t, adminToken, "mr", "comment", "proj", "1", "-m", "private words").ok(t)
585- ev = one.next(t, watchWait)
586- assert.Equal(t, "mr-notice", ev.name)
588+ ev = nextNamed(t, one, "mr-notice")
587589 assert.Contains(t, ev.data, "a comment was added")
588590 assert.Contains(t, ev.data, `href="/proj/merge-requests/1"`)
589591 assert.NotContains(t, ev.data, "private words")
590592
591593 r.writeFile(r.wt, "more.txt", []byte("more\n"))
592594 r.mustGit(r.wt, "push", "-q", r.url, "feat/search")
593- ev = one.next(t, watchWait)
595+ ev = nextNamed(t, one, "mr-notice")
594596 assert.Contains(t, ev.data, "the branch changed")
595597
596598 r.cli(t, adminToken, "mr", "close", "proj", "1").ok(t)
597- assert.Contains(t, one.next(t, watchWait).data, "was closed")
599+ assert.Contains(t, nextNamed(t, one, "mr-notice").data, "was closed")
598600
599601 // A page of another request hears nothing of this one.
600602 for drained := false; !drained; {
@@ -607,7 +609,20 @@ func TestWebMergeRequestEvents(t *testing.T) {
607609 drained = true
608610 }
609611 }
610- 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")+`"`)
612+ 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")+`&amp;target=main"`)
613+}
614+
615+// nextNamed returns the next event that carries the fragment event: the
616+// events that change nothing on the page (an id alone) are skipped.
617+func nextNamed(t *testing.T, s *sseStream, name string) sseEvent {
618+ t.Helper()
619+ for range 10 {
620+ if ev := s.next(t, watchWait); ev.name == name {
621+ return ev
622+ }
623+ }
624+ t.Fatalf("no %s event", name)
625+ return sseEvent{}
611626 }
612627
613628 func (r *mrRepo) get1(path string) string {
@@ -726,3 +741,175 @@ func TestWebMergeRequestFileWindows(t *testing.T) {
726741 }
727742
728743 var reWindowSrc = regexp.MustCompile(`data-src="([^"]*from=25[^"]*)"`)
744+
745+// advanceMain pushes a commit to main that the feature branch lacks. file is
746+// the file it adds (give search.go to conflict with the feature branch).
747+func (r *mrRepo) advanceMain(file, content string) {
748+ r.t.Helper()
749+ r.mustGit(r.wt, "checkout", "-q", "main")
750+ r.writeFile(r.wt, file, []byte(content))
751+ r.mustGit(r.wt, "push", "-q", r.url, "main")
752+ r.mustGit(r.wt, "checkout", "-q", "feat/search")
753+}
754+
755+// mrPageWith polls the owner's page of !1 until it contains want.
756+func (r *mrRepo) mrPageWith(token, want string) string {
757+ r.t.Helper()
758+ var body string
759+ require.Eventually(r.t, func() bool {
760+ body = browser{r.harness}.get("/proj/merge-requests/1", as(token)).body
761+ return strings.Contains(body, want)
762+ }, 10*time.Second, 25*time.Millisecond, "the page never showed %q", want)
763+ return body
764+}
765+
766+// The merge box shows the mergeability of the request: ready, then behind
767+// (fast-forward is disabled with its reason, the other modes are not), then
768+// in conflict (no mode is possible and the button says why).
769+func TestWebMergeRequestMergeabilityBadge(t *testing.T) {
770+ r := newMRRepo(t, domain.VisibilityPublic)
771+ token := r.signIn(t)
772+ r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Add search").ok(t)
773+
774+ body := r.mrPageWith(token, "Ready to merge")
775+ assert.Contains(t, body, `<div id="mr-mergeability" class="live-slot" role="status" data-slot>`)
776+ assert.Contains(t, body, `data-state="up_to_date"`)
777+ assert.Contains(t, body, `name="mode" value="fast-forward" checked`)
778+ assert.NotContains(t, body, `value="fast-forward" checked disabled`)
779+ assert.Contains(t, body, `id="mr-signing"`, "squash and merge commits say who signs them")
780+ assert.Contains(t, body, "signed with ermite's own key, generated automatically on first start")
781+ assert.Contains(t, body, "<code>ermite signing-key show</code>")
782+ assert.Contains(t, body, "SHA256:", "the fingerprint of the signing key")
783+ assert.Contains(t, body, `:disabled="`, "the controls follow the badge")
784+ assertNoInline(t, body)
785+
786+ // Main moves on: the branch is behind by one commit and merges cleanly.
787+ r.advanceMain("other.txt", "other\n")
788+ body = r.mrPageWith(token, "Behind main by 1 commit")
789+ assert.Contains(t, body, "merges without conflicts")
790+ assert.Contains(t, body, `data-ff-ok="false"`)
791+ assert.Contains(t, body, `data-merge-ok="true"`)
792+ assert.Regexp(t, `<input type="radio" name="mode" value="fast-forward"[^>]* disabled`, body)
793+ assert.Contains(t, body, `title="behind the target: rebase or use squash/merge"`)
794+ assert.Regexp(t, `name="mode" value="squash" checked`, body, "the first possible mode is checked")
795+ assert.NotRegexp(t, `name="mode" value="squash"[^>]* disabled`, body)
796+
797+ // A conflicting change to main: every mode is disabled and the files are listed.
798+ r.mustGit(r.wt, "checkout", "-q", "main")
799+ r.writeFile(r.wt, "search.go", []byte("package other\n"))
800+ r.mustGit(r.wt, "push", "-q", r.url, "main")
801+ body = r.mrPageWith(token, "Conflicts with main")
802+ assert.Contains(t, body, "rebase needed")
803+ assert.Contains(t, body, `<ul class="mg-paths" aria-label="conflicting files"><li><code>search.go</code></li></ul>`)
804+ assert.Regexp(t, `<button type="submit" class="btn primary" data-default disabled title="conflicts with the target"`, body)
805+ assert.NotContains(t, body, `name="mode" value="squash" checked`)
806+
807+ // The server refuses it too, and says so in the box of the page.
808+ res := browser{r.harness}.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"squash"}, "head_id": {formHead(t, body)}})
809+ assert.Equal(t, http.StatusConflict, res.status)
810+ assert.Contains(t, res.body, `<p class="d-err" role="alert">`)
811+ assert.Contains(t, res.body, "conflicts with main")
812+}
813+
814+// A merge the server refuses (the target moved after the page was read) shows
815+// the page of the request again, 409, with the reason in its merge box.
816+func TestWebMergeRequestInlineMergeError(t *testing.T) {
817+ r := newMRRepo(t, domain.VisibilityPublic)
818+ b := browser{r.harness}
819+ token := r.signIn(t)
820+ r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Add search").ok(t)
821+ body := r.mrPageWith(token, "Ready to merge")
822+ head := formHead(t, body)
823+
824+ r.advanceMain("other.txt", "other\n") // raced: the form below still says fast-forward
825+ res := b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"fast-forward"}, "head_id": {head}})
826+ assert.Equal(t, http.StatusConflict, res.status)
827+ assert.Equal(t, "no-store", res.hdr.Get("Cache-Control"))
828+ assert.Contains(t, res.body, `id="merge-menu"`, "the page of the request, not a state page")
829+ assert.Contains(t, res.body, `<p class="d-err" role="alert">`)
830+ assert.Contains(t, res.body, "the target has moved")
831+ assert.Contains(t, res.body, `<details class="menu mmenu" id="merge-menu" data-state="open" open>`, "the box is open on the error")
832+ assert.Equal(t, head, r.headOf("feat/search"))
833+ assert.NotContains(t, b.get("/proj/merge-requests/1", as(token)).body, "d-err", "a reload shows no error")
834+
835+ // A head that moved is refused the same way.
836+ r.writeFile(r.wt, "more.txt", []byte("more\n"))
837+ r.mustGit(r.wt, "push", "-q", r.url, "feat/search")
838+ res = b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"squash"}, "head_id": {head}})
839+ assert.Equal(t, http.StatusConflict, res.status)
840+ assert.Contains(t, res.body, `<p class="d-err" role="alert">`)
841+ assert.Contains(t, res.body, "branch moved")
842+
843+ // Other refusals keep the state page; a mode that does not exist is a 400.
844+ assert.Equal(t, http.StatusBadRequest, b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"rebase"}, "head_id": {head}}).status)
845+}
846+
847+// A merge commit through the page; with the branch deletion requested the
848+// merged page says the branch is being deleted, not that it is gone.
849+func TestWebMergeRequestMergeCommitAndDeletingNote(t *testing.T) {
850+ r := newMRRepo(t, domain.VisibilityPublic)
851+ b := browser{r.harness}
852+ token := r.signIn(t)
853+ r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Add search").ok(t)
854+ r.advanceMain("other.txt", "other\n")
855+ body := r.mrPageWith(token, "Behind main by 1 commit")
856+ head := formHead(t, body)
857+ main0 := r.headOf("main")
858+
859+ res := b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"merge"}, "head_id": {head}, "delete_branch": {"1"}})
860+ require.Equal(t, http.StatusSeeOther, res.status, res.body)
861+ assert.Equal(t, "/proj/merge-requests/1?deleting=1", res.hdr.Get("Location"))
862+ merged := r.headOf("main")
863+ assert.Equal(t, main0, r.bareGit("proj", "rev-parse", merged+"^1"))
864+ assert.Equal(t, head, r.bareGit("proj", "rev-parse", merged+"^2"))
865+ assert.Contains(t, r.bareGit("proj", "cat-file", "-p", merged), "gpgsig")
866+ page := b.get("/proj/merge-requests/1?deleting=1", as(token))
867+ assert.Contains(t, page.body, "merge commit")
868+ if strings.Contains(page.body, "is being deleted") {
869+ assert.Contains(t, page.body, "feat/search is being deleted")
870+ }
871+ assert.NotContains(t, page.body, "is deleted")
872+ r.forge.MergeRequests.Wait(r.ctx)
873+ assert.Empty(t, r.bareGit("proj", "for-each-ref", "refs/heads/feat/search"))
874+ assert.NotContains(t, b.get("/proj/merge-requests/1?deleting=1", as(token)).body, "is being deleted", "once gone, nothing is being deleted")
875+}
876+
877+// The badge follows the request live: a push to the target turns the fragment
878+// of the page to "checking" or "behind", and the settled result follows.
879+func TestWebMergeRequestMergeabilityLive(t *testing.T) {
880+ r := newMRRepo(t, domain.VisibilityPublic)
881+ token := r.signIn(t)
882+ r.cli(t, adminToken, "mr", "create", "proj", "--source", "feat/search", "--title", "Add search").ok(t)
883+ r.mrPageWith(token, "Ready to merge")
884+
885+ one := r.sseAs("topic=repo:proj&view=mr&n=1&ref=feat/search&target=main", "", as(token))
886+ anon := r.sse("topic=repo:proj&view=mr&n=1&ref=feat/search&target=main", "")
887+ r.advanceMain("other.txt", "other\n")
888+ deadline := time.After(watchWait)
889+ for done := false; !done; {
890+ ev := nextNamed(t, one, "mr-mergeability")
891+ select {
892+ case <-deadline:
893+ t.Fatal("the badge never showed the result")
894+ default:
895+ }
896+ if strings.Contains(ev.data, "Behind main by 1 commit") {
897+ assert.Contains(t, ev.data, `<div id="mr-mergeability" class="live-slot" role="status" data-slot>`)
898+ assert.Contains(t, ev.data, `data-ff-ok="false"`)
899+ assert.Contains(t, ev.data, `data-merge-ok="true"`)
900+ done = true
901+ } else {
902+ assert.Contains(t, ev.data, "Checking…", "before the result, the badge says it checks")
903+ }
904+ }
905+ // The box is the owner's: a visitor's stream carries no badge.
906+ for {
907+ select {
908+ case ev := <-anon.events:
909+ assert.NotEqual(t, "mr-mergeability", ev.name)
910+ continue
911+ default:
912+ }
913+ break
914+ }
915+}
M internal/ports/mocks/MergeRequestService.go +78 -0
@@ -942,6 +942,84 @@ func (_c *MergeRequestService_Merge_Call) RunAndReturn(run func(ctx context.Cont
942942 return _c
943943 }
944944
945+// Mergeability provides a mock function for the type MergeRequestService
946+func (_mock *MergeRequestService) Mergeability(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Mergeability, error) {
947+ ret := _mock.Called(ctx, p, name, number)
948+
949+ if len(ret) == 0 {
950+ panic("no return value specified for Mergeability")
951+ }
952+
953+ var r0 domain.Mergeability
954+ var r1 error
955+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) (domain.Mergeability, error)); ok {
956+ return returnFunc(ctx, p, name, number)
957+ }
958+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) domain.Mergeability); ok {
959+ r0 = returnFunc(ctx, p, name, number)
960+ } else {
961+ r0 = ret.Get(0).(domain.Mergeability)
962+ }
963+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
964+ r1 = returnFunc(ctx, p, name, number)
965+ } else {
966+ r1 = ret.Error(1)
967+ }
968+ return r0, r1
969+}
970+
971+// MergeRequestService_Mergeability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mergeability'
972+type MergeRequestService_Mergeability_Call struct {
973+ *mock.Call
974+}
975+
976+// Mergeability is a helper method to define mock.On call
977+// - ctx context.Context
978+// - p domain.Principal
979+// - name domain.RepoName
980+// - number int64
981+func (_e *MergeRequestService_Expecter) Mergeability(ctx any, p any, name any, number any) *MergeRequestService_Mergeability_Call {
982+ return &MergeRequestService_Mergeability_Call{Call: _e.mock.On("Mergeability", ctx, p, name, number)}
983+}
984+
985+func (_c *MergeRequestService_Mergeability_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64)) *MergeRequestService_Mergeability_Call {
986+ _c.Call.Run(func(args mock.Arguments) {
987+ var arg0 context.Context
988+ if args[0] != nil {
989+ arg0 = args[0].(context.Context)
990+ }
991+ var arg1 domain.Principal
992+ if args[1] != nil {
993+ arg1 = args[1].(domain.Principal)
994+ }
995+ var arg2 domain.RepoName
996+ if args[2] != nil {
997+ arg2 = args[2].(domain.RepoName)
998+ }
999+ var arg3 int64
1000+ if args[3] != nil {
1001+ arg3 = args[3].(int64)
1002+ }
1003+ run(
1004+ arg0,
1005+ arg1,
1006+ arg2,
1007+ arg3,
1008+ )
1009+ })
1010+ return _c
1011+}
1012+
1013+func (_c *MergeRequestService_Mergeability_Call) Return(mergeability domain.Mergeability, err error) *MergeRequestService_Mergeability_Call {
1014+ _c.Call.Return(mergeability, err)
1015+ return _c
1016+}
1017+
1018+func (_c *MergeRequestService_Mergeability_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Mergeability, error)) *MergeRequestService_Mergeability_Call {
1019+ _c.Call.Return(run)
1020+ return _c
1021+}
1022+
9451023 // ResolveComment provides a mock function for the type MergeRequestService
9461024 func (_mock *MergeRequestService) ResolveComment(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, commentID int64, resolved bool) (domain.Comment, error) {
9471025 ret := _mock.Called(ctx, p, name, number, commentID, resolved)
M internal/ports/services.go +4 -0
@@ -124,6 +124,10 @@ type MergeRequestService interface {
124124 // CountOpen is the number of open merge requests of a repository the
125125 // principal can read. It reads no Git: the tab of every page shows it.
126126 CountOpen(ctx context.Context, p domain.Principal, name domain.RepoName) (int, error)
127+ // Mergeability is the mergeability of an open merge request, "checking"
128+ // until the server has a result for the current tips; zero for a request
129+ // that is not open. It reads no diff: the live pages call it on each change.
130+ Mergeability(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Mergeability, error)
127131 }
128132
129133 // IssueService is the issue use cases. *usecase.Issues implements it.
M internal/usecase/mergerequests.go +22 -0
@@ -440,6 +440,28 @@ func (m *MergeRequests) CountOpen(ctx context.Context, p domain.Principal, name
440440 return n, nil
441441 }
442442
443+// Mergeability implements ports.MergeRequestService: the mergeability of an
444+// open merge request for the current tips, queued when there is no up-to-date
445+// result. It is the zero value for a request that is not open, or without a
446+// mergeability service.
447+func (m *MergeRequests) Mergeability(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Mergeability, error) {
448+ _, mr, err := m.get(ctx, p, name, number)
449+ if err != nil {
450+ return domain.Mergeability{}, err
451+ }
452+ if m.mergeab == nil || !mr.IsOpen() {
453+ return domain.Mergeability{}, nil
454+ }
455+ if mr, err = m.withHead(ctx, mr); err != nil {
456+ return domain.Mergeability{}, err
457+ }
458+ tip, err := m.branchOrZero(ctx, name, mr.Target)
459+ if err != nil {
460+ return domain.Mergeability{}, err
461+ }
462+ return m.mergeab.Read(ctx, mr, tip), nil
463+}
464+
443465 func encodeMRToken(last domain.MergeRequest) string {
444466 return base64.RawURLEncoding.EncodeToString([]byte(mrTokenPrefix + strconv.FormatInt(last.Number, 10)))
445467 }
M skills/ermite/CLI.md +5 -4
@@ -11,7 +11,7 @@ Errors are one line on standard error (`ermite: ...`) with exit status 1. `--jso
1111 | `repo list [--json]` | none | repositories you can see |
1212 | `repo show <name> [--json]` | none | |
1313 | `repo create <name> [--description TEXT] [--visibility public]` | `repo:write` | private by default |
14-| `repo edit <name> [--description TEXT] [--visibility V] [--default-branch B] [--merge-modes fast-forward,squash] [--failing-checks-block=false]` | `repo:admin` | only the given flags change; `--description ""` clears it; the branch must exist |
14+| `repo edit <name> [--description TEXT] [--visibility V] [--default-branch B] [--merge-modes fast-forward,squash,merge] [--failing-checks-block=false]` | `repo:admin` | only the given flags change; every merge mode is allowed by default, list the ones to keep; `--description ""` clears it; the branch must exist |
1515 | `repo delete <name> [--yes]` | `repo:admin` | |
1616
1717 Repository JSON: `name`, `description`, `visibility`, `default_branch`, `created_at`, `pushed_at` (null before the first push), `merge_modes`, `failing_checks_block`.
@@ -56,12 +56,12 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl
5656 |---|---|---|
5757 | `mr create <repo> --source B --title T [--target B] [--description TEXT\|-]` | `mr:write` | target defaults to the default branch; title of 120 characters max; `-` reads standard input |
5858 | `mr list <repo> [--state open\|merged\|closed\|all] [--json]` | none | default `open` |
59-| `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers |
59+| `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers; the `mergeability` line (JSON `mergeability`, null unless open) is `up_to_date`, `behind` (N commits, merges cleanly), `conflict` (paths) or `checking` |
6060 | `mr comment <repo> <n> -m TEXT\|- [--path P --line N\|A-B --side old\|new] [--reply ID] [--approve\|--request-changes]` | `mr:write`; verdicts `mr:review` | Markdown note (8000 characters max); the lines must be in the current diff on one side; a deleted file uses its old path; `--reply` takes a root note (`12`, `#12` or `^12`), inherits its anchor and cannot carry a verdict; a verdict note may have no message |
6161 | `mr edit-comment <repo> <n> <comment-id> -m TEXT\|-` | `mr:write` | author only, open merge request; the verdict is unchanged |
6262 | `mr delete-comment <repo> <n> <comment-id>` | `mr:write` | author only; the text is lost, the verdict is kept |
6363 | `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | on the thread root, covers its replies; doing it twice is not an error |
64-| `mr merge <repo> <n> --head <full-id> [--squash] [--delete-branch]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head; `--delete-branch` deletes the source after the merge (refused for the default branch or a branch another open request uses; JSON adds `source_branch_deleted`) |
64+| `mr merge <repo> <n> --head <full-id> [--mode fast-forward\|squash\|merge] [--delete-branch]` | `mr:merge` | refused if the branch moved, if `fast-forward` and the target is not an ancestor of the head (behind: use `squash` or `merge`), or if `squash`/`merge` conflict; squash and merge commits are signed by the server key (`signing-key show`); answers once the target moved; `--delete-branch` deletes the source after the merge (refused for the default branch or a branch another open request uses; JSON adds `source_branch_deleted`) |
6565 | `mr close <repo> <n>` | `mr:write` | a closed merge request cannot be reopened |
6666
6767 `ermite mr create` and `ermite mr close` print a confirmation followed by the full
@@ -106,6 +106,7 @@ Workflow JSON: `repo`, `file` (always `.github/workflows/<file>`), `name` (the `
106106 | `repo_created`, `repo_updated`, `repo_deleted` | `repo`, `repository` |
107107 | `pushed` | `repo`, `ref_updates` (`ref`, `old_id`, `new_id`; an empty id means created or deleted) |
108108 | `merge_request_opened`, `merge_request_merged`, `merge_request_closed` | `repo`, `merge_request` |
109+| `merge_request_mergeability` | `repo`, `number`, `mergeability` (`state`: `checking`, `up_to_date`, `behind`, `conflict`; `behind`, `behind_capped`, `conflict_paths`, `conflict_paths_truncated`); sent when the state changes |
109110 | `comment_added`, `comment_edited`, `comment_deleted`, `comment_resolved` | `repo`, `number`, `comment` (a verdict note has `verdict` set) |
110111 | `gap` | none: events were lost, reload the state |
111112
@@ -116,7 +117,7 @@ Workflow JSON: `repo`, `file` (always `.github/workflows/<file>`), `name` (the `
116117 | `serve` | configured by `ERMITE_*` environment variables only (see the project README) |
117118 | `version [--json]` | client version; with a server URL (`ERMITE_URL` or the config file), the server version and features |
118119 | `config show [--json]` / `config set <url\|token.cmd> <value>` / `config unset <key>` | the CLI config file; `token.cmd` is a command that prints the token (any secret manager), never the token itself; `show` never runs it |
119-| `signing-key show` | public key and `allowed_signers` line to check squash commits with `git verify-commit` |
120+| `signing-key show` | public key and `allowed_signers` line to check squash and merge commits with `git verify-commit` |
120121 | `signing-key trust list [--json]` | keys commit signatures are checked against (the server key first) |
121122 | `signing-key trust add --format ssh\|gpg --title <t> --email <e>... <file\|->` / `trust remove <id>` | owner only; a commit shows verified when its committer email is listed for the key |
122123 | `web login [--json]` | one-time sign-in link (about 60 s); needs the admin token or a token with every scope, no repository list and no branch prefix; the link is a secret |
M skills/ermite/SKILL.md +2 -2
@@ -37,9 +37,9 @@ git push https://git.example.com/<repo>.git main # password = token,
3737 2. Read: `ermite mr show <repo> <n> --diff --json`. Keep `head_id`; take line numbers from the hunks.
3838 3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`; `--line 10-14` covers a range on one side. Without `--path` the comment is general. The line must appear in the diff. Notes are Markdown; `^N` refers to note N.
3939 4. End a review with a verdict (needs `mr:review`): `--approve` or `--request-changes`, alone or with a message. Verdicts are informative and cannot be edited; a new verdict replaces the previous one.
40-5. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server and `--delete-branch` to delete the source branch afterwards.
40+5. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--mode squash` for one commit or `--mode merge` for a merge commit (both signed by the server's key, see `ermite signing-key show`) and `--delete-branch` to delete the source branch afterwards.
4141
42-The merge is refused when the branch moved since `head_id` (review again) or when the target is not an ancestor of the head (rebase and push again). The repository may allow only one merge mode.
42+The merge is refused when the branch moved since `head_id` (review again) or, for `fast-forward`, when the target is not an ancestor of the head (rebase and push again, or use `squash`/`merge`), or when `squash`/`merge` conflict. `ermite mr show` prints the `mergeability` line (up to date, behind by N, conflict with paths). The repository may allow only some merge modes.
4343
4444 ### Act on a review
4545

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