detached 70c00ad

#7 feat(mr): live mergeability, merge commit mode and inline merge errors

state
merged · 70c00ad · fast-forward
branch
feat/mr-mergeability → main
opened
bnema ·
updated

Merge requests show live mergeability and gain a signed merge-commit mode.

  • Mergeability: a background check per repository (merge-tree) reports checking, up to date, behind N or conflict with paths. It is stored, exposed in the API (feature merge_requests.mergeability), published as a Watch event for every settled result on new branch tips, and shown live in the CLI and the web merge box. Failed checks retry per repository with backoff (1 s to 1 min, 5 attempts).
  • Merge modes: fast-forward (requires up to date), squash (uses the merge-tree tree, so a behind but clean branch can be squashed) and merge commit (two parents, signed by the instance key, see 'ermite signing-key show'). Every mode is allowed by default; existing repos are upgraded once. Restrict with 'ermite repo edit --merge-modes'.
  • Merge answer: a refused merge re-renders the page with an inline error (409), never an error page. Post-merge side effects (source branch deletion, events) run after the response.
  • Requires bnema.dev/ermite-api v0.10.0.

Checks: build, vet, hexcheck -fail-on warn, go test -count=1 ./..., race on usecase and app, JS tests.

CommitSubjectAuthorDateSignatureStatus
70c00adtest: use Store.Mutate for the merge-mode upgrade test and skip id-only events in the inbox stream testbriceGPG Verified
bda2a5efix(mr): wait for the async source deletion before pruning in the squash test; simplify the retry pop conditionbriceGPG Verified
1475ed7build(deps): require ermite-api v0.10.0briceGPG Verified
fb5ff06test(mr): cover stale retry timers, branch disappearance and CAS conflict; drop unused clockbriceGPG Verified
c22da12fix(mr): keep replacement retries from stale timers and log reconcile failuresbriceGPG Verified
1036507refactor(mr): reconcile mergeability once per repositorybriceGPG Verified
0019f14refactor(mr): retry failed mergeability work per repository onlybriceGPG Verified
99b00c3fix(mr): count retry attempts only for the keys that wait in the schedulerbriceGPG Verified
33a90cdfix(mr): arm one retry timer per earliest retry in the mergeability schedulerbriceGPG Verified
41023defix(mr): bound mergeability retries with one scheduler and retry failed publishesbriceGPG Verified
0e2e418feat(cli): report the mergeability featurebriceGPG Verified
bfa2dcdfix(rpc): report checking mergeability on open merge requestsbriceGPG Verified
5ad3fbefix(events): deliver mergeability events on owner topicsbriceGPG Verified
f09b4fbfix(mr): bound conflict paths with exact-sized copiesbriceGPG Verified
f8b1d20fix(mr): retry failed mergeability checks with backoffbriceGPG Verified
f8bb86dfix(mr): alternate pending and dirty work in the mergeability queuebriceGPG Verified
a79212efix(mr): refresh live-event goldens and guard shared issue numbersbriceGPG Verified
5d95127fix(web): enable the merge button after choosing a mode by keyboardbriceGPG Verified
1b45752fix(web): keep the merge box inside its popover and the MR URL after a refused mergebriceGPG Verified
b5b4bf9feat(web,cli): live mergeability in the merge box, merge modes and inline merge errorsbriceGPG Verified
fc9623bfix(mr): safe async source deletion, single-run mergeability queue and domain-side merge-tree boundsbriceGPG Verified
bf27c54feat(mr): per-mode merge checks, signed merge commits and side effects after the responsebriceGPG Verified
b707720refactor(kvstore): decode the mergeability record without mutating itbriceGPG Verified
23c66c0feat(mr): background mergeability checks, their Watch event and API fieldbriceGPG Verified
8f36a2ffeat(mr): mergeability types, merge-tree checks, storage record and merge commit modebriceGPG Verified

Checks 8

89 files · +6371 -1025
8/8
M README.md +1 -1
M docs/cli.md +5 -4
M docs/commit-signatures.md +1 -1
M docs/configuration.md +2 -2
M docs/merge-requests.md +45 -7
M gen/ermite/storage/merge_request.pb.go +216 -24
M go.mod +1 -1
M go.sum +2 -2
M internal/adapters/client/client_test.go +3 -3
M internal/adapters/client/features.go +4 -1
M internal/adapters/gitcli/git.go +12 -2
M internal/adapters/gitcli/merge_test.go +83 -0
A internal/adapters/gitcli/mergeability.go +87 -0
M internal/adapters/kvstore/ci_tip.go +29 -0
A internal/adapters/kvstore/ci_tip_test.go +34 -0
M internal/adapters/kvstore/mergerequests.go +150 -5
M internal/adapters/kvstore/mergerequests_open_test.go +25 -0
M internal/adapters/kvstore/mergerequests_test.go +120 -0
M internal/adapters/rpc/api.go +1 -1
M internal/adapters/rpc/convert.go +7 -0
M internal/adapters/rpc/convert_test.go +23 -1
M internal/adapters/rpc/mergerequest.go +47 -3
M internal/adapters/rpc/mergerequest_test.go +1 -0
M internal/adapters/web/events.go +6 -2
M internal/adapters/web/fragments.go +58 -8
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 +156 -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,16 @@ 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
270+ // Canonical is the address of the request, set when the page answers a
271+ // refused action from the address of that action: the script puts it in
272+ // the address bar, so that a reload does not post again.
273+ Canonical string
236274
237275 Since *SinceNav
238276 SinceNote string
@@ -346,10 +384,59 @@ func modeName(m domain.MergeMode) string {
346384 return "fast-forward"
347385 case domain.MergeModeSquash:
348386 return "squash"
387+ case domain.MergeModeMerge:
388+ return "merge commit"
349389 }
350390 return ""
351391 }
352392
393+// mergeabilityView converts the mergeability of a request into the badge and
394+// the flags of the controls of the merge box. A request without result is
395+// being checked.
396+func mergeabilityView(m domain.Mergeability, target string) MergeabilityView {
397+ v := MergeabilityView{State: string(domain.MergeabilityChecking), Label: "Checking…"}
398+ v.FFOk, v.FFReason = m.Allows(domain.MergeModeFastForward)
399+ v.MergeOk, v.MergeReason = m.Allows(domain.MergeModeSquash)
400+ target = visibleText(target)
401+ switch m.State {
402+ case domain.MergeabilityUpToDate:
403+ v.State, v.Label = string(m.State), "Ready to merge"
404+ case domain.MergeabilityBehind:
405+ v.State, v.Label, v.Detail = string(m.State), "Behind "+target+" by "+behindText(m), "merges without conflicts"
406+ case domain.MergeabilityConflict:
407+ v.State, v.Label, v.Detail = string(m.State), "Conflicts with "+target, "rebase needed"
408+ v.Paths, v.PathsNote = conflictList(m)
409+ }
410+ return v
411+}
412+
413+// behindText is "2 commits", "1 commit" or "1000+ commits".
414+func behindText(m domain.Mergeability) string {
415+ if m.Behind == 1 && !m.BehindCapped {
416+ return "1 commit"
417+ }
418+ n := itoa(m.Behind)
419+ if m.BehindCapped {
420+ n += "+"
421+ }
422+ return n + " commits"
423+}
424+
425+// conflictList is the conflicting paths to show, and the note that says what
426+// the list lacks.
427+func conflictList(m domain.Mergeability) (paths []string, note string) {
428+ for _, p := range m.ConflictPaths {
429+ paths = append(paths, visibleText(p))
430+ }
431+ switch {
432+ case len(paths) == 0:
433+ note = "no common history."
434+ case m.ConflictPathsTruncated:
435+ note = "more files conflict than are listed."
436+ }
437+ return paths, note
438+}
439+
353440 // mrRow converts a merge request of a listing.
354441 func (s *Server) mrRow(repo string, m domain.MergeRequest) MRRow {
355442 row := MRRow{
@@ -540,6 +627,9 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
540627 p.MergedHref = repoHref(d.name(), "commit", m.MergedCommitID.String())
541628 p.MergeMode = modeName(m.MergeMode)
542629 }
630+ if signed && m.State == domain.MRStateMerged && r.URL.Query().Get("deleting") == "1" && d.hasBranch(m.Source) {
631+ p.Deleting = visibleText(m.Source)
632+ }
543633 for _, mode := range d.repo.Settings.AllowedMergeModes {
544634 if name := modeName(mode); name != "" {
545635 p.Modes = append(p.Modes, name)
@@ -624,11 +714,18 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
624714 p.Invisible = invisible
625715 p.DiffView = newDiffView(r, lines, withFrom(self+"?view=unified", from), withFrom(self+"?view=split", from))
626716 if signed && open {
627- p.Box = s.mergeBox(d, n, m.Source, head, lastRead)
717+ p.Box = s.mergeBox(d, n, m.Source, head, lastRead, mergeabilityView(res.Mergeability, m.Target))
718+ p.Box.Error = mergeErrorOf(ctx)
628719 if m.IsDraft() {
629720 p.Box.ReadyAction = mrHref(d.name(), n, "ready")
630721 }
631722 }
723+ if mergeErrorOf(ctx) != "" {
724+ p.Canonical = self
725+ }
726+ if p.Box == nil {
727+ p.MergeError = mergeErrorOf(ctx) // the request is not open any more
728+ }
632729 if canWrite {
633730 p.GeneralForm = &CommentForm{
634731 ID: "cf-general", Action: mrHref(d.name(), n, "comments"), Title: "comment on the merge request",
@@ -652,7 +749,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
652749 l.Crumbs = append(l.Crumbs, Crumb{Label: "since", Current: true})
653750 }
654751 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))
752+ 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))
656753 l.HasRail = true
657754 l.MR = true
658755 return MergeRequest(l, p), http.StatusOK, nil
@@ -669,37 +766,78 @@ func (d *repoData) hasBranch(name string) bool {
669766 }
670767
671768 // 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 {
769+func (s *Server) mergeBox(d *repoData, n int64, source, head, lastRead string, mg MergeabilityView) *MergeBox {
673770 b := &MergeBox{
674771 MergeAction: mrHref(d.name(), n, "merge"), CloseAction: mrHref(d.name(), n, "close"),
675772 ReadAction: mrHref(d.name(), n, "read"), Head: head, HeadGone: head == "",
676773 CanMarkRead: head != "" && head != lastRead,
677774 CanDeleteBranch: source != d.repo.DefaultBranch,
775+ Mergeability: mg,
776+ }
777+ var lines []string
778+ cmd := ""
779+ if head != "" {
780+ cmd = "ermite mr merge " + shellWord(d.name()) + " " + strconv.FormatInt(n, 10) + " --head " + head
678781 }
679- ff, sq := false, false
680782 for _, mode := range d.repo.Settings.AllowedMergeModes {
681783 switch mode {
682784 case domain.MergeModeFastForward:
683- ff = true
684- b.Modes = append(b.Modes, ModeOption{Value: "fast-forward", Label: "fast-forward"})
785+ b.Modes = append(b.Modes, ModeOption{Value: "fast-forward", Label: "fast-forward", Disabled: !mg.FFOk, Reason: reasonIf(!mg.FFOk, mg.FFReason)})
786+ lines = append(lines, cmd)
685787 case domain.MergeModeSquash:
686- sq = true
687- b.Modes = append(b.Modes, ModeOption{Value: "squash", Label: "squash (one signed commit)"})
788+ b.Signed = true
789+ b.Modes = append(b.Modes, ModeOption{Value: "squash", Label: "squash (one signed commit)", Disabled: !mg.MergeOk, Reason: reasonIf(!mg.MergeOk, mg.MergeReason)})
790+ lines = append(lines, cmd+" --mode squash")
791+ case domain.MergeModeMerge:
792+ b.Signed = true
793+ b.Modes = append(b.Modes, ModeOption{Value: "merge", Label: "merge commit (signed)", Disabled: !mg.MergeOk, Reason: reasonIf(!mg.MergeOk, mg.MergeReason)})
794+ lines = append(lines, cmd+" --mode merge")
688795 }
689796 }
797+ if b.Signed && s.cfg.SigningKey != nil {
798+ b.Fingerprint = s.cfg.SigningKey.Info().Key.Fingerprint
799+ }
690800 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- }
801+ b.CLI = Clone{ID: "cli-merge", Lines: lines}
699802 }
700803 return b
701804 }
702805
806+func reasonIf(cond bool, reason string) string {
807+ if cond {
808+ return reason
809+ }
810+ return ""
811+}
812+
813+// firstModeOK is the index of the first mode that is possible, -1 for none.
814+func (b *MergeBox) firstModeOK() int {
815+ for i, m := range b.Modes {
816+ if !m.Disabled {
817+ return i
818+ }
819+ }
820+ return -1
821+}
822+
823+// Checked is the mode the form has checked: the first that is possible, else
824+// the first (it is disabled, and a live update may enable it).
825+func (b *MergeBox) Checked() int { return max(b.firstModeOK(), 0) }
826+
827+// CanMerge tells whether some mode is possible: the button is enabled.
828+func (b *MergeBox) CanMerge() bool { return b.firstModeOK() >= 0 }
829+
830+// MergeTitle is why the button is disabled, empty when it is not.
831+func (b *MergeBox) MergeTitle() string {
832+ if b.CanMerge() {
833+ return ""
834+ }
835+ if len(b.Modes) == 0 {
836+ return "the repository allows no merge mode"
837+ }
838+ return b.Modes[0].Reason
839+}
840+
703841 // shellWord quotes s for a shell unless it is made of safe characters, which
704842 // a repository name always is.
705843 func shellWord(s string) string {
M internal/adapters/web/mr.templ +92 -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">
@@ -178,6 +187,9 @@ templ MergeRequest(l Layout, p MRPage) {
178187 <div
179188 id="diffs"
180189 data-mr
190+ if p.Canonical != "" {
191+ data-canonical={ p.Canonical }
192+ }
181193 if p.Both {
182194 data-diff={ p.View }
183195 } else {
@@ -446,8 +458,13 @@ templ mrBar(p MRPage) {
446458 // mergeMenu is the merge of an open request, for the owner: the form (it
447459 // carries the head that was read), or "Ready for review" for a draft. The
448460 // mode and the branch deletion have keys (1-3, D) while it is open.
461+//
462+// The badge is a live slot (mr-mergeability); the modes and the button read
463+// its data-* attributes (hx-live bindings), so that they follow the slot when
464+// the stream replaces its content. The server renders the same state in the
465+// attributes, for a page without scripts.
449466 templ mergeMenu(p MRPage) {
450- <details class="menu mmenu" id="merge-menu" data-state="open">
467+ <details class="menu mmenu" id="merge-menu" data-state="open" open?={ p.Box.Error != "" }>
451468 <summary class="btn primary" aria-keyshortcuts="Shift+M">
452469 if p.Box.ReadyAction != "" {
453470 Draft
@@ -469,19 +486,32 @@ templ mergeMenu(p MRPage) {
469486 <dt>into</dt>
470487 <dd>{ p.Target }</dd>
471488 </dl>
489+ if p.Box.Error != "" {
490+ <p class="d-err" role="alert">{ p.Box.Error }</p>
491+ }
472492 if p.Box.ReadyAction != "" {
473493 <p class="d-draft">this is a draft: it cannot be merged until it is ready for review.</p>
474494 <form class="d-btns" method="post" action={ templ.SafeURL(p.Box.ReadyAction) }>
475495 <button type="submit" class="btn primary" data-default title="remove the draft prefix from the title">Ready for review</button>
476496 </form>
477497 } else if !p.Box.HeadGone {
498+ @mrMergeabilitySlot(p.Box.Mergeability)
478499 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>
479500 <input type="hidden" name="head_id" value={ p.Box.Head }/>
480501 <fieldset class="d-modes">
481502 <legend class="sr">merge mode</legend>
482503 for i, m := range p.Box.Modes {
483504 <label>
484- <input type="radio" name="mode" value={ m.Value } checked?={ i == 0 }/>
505+ <input
506+ type="radio"
507+ name="mode"
508+ value={ m.Value }
509+ checked?={ i == p.Box.Checked() }
510+ disabled?={ m.Disabled }
511+ title={ m.Reason }
512+ :disabled="this.value === 'fast-forward' ? q('[data-mergeability]').data.ffOk !== true : q('[data-mergeability]').data.mergeOk !== true"
513+ :title="this.value === 'fast-forward' ? q('[data-mergeability]').data.ffReason : q('[data-mergeability]').data.mergeReason"
514+ />
485515 { m.Label }
486516 if i < 9 {
487517 <kbd aria-hidden="true">{ itoa(i + 1) }</kbd>
@@ -489,11 +519,28 @@ templ mergeMenu(p MRPage) {
489519 </label>
490520 }
491521 </fieldset>
522+ if p.Box.Signed {
523+ <p class="d-note" id="mr-signing">
524+ squash and merge commits are signed with ermite's own key (generated on first start).
525+ if p.Box.Fingerprint != "" {
526+ fingerprint <code>{ p.Box.Fingerprint }</code>
527+ }
528+ <code>ermite signing-key show</code> prints it.
529+ </p>
530+ }
492531 if p.Box.CanDeleteBranch {
493532 <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>
494533 }
495534 <div class="d-btns">
496- <button type="submit" class="btn primary" data-default>Merge into { p.Target }</button>
535+ <button
536+ type="submit"
537+ class="btn primary"
538+ data-default
539+ disabled?={ !p.Box.CanMerge() }
540+ title={ p.Box.MergeTitle() }
541+ :disabled="!this.form.querySelector('input[name=mode]:checked:not(:disabled)')"
542+ :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''"
543+ >Merge into { p.Target }</button>
497544 </div>
498545 </form>
499546 }
@@ -501,6 +548,47 @@ templ mergeMenu(p MRPage) {
501548 </details>
502549 }
503550
551+// MRMergeability is the content of the mergeability slot: the badge, and the
552+// state the controls of the merge box read.
553+templ MRMergeability(v MergeabilityView) {
554+ <span
555+ data-mergeability
556+ data-state={ v.State }
557+ data-ff-ok={ strconv.FormatBool(v.FFOk) }
558+ data-merge-ok={ strconv.FormatBool(v.MergeOk) }
559+ data-ff-reason={ v.FFReason }
560+ data-merge-reason={ v.MergeReason }
561+ >
562+ <span class="badge" data-state={ v.State }>{ v.Label }</span>
563+ if v.Detail != "" {
564+ { " " }<span class="dim">{ v.Detail }</span>
565+ }
566+ </span>
567+ if len(v.Paths) > 0 {
568+ <ul class="mg-paths" aria-label="conflicting files">
569+ for _, path := range v.Paths {
570+ <li><code>{ path }</code></li>
571+ }
572+ </ul>
573+ }
574+ if v.PathsNote != "" {
575+ <p class="dim">{ v.PathsNote }</p>
576+ }
577+}
578+
579+// mrMergeabilitySlot is the status region of the badge, with its content: the
580+// stream replaces the children, never the element (see LiveSlot).
581+templ mrMergeabilitySlot(v MergeabilityView) {
582+ <div id="mr-mergeability" class="live-slot" role="status" data-slot>
583+ @MRMergeability(v)
584+ </div>
585+}
586+
587+// MRMergeabilityFragment is the slot as the stream sends it.
588+templ MRMergeabilityFragment(v MergeabilityView) {
589+ @mrMergeabilitySlot(v)
590+}
591+
504592 // moreMenu is what else the owner does with an open request: mark the head
505593 // read, close it, merge it from a terminal.
506594 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 +1185 -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,106 @@ 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 }
1028+ if p.Canonical != "" {
1029+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, " data-canonical=\"")
1030+ if templ_7745c5c3_Err != nil {
1031+ return templ_7745c5c3_Err
1032+ }
1033+ var templ_7745c5c3_Var51 string
1034+ templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Canonical)
1035+ if templ_7745c5c3_Err != nil {
1036+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 191, Col: 32}
1037+ }
1038+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
1039+ if templ_7745c5c3_Err != nil {
1040+ return templ_7745c5c3_Err
1041+ }
1042+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\"")
1043+ if templ_7745c5c3_Err != nil {
1044+ return templ_7745c5c3_Err
1045+ }
1046+ }
9791047 if p.Both {
980- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " data-diff=\"")
1048+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " data-diff=\"")
9811049 if templ_7745c5c3_Err != nil {
9821050 return templ_7745c5c3_Err
9831051 }
984- var templ_7745c5c3_Var49 string
985- templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
1052+ var templ_7745c5c3_Var52 string
1053+ templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
9861054 if templ_7745c5c3_Err != nil {
987- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 182, Col: 22}
1055+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 194, Col: 22}
9881056 }
989- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49)
1057+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52)
9901058 if templ_7745c5c3_Err != nil {
9911059 return templ_7745c5c3_Err
9921060 }
993- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\"")
1061+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\"")
9941062 if templ_7745c5c3_Err != nil {
9951063 return templ_7745c5c3_Err
9961064 }
9971065 } else {
998- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " data-view=\"")
1066+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " data-view=\"")
9991067 if templ_7745c5c3_Err != nil {
10001068 return templ_7745c5c3_Err
10011069 }
1002- var templ_7745c5c3_Var50 string
1003- templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
1070+ var templ_7745c5c3_Var53 string
1071+ templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
10041072 if templ_7745c5c3_Err != nil {
1005- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 184, Col: 22}
1073+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 196, Col: 22}
10061074 }
1007- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
1075+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53)
10081076 if templ_7745c5c3_Err != nil {
10091077 return templ_7745c5c3_Err
10101078 }
1011- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\"")
1079+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"")
10121080 if templ_7745c5c3_Err != nil {
10131081 return templ_7745c5c3_Err
10141082 }
10151083 }
10161084 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\"")
1085+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
10181086 if templ_7745c5c3_Err != nil {
10191087 return templ_7745c5c3_Err
10201088 }
10211089 }
1022- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, ">")
1090+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, ">")
10231091 if templ_7745c5c3_Err != nil {
10241092 return templ_7745c5c3_Err
10251093 }
@@ -1030,38 +1098,38 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
10301098 }
10311099 }
10321100 if len(p.Files) == 0 {
1033- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<p class=\"note\">no changed files.</p>")
1101+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"note\">no changed files.</p>")
10341102 if templ_7745c5c3_Err != nil {
10351103 return templ_7745c5c3_Err
10361104 }
10371105 }
1038- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</div><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
1106+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</div><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
10391107 if templ_7745c5c3_Err != nil {
10401108 return templ_7745c5c3_Err
10411109 }
1042- var templ_7745c5c3_Var51 string
1043- templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved))
1110+ var templ_7745c5c3_Var54 string
1111+ templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved))
10441112 if templ_7745c5c3_Err != nil {
1045- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 97}
1113+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 209, Col: 97}
10461114 }
1047- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
1115+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
10481116 if templ_7745c5c3_Err != nil {
10491117 return templ_7745c5c3_Err
10501118 }
1051- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " open / ")
1119+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, " open / ")
10521120 if templ_7745c5c3_Err != nil {
10531121 return templ_7745c5c3_Err
10541122 }
1055- var templ_7745c5c3_Var52 string
1056- templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments))
1123+ var templ_7745c5c3_Var55 string
1124+ templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments))
10571125 if templ_7745c5c3_Err != nil {
1058- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 197, Col: 126}
1126+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 209, Col: 126}
10591127 }
1060- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
1128+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
10611129 if templ_7745c5c3_Err != nil {
10621130 return templ_7745c5c3_Err
10631131 }
1064- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</span></h2>")
1132+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "</span></h2>")
10651133 if templ_7745c5c3_Err != nil {
10661134 return templ_7745c5c3_Err
10671135 }
@@ -1077,17 +1145,17 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
10771145 return templ_7745c5c3_Err
10781146 }
10791147 }
1080- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " ")
1148+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, " ")
10811149 if templ_7745c5c3_Err != nil {
10821150 return templ_7745c5c3_Err
10831151 }
10841152 if p.NComments == 0 {
1085- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<p class=\"dim\">no comments yet.</p>")
1153+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<p class=\"dim\">no comments yet.</p>")
10861154 if templ_7745c5c3_Err != nil {
10871155 return templ_7745c5c3_Err
10881156 }
10891157 }
1090- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " ")
1158+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, " ")
10911159 if templ_7745c5c3_Err != nil {
10921160 return templ_7745c5c3_Err
10931161 }
@@ -1123,49 +1191,49 @@ func branchLink(name, href string) templ.Component {
11231191 }()
11241192 }
11251193 ctx = templ.InitializeContext(ctx)
1126- templ_7745c5c3_Var53 := templ.GetChildren(ctx)
1127- if templ_7745c5c3_Var53 == nil {
1128- templ_7745c5c3_Var53 = templ.NopComponent
1194+ templ_7745c5c3_Var56 := templ.GetChildren(ctx)
1195+ if templ_7745c5c3_Var56 == nil {
1196+ templ_7745c5c3_Var56 = templ.NopComponent
11291197 }
11301198 ctx = templ.ClearChildren(ctx)
11311199 if href != "" {
1132- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "<a href=\"")
1200+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "<a href=\"")
11331201 if templ_7745c5c3_Err != nil {
11341202 return templ_7745c5c3_Err
11351203 }
1136- var templ_7745c5c3_Var54 templ.SafeURL
1137- templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
1204+ var templ_7745c5c3_Var57 templ.SafeURL
1205+ templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
11381206 if templ_7745c5c3_Err != nil {
1139- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 31}
1207+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 227, Col: 31}
11401208 }
1141- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
1209+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
11421210 if templ_7745c5c3_Err != nil {
11431211 return templ_7745c5c3_Err
11441212 }
1145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "\">")
1213+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\">")
11461214 if templ_7745c5c3_Err != nil {
11471215 return templ_7745c5c3_Err
11481216 }
1149- var templ_7745c5c3_Var55 string
1150- templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(name)
1217+ var templ_7745c5c3_Var58 string
1218+ templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(name)
11511219 if templ_7745c5c3_Err != nil {
1152- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 215, Col: 40}
1220+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 227, Col: 40}
11531221 }
1154- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
1222+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
11551223 if templ_7745c5c3_Err != nil {
11561224 return templ_7745c5c3_Err
11571225 }
1158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</a>")
1226+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</a>")
11591227 if templ_7745c5c3_Err != nil {
11601228 return templ_7745c5c3_Err
11611229 }
11621230 } else {
1163- var templ_7745c5c3_Var56 string
1164- templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(name)
1231+ var templ_7745c5c3_Var59 string
1232+ templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(name)
11651233 if templ_7745c5c3_Err != nil {
1166- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 217, Col: 8}
1234+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 229, Col: 8}
11671235 }
1168- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
1236+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
11691237 if templ_7745c5c3_Err != nil {
11701238 return templ_7745c5c3_Err
11711239 }
@@ -1193,9 +1261,9 @@ func lineNotes(notes []CommentView, form *CommentForm, suffix string) templ.Comp
11931261 }()
11941262 }
11951263 ctx = templ.InitializeContext(ctx)
1196- templ_7745c5c3_Var57 := templ.GetChildren(ctx)
1197- if templ_7745c5c3_Var57 == nil {
1198- templ_7745c5c3_Var57 = templ.NopComponent
1264+ templ_7745c5c3_Var60 := templ.GetChildren(ctx)
1265+ if templ_7745c5c3_Var60 == nil {
1266+ templ_7745c5c3_Var60 = templ.NopComponent
11991267 }
12001268 ctx = templ.ClearChildren(ctx)
12011269 for _, c := range notes {
@@ -1232,182 +1300,182 @@ func commentView(c CommentView, suffix string) templ.Component {
12321300 }()
12331301 }
12341302 ctx = templ.InitializeContext(ctx)
1235- templ_7745c5c3_Var58 := templ.GetChildren(ctx)
1236- if templ_7745c5c3_Var58 == nil {
1237- templ_7745c5c3_Var58 = templ.NopComponent
1303+ templ_7745c5c3_Var61 := templ.GetChildren(ctx)
1304+ if templ_7745c5c3_Var61 == nil {
1305+ templ_7745c5c3_Var61 = templ.NopComponent
12381306 }
12391307 ctx = templ.ClearChildren(ctx)
1240- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<div class=\"cmt\" id=\"")
1308+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<div class=\"cmt\" id=\"")
12411309 if templ_7745c5c3_Err != nil {
12421310 return templ_7745c5c3_Err
12431311 }
1244- var templ_7745c5c3_Var59 string
1245- templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)
1312+ var templ_7745c5c3_Var62 string
1313+ templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)
12461314 if templ_7745c5c3_Err != nil {
1247- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 42}
1315+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 42}
12481316 }
1249- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
1317+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
12501318 if templ_7745c5c3_Err != nil {
12511319 return templ_7745c5c3_Err
12521320 }
1253- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\" data-resolved=\"")
1321+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" data-resolved=\"")
12541322 if templ_7745c5c3_Err != nil {
12551323 return templ_7745c5c3_Err
12561324 }
1257- var templ_7745c5c3_Var60 string
1258- templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))
1325+ var templ_7745c5c3_Var63 string
1326+ templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))
12591327 if templ_7745c5c3_Err != nil {
1260- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 81}
1328+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 81}
12611329 }
1262- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
1330+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
12631331 if templ_7745c5c3_Err != nil {
12641332 return templ_7745c5c3_Err
12651333 }
1266- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"")
1334+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\"")
12671335 if templ_7745c5c3_Err != nil {
12681336 return templ_7745c5c3_Err
12691337 }
12701338 if c.Reply != nil {
1271- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " data-thread")
1339+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, " data-thread")
12721340 if templ_7745c5c3_Err != nil {
12731341 return templ_7745c5c3_Err
12741342 }
12751343 }
1276- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "><p class=\"c-head\"><span class=\"c-who\">")
1344+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "><p class=\"c-head\"><span class=\"c-who\">")
12771345 if templ_7745c5c3_Err != nil {
12781346 return templ_7745c5c3_Err
12791347 }
1280- var templ_7745c5c3_Var61 string
1281- templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
1348+ var templ_7745c5c3_Var64 string
1349+ templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
12821350 if templ_7745c5c3_Err != nil {
1283- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 33}
1351+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 250, Col: 33}
12841352 }
1285- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
1353+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
12861354 if templ_7745c5c3_Err != nil {
12871355 return templ_7745c5c3_Err
12881356 }
1289- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span> ")
1357+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</span> ")
12901358 if templ_7745c5c3_Err != nil {
12911359 return templ_7745c5c3_Err
12921360 }
12931361 if c.WhenAt != "" {
1294- var templ_7745c5c3_Var62 string
1295- templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1362+ var templ_7745c5c3_Var65 string
1363+ templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
12961364 if templ_7745c5c3_Err != nil {
1297- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 9}
1365+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 9}
12981366 }
1299- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
1367+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
13001368 if templ_7745c5c3_Err != nil {
13011369 return templ_7745c5c3_Err
13021370 }
1303- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<time class=\"mute\" datetime=\"")
1371+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "<time class=\"mute\" datetime=\"")
13041372 if templ_7745c5c3_Err != nil {
13051373 return templ_7745c5c3_Err
13061374 }
1307- var templ_7745c5c3_Var63 string
1308- templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
1375+ var templ_7745c5c3_Var66 string
1376+ templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
13091377 if templ_7745c5c3_Err != nil {
1310- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 49}
1378+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 49}
13111379 }
1312- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
1380+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
13131381 if templ_7745c5c3_Err != nil {
13141382 return templ_7745c5c3_Err
13151383 }
1316- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "\" title=\"")
1384+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\" title=\"")
13171385 if templ_7745c5c3_Err != nil {
13181386 return templ_7745c5c3_Err
13191387 }
1320- var templ_7745c5c3_Var64 string
1321- templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)
1388+ var templ_7745c5c3_Var67 string
1389+ templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)
13221390 if templ_7745c5c3_Err != nil {
1323- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 65}
1391+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 65}
13241392 }
1325- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
1393+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
13261394 if templ_7745c5c3_Err != nil {
13271395 return templ_7745c5c3_Err
13281396 }
1329- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\">")
1397+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\">")
13301398 if templ_7745c5c3_Err != nil {
13311399 return templ_7745c5c3_Err
13321400 }
1333- var templ_7745c5c3_Var65 string
1334- templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
1401+ var templ_7745c5c3_Var68 string
1402+ templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
13351403 if templ_7745c5c3_Err != nil {
1336- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 76}
1404+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 76}
13371405 }
1338- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
1406+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
13391407 if templ_7745c5c3_Err != nil {
13401408 return templ_7745c5c3_Err
13411409 }
1342- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "</time> ")
1410+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</time> ")
13431411 if templ_7745c5c3_Err != nil {
13441412 return templ_7745c5c3_Err
13451413 }
13461414 }
13471415 if c.Resolved {
1348- var templ_7745c5c3_Var66 string
1349- templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1416+ var templ_7745c5c3_Var69 string
1417+ templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13501418 if templ_7745c5c3_Err != nil {
1351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 243, Col: 9}
1419+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 255, Col: 9}
13521420 }
1353- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
1421+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
13541422 if templ_7745c5c3_Err != nil {
13551423 return templ_7745c5c3_Err
13561424 }
1357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<span class=\"c-state\">resolved</span> ")
1425+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<span class=\"c-state\">resolved</span> ")
13581426 if templ_7745c5c3_Err != nil {
13591427 return templ_7745c5c3_Err
13601428 }
13611429 }
13621430 if c.Outdated {
1363- var templ_7745c5c3_Var67 string
1364- templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1431+ var templ_7745c5c3_Var70 string
1432+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13651433 if templ_7745c5c3_Err != nil {
1366- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 246, Col: 9}
1434+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 9}
13671435 }
1368- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
1436+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
13691437 if templ_7745c5c3_Err != nil {
13701438 return templ_7745c5c3_Err
13711439 }
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> ")
1440+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")
13731441 if templ_7745c5c3_Err != nil {
13741442 return templ_7745c5c3_Err
13751443 }
13761444 }
13771445 if c.Where != "" {
1378- var templ_7745c5c3_Var68 string
1379- templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1446+ var templ_7745c5c3_Var71 string
1447+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13801448 if templ_7745c5c3_Err != nil {
1381- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 9}
1449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 261, Col: 9}
13821450 }
1383- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
1451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
13841452 if templ_7745c5c3_Err != nil {
13851453 return templ_7745c5c3_Err
13861454 }
1387- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"c-where mute\">")
1455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<span class=\"c-where mute\">")
13881456 if templ_7745c5c3_Err != nil {
13891457 return templ_7745c5c3_Err
13901458 }
1391- var templ_7745c5c3_Var69 string
1392- templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)
1459+ var templ_7745c5c3_Var72 string
1460+ templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)
13931461 if templ_7745c5c3_Err != nil {
1394- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 249, Col: 47}
1462+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 261, Col: 47}
13951463 }
1396- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
1464+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72))
13971465 if templ_7745c5c3_Err != nil {
13981466 return templ_7745c5c3_Err
13991467 }
1400- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</span>")
1468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</span>")
14011469 if templ_7745c5c3_Err != nil {
14021470 return templ_7745c5c3_Err
14031471 }
14041472 }
1405- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</p>")
1473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "</p>")
14061474 if templ_7745c5c3_Err != nil {
14071475 return templ_7745c5c3_Err
14081476 }
14091477 if len(c.Parts) > 0 {
1410- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"c-text\">")
1478+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "<p class=\"c-text\">")
14111479 if templ_7745c5c3_Err != nil {
14121480 return templ_7745c5c3_Err
14131481 }
@@ -1415,24 +1483,24 @@ func commentView(c CommentView, suffix string) templ.Component {
14151483 if templ_7745c5c3_Err != nil {
14161484 return templ_7745c5c3_Err
14171485 }
1418- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</p>")
1486+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</p>")
14191487 if templ_7745c5c3_Err != nil {
14201488 return templ_7745c5c3_Err
14211489 }
14221490 } else if c.Deleted {
14231491 if c.Verdict != "" {
1424- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<p class=\"mute\">text deleted</p>")
1492+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "<p class=\"mute\">text deleted</p>")
14251493 if templ_7745c5c3_Err != nil {
14261494 return templ_7745c5c3_Err
14271495 }
14281496 } else {
1429- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<p class=\"mute\">deleted note</p>")
1497+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "<p class=\"mute\">deleted note</p>")
14301498 if templ_7745c5c3_Err != nil {
14311499 return templ_7745c5c3_Err
14321500 }
14331501 }
14341502 } else {
1435- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<div class=\"c-body prose\">")
1503+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "<div class=\"c-body prose\">")
14361504 if templ_7745c5c3_Err != nil {
14371505 return templ_7745c5c3_Err
14381506 }
@@ -1447,147 +1515,147 @@ func commentView(c CommentView, suffix string) templ.Component {
14471515 return templ_7745c5c3_Err
14481516 }
14491517 }
1450- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</div>")
1518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "</div>")
14511519 if templ_7745c5c3_Err != nil {
14521520 return templ_7745c5c3_Err
14531521 }
14541522 }
14551523 if c.Edited {
1456- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<span class=\"mute\">edited</span> ")
1524+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<span class=\"mute\">edited</span> ")
14571525 if templ_7745c5c3_Err != nil {
14581526 return templ_7745c5c3_Err
14591527 }
14601528 }
14611529 if c.Verdict != "" {
1462- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<span class=\"c-state\">")
1530+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<span class=\"c-state\">")
14631531 if templ_7745c5c3_Err != nil {
14641532 return templ_7745c5c3_Err
14651533 }
1466- var templ_7745c5c3_Var70 string
1467- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict)
1534+ var templ_7745c5c3_Var73 string
1535+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict)
14681536 if templ_7745c5c3_Err != nil {
1469- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 36}
1537+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 36}
14701538 }
1471- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
1539+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
14721540 if templ_7745c5c3_Err != nil {
14731541 return templ_7745c5c3_Err
14741542 }
1475- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</span> ")
1543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</span> ")
14761544 if templ_7745c5c3_Err != nil {
14771545 return templ_7745c5c3_Err
14781546 }
14791547 }
14801548 if c.Range != "" {
1481- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "<span class=\"mute\">")
1549+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<span class=\"mute\">")
14821550 if templ_7745c5c3_Err != nil {
14831551 return templ_7745c5c3_Err
14841552 }
1485- var templ_7745c5c3_Var71 string
1486- templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range)
1553+ var templ_7745c5c3_Var74 string
1554+ templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range)
14871555 if templ_7745c5c3_Err != nil {
1488- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 31}
1556+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 31}
14891557 }
1490- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
1558+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74))
14911559 if templ_7745c5c3_Err != nil {
14921560 return templ_7745c5c3_Err
14931561 }
1494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</span> ")
1562+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "</span> ")
14951563 if templ_7745c5c3_Err != nil {
14961564 return templ_7745c5c3_Err
14971565 }
14981566 }
14991567 if c.Quote != "" {
1500- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
1568+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
15011569 if templ_7745c5c3_Err != nil {
15021570 return templ_7745c5c3_Err
15031571 }
1504- var templ_7745c5c3_Var72 string
1505- templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote)
1572+ var templ_7745c5c3_Var75 string
1573+ templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote)
15061574 if templ_7745c5c3_Err != nil {
1507- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 279, Col: 65}
1575+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 65}
15081576 }
1509- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
1577+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75)
15101578 if templ_7745c5c3_Err != nil {
15111579 return templ_7745c5c3_Err
15121580 }
1513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "\">Quote</button> ")
1581+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "\">Quote</button> ")
15141582 if templ_7745c5c3_Err != nil {
15151583 return templ_7745c5c3_Err
15161584 }
15171585 }
15181586 if c.Edit != nil {
1519- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "<details><summary>Edit</summary><form method=\"post\" action=\"")
1587+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<details><summary>Edit</summary><form method=\"post\" action=\"")
15201588 if templ_7745c5c3_Err != nil {
15211589 return templ_7745c5c3_Err
15221590 }
1523- var templ_7745c5c3_Var73 templ.SafeURL
1524- templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action))
1591+ var templ_7745c5c3_Var76 templ.SafeURL
1592+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action))
15251593 if templ_7745c5c3_Err != nil {
1526- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 92}
1594+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 92}
15271595 }
1528- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1596+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
15291597 if templ_7745c5c3_Err != nil {
15301598 return templ_7745c5c3_Err
15311599 }
1532- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "\"><input type=\"hidden\" name=\"from\" value=\"")
1600+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\"><input type=\"hidden\" name=\"from\" value=\"")
15331601 if templ_7745c5c3_Err != nil {
15341602 return templ_7745c5c3_Err
15351603 }
1536- var templ_7745c5c3_Var74 string
1537- templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From))
1604+ var templ_7745c5c3_Var77 string
1605+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From))
15381606 if templ_7745c5c3_Err != nil {
1539- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 153}
1607+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 153}
15401608 }
1541- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)
1609+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
15421610 if templ_7745c5c3_Err != nil {
15431611 return templ_7745c5c3_Err
15441612 }
1545- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "\"><textarea name=\"text\" maxlength=\"8000\">")
1613+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "\"><textarea name=\"text\" maxlength=\"8000\">")
15461614 if templ_7745c5c3_Err != nil {
15471615 return templ_7745c5c3_Err
15481616 }
1549- var templ_7745c5c3_Var75 string
1550- templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text)
1617+ var templ_7745c5c3_Var78 string
1618+ templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text)
15511619 if templ_7745c5c3_Err != nil {
1552- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 282, Col: 209}
1620+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 209}
15531621 }
1554- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
1622+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
15551623 if templ_7745c5c3_Err != nil {
15561624 return templ_7745c5c3_Err
15571625 }
1558- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
1626+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
15591627 if templ_7745c5c3_Err != nil {
15601628 return templ_7745c5c3_Err
15611629 }
15621630 }
15631631 if c.Delete != nil {
1564- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<details><summary>Delete</summary><form method=\"post\" action=\"")
1632+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<details><summary>Delete</summary><form method=\"post\" action=\"")
15651633 if templ_7745c5c3_Err != nil {
15661634 return templ_7745c5c3_Err
15671635 }
1568- var templ_7745c5c3_Var76 templ.SafeURL
1569- templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action))
1636+ var templ_7745c5c3_Var79 templ.SafeURL
1637+ templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action))
15701638 if templ_7745c5c3_Err != nil {
1571- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 96}
1639+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 96}
15721640 }
1573- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
1641+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79))
15741642 if templ_7745c5c3_Err != nil {
15751643 return templ_7745c5c3_Err
15761644 }
1577- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "\"><input type=\"hidden\" name=\"from\" value=\"")
1645+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "\"><input type=\"hidden\" name=\"from\" value=\"")
15781646 if templ_7745c5c3_Err != nil {
15791647 return templ_7745c5c3_Err
15801648 }
1581- var templ_7745c5c3_Var77 string
1582- templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From))
1649+ var templ_7745c5c3_Var80 string
1650+ templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From))
15831651 if templ_7745c5c3_Err != nil {
1584- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 159}
1652+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 159}
15851653 }
1586- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
1654+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
15871655 if templ_7745c5c3_Err != nil {
15881656 return templ_7745c5c3_Err
15891657 }
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> ")
1658+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "\"><p>Delete this note?</p><button class=\"btn\" type=\"submit\">Confirm delete</button></form></details> ")
15911659 if templ_7745c5c3_Err != nil {
15921660 return templ_7745c5c3_Err
15931661 }
@@ -1599,7 +1667,7 @@ func commentView(c CommentView, suffix string) templ.Component {
15991667 }
16001668 }
16011669 if c.Reply != nil {
1602- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<details data-reply><summary>Reply</summary>")
1670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<details data-reply><summary>Reply</summary>")
16031671 if templ_7745c5c3_Err != nil {
16041672 return templ_7745c5c3_Err
16051673 }
@@ -1607,80 +1675,80 @@ func commentView(c CommentView, suffix string) templ.Component {
16071675 if templ_7745c5c3_Err != nil {
16081676 return templ_7745c5c3_Err
16091677 }
1610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "</details> ")
1678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "</details> ")
16111679 if templ_7745c5c3_Err != nil {
16121680 return templ_7745c5c3_Err
16131681 }
16141682 }
16151683 if c.Resolve != nil {
1616- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "<form class=\"c-act\" method=\"post\" action=\"")
1684+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "<form class=\"c-act\" method=\"post\" action=\"")
16171685 if templ_7745c5c3_Err != nil {
16181686 return templ_7745c5c3_Err
16191687 }
1620- var templ_7745c5c3_Var78 templ.SafeURL
1621- templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
1688+ var templ_7745c5c3_Var81 templ.SafeURL
1689+ templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
16221690 if templ_7745c5c3_Err != nil {
1623- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 296, Col: 77}
1691+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 308, Col: 77}
16241692 }
1625- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
1693+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
16261694 if templ_7745c5c3_Err != nil {
16271695 return templ_7745c5c3_Err
16281696 }
1629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
1697+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
16301698 if templ_7745c5c3_Err != nil {
16311699 return templ_7745c5c3_Err
16321700 }
1633- var templ_7745c5c3_Var79 string
1634- templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
1701+ var templ_7745c5c3_Var82 string
1702+ templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
16351703 if templ_7745c5c3_Err != nil {
1636- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 74}
1704+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 309, Col: 74}
16371705 }
1638- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79)
1706+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)
16391707 if templ_7745c5c3_Err != nil {
16401708 return templ_7745c5c3_Err
16411709 }
1642- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "\"> ")
1710+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\"> ")
16431711 if templ_7745c5c3_Err != nil {
16441712 return templ_7745c5c3_Err
16451713 }
16461714 if c.Resolve.From > 0 {
1647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<input type=\"hidden\" name=\"from\" value=\"")
1715+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "<input type=\"hidden\" name=\"from\" value=\"")
16481716 if templ_7745c5c3_Err != nil {
16491717 return templ_7745c5c3_Err
16501718 }
1651- var templ_7745c5c3_Var80 string
1652- templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
1719+ var templ_7745c5c3_Var83 string
1720+ templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
16531721 if templ_7745c5c3_Err != nil {
1654- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 299, Col: 66}
1722+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 311, Col: 66}
16551723 }
1656- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
1724+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83)
16571725 if templ_7745c5c3_Err != nil {
16581726 return templ_7745c5c3_Err
16591727 }
1660- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "\"> ")
1728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "\"> ")
16611729 if templ_7745c5c3_Err != nil {
16621730 return templ_7745c5c3_Err
16631731 }
16641732 }
1665- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "<button type=\"submit\" class=\"btn\">")
1733+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<button type=\"submit\" class=\"btn\">")
16661734 if templ_7745c5c3_Err != nil {
16671735 return templ_7745c5c3_Err
16681736 }
1669- var templ_7745c5c3_Var81 string
1670- templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
1737+ var templ_7745c5c3_Var84 string
1738+ templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
16711739 if templ_7745c5c3_Err != nil {
1672- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 301, Col: 55}
1740+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 55}
16731741 }
1674- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
1742+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84))
16751743 if templ_7745c5c3_Err != nil {
16761744 return templ_7745c5c3_Err
16771745 }
1678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "</button></form>")
1746+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "</button></form>")
16791747 if templ_7745c5c3_Err != nil {
16801748 return templ_7745c5c3_Err
16811749 }
16821750 }
1683- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "</div>")
1751+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</div>")
16841752 if templ_7745c5c3_Err != nil {
16851753 return templ_7745c5c3_Err
16861754 }
@@ -1712,214 +1780,214 @@ func commentForm(f CommentForm, suffix string) templ.Component {
17121780 }()
17131781 }
17141782 ctx = templ.InitializeContext(ctx)
1715- templ_7745c5c3_Var82 := templ.GetChildren(ctx)
1716- if templ_7745c5c3_Var82 == nil {
1717- templ_7745c5c3_Var82 = templ.NopComponent
1783+ templ_7745c5c3_Var85 := templ.GetChildren(ctx)
1784+ if templ_7745c5c3_Var85 == nil {
1785+ templ_7745c5c3_Var85 = templ.NopComponent
17181786 }
17191787 ctx = templ.ClearChildren(ctx)
1720- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "<form class=\"cform\" method=\"post\" action=\"")
1788+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "<form class=\"cform\" method=\"post\" action=\"")
17211789 if templ_7745c5c3_Err != nil {
17221790 return templ_7745c5c3_Err
17231791 }
1724- var templ_7745c5c3_Var83 templ.SafeURL
1725- templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
1792+ var templ_7745c5c3_Var86 templ.SafeURL
1793+ templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
17261794 if templ_7745c5c3_Err != nil {
1727- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 316, Col: 67}
1795+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 328, Col: 67}
17281796 }
1729- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
1797+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86))
17301798 if templ_7745c5c3_Err != nil {
17311799 return templ_7745c5c3_Err
17321800 }
1733- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\"><label for=\"")
1801+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "\"><label for=\"")
17341802 if templ_7745c5c3_Err != nil {
17351803 return templ_7745c5c3_Err
17361804 }
1737- var templ_7745c5c3_Var84 string
1738- templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1805+ var templ_7745c5c3_Var87 string
1806+ templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
17391807 if templ_7745c5c3_Err != nil {
1740- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 28}
1808+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 329, Col: 28}
17411809 }
1742- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84)
1810+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87)
17431811 if templ_7745c5c3_Err != nil {
17441812 return templ_7745c5c3_Err
17451813 }
1746- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\">")
1814+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "\">")
17471815 if templ_7745c5c3_Err != nil {
17481816 return templ_7745c5c3_Err
17491817 }
1750- var templ_7745c5c3_Var85 string
1751- templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
1818+ var templ_7745c5c3_Var88 string
1819+ templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
17521820 if templ_7745c5c3_Err != nil {
1753- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 317, Col: 40}
1821+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 329, Col: 40}
17541822 }
1755- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
1823+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88))
17561824 if templ_7745c5c3_Err != nil {
17571825 return templ_7745c5c3_Err
17581826 }
1759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "</label> <textarea id=\"")
1827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "</label> <textarea id=\"")
17601828 if templ_7745c5c3_Err != nil {
17611829 return templ_7745c5c3_Err
17621830 }
1763- var templ_7745c5c3_Var86 string
1764- templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1831+ var templ_7745c5c3_Var89 string
1832+ templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
17651833 if templ_7745c5c3_Err != nil {
1766- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 21}
1834+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 21}
17671835 }
1768- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86)
1836+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89)
17691837 if templ_7745c5c3_Err != nil {
17701838 return templ_7745c5c3_Err
17711839 }
1772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "\" name=\"text\" rows=\"3\" maxlength=\"8000\"")
1840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "\" name=\"text\" rows=\"3\" maxlength=\"8000\"")
17731841 if templ_7745c5c3_Err != nil {
17741842 return templ_7745c5c3_Err
17751843 }
17761844 if f.Plain {
1777- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, " required placeholder=\"plain text, 8000 characters at most\"")
1845+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, " required placeholder=\"plain text, 8000 characters at most\"")
17781846 if templ_7745c5c3_Err != nil {
17791847 return templ_7745c5c3_Err
17801848 }
17811849 } else {
1782- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, " placeholder=\"Markdown, 8000 characters at most\"")
1850+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, " placeholder=\"Markdown, 8000 characters at most\"")
17831851 if templ_7745c5c3_Err != nil {
17841852 return templ_7745c5c3_Err
17851853 }
17861854 }
17871855 if f.Focus {
1788- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, " autofocus")
1856+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, " autofocus")
17891857 if templ_7745c5c3_Err != nil {
17901858 return templ_7745c5c3_Err
17911859 }
17921860 }
1793- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "></textarea> ")
1861+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "></textarea> ")
17941862 if templ_7745c5c3_Err != nil {
17951863 return templ_7745c5c3_Err
17961864 }
17971865 if f.Path != "" {
1798- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "<input type=\"hidden\" name=\"path\" value=\"")
1866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "<input type=\"hidden\" name=\"path\" value=\"")
17991867 if templ_7745c5c3_Err != nil {
18001868 return templ_7745c5c3_Err
18011869 }
1802- var templ_7745c5c3_Var87 string
1803- templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
1870+ var templ_7745c5c3_Var90 string
1871+ templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
18041872 if templ_7745c5c3_Err != nil {
1805- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 50}
1873+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 346, Col: 50}
18061874 }
1807- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87)
1875+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
18081876 if templ_7745c5c3_Err != nil {
18091877 return templ_7745c5c3_Err
18101878 }
1811- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "\"> <input type=\"hidden\" name=\"line\" value=\"")
1879+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "\"> <input type=\"hidden\" name=\"line\" value=\"")
18121880 if templ_7745c5c3_Err != nil {
18131881 return templ_7745c5c3_Err
18141882 }
1815- var templ_7745c5c3_Var88 string
1816- templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
1883+ var templ_7745c5c3_Var91 string
1884+ templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
18171885 if templ_7745c5c3_Err != nil {
1818- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 335, Col: 50}
1886+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 347, Col: 50}
18191887 }
1820- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88)
1888+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91)
18211889 if templ_7745c5c3_Err != nil {
18221890 return templ_7745c5c3_Err
18231891 }
1824- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "\"> <input type=\"hidden\" name=\"side\" value=\"")
1892+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "\"> <input type=\"hidden\" name=\"side\" value=\"")
18251893 if templ_7745c5c3_Err != nil {
18261894 return templ_7745c5c3_Err
18271895 }
1828- var templ_7745c5c3_Var89 string
1829- templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
1896+ var templ_7745c5c3_Var92 string
1897+ templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
18301898 if templ_7745c5c3_Err != nil {
1831- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 50}
1899+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 348, Col: 50}
18321900 }
1833- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89)
1901+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
18341902 if templ_7745c5c3_Err != nil {
18351903 return templ_7745c5c3_Err
18361904 }
1837- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
1905+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
18381906 if templ_7745c5c3_Err != nil {
18391907 return templ_7745c5c3_Err
18401908 }
1841- var templ_7745c5c3_Var90 string
1842- templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine)
1909+ var templ_7745c5c3_Var93 string
1910+ templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine)
18431911 if templ_7745c5c3_Err != nil {
1844- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 337, Col: 86}
1912+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 349, Col: 86}
18451913 }
1846- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
1914+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var93)
18471915 if templ_7745c5c3_Err != nil {
18481916 return templ_7745c5c3_Err
18491917 }
1850- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "\"></label> ")
1918+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"></label> ")
18511919 if templ_7745c5c3_Err != nil {
18521920 return templ_7745c5c3_Err
18531921 }
18541922 }
18551923 if f.ReplyTo != "" {
1856- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "<input type=\"hidden\" name=\"reply_to\" value=\"")
1924+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<input type=\"hidden\" name=\"reply_to\" value=\"")
18571925 if templ_7745c5c3_Err != nil {
18581926 return templ_7745c5c3_Err
18591927 }
1860- var templ_7745c5c3_Var91 string
1861- templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo)
1928+ var templ_7745c5c3_Var94 string
1929+ templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo)
18621930 if templ_7745c5c3_Err != nil {
1863- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 340, Col: 57}
1931+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 352, Col: 57}
18641932 }
1865- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91)
1933+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var94)
18661934 if templ_7745c5c3_Err != nil {
18671935 return templ_7745c5c3_Err
18681936 }
1869- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "\"> ")
1937+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "\"> ")
18701938 if templ_7745c5c3_Err != nil {
18711939 return templ_7745c5c3_Err
18721940 }
18731941 }
18741942 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>")
1943+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "<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>")
18761944 if templ_7745c5c3_Err != nil {
18771945 return templ_7745c5c3_Err
18781946 }
18791947 }
18801948 if f.From > 0 {
1881- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "<input type=\"hidden\" name=\"from\" value=\"")
1949+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<input type=\"hidden\" name=\"from\" value=\"")
18821950 if templ_7745c5c3_Err != nil {
18831951 return templ_7745c5c3_Err
18841952 }
1885- var templ_7745c5c3_Var92 string
1886- templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
1953+ var templ_7745c5c3_Var95 string
1954+ templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
18871955 if templ_7745c5c3_Err != nil {
1888- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 346, Col: 56}
1956+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 358, Col: 56}
18891957 }
1890- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
1958+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var95)
18911959 if templ_7745c5c3_Err != nil {
18921960 return templ_7745c5c3_Err
18931961 }
1894- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "\">")
1962+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\">")
18951963 if templ_7745c5c3_Err != nil {
18961964 return templ_7745c5c3_Err
18971965 }
18981966 }
1899- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
1967+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
19001968 if templ_7745c5c3_Err != nil {
19011969 return templ_7745c5c3_Err
19021970 }
19031971 if f.Cancel != "" {
1904- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<a class=\"btn\" href=\"")
1972+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<a class=\"btn\" href=\"")
19051973 if templ_7745c5c3_Err != nil {
19061974 return templ_7745c5c3_Err
19071975 }
1908- var templ_7745c5c3_Var93 templ.SafeURL
1909- templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
1976+ var templ_7745c5c3_Var96 templ.SafeURL
1977+ templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
19101978 if templ_7745c5c3_Err != nil {
1911- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 351, Col: 49}
1979+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 363, Col: 49}
19121980 }
1913- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))
1981+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
19141982 if templ_7745c5c3_Err != nil {
19151983 return templ_7745c5c3_Err
19161984 }
1917- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\">Cancel</a>")
1985+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "\">Cancel</a>")
19181986 if templ_7745c5c3_Err != nil {
19191987 return templ_7745c5c3_Err
19201988 }
19211989 }
1922- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "</div></form>")
1990+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</div></form>")
19231991 if templ_7745c5c3_Err != nil {
19241992 return templ_7745c5c3_Err
19251993 }
@@ -1943,25 +2011,25 @@ func mrRail(p MRPage) templ.Component {
19432011 }()
19442012 }
19452013 ctx = templ.InitializeContext(ctx)
1946- templ_7745c5c3_Var94 := templ.GetChildren(ctx)
1947- if templ_7745c5c3_Var94 == nil {
1948- templ_7745c5c3_Var94 = templ.NopComponent
2014+ templ_7745c5c3_Var97 := templ.GetChildren(ctx)
2015+ if templ_7745c5c3_Var97 == nil {
2016+ templ_7745c5c3_Var97 = templ.NopComponent
19492017 }
19502018 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\">")
2019+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
19522020 if templ_7745c5c3_Err != nil {
19532021 return templ_7745c5c3_Err
19542022 }
1955- var templ_7745c5c3_Var95 string
1956- templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
2023+ var templ_7745c5c3_Var98 string
2024+ templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
19572025 if templ_7745c5c3_Err != nil {
1958- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 360, Col: 49}
2026+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 372, Col: 49}
19592027 }
1960- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))
2028+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
19612029 if templ_7745c5c3_Err != nil {
19622030 return templ_7745c5c3_Err
19632031 }
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>")
2032+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "</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>")
19652033 if templ_7745c5c3_Err != nil {
19662034 return templ_7745c5c3_Err
19672035 }
@@ -1969,71 +2037,71 @@ func mrRail(p MRPage) templ.Component {
19692037 if templ_7745c5c3_Err != nil {
19702038 return templ_7745c5c3_Err
19712039 }
1972- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "</section>")
2040+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</section>")
19732041 if templ_7745c5c3_Err != nil {
19742042 return templ_7745c5c3_Err
19752043 }
19762044 if len(p.Verdicts) > 0 {
1977- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "<section><h2>Verdicts</h2><ul>")
2045+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<section><h2>Verdicts</h2><ul>")
19782046 if templ_7745c5c3_Err != nil {
19792047 return templ_7745c5c3_Err
19802048 }
19812049 for _, v := range p.Verdicts {
1982- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<li><a href=\"")
2050+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "<li><a href=\"")
19832051 if templ_7745c5c3_Err != nil {
19842052 return templ_7745c5c3_Err
19852053 }
1986- var templ_7745c5c3_Var96 templ.SafeURL
1987- templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
2054+ var templ_7745c5c3_Var99 templ.SafeURL
2055+ templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
19882056 if templ_7745c5c3_Err != nil {
1989- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 37}
2057+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 37}
19902058 }
1991- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
2059+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
19922060 if templ_7745c5c3_Err != nil {
19932061 return templ_7745c5c3_Err
19942062 }
1995- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "\">")
2063+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "\">")
19962064 if templ_7745c5c3_Err != nil {
19972065 return templ_7745c5c3_Err
19982066 }
1999- var templ_7745c5c3_Var97 string
2000- templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
2067+ var templ_7745c5c3_Var100 string
2068+ templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
20012069 if templ_7745c5c3_Err != nil {
2002- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 50}
2070+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 50}
20032071 }
2004- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97))
2072+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
20052073 if templ_7745c5c3_Err != nil {
20062074 return templ_7745c5c3_Err
20072075 }
2008- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, ": ")
2076+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, ": ")
20092077 if templ_7745c5c3_Err != nil {
20102078 return templ_7745c5c3_Err
20112079 }
2012- var templ_7745c5c3_Var98 string
2013- templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2080+ var templ_7745c5c3_Var101 string
2081+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
20142082 if templ_7745c5c3_Err != nil {
2015- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 63}
2083+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 63}
20162084 }
2017- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
2085+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
20182086 if templ_7745c5c3_Err != nil {
20192087 return templ_7745c5c3_Err
20202088 }
2021- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</a> ")
2089+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "</a> ")
20222090 if templ_7745c5c3_Err != nil {
20232091 return templ_7745c5c3_Err
20242092 }
20252093 if v.Stale {
2026- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<span class=\"mute\">(stale)</span>")
2094+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "<span class=\"mute\">(stale)</span>")
20272095 if templ_7745c5c3_Err != nil {
20282096 return templ_7745c5c3_Err
20292097 }
20302098 }
2031- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "</li>")
2099+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</li>")
20322100 if templ_7745c5c3_Err != nil {
20332101 return templ_7745c5c3_Err
20342102 }
20352103 }
2036- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</ul></section>")
2104+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</ul></section>")
20372105 if templ_7745c5c3_Err != nil {
20382106 return templ_7745c5c3_Err
20392107 }
@@ -2042,74 +2110,74 @@ func mrRail(p MRPage) templ.Component {
20422110 if templ_7745c5c3_Err != nil {
20432111 return templ_7745c5c3_Err
20442112 }
2045- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
2113+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
20462114 if templ_7745c5c3_Err != nil {
20472115 return templ_7745c5c3_Err
20482116 }
2049- var templ_7745c5c3_Var99 string
2050- templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
2117+ var templ_7745c5c3_Var102 string
2118+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
20512119 if templ_7745c5c3_Err != nil {
2052- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 384, Col: 71}
2120+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 71}
20532121 }
2054- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
2122+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
20552123 if templ_7745c5c3_Err != nil {
20562124 return templ_7745c5c3_Err
20572125 }
2058- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "</span></h2><ul class=\"pcommits\">")
2126+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</span></h2><ul class=\"pcommits\">")
20592127 if templ_7745c5c3_Err != nil {
20602128 return templ_7745c5c3_Err
20612129 }
20622130 for _, c := range p.Commits {
2063- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<li")
2131+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "<li")
20642132 if templ_7745c5c3_Err != nil {
20652133 return templ_7745c5c3_Err
20662134 }
20672135 if c.New {
2068- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, " data-new")
2136+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, " data-new")
20692137 if templ_7745c5c3_Err != nil {
20702138 return templ_7745c5c3_Err
20712139 }
20722140 }
2073- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "><a href=\"")
2141+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "><a href=\"")
20742142 if templ_7745c5c3_Err != nil {
20752143 return templ_7745c5c3_Err
20762144 }
2077- var templ_7745c5c3_Var100 templ.SafeURL
2078- templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
2145+ var templ_7745c5c3_Var103 templ.SafeURL
2146+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
20792147 if templ_7745c5c3_Err != nil {
2080- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 36}
2148+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 36}
20812149 }
2082- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
2150+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
20832151 if templ_7745c5c3_Err != nil {
20842152 return templ_7745c5c3_Err
20852153 }
2086- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "\">")
2154+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\">")
20872155 if templ_7745c5c3_Err != nil {
20882156 return templ_7745c5c3_Err
20892157 }
2090- var templ_7745c5c3_Var101 string
2091- templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
2158+ var templ_7745c5c3_Var104 string
2159+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
20922160 if templ_7745c5c3_Err != nil {
2093- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 388, Col: 48}
2161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 48}
20942162 }
2095- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
2163+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
20962164 if templ_7745c5c3_Err != nil {
20972165 return templ_7745c5c3_Err
20982166 }
2099- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</a> <span class=\"s\">")
2167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</a> <span class=\"s\">")
21002168 if templ_7745c5c3_Err != nil {
21012169 return templ_7745c5c3_Err
21022170 }
2103- var templ_7745c5c3_Var102 string
2104- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
2171+ var templ_7745c5c3_Var105 string
2172+ templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
21052173 if templ_7745c5c3_Err != nil {
2106- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 389, Col: 32}
2174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 401, Col: 32}
21072175 }
2108- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
2176+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
21092177 if templ_7745c5c3_Err != nil {
21102178 return templ_7745c5c3_Err
21112179 }
2112- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</span> ")
2180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "</span> ")
21132181 if templ_7745c5c3_Err != nil {
21142182 return templ_7745c5c3_Err
21152183 }
@@ -2119,138 +2187,138 @@ func mrRail(p MRPage) templ.Component {
21192187 return templ_7745c5c3_Err
21202188 }
21212189 }
2122- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "<span class=\"mute\">")
2190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<span class=\"mute\">")
21232191 if templ_7745c5c3_Err != nil {
21242192 return templ_7745c5c3_Err
21252193 }
2126- var templ_7745c5c3_Var103 string
2127- templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
2194+ var templ_7745c5c3_Var106 string
2195+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
21282196 if templ_7745c5c3_Err != nil {
2129- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 394, Col: 16}
2197+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 406, Col: 16}
21302198 }
2131- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
2199+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
21322200 if templ_7745c5c3_Err != nil {
21332201 return templ_7745c5c3_Err
21342202 }
2135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, " ")
2203+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, " ")
21362204 if templ_7745c5c3_Err != nil {
21372205 return templ_7745c5c3_Err
21382206 }
21392207 if c.WhenAt != "" {
2140- var templ_7745c5c3_Var104 string
2141- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
2208+ var templ_7745c5c3_Var107 string
2209+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
21422210 if templ_7745c5c3_Err != nil {
2143- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 15}
2211+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 15}
21442212 }
2145- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
2213+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
21462214 if templ_7745c5c3_Err != nil {
21472215 return templ_7745c5c3_Err
21482216 }
2149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "<time datetime=\"")
2217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "<time datetime=\"")
21502218 if templ_7745c5c3_Err != nil {
21512219 return templ_7745c5c3_Err
21522220 }
2153- var templ_7745c5c3_Var105 string
2154- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
2221+ var templ_7745c5c3_Var108 string
2222+ templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
21552223 if templ_7745c5c3_Err != nil {
2156- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 42}
2224+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 42}
21572225 }
2158- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105)
2226+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var108)
21592227 if templ_7745c5c3_Err != nil {
21602228 return templ_7745c5c3_Err
21612229 }
2162- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "\">")
2230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\">")
21632231 if templ_7745c5c3_Err != nil {
21642232 return templ_7745c5c3_Err
21652233 }
2166- var templ_7745c5c3_Var106 string
2167- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
2234+ var templ_7745c5c3_Var109 string
2235+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
21682236 if templ_7745c5c3_Err != nil {
2169- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 53}
2237+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 53}
21702238 }
2171- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
2239+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
21722240 if templ_7745c5c3_Err != nil {
21732241 return templ_7745c5c3_Err
21742242 }
2175- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "</time> ")
2243+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "</time> ")
21762244 if templ_7745c5c3_Err != nil {
21772245 return templ_7745c5c3_Err
21782246 }
21792247 }
21802248 if c.New {
2181- var templ_7745c5c3_Var107 string
2182- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
2249+ var templ_7745c5c3_Var110 string
2250+ templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
21832251 if templ_7745c5c3_Err != nil {
2184- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 399, Col: 18}
2252+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 18}
21852253 }
2186- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
2254+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))
21872255 if templ_7745c5c3_Err != nil {
21882256 return templ_7745c5c3_Err
21892257 }
21902258 }
2191- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</span></li>")
2259+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "</span></li>")
21922260 if templ_7745c5c3_Err != nil {
21932261 return templ_7745c5c3_Err
21942262 }
21952263 }
2196- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</ul>")
2264+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "</ul>")
21972265 if templ_7745c5c3_Err != nil {
21982266 return templ_7745c5c3_Err
21992267 }
22002268 if len(p.Commits) == 0 {
2201- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"dim\">no commit to merge.</p>")
2269+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "<p class=\"dim\">no commit to merge.</p>")
22022270 if templ_7745c5c3_Err != nil {
22032271 return templ_7745c5c3_Err
22042272 }
22052273 }
22062274 if p.CommitsNote != "" {
2207- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<p class=\"note\">")
2275+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "<p class=\"note\">")
22082276 if templ_7745c5c3_Err != nil {
22092277 return templ_7745c5c3_Err
22102278 }
2211- var templ_7745c5c3_Var108 string
2212- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
2279+ var templ_7745c5c3_Var111 string
2280+ templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
22132281 if templ_7745c5c3_Err != nil {
2214- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 409, Col: 34}
2282+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 34}
22152283 }
2216- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
2284+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
22172285 if templ_7745c5c3_Err != nil {
22182286 return templ_7745c5c3_Err
22192287 }
2220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</p>")
2288+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</p>")
22212289 if templ_7745c5c3_Err != nil {
22222290 return templ_7745c5c3_Err
22232291 }
22242292 }
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>")
2293+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
22262294 if templ_7745c5c3_Err != nil {
22272295 return templ_7745c5c3_Err
22282296 }
2229- var templ_7745c5c3_Var109 string
2230- templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
2297+ var templ_7745c5c3_Var112 string
2298+ templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
22312299 if templ_7745c5c3_Err != nil {
2232- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 36}
2300+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 428, Col: 36}
22332301 }
2234- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
2302+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
22352303 if templ_7745c5c3_Err != nil {
22362304 return templ_7745c5c3_Err
22372305 }
2238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "</dd><dt>checks</dt><dd>")
2306+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</dd><dt>checks</dt><dd>")
22392307 if templ_7745c5c3_Err != nil {
22402308 return templ_7745c5c3_Err
22412309 }
22422310 if p.ChecksBlock {
2243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "failing ones block the merge")
2311+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "failing ones block the merge")
22442312 if templ_7745c5c3_Err != nil {
22452313 return templ_7745c5c3_Err
22462314 }
22472315 } else {
2248- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "do not block the merge")
2316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "do not block the merge")
22492317 if templ_7745c5c3_Err != nil {
22502318 return templ_7745c5c3_Err
22512319 }
22522320 }
2253- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "</dd></dl></section>")
2321+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "</dd></dl></section>")
22542322 if templ_7745c5c3_Err != nil {
22552323 return templ_7745c5c3_Err
22562324 }
@@ -2278,51 +2346,51 @@ func mrBar(p MRPage) templ.Component {
22782346 }()
22792347 }
22802348 ctx = templ.InitializeContext(ctx)
2281- templ_7745c5c3_Var110 := templ.GetChildren(ctx)
2282- if templ_7745c5c3_Var110 == nil {
2283- templ_7745c5c3_Var110 = templ.NopComponent
2349+ templ_7745c5c3_Var113 := templ.GetChildren(ctx)
2350+ if templ_7745c5c3_Var113 == nil {
2351+ templ_7745c5c3_Var113 = templ.NopComponent
22842352 }
22852353 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\">")
2354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")
22872355 if templ_7745c5c3_Err != nil {
22882356 return templ_7745c5c3_Err
22892357 }
2290- var templ_7745c5c3_Var111 string
2291- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
2358+ var templ_7745c5c3_Var114 string
2359+ templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
22922360 if templ_7745c5c3_Err != nil {
2293- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 55}
2361+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 55}
22942362 }
2295- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2363+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))
22962364 if templ_7745c5c3_Err != nil {
22972365 return templ_7745c5c3_Err
22982366 }
2299- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " · <span class=\"plus\">+")
2367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, " · <span class=\"plus\">+")
23002368 if templ_7745c5c3_Err != nil {
23012369 return templ_7745c5c3_Err
23022370 }
2303- var templ_7745c5c3_Var112 string
2304- templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
2371+ var templ_7745c5c3_Var115 string
2372+ templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
23052373 if templ_7745c5c3_Err != nil {
2306- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 95}
2374+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 95}
23072375 }
2308- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
2376+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
23092377 if templ_7745c5c3_Err != nil {
23102378 return templ_7745c5c3_Err
23112379 }
2312- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</span> <span class=\"minus\">-")
2380+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "</span> <span class=\"minus\">-")
23132381 if templ_7745c5c3_Err != nil {
23142382 return templ_7745c5c3_Err
23152383 }
2316- var templ_7745c5c3_Var113 string
2317- templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
2384+ var templ_7745c5c3_Var116 string
2385+ templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
23182386 if templ_7745c5c3_Err != nil {
2319- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 140}
2387+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 140}
23202388 }
2321- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
2389+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
23222390 if templ_7745c5c3_Err != nil {
23232391 return templ_7745c5c3_Err
23242392 }
2325- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "</span></span>")
2393+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "</span></span>")
23262394 if templ_7745c5c3_Err != nil {
23272395 return templ_7745c5c3_Err
23282396 }
@@ -2330,7 +2398,7 @@ func mrBar(p MRPage) templ.Component {
23302398 if templ_7745c5c3_Err != nil {
23312399 return templ_7745c5c3_Err
23322400 }
2333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "<span class=\"grow\"></span>")
2401+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "<span class=\"grow\"></span>")
23342402 if templ_7745c5c3_Err != nil {
23352403 return templ_7745c5c3_Err
23362404 }
@@ -2343,7 +2411,7 @@ func mrBar(p MRPage) templ.Component {
23432411 if templ_7745c5c3_Err != nil {
23442412 return templ_7745c5c3_Err
23452413 }
2346- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " ")
2414+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, " ")
23472415 if templ_7745c5c3_Err != nil {
23482416 return templ_7745c5c3_Err
23492417 }
@@ -2352,7 +2420,7 @@ func mrBar(p MRPage) templ.Component {
23522420 return templ_7745c5c3_Err
23532421 }
23542422 }
2355- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "</div>")
2423+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</div>")
23562424 if templ_7745c5c3_Err != nil {
23572425 return templ_7745c5c3_Err
23582426 }
@@ -2363,6 +2431,11 @@ func mrBar(p MRPage) templ.Component {
23632431 // mergeMenu is the merge of an open request, for the owner: the form (it
23642432 // carries the head that was read), or "Ready for review" for a draft. The
23652433 // mode and the branch deletion have keys (1-3, D) while it is open.
2434+//
2435+// The badge is a live slot (mr-mergeability); the modes and the button read
2436+// its data-* attributes (hx-live bindings), so that they follow the slot when
2437+// the stream replaces its content. The server renders the same state in the
2438+// attributes, for a page without scripts.
23662439 func mergeMenu(p MRPage) templ.Component {
23672440 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
23682441 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -2379,219 +2452,594 @@ func mergeMenu(p MRPage) templ.Component {
23792452 }()
23802453 }
23812454 ctx = templ.InitializeContext(ctx)
2382- templ_7745c5c3_Var114 := templ.GetChildren(ctx)
2383- if templ_7745c5c3_Var114 == nil {
2384- templ_7745c5c3_Var114 = templ.NopComponent
2455+ templ_7745c5c3_Var117 := templ.GetChildren(ctx)
2456+ if templ_7745c5c3_Var117 == nil {
2457+ templ_7745c5c3_Var117 = templ.NopComponent
23852458 }
23862459 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\">")
2460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"")
2461+ if templ_7745c5c3_Err != nil {
2462+ return templ_7745c5c3_Err
2463+ }
2464+ if p.Box.Error != "" {
2465+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, " open")
2466+ if templ_7745c5c3_Err != nil {
2467+ return templ_7745c5c3_Err
2468+ }
2469+ }
2470+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
23882471 if templ_7745c5c3_Err != nil {
23892472 return templ_7745c5c3_Err
23902473 }
23912474 if p.Box.ReadyAction != "" {
2392- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "Draft ")
2475+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "Draft ")
23932476 if templ_7745c5c3_Err != nil {
23942477 return templ_7745c5c3_Err
23952478 }
23962479 } else {
2397- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "Merge ")
2480+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "Merge ")
23982481 if templ_7745c5c3_Err != nil {
23992482 return templ_7745c5c3_Err
24002483 }
24012484 }
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>")
2485+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>")
24032486 if templ_7745c5c3_Err != nil {
24042487 return templ_7745c5c3_Err
24052488 }
24062489 if p.Box.HeadGone {
2407- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "gone: the source branch was deleted")
2490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "gone: the source branch was deleted")
24082491 if templ_7745c5c3_Err != nil {
24092492 return templ_7745c5c3_Err
24102493 }
24112494 } else {
2412- var templ_7745c5c3_Var115 string
2413- templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
2495+ var templ_7745c5c3_Var118 string
2496+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
24142497 if templ_7745c5c3_Err != nil {
2415- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 466, Col: 19}
2498+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 483, Col: 19}
24162499 }
2417- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
2500+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
24182501 if templ_7745c5c3_Err != nil {
24192502 return templ_7745c5c3_Err
24202503 }
24212504 }
2422- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</dd><dt>into</dt><dd>")
2505+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "</dd><dt>into</dt><dd>")
24232506 if templ_7745c5c3_Err != nil {
24242507 return templ_7745c5c3_Err
24252508 }
2426- var templ_7745c5c3_Var116 string
2427- templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2509+ var templ_7745c5c3_Var119 string
2510+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
24282511 if templ_7745c5c3_Err != nil {
2429- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 18}
2512+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 18}
24302513 }
2431- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
2514+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
24322515 if templ_7745c5c3_Err != nil {
24332516 return templ_7745c5c3_Err
24342517 }
2435- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "</dd></dl>")
2518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</dd></dl>")
24362519 if templ_7745c5c3_Err != nil {
24372520 return templ_7745c5c3_Err
24382521 }
2522+ if p.Box.Error != "" {
2523+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<p class=\"d-err\" role=\"alert\">")
2524+ if templ_7745c5c3_Err != nil {
2525+ return templ_7745c5c3_Err
2526+ }
2527+ var templ_7745c5c3_Var120 string
2528+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Error)
2529+ if templ_7745c5c3_Err != nil {
2530+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 490, Col: 47}
2531+ }
2532+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2533+ if templ_7745c5c3_Err != nil {
2534+ return templ_7745c5c3_Err
2535+ }
2536+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</p>")
2537+ if templ_7745c5c3_Err != nil {
2538+ return templ_7745c5c3_Err
2539+ }
2540+ }
24392541 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=\"")
2542+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<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=\"")
24412543 if templ_7745c5c3_Err != nil {
24422544 return templ_7745c5c3_Err
24432545 }
2444- var templ_7745c5c3_Var117 templ.SafeURL
2445- templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
2546+ var templ_7745c5c3_Var121 templ.SafeURL
2547+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
24462548 if templ_7745c5c3_Err != nil {
2447- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 474, Col: 80}
2549+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 494, Col: 80}
24482550 }
2449- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
2551+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
24502552 if templ_7745c5c3_Err != nil {
24512553 return templ_7745c5c3_Err
24522554 }
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>")
2555+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")
24542556 if templ_7745c5c3_Err != nil {
24552557 return templ_7745c5c3_Err
24562558 }
24572559 } else if !p.Box.HeadGone {
2458- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<form class=\"d-acts\" method=\"post\" action=\"")
2560+ templ_7745c5c3_Err = mrMergeabilitySlot(p.Box.Mergeability).Render(ctx, templ_7745c5c3_Buffer)
24592561 if templ_7745c5c3_Err != nil {
24602562 return templ_7745c5c3_Err
24612563 }
2462- var templ_7745c5c3_Var118 templ.SafeURL
2463- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2564+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, " <form class=\"d-acts\" method=\"post\" action=\"")
24642565 if templ_7745c5c3_Err != nil {
2465- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 478, Col: 80}
2566+ return templ_7745c5c3_Err
24662567 }
2467- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2568+ var templ_7745c5c3_Var122 templ.SafeURL
2569+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2570+ if templ_7745c5c3_Err != nil {
2571+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 499, Col: 80}
2572+ }
2573+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
24682574 if templ_7745c5c3_Err != nil {
24692575 return templ_7745c5c3_Err
24702576 }
2471- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2577+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
24722578 if templ_7745c5c3_Err != nil {
24732579 return templ_7745c5c3_Err
24742580 }
2475- var templ_7745c5c3_Var119 string
2476- templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2581+ var templ_7745c5c3_Var123 string
2582+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
24772583 if templ_7745c5c3_Err != nil {
2478- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 59}
2584+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 500, Col: 59}
24792585 }
2480- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var119)
2586+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
24812587 if templ_7745c5c3_Err != nil {
24822588 return templ_7745c5c3_Err
24832589 }
2484- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
2590+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
24852591 if templ_7745c5c3_Err != nil {
24862592 return templ_7745c5c3_Err
24872593 }
24882594 for i, m := range p.Box.Modes {
2489- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "<label><input type=\"radio\" name=\"mode\" value=\"")
2595+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<label><input type=\"radio\" name=\"mode\" value=\"")
24902596 if templ_7745c5c3_Err != nil {
24912597 return templ_7745c5c3_Err
24922598 }
2493- var templ_7745c5c3_Var120 string
2494- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2599+ var templ_7745c5c3_Var124 string
2600+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
24952601 if templ_7745c5c3_Err != nil {
2496- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 55}
2602+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 508, Col: 24}
24972603 }
2498- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var120)
2604+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
24992605 if templ_7745c5c3_Err != nil {
25002606 return templ_7745c5c3_Err
25012607 }
2502- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "\"")
2608+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "\"")
25032609 if templ_7745c5c3_Err != nil {
25042610 return templ_7745c5c3_Err
25052611 }
2506- if i == 0 {
2507- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, " checked")
2612+ if i == p.Box.Checked() {
2613+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, " checked")
25082614 if templ_7745c5c3_Err != nil {
25092615 return templ_7745c5c3_Err
25102616 }
25112617 }
2512- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "> ")
2618+ if m.Disabled {
2619+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, " disabled")
2620+ if templ_7745c5c3_Err != nil {
2621+ return templ_7745c5c3_Err
2622+ }
2623+ }
2624+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " title=\"")
25132625 if templ_7745c5c3_Err != nil {
25142626 return templ_7745c5c3_Err
25152627 }
2516- var templ_7745c5c3_Var121 string
2517- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2628+ var templ_7745c5c3_Var125 string
2629+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)
25182630 if templ_7745c5c3_Err != nil {
2519- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 485, Col: 17}
2631+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 25}
25202632 }
2521- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2633+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var125)
25222634 if templ_7745c5c3_Err != nil {
25232635 return templ_7745c5c3_Err
25242636 }
2525- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, " ")
2637+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "\" :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\"> ")
2638+ if templ_7745c5c3_Err != nil {
2639+ return templ_7745c5c3_Err
2640+ }
2641+ var templ_7745c5c3_Var126 string
2642+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2643+ if templ_7745c5c3_Err != nil {
2644+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 515, Col: 17}
2645+ }
2646+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2647+ if templ_7745c5c3_Err != nil {
2648+ return templ_7745c5c3_Err
2649+ }
2650+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, " ")
25262651 if templ_7745c5c3_Err != nil {
25272652 return templ_7745c5c3_Err
25282653 }
25292654 if i < 9 {
2530- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<kbd aria-hidden=\"true\">")
2655+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "<kbd aria-hidden=\"true\">")
25312656 if templ_7745c5c3_Err != nil {
25322657 return templ_7745c5c3_Err
25332658 }
2534- var templ_7745c5c3_Var122 string
2535- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
2659+ var templ_7745c5c3_Var127 string
2660+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
25362661 if templ_7745c5c3_Err != nil {
2537- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 46}
2662+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 517, Col: 46}
25382663 }
2539- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2664+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
25402665 if templ_7745c5c3_Err != nil {
25412666 return templ_7745c5c3_Err
25422667 }
2543- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</kbd>")
2668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "</kbd>")
25442669 if templ_7745c5c3_Err != nil {
25452670 return templ_7745c5c3_Err
25462671 }
25472672 }
2548- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</label>")
2673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</label>")
25492674 if templ_7745c5c3_Err != nil {
25502675 return templ_7745c5c3_Err
25512676 }
25522677 }
2553- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</fieldset>")
2678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</fieldset>")
25542679 if templ_7745c5c3_Err != nil {
25552680 return templ_7745c5c3_Err
25562681 }
2682+ if p.Box.Signed {
2683+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "<p class=\"d-note\" id=\"mr-signing\">squash and merge commits are signed with ermite's own key (generated on first start). ")
2684+ if templ_7745c5c3_Err != nil {
2685+ return templ_7745c5c3_Err
2686+ }
2687+ if p.Box.Fingerprint != "" {
2688+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "fingerprint <code>")
2689+ if templ_7745c5c3_Err != nil {
2690+ return templ_7745c5c3_Err
2691+ }
2692+ var templ_7745c5c3_Var128 string
2693+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
2694+ if templ_7745c5c3_Err != nil {
2695+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 526, Col: 45}
2696+ }
2697+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2698+ if templ_7745c5c3_Err != nil {
2699+ return templ_7745c5c3_Err
2700+ }
2701+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</code> ")
2702+ if templ_7745c5c3_Err != nil {
2703+ return templ_7745c5c3_Err
2704+ }
2705+ }
2706+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "<code>ermite signing-key show</code> prints it.</p>")
2707+ if templ_7745c5c3_Err != nil {
2708+ return templ_7745c5c3_Err
2709+ }
2710+ }
25572711 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 ")
2712+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
25592713 if templ_7745c5c3_Err != nil {
25602714 return templ_7745c5c3_Err
25612715 }
2562- var templ_7745c5c3_Var123 string
2563- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2716+ var templ_7745c5c3_Var129 string
2717+ templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
25642718 if templ_7745c5c3_Err != nil {
2565- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 493, Col: 102}
2719+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 102}
25662720 }
2567- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
2721+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var129))
25682722 if templ_7745c5c3_Err != nil {
25692723 return templ_7745c5c3_Err
25702724 }
2571- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2725+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
25722726 if templ_7745c5c3_Err != nil {
25732727 return templ_7745c5c3_Err
25742728 }
25752729 }
2576- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default>Merge into ")
2730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")
25772731 if templ_7745c5c3_Err != nil {
25782732 return templ_7745c5c3_Err
25792733 }
2580- var templ_7745c5c3_Var124 string
2581- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2734+ if !p.Box.CanMerge() {
2735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, " disabled")
2736+ if templ_7745c5c3_Err != nil {
2737+ return templ_7745c5c3_Err
2738+ }
2739+ }
2740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, " title=\"")
2741+ if templ_7745c5c3_Err != nil {
2742+ return templ_7745c5c3_Err
2743+ }
2744+ var templ_7745c5c3_Var130 string
2745+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
25822746 if templ_7745c5c3_Err != nil {
2583- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 82}
2747+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 540, Col: 33}
25842748 }
2585- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
2749+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
25862750 if templ_7745c5c3_Err != nil {
25872751 return templ_7745c5c3_Err
25882752 }
2589- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "</button></div></form>")
2753+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "\" :disabled=\"!this.form.querySelector('input[name=mode]:checked:not(:disabled)')\" :title=\"(this.form.querySelector('input[name=mode]:checked') || {}).title || ''\">Merge into ")
2754+ if templ_7745c5c3_Err != nil {
2755+ return templ_7745c5c3_Err
2756+ }
2757+ var templ_7745c5c3_Var131 string
2758+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2759+ if templ_7745c5c3_Err != nil {
2760+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 28}
2761+ }
2762+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
2763+ if templ_7745c5c3_Err != nil {
2764+ return templ_7745c5c3_Err
2765+ }
2766+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "</button></div></form>")
25902767 if templ_7745c5c3_Err != nil {
25912768 return templ_7745c5c3_Err
25922769 }
25932770 }
2594- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</div></details>")
2771+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "</div></details>")
2772+ if templ_7745c5c3_Err != nil {
2773+ return templ_7745c5c3_Err
2774+ }
2775+ return nil
2776+ })
2777+}
2778+
2779+// MRMergeability is the content of the mergeability slot: the badge, and the
2780+// state the controls of the merge box read.
2781+func MRMergeability(v MergeabilityView) templ.Component {
2782+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2783+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
2784+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
2785+ return templ_7745c5c3_CtxErr
2786+ }
2787+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
2788+ if !templ_7745c5c3_IsBuffer {
2789+ defer func() {
2790+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
2791+ if templ_7745c5c3_Err == nil {
2792+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
2793+ }
2794+ }()
2795+ }
2796+ ctx = templ.InitializeContext(ctx)
2797+ templ_7745c5c3_Var132 := templ.GetChildren(ctx)
2798+ if templ_7745c5c3_Var132 == nil {
2799+ templ_7745c5c3_Var132 = templ.NopComponent
2800+ }
2801+ ctx = templ.ClearChildren(ctx)
2802+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "<span data-mergeability data-state=\"")
2803+ if templ_7745c5c3_Err != nil {
2804+ return templ_7745c5c3_Err
2805+ }
2806+ var templ_7745c5c3_Var133 string
2807+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2808+ if templ_7745c5c3_Err != nil {
2809+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 22}
2810+ }
2811+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var133)
2812+ if templ_7745c5c3_Err != nil {
2813+ return templ_7745c5c3_Err
2814+ }
2815+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\" data-ff-ok=\"")
2816+ if templ_7745c5c3_Err != nil {
2817+ return templ_7745c5c3_Err
2818+ }
2819+ var templ_7745c5c3_Var134 string
2820+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
2821+ if templ_7745c5c3_Err != nil {
2822+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 557, Col: 41}
2823+ }
2824+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var134)
2825+ if templ_7745c5c3_Err != nil {
2826+ return templ_7745c5c3_Err
2827+ }
2828+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\" data-merge-ok=\"")
2829+ if templ_7745c5c3_Err != nil {
2830+ return templ_7745c5c3_Err
2831+ }
2832+ var templ_7745c5c3_Var135 string
2833+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
2834+ if templ_7745c5c3_Err != nil {
2835+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 558, Col: 47}
2836+ }
2837+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
2838+ if templ_7745c5c3_Err != nil {
2839+ return templ_7745c5c3_Err
2840+ }
2841+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\" data-ff-reason=\"")
2842+ if templ_7745c5c3_Err != nil {
2843+ return templ_7745c5c3_Err
2844+ }
2845+ var templ_7745c5c3_Var136 string
2846+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
2847+ if templ_7745c5c3_Err != nil {
2848+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 559, Col: 29}
2849+ }
2850+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136)
2851+ if templ_7745c5c3_Err != nil {
2852+ return templ_7745c5c3_Err
2853+ }
2854+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "\" data-merge-reason=\"")
2855+ if templ_7745c5c3_Err != nil {
2856+ return templ_7745c5c3_Err
2857+ }
2858+ var templ_7745c5c3_Var137 string
2859+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
2860+ if templ_7745c5c3_Err != nil {
2861+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 35}
2862+ }
2863+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137)
2864+ if templ_7745c5c3_Err != nil {
2865+ return templ_7745c5c3_Err
2866+ }
2867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\"><span class=\"badge\" data-state=\"")
2868+ if templ_7745c5c3_Err != nil {
2869+ return templ_7745c5c3_Err
2870+ }
2871+ var templ_7745c5c3_Var138 string
2872+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2873+ if templ_7745c5c3_Err != nil {
2874+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 562, Col: 42}
2875+ }
2876+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2877+ if templ_7745c5c3_Err != nil {
2878+ return templ_7745c5c3_Err
2879+ }
2880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\">")
2881+ if templ_7745c5c3_Err != nil {
2882+ return templ_7745c5c3_Err
2883+ }
2884+ var templ_7745c5c3_Var139 string
2885+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2886+ if templ_7745c5c3_Err != nil {
2887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 562, Col: 54}
2888+ }
2889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
2890+ if templ_7745c5c3_Err != nil {
2891+ return templ_7745c5c3_Err
2892+ }
2893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span> ")
2894+ if templ_7745c5c3_Err != nil {
2895+ return templ_7745c5c3_Err
2896+ }
2897+ if v.Detail != "" {
2898+ var templ_7745c5c3_Var140 string
2899+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2900+ if templ_7745c5c3_Err != nil {
2901+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 8}
2902+ }
2903+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2904+ if templ_7745c5c3_Err != nil {
2905+ return templ_7745c5c3_Err
2906+ }
2907+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<span class=\"dim\">")
2908+ if templ_7745c5c3_Err != nil {
2909+ return templ_7745c5c3_Err
2910+ }
2911+ var templ_7745c5c3_Var141 string
2912+ templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
2913+ if templ_7745c5c3_Err != nil {
2914+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 38}
2915+ }
2916+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
2917+ if templ_7745c5c3_Err != nil {
2918+ return templ_7745c5c3_Err
2919+ }
2920+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</span>")
2921+ if templ_7745c5c3_Err != nil {
2922+ return templ_7745c5c3_Err
2923+ }
2924+ }
2925+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</span> ")
2926+ if templ_7745c5c3_Err != nil {
2927+ return templ_7745c5c3_Err
2928+ }
2929+ if len(v.Paths) > 0 {
2930+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")
2931+ if templ_7745c5c3_Err != nil {
2932+ return templ_7745c5c3_Err
2933+ }
2934+ for _, path := range v.Paths {
2935+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<li><code>")
2936+ if templ_7745c5c3_Err != nil {
2937+ return templ_7745c5c3_Err
2938+ }
2939+ var templ_7745c5c3_Var142 string
2940+ templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(path)
2941+ if templ_7745c5c3_Err != nil {
2942+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 570, Col: 20}
2943+ }
2944+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142))
2945+ if templ_7745c5c3_Err != nil {
2946+ return templ_7745c5c3_Err
2947+ }
2948+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</code></li>")
2949+ if templ_7745c5c3_Err != nil {
2950+ return templ_7745c5c3_Err
2951+ }
2952+ }
2953+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</ul>")
2954+ if templ_7745c5c3_Err != nil {
2955+ return templ_7745c5c3_Err
2956+ }
2957+ }
2958+ if v.PathsNote != "" {
2959+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<p class=\"dim\">")
2960+ if templ_7745c5c3_Err != nil {
2961+ return templ_7745c5c3_Err
2962+ }
2963+ var templ_7745c5c3_Var143 string
2964+ templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
2965+ if templ_7745c5c3_Err != nil {
2966+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 575, Col: 30}
2967+ }
2968+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143))
2969+ if templ_7745c5c3_Err != nil {
2970+ return templ_7745c5c3_Err
2971+ }
2972+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</p>")
2973+ if templ_7745c5c3_Err != nil {
2974+ return templ_7745c5c3_Err
2975+ }
2976+ }
2977+ return nil
2978+ })
2979+}
2980+
2981+// mrMergeabilitySlot is the status region of the badge, with its content: the
2982+// stream replaces the children, never the element (see LiveSlot).
2983+func mrMergeabilitySlot(v MergeabilityView) templ.Component {
2984+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2985+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
2986+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
2987+ return templ_7745c5c3_CtxErr
2988+ }
2989+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
2990+ if !templ_7745c5c3_IsBuffer {
2991+ defer func() {
2992+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
2993+ if templ_7745c5c3_Err == nil {
2994+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
2995+ }
2996+ }()
2997+ }
2998+ ctx = templ.InitializeContext(ctx)
2999+ templ_7745c5c3_Var144 := templ.GetChildren(ctx)
3000+ if templ_7745c5c3_Var144 == nil {
3001+ templ_7745c5c3_Var144 = templ.NopComponent
3002+ }
3003+ ctx = templ.ClearChildren(ctx)
3004+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "<div id=\"mr-mergeability\" class=\"live-slot\" role=\"status\" data-slot>")
3005+ if templ_7745c5c3_Err != nil {
3006+ return templ_7745c5c3_Err
3007+ }
3008+ templ_7745c5c3_Err = MRMergeability(v).Render(ctx, templ_7745c5c3_Buffer)
3009+ if templ_7745c5c3_Err != nil {
3010+ return templ_7745c5c3_Err
3011+ }
3012+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "</div>")
3013+ if templ_7745c5c3_Err != nil {
3014+ return templ_7745c5c3_Err
3015+ }
3016+ return nil
3017+ })
3018+}
3019+
3020+// MRMergeabilityFragment is the slot as the stream sends it.
3021+func MRMergeabilityFragment(v MergeabilityView) templ.Component {
3022+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3023+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
3024+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
3025+ return templ_7745c5c3_CtxErr
3026+ }
3027+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
3028+ if !templ_7745c5c3_IsBuffer {
3029+ defer func() {
3030+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
3031+ if templ_7745c5c3_Err == nil {
3032+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
3033+ }
3034+ }()
3035+ }
3036+ ctx = templ.InitializeContext(ctx)
3037+ templ_7745c5c3_Var145 := templ.GetChildren(ctx)
3038+ if templ_7745c5c3_Var145 == nil {
3039+ templ_7745c5c3_Var145 = templ.NopComponent
3040+ }
3041+ ctx = templ.ClearChildren(ctx)
3042+ templ_7745c5c3_Err = mrMergeabilitySlot(v).Render(ctx, templ_7745c5c3_Buffer)
25953043 if templ_7745c5c3_Err != nil {
25963044 return templ_7745c5c3_Err
25973045 }
@@ -2617,66 +3065,66 @@ func moreMenu(p MRPage) templ.Component {
26173065 }()
26183066 }
26193067 ctx = templ.InitializeContext(ctx)
2620- templ_7745c5c3_Var125 := templ.GetChildren(ctx)
2621- if templ_7745c5c3_Var125 == nil {
2622- templ_7745c5c3_Var125 = templ.NopComponent
3068+ templ_7745c5c3_Var146 := templ.GetChildren(ctx)
3069+ if templ_7745c5c3_Var146 == nil {
3070+ templ_7745c5c3_Var146 = templ.NopComponent
26233071 }
26243072 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\">")
3073+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "<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\">")
26263074 if templ_7745c5c3_Err != nil {
26273075 return templ_7745c5c3_Err
26283076 }
26293077 if p.Box.CanMarkRead {
2630- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<form method=\"post\" action=\"")
3078+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "<form method=\"post\" action=\"")
26313079 if templ_7745c5c3_Err != nil {
26323080 return templ_7745c5c3_Err
26333081 }
2634- var templ_7745c5c3_Var126 templ.SafeURL
2635- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
3082+ var templ_7745c5c3_Var147 templ.SafeURL
3083+ templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
26363084 if templ_7745c5c3_Err != nil {
2637- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 64}
3085+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 599, Col: 64}
26383086 }
2639- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
3087+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
26403088 if templ_7745c5c3_Err != nil {
26413089 return templ_7745c5c3_Err
26423090 }
2643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
3091+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
26443092 if templ_7745c5c3_Err != nil {
26453093 return templ_7745c5c3_Err
26463094 }
2647- var templ_7745c5c3_Var127 string
2648- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
3095+ var templ_7745c5c3_Var148 string
3096+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
26493097 if templ_7745c5c3_Err != nil {
2650- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 59}
3098+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 600, Col: 59}
26513099 }
2652- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127)
3100+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148)
26533101 if templ_7745c5c3_Err != nil {
26543102 return templ_7745c5c3_Err
26553103 }
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>")
3104+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "\"> <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>")
26573105 if templ_7745c5c3_Err != nil {
26583106 return templ_7745c5c3_Err
26593107 }
26603108 }
2661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "<form method=\"post\" action=\"")
3109+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "<form method=\"post\" action=\"")
26623110 if templ_7745c5c3_Err != nil {
26633111 return templ_7745c5c3_Err
26643112 }
2665- var templ_7745c5c3_Var128 templ.SafeURL
2666- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
3113+ var templ_7745c5c3_Var149 templ.SafeURL
3114+ templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
26673115 if templ_7745c5c3_Err != nil {
2668- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 64}
3116+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 604, Col: 64}
26693117 }
2670- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
3118+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))
26713119 if templ_7745c5c3_Err != nil {
26723120 return templ_7745c5c3_Err
26733121 }
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>")
3122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
26753123 if templ_7745c5c3_Err != nil {
26763124 return templ_7745c5c3_Err
26773125 }
26783126 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>")
3127+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
26803128 if templ_7745c5c3_Err != nil {
26813129 return templ_7745c5c3_Err
26823130 }
@@ -2684,12 +3132,12 @@ func moreMenu(p MRPage) templ.Component {
26843132 if templ_7745c5c3_Err != nil {
26853133 return templ_7745c5c3_Err
26863134 }
2687- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</details>")
3135+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "</details>")
26883136 if templ_7745c5c3_Err != nil {
26893137 return templ_7745c5c3_Err
26903138 }
26913139 }
2692- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</div></details>")
3140+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "</div></details>")
26933141 if templ_7745c5c3_Err != nil {
26943142 return templ_7745c5c3_Err
26953143 }
@@ -2713,12 +3161,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
27133161 }()
27143162 }
27153163 ctx = templ.InitializeContext(ctx)
2716- templ_7745c5c3_Var129 := templ.GetChildren(ctx)
2717- if templ_7745c5c3_Var129 == nil {
2718- templ_7745c5c3_Var129 = templ.NopComponent
3164+ templ_7745c5c3_Var150 := templ.GetChildren(ctx)
3165+ if templ_7745c5c3_Var150 == nil {
3166+ templ_7745c5c3_Var150 = templ.NopComponent
27193167 }
27203168 ctx = templ.ClearChildren(ctx)
2721- templ_7745c5c3_Var130 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3169+ templ_7745c5c3_Var151 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
27223170 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
27233171 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
27243172 if !templ_7745c5c3_IsBuffer {
@@ -2730,263 +3178,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
27303178 }()
27313179 }
27323180 ctx = templ.InitializeContext(ctx)
2733- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "<header class=\"show mr-h\"><h1>")
3181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "<header class=\"show mr-h\"><h1>")
27343182 if templ_7745c5c3_Err != nil {
27353183 return templ_7745c5c3_Err
27363184 }
2737- var templ_7745c5c3_Var131 string
2738- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3185+ var templ_7745c5c3_Var152 string
3186+ templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
27393187 if templ_7745c5c3_Err != nil {
2740- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 17}
3188+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 620, Col: 17}
27413189 }
2742- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
3190+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
27433191 if templ_7745c5c3_Err != nil {
27443192 return templ_7745c5c3_Err
27453193 }
2746- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, " → ")
3194+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, " → ")
27473195 if templ_7745c5c3_Err != nil {
27483196 return templ_7745c5c3_Err
27493197 }
2750- var templ_7745c5c3_Var132 string
2751- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3198+ var templ_7745c5c3_Var153 string
3199+ templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
27523200 if templ_7745c5c3_Err != nil {
2753- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 34}
3201+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 620, Col: 34}
27543202 }
2755- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
3203+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
27563204 if templ_7745c5c3_Err != nil {
27573205 return templ_7745c5c3_Err
27583206 }
2759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
3207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
27603208 if templ_7745c5c3_Err != nil {
27613209 return templ_7745c5c3_Err
27623210 }
2763- var templ_7745c5c3_Var133 templ.SafeURL
2764- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
3211+ var templ_7745c5c3_Var154 templ.SafeURL
3212+ templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
27653213 if templ_7745c5c3_Err != nil {
2766- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 535, Col: 45}
3214+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 623, Col: 45}
27673215 }
2768- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
3216+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
27693217 if templ_7745c5c3_Err != nil {
27703218 return templ_7745c5c3_Err
27713219 }
2772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "\">")
3220+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "\">")
27733221 if templ_7745c5c3_Err != nil {
27743222 return templ_7745c5c3_Err
27753223 }
2776- var templ_7745c5c3_Var134 string
2777- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3224+ var templ_7745c5c3_Var155 string
3225+ templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
27783226 if templ_7745c5c3_Err != nil {
2779- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 535, Col: 58}
3227+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 623, Col: 58}
27803228 }
2781- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
3229+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
27823230 if templ_7745c5c3_Err != nil {
27833231 return templ_7745c5c3_Err
27843232 }
2785- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "</a></dd><dt>target</dt><dd><a href=\"")
3233+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</a></dd><dt>target</dt><dd><a href=\"")
27863234 if templ_7745c5c3_Err != nil {
27873235 return templ_7745c5c3_Err
27883236 }
2789- var templ_7745c5c3_Var135 templ.SafeURL
2790- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
3237+ var templ_7745c5c3_Var156 templ.SafeURL
3238+ templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
27913239 if templ_7745c5c3_Err != nil {
2792- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 537, Col: 45}
3240+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 625, Col: 45}
27933241 }
2794- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))
3242+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
27953243 if templ_7745c5c3_Err != nil {
27963244 return templ_7745c5c3_Err
27973245 }
2798- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "\">")
3246+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "\">")
27993247 if templ_7745c5c3_Err != nil {
28003248 return templ_7745c5c3_Err
28013249 }
2802- var templ_7745c5c3_Var136 string
2803- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3250+ var templ_7745c5c3_Var157 string
3251+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
28043252 if templ_7745c5c3_Err != nil {
2805- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 537, Col: 58}
3253+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 625, Col: 58}
28063254 }
2807- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
3255+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
28083256 if templ_7745c5c3_Err != nil {
28093257 return templ_7745c5c3_Err
28103258 }
2811- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "</a></dd><dt>ahead</dt><dd>")
3259+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "</a></dd><dt>ahead</dt><dd>")
28123260 if templ_7745c5c3_Err != nil {
28133261 return templ_7745c5c3_Err
28143262 }
2815- var templ_7745c5c3_Var137 string
2816- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
3263+ var templ_7745c5c3_Var158 string
3264+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
28173265 if templ_7745c5c3_Err != nil {
2818- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 539, Col: 21}
3266+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 627, Col: 21}
28193267 }
2820- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
3268+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
28213269 if templ_7745c5c3_Err != nil {
28223270 return templ_7745c5c3_Err
28233271 }
2824- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</dd></dl></header>")
3272+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "</dd></dl></header>")
28253273 if templ_7745c5c3_Err != nil {
28263274 return templ_7745c5c3_Err
28273275 }
28283276 if p.Note != "" {
2829- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "<p class=\"notice\">")
3277+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "<p class=\"notice\">")
28303278 if templ_7745c5c3_Err != nil {
28313279 return templ_7745c5c3_Err
28323280 }
2833- var templ_7745c5c3_Var138 string
2834- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
3281+ var templ_7745c5c3_Var159 string
3282+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
28353283 if templ_7745c5c3_Err != nil {
2836- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 29}
3284+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 631, Col: 29}
28373285 }
2838- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
3286+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
28393287 if templ_7745c5c3_Err != nil {
28403288 return templ_7745c5c3_Err
28413289 }
2842- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</p>")
3290+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "</p>")
28433291 if templ_7745c5c3_Err != nil {
28443292 return templ_7745c5c3_Err
28453293 }
28463294 }
2847- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, " ")
3295+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, " ")
28483296 if templ_7745c5c3_Err != nil {
28493297 return templ_7745c5c3_Err
28503298 }
28513299 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=\"")
3300+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
28533301 if templ_7745c5c3_Err != nil {
28543302 return templ_7745c5c3_Err
28553303 }
2856- var templ_7745c5c3_Var139 templ.SafeURL
2857- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
3304+ var templ_7745c5c3_Var160 templ.SafeURL
3305+ templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
28583306 if templ_7745c5c3_Err != nil {
2859- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 114}
3307+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 634, Col: 114}
28603308 }
2861- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
3309+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
28623310 if templ_7745c5c3_Err != nil {
28633311 return templ_7745c5c3_Err
28643312 }
2865- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\">")
3313+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "\">")
28663314 if templ_7745c5c3_Err != nil {
28673315 return templ_7745c5c3_Err
28683316 }
2869- var templ_7745c5c3_Var140 string
2870- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
3317+ var templ_7745c5c3_Var161 string
3318+ templ_7745c5c3_Var161, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
28713319 if templ_7745c5c3_Err != nil {
2872- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 135}
3320+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 634, Col: 135}
28733321 }
2874- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
3322+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var161))
28753323 if templ_7745c5c3_Err != nil {
28763324 return templ_7745c5c3_Err
28773325 }
2878- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "</a></p>")
3326+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "</a></p>")
28793327 if templ_7745c5c3_Err != nil {
28803328 return templ_7745c5c3_Err
28813329 }
28823330 }
2883- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, " ")
3331+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, " ")
28843332 if templ_7745c5c3_Err != nil {
28853333 return templ_7745c5c3_Err
28863334 }
28873335 if p.Create != nil {
2888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "<form class=\"cform create\" method=\"post\" action=\"")
3336+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "<form class=\"cform create\" method=\"post\" action=\"")
28893337 if templ_7745c5c3_Err != nil {
28903338 return templ_7745c5c3_Err
28913339 }
2892- var templ_7745c5c3_Var141 templ.SafeURL
2893- templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
3340+ var templ_7745c5c3_Var162 templ.SafeURL
3341+ templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
28943342 if templ_7745c5c3_Err != nil {
2895- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 549, Col: 83}
3343+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 637, Col: 83}
28963344 }
2897- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
3345+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var162))
28983346 if templ_7745c5c3_Err != nil {
28993347 return templ_7745c5c3_Err
29003348 }
2901- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "\"><input type=\"hidden\" name=\"source\" value=\"")
3349+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, "\"><input type=\"hidden\" name=\"source\" value=\"")
29023350 if templ_7745c5c3_Err != nil {
29033351 return templ_7745c5c3_Err
29043352 }
2905- var templ_7745c5c3_Var142 string
2906- templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
3353+ var templ_7745c5c3_Var163 string
3354+ templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
29073355 if templ_7745c5c3_Err != nil {
2908- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 550, Col: 62}
3356+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 638, Col: 62}
29093357 }
2910- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
3358+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var163)
29113359 if templ_7745c5c3_Err != nil {
29123360 return templ_7745c5c3_Err
29133361 }
2914- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\"> <input type=\"hidden\" name=\"target\" value=\"")
3362+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "\"> <input type=\"hidden\" name=\"target\" value=\"")
29153363 if templ_7745c5c3_Err != nil {
29163364 return templ_7745c5c3_Err
29173365 }
2918- var templ_7745c5c3_Var143 string
2919- templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
3366+ var templ_7745c5c3_Var164 string
3367+ templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
29203368 if templ_7745c5c3_Err != nil {
2921- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 551, Col: 62}
3369+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 639, Col: 62}
29223370 }
2923- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
3371+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var164)
29243372 if templ_7745c5c3_Err != nil {
29253373 return templ_7745c5c3_Err
29263374 }
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=\"")
3375+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 311, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
29283376 if templ_7745c5c3_Err != nil {
29293377 return templ_7745c5c3_Err
29303378 }
2931- var templ_7745c5c3_Var144 string
2932- templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
3379+ var templ_7745c5c3_Var165 string
3380+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
29333381 if templ_7745c5c3_Err != nil {
2934- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 553, Col: 97}
3382+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 641, Col: 97}
29353383 }
2936- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
3384+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var165)
29373385 if templ_7745c5c3_Err != nil {
29383386 return templ_7745c5c3_Err
29393387 }
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>")
3388+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 312, "\" 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>")
29413389 if templ_7745c5c3_Err != nil {
29423390 return templ_7745c5c3_Err
29433391 }
29443392 }
2945- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, " ")
3393+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, " ")
29463394 if templ_7745c5c3_Err != nil {
29473395 return templ_7745c5c3_Err
29483396 }
29493397 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\">")
3398+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
29513399 if templ_7745c5c3_Err != nil {
29523400 return templ_7745c5c3_Err
29533401 }
2954- var templ_7745c5c3_Var145 string
2955- templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
3402+ var templ_7745c5c3_Var166 string
3403+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
29563404 if templ_7745c5c3_Err != nil {
2957- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 57}
3405+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 651, Col: 57}
29583406 }
2959- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
3407+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
29603408 if templ_7745c5c3_Err != nil {
29613409 return templ_7745c5c3_Err
29623410 }
2963- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, " · <span class=\"plus\">+")
3411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, " · <span class=\"plus\">+")
29643412 if templ_7745c5c3_Err != nil {
29653413 return templ_7745c5c3_Err
29663414 }
2967- var templ_7745c5c3_Var146 string
2968- templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
3415+ var templ_7745c5c3_Var167 string
3416+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
29693417 if templ_7745c5c3_Err != nil {
2970- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 97}
3418+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 651, Col: 97}
29713419 }
2972- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
3420+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
29733421 if templ_7745c5c3_Err != nil {
29743422 return templ_7745c5c3_Err
29753423 }
2976- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</span> <span class=\"minus\">-")
3424+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "</span> <span class=\"minus\">-")
29773425 if templ_7745c5c3_Err != nil {
29783426 return templ_7745c5c3_Err
29793427 }
2980- var templ_7745c5c3_Var147 string
2981- templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
3428+ var templ_7745c5c3_Var168 string
3429+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
29823430 if templ_7745c5c3_Err != nil {
2983- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 142}
3431+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 651, Col: 142}
29843432 }
2985- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
3433+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
29863434 if templ_7745c5c3_Err != nil {
29873435 return templ_7745c5c3_Err
29883436 }
2989- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "</span></span> <span class=\"grow\"></span>")
3437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "</span></span> <span class=\"grow\"></span>")
29903438 if templ_7745c5c3_Err != nil {
29913439 return templ_7745c5c3_Err
29923440 }
@@ -2994,64 +3442,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
29943442 if templ_7745c5c3_Err != nil {
29953443 return templ_7745c5c3_Err
29963444 }
2997- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</div>")
3445+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, "</div>")
29983446 if templ_7745c5c3_Err != nil {
29993447 return templ_7745c5c3_Err
30003448 }
30013449 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>")
3450+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 319, "<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>")
30033451 if templ_7745c5c3_Err != nil {
30043452 return templ_7745c5c3_Err
30053453 }
30063454 }
3007- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, " <div id=\"diffs\"")
3455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, " <div id=\"diffs\"")
30083456 if templ_7745c5c3_Err != nil {
30093457 return templ_7745c5c3_Err
30103458 }
30113459 if p.Both {
3012- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, " data-diff=\"")
3460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " data-diff=\"")
30133461 if templ_7745c5c3_Err != nil {
30143462 return templ_7745c5c3_Err
30153463 }
3016- var templ_7745c5c3_Var148 string
3017- templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3464+ var templ_7745c5c3_Var169 string
3465+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
30183466 if templ_7745c5c3_Err != nil {
3019- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 576, Col: 23}
3467+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 664, Col: 23}
30203468 }
3021- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148)
3469+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var169)
30223470 if templ_7745c5c3_Err != nil {
30233471 return templ_7745c5c3_Err
30243472 }
3025- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "\"")
3473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "\"")
30263474 if templ_7745c5c3_Err != nil {
30273475 return templ_7745c5c3_Err
30283476 }
30293477 } else {
3030- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, " data-view=\"")
3478+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, " data-view=\"")
30313479 if templ_7745c5c3_Err != nil {
30323480 return templ_7745c5c3_Err
30333481 }
3034- var templ_7745c5c3_Var149 string
3035- templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3482+ var templ_7745c5c3_Var170 string
3483+ templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
30363484 if templ_7745c5c3_Err != nil {
3037- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 23}
3485+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 666, Col: 23}
30383486 }
3039- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149)
3487+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var170)
30403488 if templ_7745c5c3_Err != nil {
30413489 return templ_7745c5c3_Err
30423490 }
3043- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "\"")
3491+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, "\"")
30443492 if templ_7745c5c3_Err != nil {
30453493 return templ_7745c5c3_Err
30463494 }
30473495 }
30483496 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\"")
3497+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
30503498 if templ_7745c5c3_Err != nil {
30513499 return templ_7745c5c3_Err
30523500 }
30533501 }
3054- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, ">")
3502+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, ">")
30553503 if templ_7745c5c3_Err != nil {
30563504 return templ_7745c5c3_Err
30573505 }
@@ -3061,14 +3509,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
30613509 return templ_7745c5c3_Err
30623510 }
30633511 }
3064- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "</div>")
3512+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, "</div>")
30653513 if templ_7745c5c3_Err != nil {
30663514 return templ_7745c5c3_Err
30673515 }
30683516 }
30693517 return nil
30703518 })
3071- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var130), templ_7745c5c3_Buffer)
3519+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var151), templ_7745c5c3_Buffer)
30723520 if templ_7745c5c3_Err != nil {
30733521 return templ_7745c5c3_Err
30743522 }
@@ -3092,173 +3540,173 @@ func compareRail(p ComparePage) templ.Component {
30923540 }()
30933541 }
30943542 ctx = templ.InitializeContext(ctx)
3095- templ_7745c5c3_Var150 := templ.GetChildren(ctx)
3096- if templ_7745c5c3_Var150 == nil {
3097- templ_7745c5c3_Var150 = templ.NopComponent
3543+ templ_7745c5c3_Var171 := templ.GetChildren(ctx)
3544+ if templ_7745c5c3_Var171 == nil {
3545+ templ_7745c5c3_Var171 = templ.NopComponent
30983546 }
30993547 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\">")
3548+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
31013549 if templ_7745c5c3_Err != nil {
31023550 return templ_7745c5c3_Err
31033551 }
3104- var templ_7745c5c3_Var151 string
3105- templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
3552+ var templ_7745c5c3_Var172 string
3553+ templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
31063554 if templ_7745c5c3_Err != nil {
3107- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 67}
3555+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 67}
31083556 }
3109- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
3557+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
31103558 if templ_7745c5c3_Err != nil {
31113559 return templ_7745c5c3_Err
31123560 }
3113- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "</span></h2><ul class=\"pcommits\">")
3561+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, "</span></h2><ul class=\"pcommits\">")
31143562 if templ_7745c5c3_Err != nil {
31153563 return templ_7745c5c3_Err
31163564 }
31173565 for _, c := range p.Ahead {
3118- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, "<li><a href=\"")
3566+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, "<li><a href=\"")
31193567 if templ_7745c5c3_Err != nil {
31203568 return templ_7745c5c3_Err
31213569 }
3122- var templ_7745c5c3_Var152 templ.SafeURL
3123- templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
3570+ var templ_7745c5c3_Var173 templ.SafeURL
3571+ templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
31243572 if templ_7745c5c3_Err != nil {
3125- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 37}
3573+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 686, Col: 37}
31263574 }
3127- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
3575+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
31283576 if templ_7745c5c3_Err != nil {
31293577 return templ_7745c5c3_Err
31303578 }
3131- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "\">")
3579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, "\">")
31323580 if templ_7745c5c3_Err != nil {
31333581 return templ_7745c5c3_Err
31343582 }
3135- var templ_7745c5c3_Var153 string
3136- templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
3583+ var templ_7745c5c3_Var174 string
3584+ templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
31373585 if templ_7745c5c3_Err != nil {
3138- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 49}
3586+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 686, Col: 49}
31393587 }
3140- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
3588+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var174))
31413589 if templ_7745c5c3_Err != nil {
31423590 return templ_7745c5c3_Err
31433591 }
3144- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "</a> <span class=\"s\">")
3592+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, "</a> <span class=\"s\">")
31453593 if templ_7745c5c3_Err != nil {
31463594 return templ_7745c5c3_Err
31473595 }
3148- var templ_7745c5c3_Var154 string
3149- templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
3596+ var templ_7745c5c3_Var175 string
3597+ templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
31503598 if templ_7745c5c3_Err != nil {
3151- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 599, Col: 32}
3599+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 687, Col: 32}
31523600 }
3153- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
3601+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var175))
31543602 if templ_7745c5c3_Err != nil {
31553603 return templ_7745c5c3_Err
31563604 }
3157- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "</span> <span class=\"mute\">")
3605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "</span> <span class=\"mute\">")
31583606 if templ_7745c5c3_Err != nil {
31593607 return templ_7745c5c3_Err
31603608 }
3161- var templ_7745c5c3_Var155 string
3162- templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
3609+ var templ_7745c5c3_Var176 string
3610+ templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
31633611 if templ_7745c5c3_Err != nil {
3164- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 16}
3612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 689, Col: 16}
31653613 }
3166- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
3614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var176))
31673615 if templ_7745c5c3_Err != nil {
31683616 return templ_7745c5c3_Err
31693617 }
3170- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, " ")
3618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, " ")
31713619 if templ_7745c5c3_Err != nil {
31723620 return templ_7745c5c3_Err
31733621 }
31743622 if c.WhenAt != "" {
3175- var templ_7745c5c3_Var156 string
3176- templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
3623+ var templ_7745c5c3_Var177 string
3624+ templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
31773625 if templ_7745c5c3_Err != nil {
3178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 15}
3626+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 15}
31793627 }
3180- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
3628+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))
31813629 if templ_7745c5c3_Err != nil {
31823630 return templ_7745c5c3_Err
31833631 }
3184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "<time datetime=\"")
3632+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "<time datetime=\"")
31853633 if templ_7745c5c3_Err != nil {
31863634 return templ_7745c5c3_Err
31873635 }
3188- var templ_7745c5c3_Var157 string
3189- templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
3636+ var templ_7745c5c3_Var178 string
3637+ templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
31903638 if templ_7745c5c3_Err != nil {
3191- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 42}
3639+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 42}
31923640 }
3193- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var157)
3641+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var178)
31943642 if templ_7745c5c3_Err != nil {
31953643 return templ_7745c5c3_Err
31963644 }
3197- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "\">")
3645+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "\">")
31983646 if templ_7745c5c3_Err != nil {
31993647 return templ_7745c5c3_Err
32003648 }
3201- var templ_7745c5c3_Var158 string
3202- templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
3649+ var templ_7745c5c3_Var179 string
3650+ templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
32033651 if templ_7745c5c3_Err != nil {
3204- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 603, Col: 53}
3652+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 53}
32053653 }
3206- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
3654+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
32073655 if templ_7745c5c3_Err != nil {
32083656 return templ_7745c5c3_Err
32093657 }
3210- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</time>")
3658+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "</time>")
32113659 if templ_7745c5c3_Err != nil {
32123660 return templ_7745c5c3_Err
32133661 }
32143662 }
3215- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</span></li>")
3663+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "</span></li>")
32163664 if templ_7745c5c3_Err != nil {
32173665 return templ_7745c5c3_Err
32183666 }
32193667 }
3220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "</ul>")
3668+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, "</ul>")
32213669 if templ_7745c5c3_Err != nil {
32223670 return templ_7745c5c3_Err
32233671 }
32243672 if len(p.Ahead) == 0 {
3225- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "<p class=\"dim\">no commit ahead of ")
3673+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, "<p class=\"dim\">no commit ahead of ")
32263674 if templ_7745c5c3_Err != nil {
32273675 return templ_7745c5c3_Err
32283676 }
3229- var templ_7745c5c3_Var159 string
3230- templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3677+ var templ_7745c5c3_Var180 string
3678+ templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
32313679 if templ_7745c5c3_Err != nil {
3232- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 610, Col: 47}
3680+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 698, Col: 47}
32333681 }
3234- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
3682+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var180))
32353683 if templ_7745c5c3_Err != nil {
32363684 return templ_7745c5c3_Err
32373685 }
3238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, ".</p>")
3686+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, ".</p>")
32393687 if templ_7745c5c3_Err != nil {
32403688 return templ_7745c5c3_Err
32413689 }
32423690 }
3243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "</section>")
3691+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, "</section>")
32443692 if templ_7745c5c3_Err != nil {
32453693 return templ_7745c5c3_Err
32463694 }
32473695 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\">")
3696+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
32493697 if templ_7745c5c3_Err != nil {
32503698 return templ_7745c5c3_Err
32513699 }
3252- var templ_7745c5c3_Var160 string
3253- templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
3700+ var templ_7745c5c3_Var181 string
3701+ templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
32543702 if templ_7745c5c3_Err != nil {
3255- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 615, Col: 66}
3703+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 703, Col: 66}
32563704 }
3257- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
3705+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var181))
32583706 if templ_7745c5c3_Err != nil {
32593707 return templ_7745c5c3_Err
32603708 }
3261- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "</span></h2>")
3709+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "</span></h2>")
32623710 if templ_7745c5c3_Err != nil {
32633711 return templ_7745c5c3_Err
32643712 }
@@ -3266,12 +3714,12 @@ func compareRail(p ComparePage) templ.Component {
32663714 if templ_7745c5c3_Err != nil {
32673715 return templ_7745c5c3_Err
32683716 }
3269- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "</section>")
3717+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</section>")
32703718 if templ_7745c5c3_Err != nil {
32713719 return templ_7745c5c3_Err
32723720 }
32733721 }
3274- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
3722+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 346, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
32753723 if templ_7745c5c3_Err != nil {
32763724 return templ_7745c5c3_Err
32773725 }
@@ -3279,7 +3727,7 @@ func compareRail(p ComparePage) templ.Component {
32793727 if templ_7745c5c3_Err != nil {
32803728 return templ_7745c5c3_Err
32813729 }
3282- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "</section>")
3730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 347, "</section>")
32833731 if templ_7745c5c3_Err != nil {
32843732 return templ_7745c5c3_Err
32853733 }
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 +21 -0
@@ -65,6 +65,12 @@
6565 & .menu-pop .btn kbd { margin-left: auto; }
6666 }
6767 .mmenu {
68+ & .menu-pop { box-sizing: border-box; min-width: 0; overflow-wrap: anywhere; }
69+ & .menu-pop > *, & form, & .d-modes, & .d-btns { min-width: 0; max-width: 100%; box-sizing: border-box; }
70+ & .d-acts { display: grid; gap: var(--s3); }
71+ & .d-modes label, & .d-check { min-width: 0; }
72+ & code { overflow-wrap: anywhere; }
73+ & .d-btns .btn { min-width: 0; max-width: 100%; white-space: normal; }
6874 & .d-modes { display: grid; gap: var(--s1); border: 0; padding: 0; margin: 0; }
6975 & .d-modes label, & .d-check {
7076 display: flex; gap: var(--s2); align-items: center; padding: 4px var(--s2); margin: 0 calc(-1 * var(--s2));
@@ -78,7 +84,22 @@
7884 & .d-btns { display: flex; margin-top: var(--s1); }
7985 & .d-btns .btn { flex: 1; justify-content: center; }
8086 & .d-draft { color: var(--ink-dim); }
87+ & .d-note { margin: 0; color: var(--ink-dim); }
88+ & .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; }
89+ /* the badge is a live slot, but not a banner */
90+ & #mr-mergeability:not(:empty) { padding: 0; font-weight: normal; background: none; border: 0; display: grid; gap: var(--s1); }
91+ & .mg-paths { margin: 0; padding-left: 2ch; max-height: 8em; overflow: auto; }
92+ & .mg-paths code { overflow-wrap: anywhere; }
93+ & .d-modes label:has(:disabled) { color: var(--ink-mute); cursor: not-allowed; }
94+ & .d-modes label:has(:disabled):hover { background: none; }
95+ & .btn:disabled { opacity: .5; cursor: not-allowed; }
8196 }
97+ .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); }
98+ .badge[data-state="up_to_date"] { border-color: var(--teal); color: var(--teal); }
99+ .badge[data-state="behind"] { border-color: var(--teal-2); }
100+ .badge[data-state="conflict"] { border-color: var(--red); color: var(--red); }
101+ .badge[data-state="checking"] { color: var(--ink-dim); border-style: dashed; }
102+ .note[role="status"] { color: var(--ink-dim); }
82103 .omenu {
83104 & > summary { font-size: 18px; line-height: 1; }
84105 & .menu-pop { width: 280px; }
M internal/adapters/web/static/js/mr.js +10 -1
@@ -213,7 +213,12 @@ 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+ const r = radios[act.mode];
217+ // A change event lets the live bindings enable the Merge button again.
218+ if (!r.disabled && !r.checked) {
219+ r.checked = true;
220+ r.dispatchEvent(new Event('change', { bubbles: true }));
221+ }
217222 }
218223 $('[data-default]', m)?.focus();
219224 return true;
@@ -285,6 +290,10 @@ window.ermiteMR = (() => {
285290 let folded = false;
286291 try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ }
287292 if (folded) foldFiles(true);
293+ // The page answers a refused action from the address of the action: show
294+ // the address of the request, so that a reload does not post again.
295+ const canonical = $('#diffs[data-canonical]')?.dataset.canonical;
296+ if (canonical && location.pathname !== canonical) history.replaceState(history.state, '', canonical + location.hash);
288297 watch();
289298 // A link to a file or a comment that is not loaded yet.
290299 const hash = () => {
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/api_helpers_test.go +14 -1
@@ -167,8 +167,21 @@ func (h *harness) watch(c *clients, resume string, topics ...string) *stream {
167167 return s
168168 }
169169
170-// next returns the next event or fails after d.
170+// next returns the next event or fails after d. Mergeability events, which
171+// arrive asynchronously after a push or an opening, are skipped: use
172+// nextAny to see them.
171173 func (s *stream) next(t *testing.T, d time.Duration) *pb.Event {
174+ t.Helper()
175+ for {
176+ ev := s.nextAny(t, d)
177+ if ev.GetMergeRequestMergeability() == nil {
178+ return ev
179+ }
180+ }
181+}
182+
183+// nextAny returns the next event, whatever its kind, or fails after d.
184+func (s *stream) nextAny(t *testing.T, d time.Duration) *pb.Event {
172185 t.Helper()
173186 select {
174187 case ev, ok := <-s.events:
M internal/app/api_test.go +1 -1
@@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) {
196196 for _, who := range allPrincipals {
197197 resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{}))
198198 require.NoError(t, err, who)
199- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, resp.Msg.GetFeatures(), who)
199+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, resp.Msg.GetFeatures(), who)
200200 assert.NotEmpty(t, resp.Msg.GetVersion())
201201 }
202202 })
M internal/app/ci_schedule_trust_test.go +6 -2
@@ -3,6 +3,7 @@
33 package app
44
55 import (
6+ "errors"
67 "strings"
78 "testing"
89 "time"
@@ -230,8 +231,11 @@ func TestMergeRecordsTheTargetAndDropsTheSourceRecord(t *testing.T) {
230231 }))
231232 require.NoError(t, err)
232233
233- _, err = tips.GetRefTip(c.ctx, c.name("proj"), "refs/heads/feat")
234- assert.ErrorIs(t, err, domain.ErrNotFound, "the deleted source branch has no record")
234+ // The deletion and the removal of the record run after the merge returned.
235+ c.waitFor("the deleted source branch has no record", func() bool {
236+ _, err := tips.GetRefTip(c.ctx, c.name("proj"), "refs/heads/feat")
237+ return errors.Is(err, domain.ErrNotFound)
238+ })
235239 main, err := tips.GetRefTip(c.ctx, c.name("proj"), "refs/heads/main")
236240 require.NoError(t, err)
237241 assert.Equal(t, c.ref("refs/heads/main"), main.Sha, "the merge recorded the new tip")
M internal/app/cli_e2e_test.go +14 -2
@@ -123,8 +123,20 @@ func (c *cliHarness) startWatch(t *testing.T, secret string, args ...string) *wa
123123 return w
124124 }
125125
126-// next decodes the next event line or fails after d.
126+// next returns the next event line or fails after d. Mergeability events,
127+// which arrive asynchronously after a push or an opening, are skipped.
127128 func (w *watchProc) next(t *testing.T, d time.Duration) (raw string) {
129+ t.Helper()
130+ for {
131+ l := w.nextAny(t, d)
132+ if !strings.Contains(l, `"type":"`+eventMRMergeability+`"`) {
133+ return l
134+ }
135+ }
136+}
137+
138+// nextAny returns the next event line, whatever its kind, or fails after d.
139+func (w *watchProc) nextAny(t *testing.T, d time.Duration) (raw string) {
128140 t.Helper()
129141 select {
130142 case l, ok := <-w.lines:
@@ -338,7 +350,7 @@ func TestAC6CLIEndToEnd(t *testing.T) {
338350 ver := decode[versionOut](t, res.stdout)
339351 assert.Equal(t, c.url, ver.Server.URL)
340352 assert.NotEmpty(t, ver.Server.Version)
341- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, ver.Server.Features)
353+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures"}, ver.Server.Features)
342354 assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures)
343355 stdout, stderr, code = c.bin(t, secret, "version")
344356 require.Equal(t, 0, code, stderr)
M internal/app/cli_mr.go +150 -24
@@ -8,6 +8,7 @@ import (
88 "fmt"
99 "io"
1010 "net/url"
11+ "slices"
1112 "strconv"
1213 "strings"
1314
@@ -31,6 +32,7 @@ const (
3132
3233 modeFastForward = "fast-forward"
3334 modeSquash = "squash"
35+ modeMerge = "merge"
3436 )
3537
3638 func stateName(s ermite.MergeRequestState) string {
@@ -65,6 +67,8 @@ func modeName(m ermite.MergeMode) string {
6567 return modeFastForward
6668 case ermite.MergeMode_MERGE_MODE_SQUASH:
6769 return modeSquash
70+ case ermite.MergeMode_MERGE_MODE_MERGE_COMMIT:
71+ return modeMerge
6872 }
6973 return ""
7074 }
@@ -75,8 +79,10 @@ func parseMode(s string) (ermite.MergeMode, error) {
7579 return ermite.MergeMode_MERGE_MODE_FAST_FORWARD, nil
7680 case modeSquash:
7781 return ermite.MergeMode_MERGE_MODE_SQUASH, nil
82+ case modeMerge:
83+ return ermite.MergeMode_MERGE_MODE_MERGE_COMMIT, nil
7884 }
79- return 0, fmt.Errorf("invalid merge mode %q: use %s or %s", s, modeFastForward, modeSquash)
85+ return 0, fmt.Errorf("invalid merge mode %q: use %s, %s or %s", s, modeFastForward, modeSquash, modeMerge)
8086 }
8187
8288 func sideName(s ermite.DiffSide) string {
@@ -117,6 +123,71 @@ type jsonMergeRequest struct {
117123 MergeMode string `json:"merge_mode"`
118124 }
119125
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+
120191 // jsonMerged is the output of `mr merge --json`.
121192 type jsonMerged struct {
122193 jsonMergeRequest
@@ -173,9 +244,15 @@ const jsonMRHelp = `
173244 JSON fields of a merge request (--json): repo, url (full web URL), number, title, description, source_branch,
174245 target_branch, state ("open", "merged" or "closed"), author (token label), created_at and
175246 updated_at (RFC 3339 UTC), head_id (the commit the source branch points to; empty when the
176-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
177248 unless merged).`
178249
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+
179256 const jsonCommentHelp = `
180257 JSON fields of a comment: id, author, path, line and side ("old" or "new"; empty path, line 0
181258 and empty side for a general comment), commit_id (the head when it was written), text,
@@ -412,6 +489,8 @@ type jsonCommit struct {
412489 // jsonMRDetail is the stable JSON shape of `mr show --json`.
413490 type jsonMRDetail struct {
414491 jsonMergeRequest
492+ // Mergeability is null unless the request is open and the server computes it.
493+ Mergeability *jsonMergeability `json:"mergeability"`
415494 Commits []jsonCommit `json:"commits"`
416495 CommitsTruncated bool `json:"commits_truncated"`
417496 DiffBaseID string `json:"diff_base_id"`
@@ -444,6 +523,7 @@ func lineKindName(k ermite.LineKind) string {
444523 func toJSONDetail(d *ermite.GetMergeRequestResponse, withDiff bool, baseURL string) jsonMRDetail {
445524 out := jsonMRDetail{
446525 jsonMergeRequest: toJSONMergeRequest(d.GetMergeRequest(), baseURL),
526+ Mergeability: toJSONMergeability(d.GetMergeRequest().GetMergeability()),
447527 Commits: []jsonCommit{}, CommitsTruncated: d.GetCommitsTruncated(),
448528 DiffBaseID: d.GetDiffBaseId(),
449529 Stats: jsonStats{
@@ -496,7 +576,9 @@ func newMRShowCmd(env *cliEnv) *cobra.Command {
496576 "--since-last-read shows the diff since the head recorded by --mark-read (or by the web) instead of\n" +
497577 "the whole change; after a force-push it still works when the recorded commit exists, else the\n" +
498578 "whole change is shown and the output says so. --mark-read records the head shown as read; it needs\n" +
499- "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" +
500582 "--json adds to the merge request fields: commits (id, author, summary), commits_truncated,\n" +
501583 "diff_base_id (the commit the diff starts from), stats (files_changed, additions, deletions),\n" +
502584 "diff_truncated, files (old_path, new_path, status, additions, deletions, binary, truncated and,\n" +
@@ -569,6 +651,9 @@ func printMRDetail(w io.Writer, d *ermite.GetMergeRequestResponse, withDiff, sin
569651 if m.GetState() == ermite.MergeRequestState_MERGE_REQUEST_STATE_MERGED {
570652 pairs = append(pairs, [2]string{"merged", orDash(m.GetMergedCommitId()) + " (" + modeName(m.GetMergeMode()) + ")"})
571653 }
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+ }
572657 if len(d.GetVerdicts()) > 0 {
573658 var verdicts []string
574659 for _, v := range d.GetVerdicts() {
@@ -957,36 +1042,52 @@ func newMRResolveCmd(env *cliEnv) *cobra.Command {
9571042 }
9581043
9591044 func newMRMergeCmd(env *cliEnv) *cobra.Command {
960- var squash, deleteBranch bool
1045+ var mode string
1046+ var deleteBranch bool
9611047 var head string
9621048 cmd := &cobra.Command{
9631049 Use: "merge <repo> <number>",
9641050 Short: "Merge a merge request",
965- Long: "Merge an open merge request. The default is a fast-forward: the target branch moves to the head\n" +
966- "and no commit is created. --squash makes one new commit with the tree of the head and the target\n" +
967- "as only parent, signed by the server's key (see `ermite signing-key show`). The target must be an\n" +
968- "ancestor of the head, else rebase the branch and push it again; the repository settings may allow\n" +
969- "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" +
9701063 "The server merges only if the source branch still points to the head you reviewed, so --head is\n" +
9711064 "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" +
9721065 "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" +
9731066 "mr:merge scope.\n\n" +
974- "--delete-branch deletes the source branch after the merge. The merge is refused when the branch\n" +
975- "is the default branch or used by another open merge request. If the deletion fails after the\n" +
976- "merge, the merge stands, the branch is kept and a warning goes to standard error." + jsonMRHelp +
977- "\n--json adds source_branch_deleted (true when the branch was deleted).",
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 +
1071+ "\n--json adds source_branch_deleted (true when the deletion was requested: the server deletes the branch right after the merge).",
9781072 Args: cobra.ExactArgs(2),
9791073 ValidArgsFunction: completeMRRepoThenNumber(env),
9801074 }
981- 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)")
9821076 cmd.Flags().StringVar(&head, "head", "", "full id of the head you reviewed, from `ermite mr show` (required)")
9831077 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+ })
9841081 asJSON := addJSONFlag(cmd)
9851082 cmd.RunE = func(cmd *cobra.Command, args []string) error {
9861083 n, err := parseNumber(args[1])
9871084 if err != nil {
9881085 return err
9891086 }
1087+ pbMode, err := parseMode(mode)
1088+ if err != nil {
1089+ return err
1090+ }
9901091 head = strings.TrimSpace(head)
9911092 if head == "" {
9921093 return fmt.Errorf("--head is required: run `ermite mr show %s %d` and give the head id it prints, "+
@@ -997,35 +1098,60 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
9971098 return err
9981099 }
9991100 ctx := env.ctx(cmd)
1000- mode := ermite.MergeMode_MERGE_MODE_FAST_FORWARD
1001- if squash {
1002- 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+ }
10031105 }
10041106 res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{
1005- Repo: args[0], Number: n, Mode: mode, HeadId: head, DeleteSourceBranch: deleteBranch,
1107+ Repo: args[0], Number: n, Mode: pbMode, HeadId: head, DeleteSourceBranch: deleteBranch,
10061108 }))
10071109 if err != nil {
10081110 return c.Describe(err)
10091111 }
10101112 m := res.Msg.GetMergeRequest()
10111113 deleted := res.Msg.GetSourceBranchDeleted()
1012- if deleteBranch && !deleted {
1013- _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "ermite: warning: the branch %s was kept (it moved after the merge, or the server cannot delete it)\n",
1014- clean(m.GetSourceBranch()))
1015- }
10161114 if *asJSON {
10171115 return writeJSON(cmd.OutOrStdout(), jsonMerged{jsonMergeRequest: toJSONMergeRequest(m, c.URL()), SourceBranchDeleted: deleted})
10181116 }
1019- _, 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(),
10201119 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+ }
10211123 if err == nil && deleted {
1022- _, err = fmt.Fprintf(cmd.OutOrStdout(), "deleted branch %s\n", clean(m.GetSourceBranch()))
1124+ _, err = fmt.Fprintf(out, "deleting branch %s (done by the server after the merge)\n", clean(m.GetSourceBranch()))
10231125 }
10241126 return err
10251127 }
10261128 return cmd
10271129 }
10281130
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+
10291155 func newMRCloseCmd(env *cliEnv) *cobra.Command {
10301156 cmd := &cobra.Command{
10311157 Use: "close <repo> <number>",
M internal/app/cli_mr_e2e_test.go +6 -6
@@ -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
@@ -381,7 +381,7 @@ func TestRepoEditMergeSettingsCLI(t *testing.T) {
381381 MergeModes []string `json:"merge_modes"`
382382 FailingChecksBlock bool `json:"failing_checks_block"`
383383 }](t, res.stdout)
384- assert.Equal(t, []string{"fast-forward", "squash"}, repo.MergeModes)
384+ assert.Equal(t, []string{"fast-forward", "squash", "merge"}, repo.MergeModes)
385385 assert.True(t, repo.FailingChecksBlock)
386386
387387 res = c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "squash", "--json").ok(t)
M internal/app/cli_mr_test.go +83 -7
@@ -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...)
@@ -326,7 +402,7 @@ func TestMRMergeDeleteBranch(t *testing.T) {
326402 url := f.serve(t)
327403
328404 res := runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch").ok(t)
329- assert.Equal(t, "merged alpha!1 into main (fast-forward) at "+headA[:10]+"\ndeleted branch feat/x\n", res.stdout)
405+ assert.Equal(t, "merged alpha!1 into main (fast-forward) at "+headA[:10]+"\ndeleting branch feat/x (done by the server after the merge)\n", res.stdout)
330406 assert.Empty(t, res.stderr)
331407 _, reqs := f.snapshot()
332408 assert.True(t, reqs[0].(*ermite.MergeMergeRequestRequest).GetDeleteSourceBranch())
@@ -334,11 +410,11 @@ func TestMRMergeDeleteBranch(t *testing.T) {
334410 res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch", "--json").ok(t)
335411 assert.Contains(t, res.stdout, `"source_branch_deleted": true`)
336412
337- // The merge stands when the branch is kept: a warning, no failure.
413+ // Deletion is asynchronous: the CLI makes no claim when the server did not
414+ // request it.
338415 deleted = false
339416 res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch").ok(t)
340417 assert.Equal(t, "merged alpha!1 into main (fast-forward) at "+headA[:10]+"\n", res.stdout)
341- assert.Contains(t, res.stderr, "ermite: warning: the branch feat/x was kept")
342418
343419 // Without the flag nothing is asked and nothing is said.
344420 res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA).ok(t)
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_version.go +1 -0
@@ -114,6 +114,7 @@ var featureUse = map[string]string{
114114 client.FeatureMergeRequests: "`ermite mr` needs: the commands will fail on this server",
115115 client.FeatureMergeDeleteBranch: "`ermite mr merge --delete-branch` needs: the merge works but the branch is kept",
116116 client.FeatureIssues: "`ermite issue` needs: the commands will fail on this server, and `repo edit --issues` and `token create --scope issue:write` are refused",
117+ client.FeatureMergeability: "`ermite mr show` uses to show whether the request merges: the line is missing",
117118 client.FeatureSigningKey: "`ermite signing-key show` needs: the command will fail on this server",
118119 client.FeatureSession: "`ermite web login` needs: the command will fail on this server",
119120 client.FeatureTokenBranchPrefixes: "`ermite token create --branch-prefix` needs: the command is refused, and `token list` does not show the limits",
M internal/app/cli_version_test.go +5 -5
@@ -17,7 +17,7 @@ import (
1717 )
1818
1919 // allFeatures are the features this client uses.
20-var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "passkeys"}
20+var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "passkeys"}
2121
2222 // Every feature the client uses needs a sentence for the version warning.
2323 func TestEveryUsedFeatureExplainsWhatBreaks(t *testing.T) {
@@ -45,15 +45,15 @@ func TestVersionWithServer(t *testing.T) {
4545 f := versionFake(allFeatures...)
4646 url := f.serve(t)
4747 res := runCLI(t, envFor(url), "", "version").ok(t)
48- assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, issues, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.workflows, ci.artifacts, commit_signatures, passkeys\n", res.stdout)
48+ assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, issues, merge_requests.mergeability, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.workflows, ci.artifacts, commit_signatures, passkeys\n", res.stdout)
4949 assert.Empty(t, res.stderr, "no warning when every feature is there")
5050
5151 res = runCLI(t, envFor(url), "", "version", "--json").ok(t)
52- assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","issues","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","passkeys"],"missing_features":[]}}`, res.stdout)
52+ assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","passkeys"],"missing_features":[]}}`, res.stdout)
5353 }
5454
5555 func TestVersionWarnsAboutMissingFeatures(t *testing.T) {
56- f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "issues", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "passkeys")
56+ f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "passkeys")
5757 url := f.serve(t)
5858 res := runCLI(t, envFor(url), "", "version").ok(t)
5959 assert.Contains(t, res.stdout, "v0.4.0")
@@ -61,7 +61,7 @@ func TestVersionWarnsAboutMissingFeatures(t *testing.T) {
6161 assert.NotContains(t, res.stderr, "repo.update")
6262
6363 res = runCLI(t, envFor(url), "", "version", "--json").ok(t)
64- assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","issues","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","passkeys"],"missing_features":["events.resume"]}}`, res.stdout)
64+ assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","passkeys"],"missing_features":["events.resume"]}}`, res.stdout)
6565 assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json")
6666
6767 f = versionFake()
M internal/app/cli_watch.go +30 -0
@@ -29,6 +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 set for a merge_request_mergeability event.
33+ Mergeability *jsonMergeability `json:"mergeability,omitempty"`
3234 }
3335
3436 type jsonRefUpdate struct {
@@ -52,6 +54,7 @@ const (
5254 eventCommentEdited = "comment_edited"
5355 eventCommentDeleted = "comment_deleted"
5456 eventCommentResolved = "comment_resolved"
57+ eventMRMergeability = "merge_request_mergeability"
5558 )
5659
5760 // toJSONEvent maps an event; ok is false for kinds this client does not know,
@@ -94,6 +97,9 @@ func toJSONEvent(ev *ermite.Event, baseURL string) (jsonEvent, bool) {
9497 case *ermite.Event_CommentResolved:
9598 c := toJSONComment(k.CommentResolved.GetComment())
9699 out.Type, out.Repo, out.Number, out.Comment = eventCommentResolved, k.CommentResolved.GetRepo(), k.CommentResolved.GetNumber(), &c
100+ case *ermite.Event_MergeRequestMergeability:
101+ out.Type, out.Repo, out.Number = eventMRMergeability, k.MergeRequestMergeability.GetRepo(), k.MergeRequestMergeability.GetNumber()
102+ out.Mergeability = toJSONMergeability(k.MergeRequestMergeability.GetMergeability())
97103 case *ermite.Event_Gap:
98104 out.Type = eventGap
99105 default:
@@ -126,12 +132,36 @@ func eventLine(e jsonEvent) string {
126132 detail = fmt.Sprintf("%s!%d %s -> %s", e.Repo, e.MergeRequest.Number, e.MergeRequest.SourceBranch, e.MergeRequest.TargetBranch)
127133 case eventCommentAdded, eventCommentEdited, eventCommentDeleted, eventCommentResolved:
128134 detail = fmt.Sprintf("%s!%d by %s", e.Repo, e.Number, e.Comment.Author)
135+ case eventMRMergeability:
136+ detail = fmt.Sprintf("%s!%d mergeability: %s", e.Repo, e.Number, mergeabilityEventText(e.Mergeability))
129137 case eventGap:
130138 detail = "events were lost, reload what you display"
131139 }
132140 return clean(ts + " " + e.Type + " " + detail)
133141 }
134142
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+
135165 func refUpdateText(u jsonRefUpdate) string {
136166 short := func(id string) string {
137167 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/forge.go +14 -1
@@ -137,6 +137,8 @@ type forge struct {
137137 // Inbox serves the owner's inbox of the web.
138138 Inbox *usecase.Inbox
139139 SigningKey *usecase.SigningKey
140+ // Mergeability keeps the mergeability of the open merge requests up to date.
141+ Mergeability *usecase.Mergeability
140142 // Signatures are the trusted keys and the commit signature checks
141143 // (ADR 0030).
142144 Signatures *usecase.Signatures
@@ -331,11 +333,16 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e
331333 if err := f.signatures(store, reader, signer.PublicKey(), committer, clock); err != nil {
332334 return nil, err
333335 }
336+ f.Mergeability = usecase.NewMergeability(ctx, usecase.MergeabilityConfig{
337+ Store: store.MergeRequests(), Meta: store, Git: git, Bus: f.Bus, Clock: clock,
338+ })
339+ f.Pushes.WithMergeability(f.Mergeability)
334340 f.MergeRequests = usecase.NewMergeRequests(usecase.MergeRequestsConfig{
335341 Meta: store, Store: store.MergeRequests(), Repos: git, Git: git, Signer: signer,
336342 Committer: committer, Bus: f.Bus, Clock: clock, Maint: f.maint,
337- CI: f.CI, Checks: f.CI, PullRequests: f.CI, RefTips: store.CI(),
343+ CI: f.CI, Checks: f.CI, PullRequests: f.CI, RefTips: store.CI(), Mergeability: f.Mergeability,
338344 })
345+ f.Mergeability.Start()
339346 f.MergeRequests.WithVerifier(f.Signatures)
340347 f.Issues = usecase.NewIssues(usecase.IssuesConfig{
341348 Meta: store, Store: store.Issues(), MergeRequests: store.MergeRequests(), Bus: f.Bus, Clock: clock,
@@ -512,9 +519,15 @@ func (f *forge) Close(ctx context.Context) {
512519 if f.hooks != nil {
513520 f.hooks.Close(ctx)
514521 }
522+ if f.MergeRequests != nil {
523+ f.MergeRequests.Wait(ctx) // its side effects use CI and Mergeability
524+ }
515525 if f.CI != nil {
516526 f.CI.Close(ctx)
517527 }
528+ if f.Mergeability != nil {
529+ f.Mergeability.Close(ctx)
530+ }
518531 if f.EventLog != nil {
519532 f.EventLog.Close(ctx)
520533 }
M internal/app/mr_api_test.go +3 -2
@@ -298,7 +298,7 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) {
298298
299299 got, err := h.api("").repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"}))
300300 require.NoError(t, err)
301- assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes())
301+ assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes())
302302 assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock())
303303
304304 update := func(c *clients, paths []string, s *pb.RepoSettings) (*pb.Repo, error) {
@@ -375,7 +375,8 @@ func TestMergeDeletesTheSourceOverTheAPI(t *testing.T) {
375375 Repo: "proj", Number: 1, HeadId: r.head, DeleteSourceBranch: true,
376376 }))
377377 require.NoError(t, err)
378- assert.True(t, res.Msg.GetSourceBranchDeleted())
378+ assert.True(t, res.Msg.GetSourceBranchDeleted(), "requested")
379+ r.forge.MergeRequests.Wait(r.ctx) // the deletion follows the response
379380 assert.Empty(t, r.bareGit("proj", "for-each-ref", "refs/heads/feat"))
380381
381382 // The default branch as a source: refused before anything changes.
M internal/app/testdata/web/commits-root.html +1 -1
M internal/app/testdata/web/commits-small.html +1 -1
M internal/app/testdata/web/home.html +1 -1
M internal/app/testdata/web/refs.html +1 -1
M internal/app/testdata/web/summary.html +1 -1
M internal/app/web.go +5 -0
M internal/app/web_events_test.go +9 -0
M internal/app/web_inbox_test.go +15 -3
M internal/app/web_mr_test.go +202 -10
M internal/domain/event.go +14 -0
M internal/domain/mergerequest.go +155 -16
M internal/domain/mergerequest_test.go +50 -3
A internal/domain/mergetree_bound_test.go +53 -0
M internal/ports/ci.go +3 -0
M internal/ports/dto.go +8 -2
M internal/ports/mocks/CIRefTipStore.go +69 -0
M internal/ports/mocks/MergeGit.go +162 -0
M internal/ports/mocks/MergeRequestService.go +78 -0
M internal/ports/mocks/MergeRequestStore.go +192 -0
A internal/ports/mocks/MergeabilityReader.go +111 -0
A internal/ports/mocks/MergeabilityTrigger.go +146 -0
M internal/ports/ports.go +37 -0
M internal/ports/services.go +4 -0
M internal/usecase/ci_unit_test.go +1 -0
M internal/usecase/events.go +5 -1
M internal/usecase/events_test.go +13 -0
M internal/usecase/export_test.go +46 -0
A internal/usecase/mergeability.go +467 -0
A internal/usecase/mergeability_reconcile_test.go +91 -0
A internal/usecase/mergeability_test.go +673 -0
M internal/usecase/mergerequests.go +246 -70
M internal/usecase/mergerequests_branches_test.go +1 -0
M internal/usecase/mergerequests_delete_test.go +150 -1
M internal/usecase/mergerequests_integration_test.go +11 -6
A internal/usecase/mergerequests_merge_test.go +293 -0
M internal/usecase/pushes.go +23 -11
M proto/ermite/storage/merge_request.proto +28 -0
M skills/ermite/CLI.md +5 -4
M skills/ermite/SKILL.md +2 -2

Comments0 open / 0

no comments yet.

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
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 2

Tags 43

↑↓ moveenter openesc close