detached 3398787

Merge fe192203ee99178ad1bcbf9cd0c6e9b8e02cf667 into cc7ba64ee52412f41c3ce5207bac7e47b9f01f30

commit
33987875264b2da83ba42dcad14ff1bc37fd16ae
author
ermite · · 2026-10-20 09:05 UTC
merge of
cc7ba64 + fe19220 · diff against the first parent
signature
unsigned
40 files · +2546 -1249
M docs/merge-requests.md +29 -0
@@ -104,3 +104,32 @@ report whether more exist; ancestry, rather than the bounded list, detects a
104104 rewrite. Merge-request details include at most `ports.MaxMergeRequestCommits`
105105 (100) commits. These history queries remain on the merge-request service
106106 because it owns the merge-git port.
107+
108+### Review page panels
109+
110+The main request card ends with a footer of two tabs, Commits and Checks, both
111+collapsed by default. Clicking a tab opens its panel, clicking the open tab
112+folds it again, clicking the other tab switches. Arrow keys (and Home / End)
113+move between the tabs. Without scripts the tabs are anchor links and the URL
114+fragment (`#mr-commits`, `#mr-checks`) shows the panel. The checks summary above
115+the diff opens the Checks panel and continues to update live. Commit totals use
116+`N+` when the branch list is capped: the reader's existing count operation counts all reachable
117+ancestors, not the source-minus-target range.
118+
119+The rail keeps Files (including its `g f` shortcut) and Verdicts. Merge settings
120+are read-only information in the More (⋯) menu, available to every viewer.
121+Signing-key information is also there when a merge box offers signed modes;
122+merge actions remain restricted to their existing permissions.
123+
124+## Live source-branch updates
125+
126+A push to the source branch refreshes the web review in place: the diff, files
127+rail, commits/checks footer, diff range and merge controls follow the new head.
128+The review keeps its visible file/line, folded files and open footer panel.
129+The page request still reads only one 25-file window of hunks; the visible
130+anchor's window is fetched too if needed, and other windows remain lazy.
131+
132+While a comment is focused or has unsent edits, the refresh waits. A small
133+“branch updated — refreshing after your comment” status explains the delay.
134+Sending, cancelling, or reverting the draft and leaving its form lets the
135+pending refresh proceed. Refresh never preserves or discards an unsent draft.
M internal/adapters/web/actions.templ +2 -2
@@ -434,7 +434,7 @@ templ CommitChecks(c ChecksSummary) {
434434 }
435435
436436 // MRChecksSum is the state of the checks of a merge request in the bar above
437-// the diff, a link to the checks of the rail. The event stream replaces it
437+// the diff, a link to the checks of the card footer. The event stream replaces it
438438 // with the checks section (same event, fragments.go).
439439 templ MRChecksSum(c ChecksSummary) {
440440 <a class="ck-sum mr-ck" id="mr-ck" href="#mr-checks" title={ c.Title }>
@@ -447,7 +447,7 @@ templ MRChecksSum(c ChecksSummary) {
447447 </a>
448448 }
449449
450-// MRChecks is the checks section of the rail of a merge request.
450+// MRChecks is the checks section of the footer of a merge request.
451451 templ MRChecks(c ChecksSection) {
452452 <section aria-labelledby="r-checks" id="mr-checks">
453453 <h2 id="r-checks">Checks <span class="count">{ itoa(len(c.Rows)) }</span></h2>
M internal/adapters/web/actions_templ.go +2 -2 generated file: collapsed
M internal/adapters/web/checks.go +1 -1
@@ -81,7 +81,7 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C
8181 return sum
8282 }
8383
84-// checksSection is the checks of the rail of a merge request, with the rule of
84+// checksSection is the checks of the footer of a merge request, with the rule of
8585 // the repository that applies to them.
8686 func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection {
8787 sec := ChecksSection{}
M internal/adapters/web/diff.templ +6 -2
@@ -117,7 +117,7 @@ templ diffViewMenu(p DiffView) {
117117 <details class="menu vmenu">
118118 <summary class="btn" aria-label="Diff view">
119119 if p.Both {
120- <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span>
120+ <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span>
121121 } else {
122122 { viewLabel(p.View) }
123123 }
@@ -139,12 +139,16 @@ templ viewLink(p DiffView, view, label, key, href string) {
139139 class="btn vbtn"
140140 data-view={ view }
141141 aria-keyshortcuts={ key }
142+ if view == "split" {
143+ :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false"
144+ :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'"
145+ }
142146 if p.View == view {
143147 aria-current="true"
144148 }
145149 if p.Both {
146150 data-fire={ view }
147- :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false"
151+ :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false"
148152 }
149153 >{ label }<kbd aria-hidden="true">{ key }</kbd></a>
150154 }
M internal/adapters/web/diff_templ.go +190 -184 generated file: collapsed
M internal/adapters/web/events.go +13 -3
@@ -31,7 +31,7 @@ import (
3131 // GET /-/events?topic=repo:<name>&view=summary|refs
3232 // GET /-/events?topic=repo:<name>&view=commits&ref=<branch>&head=<sha>
3333 // GET /-/events?topic=repo:<name>&view=mrs
34-// GET /-/events?topic=repo:<name>&view=mr&n=<number>&ref=<source branch>&target=<target branch>
34+// GET /-/events?topic=repo:<name>&view=mr&n=<number>&ref=<source branch>&head=<sha>&target=<target branch>
3535 // GET /-/events?topic=repo:<name>&view=issues
3636 // GET /-/events?topic=repo:<name>&view=issue&n=<number>
3737 // GET /-/events?topic=repo:<name>&view=actions
@@ -44,7 +44,9 @@ import (
4444 // lists first: the banner counts the commits between that head and the new
4545 // tip, so it stays right however many pushes came in and however often the
4646 // stream reconnected. A Last-Event-ID header resumes the stream
47-// (resume_after).
47+// (resume_after). With no header, after=<event id> supplies the resume cursor
48+// for a replacement stream. The header wins. Both use Watch's parseID
49+// validation: an invalid or expired cursor produces a gap.
4850 //
4951 // Events, each with the id of the Watch event it comes from:
5052 //
@@ -60,6 +62,10 @@ import (
6062 // the badge of the merge box (a slot like mr-notice), when the
6163 // mergeability of the request shown settles or a push moves its
6264 // source or target
65+// mr-head data: <div id="mr-head"></div>; the source branch moved.
66+// ermiteMR fetches the current page and refreshes its head-dependent
67+// regions in place, preserving the reader's position and drafts.
68+// The replacement stream uses the new head for CI filtering.
6369 // mr-notice data: <div id="mr-notice">…</div>, on the merge request and issue pages:
6470 // something changed, with the link that reloads the page
6571 // ci-run data: <tr id="run-<id>">…</tr> (the list of runs),
@@ -184,8 +190,12 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
184190 var startOnce sync.Once
185191 start := func() { startOnce.Do(func() { close(started) }) }
186192 finished := make(chan struct{})
193+ resume := r.Header.Get("Last-Event-ID")
194+ if resume == "" {
195+ resume = q.Get("after")
196+ }
187197 params := ports.WatchParams{
188- Topics: []string{topic}, ResumeAfter: r.Header.Get("Last-Event-ID"),
198+ Topics: []string{topic}, ResumeAfter: resume,
189199 // The visitor is the client of the watch, for the limit per client.
190200 Client: clientlimit.Key(clientlimit.ClientAddr(r, s.cfg.TrustedProxies)),
191201 Ready: start,
M internal/adapters/web/events_test.go +50 -0
@@ -478,3 +478,53 @@ func TestCommitsBannerWithoutMergeRequests(t *testing.T) {
478478 require.Len(t, got, 1)
479479 assert.Contains(t, got[0].html, "the branch moved — reload")
480480 }
481+
482+func TestMRPushEventsKeepMergeability(t *testing.T) {
483+ t.Parallel()
484+ svc := mocks.NewMergeRequestService(t)
485+ svc.EXPECT().Mergeability(mock.Anything, mock.Anything, mock.Anything, int64(1)).
486+ Return(domain.Mergeability{State: domain.MergeabilityUpToDate}, nil).Twice()
487+ l := liveStream{s: &Server{cfg: Config{MergeRequests: svc}}, repo: "demo", view: "mr", n: "1", ref: "feature", target: "main"}
488+ ctx := context.WithValue(t.Context(), signedInKey{}, struct{}{})
489+ for _, ref := range []string{"feature", "main"} {
490+ fs, err := l.mrPushed(ctx, domain.PushEvent{Updates: []domain.RefUpdate{{Ref: "refs/heads/" + ref}}})
491+ require.NoError(t, err)
492+ if ref == "feature" {
493+ require.Len(t, fs, 2)
494+ assert.Equal(t, "mr-head", fs[0].event)
495+ assert.Equal(t, "event: mr-head\nid: b-1\ndata: <div id=\"mr-head\"></div>\n\n", formatEvent("b-1", fs[0].event, fs[0].html, true))
496+ } else {
497+ require.Len(t, fs, 1)
498+ }
499+ assert.Equal(t, "mr-mergeability", fs[len(fs)-1].event)
500+ for _, f := range fs {
501+ assert.NotEqual(t, "mr-notice", f.event)
502+ }
503+ }
504+}
505+
506+func TestMRPushBothBranches(t *testing.T) {
507+ t.Parallel()
508+ for _, refs := range [][]string{{"feature", "main"}, {"main", "feature"}} {
509+ t.Run(strings.Join(refs, "-"), func(t *testing.T) {
510+ t.Parallel()
511+ svc := mocks.NewMergeRequestService(t)
512+ svc.EXPECT().Mergeability(mock.Anything, mock.Anything, mock.Anything, int64(1)).Return(domain.Mergeability{State: domain.MergeabilityUpToDate}, nil).Once()
513+ l := liveStream{s: &Server{cfg: Config{MergeRequests: svc}}, repo: "demo", view: "mr", n: "1", ref: "feature", target: "main"}
514+ ctx := context.WithValue(t.Context(), signedInKey{}, struct{}{})
515+ fs, err := l.mrPushed(ctx, domain.PushEvent{Updates: []domain.RefUpdate{{Ref: "refs/heads/" + refs[0]}, {Ref: "refs/heads/" + refs[1]}}})
516+ require.NoError(t, err)
517+ require.Len(t, fs, 2)
518+ assert.Equal(t, "mr-head", fs[0].event)
519+ assert.Equal(t, "mr-mergeability", fs[1].event)
520+ })
521+ }
522+}
523+
524+func TestNonMRPushRoutingNeverEmitsMRHead(t *testing.T) {
525+ t.Parallel()
526+ l := liveStream{s: &Server{}, repo: "demo", view: "mrs", n: "1", ref: "feature", target: "main"}
527+ fs, err := l.render(t.Context(), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "demo"), Updates: []domain.RefUpdate{{Ref: "refs/heads/feature"}}}})
528+ require.NoError(t, err)
529+ assert.Empty(t, fs)
530+}
M internal/adapters/web/fragments.go +2 -7
@@ -474,8 +474,7 @@ func (l *liveStream) issueNotice(ctx context.Context, number int64, text string)
474474 // mrPushed announces a push to the source branch of the merge request shown,
475475 // and has the badge follow a push to its source or its target.
476476 func (l *liveStream) mrPushed(ctx context.Context, p domain.PushEvent) ([]fragment, error) {
477- n, err := strconv.ParseInt(l.n, 10, 64)
478- if err != nil {
477+ if _, err := strconv.ParseInt(l.n, 10, 64); err != nil {
479478 return nil, nil
480479 }
481480 var out []fragment
@@ -483,11 +482,7 @@ func (l *liveStream) mrPushed(ctx context.Context, p domain.PushEvent) ([]fragme
483482 for _, u := range p.Updates {
484483 switch {
485484 case l.ref != "" && u.Ref == "refs/heads/"+l.ref:
486- notice, err := l.notice(ctx, "the branch changed — reload", mrHref(l.repo, n))
487- if err != nil {
488- return nil, err
489- }
490- out = append(out, notice...)
485+ out = append(out, fragment{event: "mr-head", html: `<div id="mr-head"></div>`})
491486 moved = true
492487 case l.target != "" && u.Ref == "refs/heads/"+l.target:
493488 moved = true
M internal/adapters/web/fragments.templ +1 -0
@@ -103,6 +103,7 @@ templ Live(l Layout) {
103103 htmx:sse:error -> ermiteLive.error(event.detail.status);
104104 htmx:sse:before:connection -> ermiteLive.gate(event);
105105 home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
106+ mr-head -> ermiteMR.headChanged();
106107 gap -> ermiteLive.reload()"
107108 ></span>
108109 }
M internal/adapters/web/fragments_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/fragments_test.go +22 -0
@@ -7,6 +7,8 @@ import (
77 "strings"
88 "testing"
99
10+ "bnema.dev/ermite/internal/domain"
11+
1012 "github.com/a-h/templ"
1113 "github.com/stretchr/testify/assert"
1214 "github.com/stretchr/testify/require"
@@ -77,3 +79,23 @@ func TestDiffControlsPassDataThroughAttributes(t *testing.T) {
7779 assert.Contains(t, link, `this.dataset.view`)
7880 assert.NotContains(t, link, "=== 'split'")
7981 }
82+
83+// The push signal is intentionally constant-sized: diff work belongs to the
84+// subsequent windowed page request, not to each subscriber's event handler.
85+func TestMRPushedHeadSignal(t *testing.T) {
86+ t.Parallel()
87+ l := liveStream{s: &Server{}, repo: "demo", view: "mr", n: "1", ref: "feature", target: "main"}
88+ for _, tt := range []struct {
89+ ref string
90+ want int
91+ }{{"refs/heads/feature", 1}, {"refs/heads/main", 0}, {"refs/heads/other", 0}} {
92+ fs, err := l.mrPushed(t.Context(), domain.PushEvent{Updates: []domain.RefUpdate{{Ref: tt.ref}}})
93+ require.NoError(t, err)
94+ require.Len(t, fs, tt.want)
95+ if tt.want > 0 {
96+ assert.Equal(t, "mr-head", fs[0].event)
97+ assert.Equal(t, `<div id="mr-head"></div>`, fs[0].html)
98+ assert.NotContains(t, fs[0].html, "the branch changed")
99+ }
100+ }
101+}
M internal/adapters/web/jstest/a11y.test.mjs +23 -0
@@ -358,3 +358,26 @@ test('activity moves a home row up only in newest activity order, preserving foc
358358 assert.ok(fresh.hasAttribute('data-cursor'));
359359 }
360360 });
361+
362+test('mobile MR layout shortcuts preserve the saved preference until back on desktop', () => {
363+ const diffs = el('div', { id: 'diffs', attrs: { 'data-mr': '', 'data-diff': 'split', 'data-mobile': 'true' } });
364+ dom.body.replaceChildren(diffs);
365+ const oldQuery = doc.querySelector;
366+ doc.querySelector = query.one;
367+ const previous = globalThis.sessionStorage;
368+ const writes = [];
369+ globalThis.sessionStorage = { setItem: (...args) => writes.push(args) };
370+ try {
371+ window.ermite.layout('unified');
372+ window.ermite.layout('split');
373+ assert.equal(diffs.dataset.diff, 'split');
374+ assert.deepEqual(writes, []);
375+ diffs.dataset.mobile = 'false';
376+ window.ermite.layout('unified');
377+ assert.equal(diffs.dataset.diff, 'unified');
378+ assert.deepEqual(writes, [['diff', 'unified']]);
379+ } finally {
380+ globalThis.sessionStorage = previous;
381+ doc.querySelector = oldQuery;
382+ }
383+});
M internal/adapters/web/jstest/live.test.mjs +43 -2
@@ -12,10 +12,11 @@ const live = src.slice(src.indexOf('window.ermiteLive = '));
1212 function setup(t) {
1313 t.mock.timers.enable({ apis: ['setTimeout'] });
1414 const hint = { dataset: { state: 'connecting' } };
15- globalThis.window = {};
15+ const listeners = {};
16+ globalThis.window = { addEventListener: (name, fn) => { listeners[name] = fn; } };
1617 globalThis.document = { querySelector: (sel) => (sel === '.hint-live' ? hint : null) };
1718 new Function(live)();
18- return { live: globalThis.window.ermiteLive, hint };
19+ return { live: globalThis.window.ermiteLive, hint, listeners };
1920 }
2021
2122 test('a drop shorter than the delay says nothing', (t) => {
@@ -38,3 +39,43 @@ test('a drop that lasts shows offline, once, until the stream is back', (t) => {
3839 live.back();
3940 assert.equal(hint.dataset.state, 'live');
4041 });
42+
43+test('stream teardown handles the reader cancellation that hx-sse leaves uncaught', async (t) => {
44+ const { live, listeners } = setup(t);
45+ assert.equal(listeners.unhandledrejection, undefined, 'no global rejection filter');
46+ const abort = Object.assign(new Error('signal is aborted without reason'), { name: 'AbortError' });
47+ let cancelled = 0;
48+ const connection = { reader: { cancel: () => { cancelled++; return Promise.reject(abort); } } };
49+ listeners['htmx:sse:after:connection']({ target: { matches: () => true }, detail: { connection } });
50+ const escaped = [];
51+ const onRejection = (reason) => escaped.push(reason);
52+ process.on('unhandledRejection', onRejection);
53+ try {
54+ // remove stands for hx-sse's cleanup: r.reader?.cancel?.(), result ignored.
55+ await live.teardown(() => { connection.reader?.cancel?.(); });
56+ await new Promise((resolve) => setImmediate(resolve));
57+ await new Promise((resolve) => setImmediate(resolve));
58+ } finally {
59+ process.off('unhandledRejection', onRejection);
60+ }
61+ assert.equal(cancelled, 1, 'the real reader is still cancelled');
62+ assert.deepEqual(escaped, [], 'no AbortError escapes the teardown');
63+});
64+
65+test('replacement stream URL carries the last connection id, including id-only events', (t) => {
66+ const { live, listeners } = setup(t);
67+ const connection = { lastEventId: 'boot-7' };
68+ listeners['htmx:sse:after:connection']({ target: { matches: () => true }, detail: { connection } });
69+ connection.lastEventId = 'boot-8'; // hx-sse updates this even for id-only events.
70+ const saved = globalThis.location;
71+ globalThis.location = { href: 'https://forge.test/demo/merge-requests/1' };
72+ const element = { url: '/-/events?topic=repo%3Ademo&head=new', getAttribute() { return this.url; },
73+ setAttribute(name, value) { assert.equal(name, 'hx-sse:connect'); this.url = value; } };
74+ try {
75+ live.resumeStream(element);
76+ const url = new URL(element.url, location.href);
77+ assert.equal(url.searchParams.get('after'), 'boot-8');
78+ assert.equal(url.searchParams.get('head'), 'new');
79+ assert.equal(url.searchParams.get('topic'), 'repo:demo');
80+ } finally { globalThis.location = saved; }
81+});
M internal/adapters/web/jstest/mr.test.mjs +430 -0
@@ -92,3 +92,433 @@ for (const dataset of [{diff:'split'}, {view:'split'}]) test(`reveal selects the
9292 globalThis.requestAnimationFrame=savedRAF;
9393 }
9494 });
95+
96+function footerHarness(extra = {}) {
97+ const saved = Object.fromEntries(['document', 'location', 'addEventListener', 'history', 'MutationObserver'].map((k) => [k, globalThis[k]]));
98+ const node = (id) => ({ id, attrs: {}, events: {}, hidden: false,
99+ setAttribute(k, v) { this.attrs[k] = v; }, getAttribute(k) { return this.attrs[k] ?? null; },
100+ addEventListener(k, v) { this.events[k] = v; }, focus() { this.focused = true; },
101+ scrollIntoView() { this.scrolled = (this.scrolled ?? 0) + 1; } });
102+ const tabs = [node('mr-tab-commits'), node('mr-tab-checks')];
103+ const panels = [node('mr-commits'), node('mr-checks-panel')];
104+ const footer = node('footer'); footer.querySelectorAll = () => panels;
105+ const nav = node('tabs'); nav.querySelectorAll = () => tabs;
106+ const listeners = {}, replaced = [];
107+ globalThis.document = { querySelector: (s) => s === '.mr-footer' ? footer : s === '[data-mr-tabs]' ? nav : s === '#mr-ck' ? extra.summary ?? null : null,
108+ querySelectorAll: (s) => s === '[data-mr-tabs] a' ? tabs : [], addEventListener(k, v) { listeners[k] = v; } };
109+ globalThis.location = { hash: '', pathname: '/demo/merge-requests/1', search: '?x=1' };
110+ globalThis.addEventListener = (k, v) => { listeners[k] = v; };
111+ globalThis.history = { replaceState: (...args) => replaced.push(args) };
112+ const restore = () => { for (const [k, v] of Object.entries(saved)) { if (v === undefined) delete globalThis[k]; else globalThis[k] = v; } };
113+ const state = () => panels.map((p) => !p.hidden);
114+ return { tabs, panels, footer, listeners, replaced, restore, state };
115+}
116+
117+test('footer tabs: closed by default, toggle, switch, arrows, hash', () => {
118+ const h = footerHarness();
119+ try {
120+ window.ermiteMR.panels();
121+ assert.deepEqual(h.state(), [false, false], 'initially closed');
122+ assert.equal(h.tabs[0].attrs['aria-expanded'], 'false');
123+ assert.deepEqual(h.replaced, [], 'initial state leaves the URL alone');
124+ h.tabs[1].events.click({ preventDefault() {} });
125+ assert.deepEqual(h.state(), [false, true]);
126+ assert.equal(h.tabs[1].attrs['aria-expanded'], 'true');
127+ assert.deepEqual(h.replaced, [[null, '', '#mr-checks-panel']]);
128+ location.hash = '#mr-checks-panel';
129+ h.tabs[1].events.click({ preventDefault() {} });
130+ assert.deepEqual(h.state(), [false, false], 'a second click closes');
131+ assert.deepEqual(h.replaced.at(-1), [null, '', '/demo/merge-requests/1?x=1'], 'closing clears the fragment');
132+ location.hash = '';
133+ h.tabs[1].events.click({ preventDefault() {} });
134+ h.tabs[0].events.click({ preventDefault() {} });
135+ assert.deepEqual(h.state(), [true, false], 'the other tab switches');
136+ h.tabs[0].events.keydown({ key: 'ArrowRight', preventDefault() {} });
137+ assert.equal(h.tabs[1].focused, true, 'arrows move focus only');
138+ assert.deepEqual(h.state(), [true, false]);
139+ location.hash = '#mr-checks'; h.replaced.length = 0; h.listeners.hashchange();
140+ assert.deepEqual(h.state(), [false, true], 'hash opens checks');
141+ assert.deepEqual(h.replaced, [], 'hash navigation does not rewrite the URL');
142+ location.hash = '#mr-commits'; h.listeners.hashchange();
143+ assert.deepEqual(h.state(), [true, false]);
144+ assert.equal(C.tabIndex('Home', 1, 2), 0);
145+ assert.equal(C.tabIndex('End', 0, 2), 1);
146+ assert.equal(C.tabIndex('ArrowLeft', 0, 2), 1);
147+ assert.equal(C.tabIndex('Escape', 0, 2), null);
148+ } finally { h.restore(); }
149+});
150+
151+test('footer: #mr-ck syncs the checks label; its click opens Checks without toggling', () => {
152+ const text0 = { v: '' }; let writes = 0;
153+ const label = { get textContent() { return text0.v; }, set textContent(v) { writes++; text0.v = v; } };
154+ const summary = { title: '1/2 failed', textContent: ' 1/2 ', parentElement: {} };
155+ const h = footerHarness({ summary });
156+ let observed = null, callback = null;
157+ globalThis.MutationObserver = class { constructor(fn) { callback = fn; } observe(target, opts) { observed = [target, opts]; } };
158+ const nav = document.querySelector('[data-mr-tabs]');
159+ nav.querySelector = (s) => s === '[data-checks-label]' ? label : null;
160+ try {
161+ window.ermiteMR.panels();
162+ assert.equal(label.textContent, '1/2 failed', 'initial label sync');
163+ assert.equal(observed[0], summary.parentElement);
164+ assert.deepEqual(observed[1], { childList: true }, 'only #mr-ck replacements wake the observer');
165+ const before = writes; callback(); callback();
166+ assert.equal(writes, before, 'an unchanged label is not written');
167+ summary.title = ''; callback();
168+ assert.equal(label.textContent, '1/2');
169+ summary.textContent = ' '; callback();
170+ assert.equal(label.textContent, 'no checks');
171+ const click = { target: { closest: (s) => s === '#mr-ck' ? summary : null } };
172+ h.listeners.click(click);
173+ assert.deepEqual(h.state(), [false, true]);
174+ h.listeners.click(click);
175+ assert.deepEqual(h.state(), [false, true], 'a second badge click keeps it open');
176+ } finally { h.restore(); }
177+});
178+
179+test('footer: panels(index) restores the open panel without touching the URL', () => {
180+ const h = footerHarness();
181+ try {
182+ location.hash = '#mr-commits';
183+ window.ermiteMR.panels(1);
184+ assert.deepEqual(h.state(), [false, true], 'a restore ignores the fragment');
185+ assert.equal(h.tabs[1].attrs['aria-expanded'], 'true');
186+ assert.deepEqual(h.replaced, []);
187+ assert.equal(h.panels[0].scrolled, undefined, 'a restore does not scroll');
188+ assert.equal(h.panels[1].scrolled, undefined);
189+ location.hash = '';
190+ window.ermiteMR.panels(-1);
191+ assert.deepEqual(h.state(), [false, false]);
192+ } finally { h.restore(); }
193+});
194+
195+test('footer: scripts take over the panels; a fragment opens and scrolls to its panel', () => {
196+ const h = footerHarness();
197+ try {
198+ location.hash = '#mr-checks';
199+ window.ermiteMR.panels();
200+ assert.equal(h.footer.attrs['data-panels'], '', 'the CSS fallback of the fragment stops');
201+ assert.deepEqual(h.state(), [false, true]);
202+ assert.equal(h.panels[1].scrolled, 1, 'the panel of the fragment is brought in');
203+ location.hash = '#mr-commits'; h.listeners.hashchange();
204+ assert.deepEqual(h.state(), [true, false]);
205+ assert.equal(h.panels[0].scrolled, 1);
206+ } finally { h.restore(); }
207+});
208+
209+test('touch range: first tap anchors, second tap opens the sorted range', () => {
210+ const base = 'https://forge.test/demo/merge-requests/1';
211+ const first = base + '?on=new:24:a.go#f1';
212+ const last = base + '?on=new:10:a.go#f1';
213+ assert.deepEqual(C.touchRange(null, first, base), { start: first, url: null });
214+ const result = C.touchRange(first, last, base);
215+ assert.equal(result.start, null);
216+ assert.equal(new URL(result.url).searchParams.get('on'), 'new:10-24:a.go');
217+ assert.deepEqual(C.touchRange(first, first, base), { start: null, url: first });
218+});
219+
220+test('touch range: a different file, side or request becomes a new anchor', () => {
221+ const base = 'https://forge.test/demo/merge-requests/1';
222+ const first = base + '?on=new:24:a.go#f1';
223+ for (const next of [first.replace('new:', 'old:'), first.replace('a.go', 'b.go'), first.replace('/1?', '/2?')]) {
224+ assert.deepEqual(C.touchRange(first, next, base), { start: next, url: null });
225+ }
226+});
227+
228+test('mobile unified fallback only redirects single-layout split pages and preserves query/hash', () => {
229+ const href = 'https://forge.test/demo/merge-requests/1?view=split&from=25&on=new:3:a.go#f26';
230+ assert.equal(C.mobileURL(href, 'split', false), null);
231+ assert.equal(C.mobileURL(href, undefined, true), null, 'both layouts are switched by CSS');
232+ assert.equal(C.mobileURL(href, 'unified', true), null, 'no redirect loop');
233+ const url = new URL(C.mobileURL(href, 'split', true));
234+ assert.equal(url.searchParams.get('view'), 'unified');
235+ assert.equal(url.searchParams.get('from'), '25');
236+ assert.equal(url.searchParams.get('on'), 'new:3:a.go');
237+ assert.equal(url.hash, '#f26');
238+});
239+
240+test('touch control: lazy links enable toolbar; ordinary taps pass through; ranges navigate and cancel', async () => {
241+ const saved = Object.fromEntries(['document', 'location', 'addEventListener', 'localStorage', 'sessionStorage', 'fetch', 'matchMedia'].map((k) => [k, globalThis[k]]));
242+ const make = () => ({
243+ attrs: {}, events: {}, children: [], textContent: '',
244+ setAttribute(k, v) { this.attrs[k] = v; },
245+ removeAttribute(k) { delete this.attrs[k]; },
246+ addEventListener(k, fn) { this.events[k] = fn; },
247+ append(...nodes) { this.children.push(...nodes); },
248+ prepend(node) { this.children.unshift(node); },
249+ querySelector() { return null; }, querySelectorAll() { return []; },
250+ });
251+ const diffs = make();
252+ diffs.dataset = { diff: 'split' };
253+ const base = 'https://forge.test/demo/merge-requests/1';
254+ const line = (n) => Object.assign(make(), { href: `${base}?on=new:${n}:a.go#f1`, textContent: String(n) });
255+ const a = line(20), b = line(10);
256+ let loaded = false;
257+ diffs.querySelector = () => loaded ? a : null;
258+ let assigned = null;
259+ globalThis.document = {
260+ querySelector: (sel) => sel === '#diffs[data-mr]' || sel === '#diffs' ? diffs : null,
261+ querySelectorAll: () => [], createElement: (tag) => tag === 'template' ? { content: { querySelectorAll: () => [] } } : make(),
262+ };
263+ globalThis.location = { href: base, pathname: '/demo/merge-requests/1', hash: '', assign(url) { assigned = url; } };
264+ globalThis.addEventListener = () => {};
265+ const changes = {};
266+ const medias = {};
267+ globalThis.matchMedia = (q) => medias[q] ??= { matches: true, addEventListener(type, fn) { changes[q] = fn; } };
268+ const media = globalThis.matchMedia('(max-width: 720px)');
269+ const touchQuery = '(pointer: coarse), (hover: none)';
270+ const resize = () => changes['(max-width: 720px)']();
271+ globalThis.fetch = async () => { loaded = true; return { ok: true, status: 200, text: async () => '' }; };
272+ globalThis.localStorage = globalThis.sessionStorage = { getItem: () => null };
273+ const click = (link, modifiers = {}) => {
274+ const e = { target: { closest: () => link }, prevented: false, stopped: false,
275+ preventDefault() { this.prevented = true; }, stopPropagation() { this.stopped = true; }, ...modifiers };
276+ diffs.events.click(e);
277+ return e;
278+ };
279+ try {
280+ window.ermiteMR.start();
281+ const bar = diffs.children[0];
282+ const [button, status] = bar.children;
283+ assert.equal(bar.hidden, true, 'no commentable links in the initial window');
284+ assert.equal(typeof diffs.events.click, 'function', 'delegation installed before lazy load');
285+ await window.ermiteMR.load({ dataset: { src: '/files?from=25' } });
286+ assert.equal(bar.hidden, false, 'lazy window refresh reveals range control');
287+ assert.equal(diffs.dataset.mobile, 'true');
288+ assert.equal(diffs.dataset.diff, 'split', 'preferred layout is retained');
289+ media.matches = false;
290+ resize();
291+ assert.equal(diffs.dataset.mobile, 'false', 'desktop menu returns to preferred layout');
292+ assert.equal(diffs.dataset.diff, 'split');
293+ assert.equal(click(a).prevented, false, 'normal single-line navigation is untouched');
294+ button.events.click();
295+ assert.equal(button.attrs['aria-pressed'], 'true');
296+ assert.equal(click(a, { shiftKey: true }).prevented, false, 'desktop range selection is untouched');
297+ const event = click(a);
298+ assert.equal(event.prevented, true);
299+ assert.equal(event.stopped, true);
300+ assert.ok('data-range-start' in a.attrs);
301+ assert.match(status.textContent, /20/);
302+ click(b);
303+ assert.equal(new URL(assigned).searchParams.get('on'), 'new:10-20:a.go');
304+ assert.equal('data-range-start' in a.attrs, false);
305+ click(a);
306+ button.events.click();
307+ assert.equal(button.attrs['aria-pressed'], 'false');
308+ assert.equal(status.textContent, '');
309+ assert.equal('data-range-start' in a.attrs, false);
310+ assert.equal(click(b).prevented, false);
311+ // The touch media query stops matching: range mode is switched off.
312+ button.events.click();
313+ click(a);
314+ assert.equal(button.attrs['aria-pressed'], 'true');
315+ assert.ok('data-range-start' in a.attrs);
316+ medias[touchQuery].matches = false;
317+ changes[touchQuery]();
318+ assert.equal(button.attrs['aria-pressed'], 'false');
319+ assert.equal(status.textContent, '');
320+ assert.equal('data-range-start' in a.attrs, false);
321+ assert.equal(click(b).prevented, false, 'clicks pass through once touch stops matching');
322+ } finally {
323+ for (const [k, v] of Object.entries(saved)) {
324+ if (v === undefined) delete globalThis[k]; else globalThis[k] = v;
325+ }
326+ }
327+});
328+
329+// Deterministic timers: no sleeps, and each test restores every mocked global.
330+function clock() {
331+ const saved = { setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout };
332+ const tasks = new Map(); let n = 0, now = 0;
333+ globalThis.setTimeout = (fn, delay = 0) => { tasks.set(++n, { fn, at: now + delay }); return n; };
334+ globalThis.clearTimeout = (id) => tasks.delete(id);
335+ return { async tick(ms = 300) { now += ms; const batch = [...tasks].filter(([, t]) => t.at <= now); for (const [id] of batch) tasks.delete(id); for (const [, t] of batch) await t.fn(); },
336+ size: () => tasks.size, restore() { Object.assign(globalThis, saved); } };
337+}
338+
339+test('head refresh defers a real textarea draft, resumes after emptying and blurring', async () => {
340+ const saved = globalThis.document, c = clock();
341+ const text = { value: 'unsent', defaultValue: '' };
342+ let fetched = 0, applied = 0; const notices = [];
343+ globalThis.document = { activeElement: text, querySelectorAll: () => [text] };
344+ const r = window.ermiteMR.refresher({ blocked: window.ermiteMR.hasDraft,
345+ fetchPage: async () => { fetched++; return 'page'; }, apply: () => applied++,
346+ notice: (v) => notices.push(v), failed: () => assert.fail('unexpected fallback') });
347+ try {
348+ r.request(); await c.tick();
349+ assert.equal(fetched, 0); assert.deepEqual(notices, [true]);
350+ text.value = ''; r.resume(); await c.tick();
351+ assert.equal(fetched, 0, 'focus still protects an empty draft');
352+ document.activeElement = null; r.resume(); await c.tick();
353+ assert.equal(fetched, 1); assert.equal(applied, 1); assert.equal(r.pending(), false);
354+ } finally { globalThis.document = saved; c.restore(); }
355+});
356+
357+test('head refresh debounces bursts, serializes requests, and reruns after an in-flight push', async () => {
358+ const c = clock(); let fetched = 0, applied = 0, resolve;
359+ const r = window.ermiteMR.refresher({ blocked: () => false,
360+ fetchPage: () => { fetched++; return new Promise((r) => { resolve = r; }); },
361+ apply: () => applied++, notice() {}, failed: () => assert.fail('unexpected failure') });
362+ try {
363+ r.request(); r.request(); r.request(); assert.equal(c.size(), 1);
364+ await c.tick(299); assert.equal(fetched, 0);
365+ await c.tick(0); assert.equal(fetched, 0);
366+ const first = c.tick(1); assert.equal(fetched, 1);
367+ r.request(); await c.tick(); assert.equal(fetched, 1, 'one fetch in flight');
368+ resolve('one'); await first; assert.equal(applied, 1); assert.equal(c.size(), 1);
369+ const second = c.tick(); assert.equal(fetched, 2); resolve('two'); await second;
370+ assert.equal(applied, 2); assert.equal(c.size(), 0);
371+ } finally { c.restore(); }
372+});
373+
374+test('typing during fetch defers even manual refresh until the draft ends', { timeout: 2000 }, async () => {
375+ const c = clock(); let blocked = false, applied = 0, resolve, fetched = 0; const notices = [];
376+ const r = window.ermiteMR.refresher({ blocked: () => blocked,
377+ fetchPage: () => { fetched++; return new Promise((r) => { resolve = r; }); }, apply: () => { applied++; },
378+ notice: (v) => notices.push(v), failed: () => assert.fail('unexpected failure') });
379+ try {
380+ r.request(); const first = c.tick(); blocked = true; resolve('page'); await first;
381+ assert.equal(applied, 0); assert.deepEqual(notices, [true]);
382+ r.request(true); const manual = c.tick();
383+ assert.equal(fetched, 1, 'manual refresh must not start a fetch during a draft');
384+ await manual; assert.equal(applied, 0);
385+ blocked = false; r.resume(); const second = c.tick(); resolve('new page'); await second;
386+ assert.equal(applied, 1); assert.equal(notices.at(-1), false);
387+ } finally { c.restore(); }
388+});
389+
390+test('fetch failures and non-MR responses take the fallback, never apply', async () => {
391+ const saved = { fetch: globalThis.fetch, location: globalThis.location, DOMParser: globalThis.DOMParser };
392+ const c = clock(); let fallback = 0;
393+ globalThis.location = { href: 'https://forge.test/demo/merge-requests/1?view=split&from=25' };
394+ const r = window.ermiteMR.refresher({ blocked: () => false, fetchPage: window.ermiteMR.fetchPage,
395+ apply: () => assert.fail('must not swap'), notice() {}, failed: () => fallback++ });
396+ try {
397+ for (const result of ['network', { ok: false }, { ok: true, redirected: true }, { ok: true, text: async () => '<html>login</html>' }]) {
398+ globalThis.fetch = async (url, opts) => {
399+ assert.equal(url, location.href); assert.equal(opts.credentials, 'same-origin');
400+ if (result === 'network') throw new Error('offline'); return result;
401+ };
402+ globalThis.DOMParser = class { parseFromString() { return { querySelector: () => null }; } };
403+ r.request(); await c.tick();
404+ }
405+ assert.equal(fallback, 4);
406+ } finally {
407+ for (const [k,v] of Object.entries(saved)) { if (v === undefined) delete globalThis[k]; else globalThis[k] = v; }
408+ c.restore();
409+ }
410+});
411+
412+test('viewport probe finds a stable path/side/line and restores its offset without line scans', () => {
413+ const names = ['document', 'location', 'innerHeight', 'innerWidth', 'scrollY', 'scrollBy', 'scrollTo', 'CSS', 'matchMedia'];
414+ const saved = Object.fromEntries(names.map((k) => [k, globalThis[k]]));
415+ const rect = (top, bottom = top + 20) => ({ top, bottom, left: 0, width: 900 });
416+ let lineTop = 160, headerTop = -200, found = true; const calls = [];
417+ const header = { getBoundingClientRect: () => rect(headerTop) };
418+ const row = { getBoundingClientRect: () => rect(lineTop), querySelector: () => link };
419+ const link = { href: 'https://forge.test/demo/merge-requests/1?on=new:42:a.go#f2', closest: () => row, getAttribute: () => link.href };
420+ const file = { getBoundingClientRect: () => rect(headerTop), id: 'f2', dataset: { file: 'a.go' }, querySelector: (s) => s.includes('summary') ? header : s.includes('href$') ? (assert.ok(s.includes('on=new:42:a.go#f3')), found ? link : null) : null };
421+ const hit = { closest: (s) => s === 'tr' ? row : file };
422+ const diffs = { dataset: { diff: 'unified' }, getBoundingClientRect: () => rect(120), contains: () => true };
423+ globalThis.document = {
424+ querySelector: (s) => s === '#diffs' ? diffs : s === '.hdr' ? { getBoundingClientRect: () => rect(0, 100) } : null,
425+ querySelectorAll: (s) => { assert.equal(s, '#diffs details.dfile', 'only files, never every line'); return found === 'gone' ? [] : [file]; },
426+ elementFromPoint: () => hit, documentElement: { scrollHeight: 10000 },
427+ };
428+ Object.assign(globalThis, { location: { href: link.href }, innerHeight: 800, innerWidth: 1200, scrollY: 2500,
429+ scrollBy: (arg) => calls.push(['by', arg]), scrollTo: (arg) => calls.push(['to', arg]), CSS: { escape: (s) => s }, matchMedia: () => ({ matches: false }) });
430+ try {
431+ const a = window.ermiteMR.scrollAnchor();
432+ assert.equal(a.file, 'a.go'); assert.equal(a.on, 'new:42:a.go'); assert.equal(a.offset, 160);
433+ file.id = 'f3'; lineTop = 260; window.ermiteMR.restoreAnchor(a);
434+ assert.deepEqual(calls.pop(), ['by', { top: 100, behavior: 'instant' }]);
435+ found = false; headerTop = -100; window.ermiteMR.restoreAnchor(a);
436+ assert.deepEqual(calls.pop(), ['by', { top: 100, behavior: 'instant' }]);
437+ found = 'gone'; window.ermiteMR.restoreAnchor({ ...a, y: 20000 });
438+ assert.deepEqual(calls.pop(), ['to', { top: 9200, behavior: 'instant' }]);
439+ } finally { for (const [k,v] of Object.entries(saved)) { if (v === undefined) delete globalThis[k]; else globalThis[k] = v; } }
440+});
441+
442+test('empty edited default and focused cform controls protect drafts', () => {
443+ const saved = document;
444+ try {
445+ globalThis.document = { querySelectorAll: () => [{ value: '', defaultValue: 'existing' }], activeElement: null };
446+ assert.equal(window.ermiteMR.hasDraft(), true);
447+ document.querySelectorAll = () => [];
448+ document.activeElement = { closest: (s) => s === '.cform' ? {} : null };
449+ assert.equal(window.ermiteMR.hasDraft(), true);
450+ } finally { globalThis.document = saved; }
451+});
452+
453+test('failure during a draft keeps pending and resumes after the draft ends', async () => {
454+ const c = clock(); let blocked = false, reject, applied = 0, count = 0;
455+ const r = window.ermiteMR.refresher({ blocked: () => blocked,
456+ fetchPage: () => ++count === 1 ? new Promise((_, no) => { reject = no; }) : Promise.resolve('page'),
457+ apply: () => applied++, notice() {}, failed: () => assert.fail('draft must defer fallback') });
458+ try {
459+ r.request(); const first = c.tick(); blocked = true; reject(new Error('offline')); await first;
460+ assert.equal(r.pending(), true); assert.equal(applied, 0);
461+ blocked = false; r.resume(); await c.tick(); assert.equal(applied, 1);
462+ } finally { c.restore(); }
463+});
464+
465+test('anchor restoration retains the server encoding of tilde and star paths', () => {
466+ const saved = { document, CSS: globalThis.CSS, scrollBy: globalThis.scrollBy };
467+ try {
468+ globalThis.CSS = { escape: (s) => s };
469+ globalThis.scrollBy = () => {};
470+ for (const path of ['a~.go', 'a*.go']) {
471+ const encoded = path.replace('*', '%2A');
472+ const row = { getBoundingClientRect: () => ({ top: 5 }) };
473+ const file = { id: 'f9', dataset: { file: path }, querySelector: (s) => {
474+ if (s === '.dt.uni') return file;
475+ assert.equal(s, 'a[href$="on=new:12:' + encoded + '#f9"]');
476+ return { closest: () => row };
477+ } };
478+ globalThis.document = { querySelectorAll: () => [file], querySelector: () => ({ dataset: { diff: 'unified' } }) };
479+ window.ermiteMR.restoreAnchor({ file: path, suffix: 'new:12:' + encoded, on: 'new:12:' + path, offset: 5 });
480+ }
481+ } finally { Object.assign(globalThis, saved); }
482+});
483+
484+for (const control of [
485+ { type: 'radio', checked: true, defaultChecked: false },
486+ { type: 'checkbox', checked: false, defaultChecked: true },
487+ { type: 'number', name: 'start_line', value: '10', defaultValue: '12' },
488+ { tagName: 'SELECT', options: [{ selected: true, defaultSelected: false }] },
489+]) test(`changed ${control.name || control.type || control.tagName} remains a draft after blur`, () => {
490+ const saved = document;
491+ try {
492+ globalThis.document = { activeElement: null, querySelectorAll(sel) {
493+ assert.equal(sel, ':is(form.cform, .cmt form, form.d-acts) :is(textarea, input, select)');
494+ return [control];
495+ } };
496+ assert.equal(window.ermiteMR.hasDraft(), true);
497+ } finally { globalThis.document = saved; }
498+});
499+
500+test('intercepted Cancel resets the entire comment form', () => {
501+ const saved = Object.fromEntries(['document', 'location', 'addEventListener', 'matchMedia'].map(k => [k, globalThis[k]]));
502+ const c = clock(), handlers = {};
503+ const diffs = { dataset: { view: 'split' }, querySelector() { return null; }, querySelectorAll() { return []; } };
504+ const radio = { type: 'radio', checked: true, defaultChecked: false };
505+ const line = { type: 'number', value: '2', defaultValue: '4' };
506+ let reset = 0, prevented = false;
507+ const form = { reset() { reset++; radio.checked = radio.defaultChecked; line.value = line.defaultValue; }, contains() { return false; } };
508+ const cancel = { closest: () => form };
509+ globalThis.document = { activeElement: null,
510+ querySelector: s => ['#diffs', '#diffs[data-mr]'].includes(s) ? diffs : null,
511+ querySelectorAll: s => s.includes('form.cform') ? [radio, line] : [],
512+ addEventListener: (type, fn) => { handlers[type] = fn; } };
513+ globalThis.location = { pathname: '/demo/merge-requests/1', hash: '', href: 'https://forge.test/demo/merge-requests/1', replace() {} };
514+ globalThis.addEventListener = () => {};
515+ globalThis.matchMedia = () => ({ matches: true, addEventListener() {} });
516+ try {
517+ window.ermiteMR.start();
518+ window.ermiteMR.headChanged();
519+ handlers.click({ target: { closest: () => cancel }, preventDefault() { prevented = true; } });
520+ assert.equal(prevented, true);
521+ assert.equal(reset, 1);
522+ assert.equal(window.ermiteMR.hasDraft(), false);
523+ } finally { Object.assign(globalThis, saved); c.restore(); }
524+});
M internal/adapters/web/mr.go +3 -0
@@ -291,6 +291,7 @@ type MRPage struct {
291291
292292 Commits []MRCommit
293293 CommitsNote string
294+ CommitCount string
294295 Modes []string
295296 ChecksBlock bool
296297 // Checks are the CI checks of the head of the request, ChecksSum their
@@ -734,7 +735,9 @@ func (s *Server) mrReadPresentation(p *MRPage, name string, n int64, res ports.M
734735 })
735736 }
736737 lastRead := res.LastReadID.String()
738+ p.CommitCount = itoa(len(p.Commits))
737739 if res.CommitsTruncated {
740+ p.CommitCount += "+"
738741 p.CommitsNote = "the branch has more commits than the " + itoa(len(p.Commits)) + " listed."
739742 }
740743
M internal/adapters/web/mr.templ +141 -112
@@ -13,6 +13,7 @@ templ MRList(l Layout, p MRListPage) {
1313 @Shell(l, mrListRail(p)) {
1414 if p.Live {
1515 @LiveSlot("mr-notice")
16+ <div id="mr-refresh-status" role="status" aria-live="polite"></div>
1617 }
1718 <div class="filter">
1819 <label for="mr-filter" class="sr">filter</label>
@@ -108,8 +109,9 @@ templ mrListRail(p MRListPage) {
108109 }
109110
110111 // MergeRequest is a merge request: its facts, its description, the diff with
111-// the notes under their lines, the general comments; and in the rail the merge
112-// form, the checks, the settings, the commits and the files.
112+// the notes under their lines, the general comments. The merge and More menus
113+// sit in the toolbar, the commits and checks in the card footer, and the rail
114+// holds the files and verdicts.
113115 templ MergeRequest(l Layout, p MRPage) {
114116 @Shell(l, mrRail(p)) {
115117 <section class="mr-card">
@@ -147,6 +149,7 @@ templ MergeRequest(l Layout, p MRPage) {
147149 @templ.Raw(string(p.Desc))
148150 </article>
149151 }
152+ @mrFooter(p)
150153 </section>
151154 if p.Deleting != "" {
152155 <p class="note" role="status">merged. the branch { p.Deleting } is being deleted.</p>
@@ -155,35 +158,40 @@ templ MergeRequest(l Layout, p MRPage) {
155158 <p class="d-err" role="alert">{ p.MergeError }</p>
156159 }
157160 @LiveSlot("mr-notice")
158- if p.Since != nil {
159- <nav class="since" aria-label="Diff range">
160- <a
161- href={ templ.SafeURL(p.Since.AllHref) }
162- if !p.Since.Current {
163- aria-current="page"
164- }
165- >all changes <span class="mute">vs { p.Target }</span></a>
166- <a
167- href={ templ.SafeURL(p.Since.SinceHref) }
168- if p.Since.Current {
169- aria-current="page"
170- }
171- >since your last read <span class="mute">{ p.Since.ReadShort } → { p.Since.HeadShort }</span></a>
172- </nav>
173- }
174- if p.SinceNote != "" {
175- <p class="notice">{ p.SinceNote }</p>
176- }
161+ <div id="mr-refresh-status" role="status" aria-live="polite"></div>
162+ <div id="mr-range" hidden?={ p.Since == nil && p.SinceNote == "" }>
163+ if p.Since != nil {
164+ <nav class="since" aria-label="Diff range">
165+ <a
166+ href={ templ.SafeURL(p.Since.AllHref) }
167+ if !p.Since.Current {
168+ aria-current="page"
169+ }
170+ >all changes <span class="mute">vs { p.Target }</span></a>
171+ <a
172+ href={ templ.SafeURL(p.Since.SinceHref) }
173+ if p.Since.Current {
174+ aria-current="page"
175+ }
176+ >since your last read <span class="mute">{ p.Since.ReadShort } → { p.Since.HeadShort }</span></a>
177+ </nav>
178+ }
179+ if p.SinceNote != "" {
180+ <p class="notice">{ p.SinceNote }</p>
181+ }
182+ </div>
177183 @mrBar(p)
178- if p.Invisible {
179- <p class="notice">
180- this diff holds invisible characters (direction controls, zero-width characters). They are shown as
181- <span class="inv">&lt;U+XXXX&gt;</span> markers, never applied.
182- </p>
183- }
184- if p.Note != "" {
185- <p class="notice">{ p.Note }</p>
186- }
184+ <div id="mr-diff-notices" hidden?={ !p.Invisible && p.Note == "" }>
185+ if p.Invisible {
186+ <p class="notice">
187+ this diff holds invisible characters (direction controls, zero-width characters). They are shown as
188+ <span class="inv">&lt;U+XXXX&gt;</span> markers, never applied.
189+ </p>
190+ }
191+ if p.Note != "" {
192+ <p class="notice">{ p.Note }</p>
193+ }
194+ </div>
187195 <div
188196 id="diffs"
189197 data-mr
@@ -206,6 +214,7 @@ templ MergeRequest(l Layout, p MRPage) {
206214 <p class="note">no changed files.</p>
207215 }
208216 </div>
217+ <div id="mr-comments">
209218 <h2 class="rule-h" id="comments"><span>Comments</span><span class="count">{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2>
210219 for _, c := range p.Loose {
211220 @commentView(c, "")
@@ -219,6 +228,7 @@ templ MergeRequest(l Layout, p MRPage) {
219228 if p.GeneralForm != nil {
220229 @commentForm(*p.GeneralForm, "")
221230 }
231+ </div>
222232 }
223233 }
224234
@@ -366,6 +376,62 @@ templ commentForm(f CommentForm, suffix string) {
366376 </form>
367377 }
368378
379+templ mrFooter(p MRPage) {
380+ <footer class="mr-footer">
381+ <nav data-mr-tabs aria-label="Branch details">
382+ <a id="mr-tab-commits" href="#mr-commits">Commits <span class="count">{ p.CommitCount }</span></a>
383+ <a id="mr-tab-checks" href="#mr-checks">
384+ { "Checks " }
385+ <span data-checks-label>
386+ if p.ChecksSum.Total > 0 {
387+ { p.ChecksSum.Title }
388+ } else {
389+ no checks
390+ }
391+ </span>
392+ </a>
393+ </nav>
394+ <div id="mr-commits" data-mr-panel role="region" aria-labelledby="mr-tab-commits">
395+ <table class="mr-commits">
396+ <thead><tr><th>Commit</th><th>Subject</th><th>Author</th><th>Date</th><th>Signature</th><th>Status</th></tr></thead>
397+ <tbody>
398+ for _, c := range p.Commits {
399+ <tr data-new?={ c.New }>
400+ <td><a href={ templ.SafeURL(c.Href) }>{ c.Short }</a></td>
401+ <td>{ c.Subject }</td>
402+ <td>{ c.Author }</td>
403+ <td>
404+ if c.WhenAt != "" {
405+ <time datetime={ c.WhenAt }>{ c.When }</time>
406+ }
407+ </td>
408+ <td>
409+ if c.Sig != nil {
410+ @signaturePill(c.Sig)
411+ }
412+ </td>
413+ <td>
414+ if c.New {
415+ <span>new</span>
416+ }
417+ </td>
418+ </tr>
419+ }
420+ </tbody>
421+ </table>
422+ if len(p.Commits) == 0 {
423+ <p class="dim">no commit to merge.</p>
424+ }
425+ if p.CommitsNote != "" {
426+ <p class="note">{ p.CommitsNote }</p>
427+ }
428+ </div>
429+ <div id="mr-checks-panel" data-mr-panel role="region" aria-labelledby="mr-tab-checks">
430+ @MRChecks(p.Checks)
431+ </div>
432+ </footer>
433+}
434+
369435 templ mrRail(p MRPage) {
370436 <section aria-labelledby="r-files" class="r-files">
371437 <h2 id="r-files">
@@ -391,51 +457,6 @@ templ mrRail(p MRPage) {
391457 </ul>
392458 </section>
393459 }
394- @MRChecks(p.Checks)
395- <section aria-labelledby="r-commits">
396- <h2 id="r-commits">Commits <span class="count">{ itoa(len(p.Commits)) }</span></h2>
397- <ul class="pcommits">
398- for _, c := range p.Commits {
399- <li data-new?={ c.New }>
400- <a href={ templ.SafeURL(c.Href) }>{ c.Short }</a>
401- <span class="s">{ c.Subject }</span>
402- if c.Sig != nil {
403- @signaturePill(c.Sig)
404- }
405- <span class="mute">
406- { c.Author }
407- if c.WhenAt != "" {
408- { " · " }<time datetime={ c.WhenAt }>{ c.When }</time>
409- }
410- if c.New {
411- { " · new" }
412- }
413- </span>
414- </li>
415- }
416- </ul>
417- if len(p.Commits) == 0 {
418- <p class="dim">no commit to merge.</p>
419- }
420- if p.CommitsNote != "" {
421- <p class="note">{ p.CommitsNote }</p>
422- }
423- </section>
424- <section aria-labelledby="r-settings">
425- <h2 id="r-settings">Settings</h2>
426- <dl class="kv">
427- <dt>modes</dt>
428- <dd>{ strings.Join(p.Modes, ", ") }</dd>
429- <dt>checks</dt>
430- <dd>
431- if p.ChecksBlock {
432- failing ones block the merge
433- } else {
434- do not block the merge
435- }
436- </dd>
437- </dl>
438- </section>
439460 }
440461
441462 // mrBar is the bar above the diff, which follows the review: the totals, the
@@ -450,8 +471,8 @@ templ mrBar(p MRPage) {
450471 @MRChecksSum(p.ChecksSum)
451472 if p.Box != nil {
452473 @mergeMenu(p)
453- @moreMenu(p)
454474 }
475+ @moreMenu(p)
455476 </div>
456477 }
457478
@@ -474,18 +495,9 @@ templ mergeMenu(p MRPage) {
474495 <kbd aria-hidden="true">M</kbd>
475496 </summary>
476497 <div class="menu-pop" role="group" aria-label="Merge">
477- <dl class="kv">
478- <dt>head</dt>
479- <dd>
480- if p.Box.HeadGone {
481- gone: the source branch was deleted
482- } else {
483- { p.HeadShort }
484- }
485- </dd>
486- <dt>into</dt>
487- <dd>{ p.Target }</dd>
488- </dl>
498+ if p.Box.HeadGone {
499+ <p class="dim">the source branch was deleted</p>
500+ }
489501 if p.Box.Error != "" {
490502 <p class="d-err" role="alert">{ p.Box.Error }</p>
491503 }
@@ -519,15 +531,6 @@ templ mergeMenu(p MRPage) {
519531 </label>
520532 }
521533 </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- }
531534 if p.Box.CanDeleteBranch {
532535 <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>
533536 }
@@ -589,26 +592,52 @@ templ MRMergeabilityFragment(v MergeabilityView) {
589592 @mrMergeabilitySlot(v)
590593 }
591594
592-// moreMenu is what else the owner does with an open request: mark the head
593-// read, close it, merge it from a terminal.
595+// moreMenu shows read-only settings to every viewer, and keeps owner actions
596+// behind the same merge-box permission as before.
594597 templ moreMenu(p MRPage) {
595598 <details class="menu omenu">
596599 <summary class="btn icon" aria-label="More actions" title="more actions">⋯</summary>
597600 <div class="menu-pop" role="group" aria-label="More actions">
598- if p.Box.CanMarkRead {
599- <form method="post" action={ templ.SafeURL(p.Box.ReadAction) }>
600- <input type="hidden" name="head_id" value={ p.Box.Head }/>
601- <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>
601+ <section class="merge-settings">
602+ <h2>Merge settings</h2>
603+ <dl class="kv">
604+ <dt>modes</dt>
605+ <dd>{ strings.Join(p.Modes, ", ") }</dd>
606+ <dt>checks</dt>
607+ <dd>
608+ if p.ChecksBlock {
609+ failing ones block the merge
610+ } else {
611+ do not block the merge
612+ }
613+ </dd>
614+ </dl>
615+ </section>
616+ if p.Box != nil {
617+ if p.Box.Signed {
618+ <p class="d-note" id="mr-signing">
619+ squash and merge commits are signed with ermite's own key (generated on first start).
620+ if p.Box.Fingerprint != "" {
621+ fingerprint <code>{ p.Box.Fingerprint }</code>
622+ }
623+ <code>ermite signing-key show</code> prints it.
624+ </p>
625+ }
626+ if p.Box.CanMarkRead {
627+ <form method="post" action={ templ.SafeURL(p.Box.ReadAction) }>
628+ <input type="hidden" name="head_id" value={ p.Box.Head }/>
629+ <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>
630+ </form>
631+ }
632+ <form method="post" action={ templ.SafeURL(p.Box.CloseAction) }>
633+ <button type="submit" title="close without merging; the branch is kept" class="btn danger">Close</button>
602634 </form>
603- }
604- <form method="post" action={ templ.SafeURL(p.Box.CloseAction) }>
605- <button type="submit" title="close without merging; the branch is kept" class="btn danger">Close</button>
606- </form>
607- if !p.Box.HeadGone && p.Box.ReadyAction == "" {
608- <details class="d-or">
609- <summary class="mute">or from a terminal</summary>
610- @CloneBlock(p.Box.CLI)
611- </details>
635+ if !p.Box.HeadGone && p.Box.ReadyAction == "" {
636+ <details class="d-or">
637+ <summary class="mute">or from a terminal</summary>
638+ @CloneBlock(p.Box.CLI)
639+ </details>
640+ }
612641 }
613642 </div>
614643 </details>
M internal/adapters/web/mr_templ.go +938 -894
@@ -55,26 +55,30 @@ func MRList(l Layout, p MRListPage) templ.Component {
5555 if templ_7745c5c3_Err != nil {
5656 return templ_7745c5c3_Err
5757 }
58+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <div id=\"mr-refresh-status\" role=\"status\" aria-live=\"polite\"></div>")
59+ if templ_7745c5c3_Err != nil {
60+ return templ_7745c5c3_Err
61+ }
5862 }
59- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " <div class=\"filter\"><label for=\"mr-filter\" class=\"sr\">filter</label> <input id=\"mr-filter\" type=\"search\" data-filter=\".mrs tbody tr\" placeholder=\"Filter by title, branch, author or state…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div>")
63+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div class=\"filter\"><label for=\"mr-filter\" class=\"sr\">filter</label> <input id=\"mr-filter\" type=\"search\" data-filter=\".mrs tbody tr\" placeholder=\"Filter by title, branch, author or state…\" autocomplete=\"off\" hx-on:input=\"ermite.filter(this)\"></div>")
6064 if templ_7745c5c3_Err != nil {
6165 return templ_7745c5c3_Err
6266 }
6367 if p.Newest == "" {
64- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2 class=\"rule-h\"><span>Open</span><span class=\"count\">")
68+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h2 class=\"rule-h\"><span>Open</span><span class=\"count\">")
6569 if templ_7745c5c3_Err != nil {
6670 return templ_7745c5c3_Err
6771 }
6872 var templ_7745c5c3_Var3 string
6973 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Open)))
7074 if templ_7745c5c3_Err != nil {
71- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 29, Col: 78}
75+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 30, Col: 78}
7276 }
7377 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
7478 if templ_7745c5c3_Err != nil {
7579 return templ_7745c5c3_Err
7680 }
77- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span></h2>")
81+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></h2>")
7882 if templ_7745c5c3_Err != nil {
7983 return templ_7745c5c3_Err
8084 }
@@ -82,54 +86,54 @@ func MRList(l Layout, p MRListPage) templ.Component {
8286 if templ_7745c5c3_Err != nil {
8387 return templ_7745c5c3_Err
8488 }
85- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
89+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ")
8690 if templ_7745c5c3_Err != nil {
8791 return templ_7745c5c3_Err
8892 }
8993 if len(p.Open) == 0 {
90- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"lede dim\">no open merge requests.</p>")
94+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"lede dim\">no open merge requests.</p>")
9195 if templ_7745c5c3_Err != nil {
9296 return templ_7745c5c3_Err
9397 }
9498 }
95- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
99+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
96100 if templ_7745c5c3_Err != nil {
97101 return templ_7745c5c3_Err
98102 }
99103 if p.OpenNote != "" {
100- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"note\">")
104+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"note\">")
101105 if templ_7745c5c3_Err != nil {
102106 return templ_7745c5c3_Err
103107 }
104108 var templ_7745c5c3_Var4 string
105109 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p.OpenNote)
106110 if templ_7745c5c3_Err != nil {
107- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 35, Col: 32}
111+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 36, Col: 32}
108112 }
109113 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
110114 if templ_7745c5c3_Err != nil {
111115 return templ_7745c5c3_Err
112116 }
113- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p>")
117+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p>")
114118 if templ_7745c5c3_Err != nil {
115119 return templ_7745c5c3_Err
116120 }
117121 }
118122 }
119- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " <h2 class=\"rule-h\"><span>Merged and closed</span><span class=\"count\">")
123+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " <h2 class=\"rule-h\"><span>Merged and closed</span><span class=\"count\">")
120124 if templ_7745c5c3_Err != nil {
121125 return templ_7745c5c3_Err
122126 }
123127 var templ_7745c5c3_Var5 string
124128 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Done)))
125129 if templ_7745c5c3_Err != nil {
126- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 38, Col: 90}
130+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 39, Col: 90}
127131 }
128132 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
129133 if templ_7745c5c3_Err != nil {
130134 return templ_7745c5c3_Err
131135 }
132- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span></h2>")
136+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></h2>")
133137 if templ_7745c5c3_Err != nil {
134138 return templ_7745c5c3_Err
135139 }
@@ -137,74 +141,74 @@ func MRList(l Layout, p MRListPage) templ.Component {
137141 if templ_7745c5c3_Err != nil {
138142 return templ_7745c5c3_Err
139143 }
140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ")
144+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
141145 if templ_7745c5c3_Err != nil {
142146 return templ_7745c5c3_Err
143147 }
144148 if len(p.Done) == 0 {
145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<p class=\"lede dim\">nothing merged or closed yet.</p>")
149+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"lede dim\">nothing merged or closed yet.</p>")
146150 if templ_7745c5c3_Err != nil {
147151 return templ_7745c5c3_Err
148152 }
149153 }
150- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
154+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ")
151155 if templ_7745c5c3_Err != nil {
152156 return templ_7745c5c3_Err
153157 }
154158 if p.Older != "" || p.Newest != "" {
155- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<nav class=\"pager\" aria-label=\"Pages\">")
159+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<nav class=\"pager\" aria-label=\"Pages\">")
156160 if templ_7745c5c3_Err != nil {
157161 return templ_7745c5c3_Err
158162 }
159163 if p.Newest != "" {
160- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<a class=\"btn\" href=\"")
164+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a class=\"btn\" href=\"")
161165 if templ_7745c5c3_Err != nil {
162166 return templ_7745c5c3_Err
163167 }
164168 var templ_7745c5c3_Var6 templ.SafeURL
165169 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest))
166170 if templ_7745c5c3_Err != nil {
167- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 46, Col: 50}
171+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 47, Col: 50}
168172 }
169173 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
170174 if templ_7745c5c3_Err != nil {
171175 return templ_7745c5c3_Err
172176 }
173- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">← Newest</a> ")
177+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">← Newest</a> ")
174178 if templ_7745c5c3_Err != nil {
175179 return templ_7745c5c3_Err
176180 }
177181 } else {
178- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ")
182+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"btn\" aria-disabled=\"true\">← Newest</span> ")
179183 if templ_7745c5c3_Err != nil {
180184 return templ_7745c5c3_Err
181185 }
182186 }
183187 if p.Older != "" {
184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a class=\"btn\" href=\"")
188+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<a class=\"btn\" href=\"")
185189 if templ_7745c5c3_Err != nil {
186190 return templ_7745c5c3_Err
187191 }
188192 var templ_7745c5c3_Var7 templ.SafeURL
189193 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older))
190194 if templ_7745c5c3_Err != nil {
191- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 51, Col: 49}
195+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 52, Col: 49}
192196 }
193197 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
194198 if templ_7745c5c3_Err != nil {
195199 return templ_7745c5c3_Err
196200 }
197- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" rel=\"next\">Older →</a>")
201+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" rel=\"next\">Older →</a>")
198202 if templ_7745c5c3_Err != nil {
199203 return templ_7745c5c3_Err
200204 }
201205 } else {
202- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>")
206+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"btn\" aria-disabled=\"true\">Older →</span>")
203207 if templ_7745c5c3_Err != nil {
204208 return templ_7745c5c3_Err
205209 }
206210 }
207- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</nav>")
211+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</nav>")
208212 if templ_7745c5c3_Err != nil {
209213 return templ_7745c5c3_Err
210214 }
@@ -241,180 +245,180 @@ func mrRows(rows []MRRow, label string) templ.Component {
241245 }
242246 ctx = templ.ClearChildren(ctx)
243247 if len(rows) > 0 {
244- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<table class=\"list mrs\" aria-label=\"")
248+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<table class=\"list mrs\" aria-label=\"")
245249 if templ_7745c5c3_Err != nil {
246250 return templ_7745c5c3_Err
247251 }
248252 var templ_7745c5c3_Var9 string
249253 templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(label)
250254 if templ_7745c5c3_Err != nil {
251- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 62, Col: 44}
255+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 63, Col: 44}
252256 }
253257 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
254258 if templ_7745c5c3_Err != nil {
255259 return templ_7745c5c3_Err
256260 }
257- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><tbody data-rows>")
261+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><tbody data-rows>")
258262 if templ_7745c5c3_Err != nil {
259263 return templ_7745c5c3_Err
260264 }
261265 for _, m := range rows {
262- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<tr")
266+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr")
263267 if templ_7745c5c3_Err != nil {
264268 return templ_7745c5c3_Err
265269 }
266270 if m.Cursor {
267- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " data-cursor")
271+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " data-cursor")
268272 if templ_7745c5c3_Err != nil {
269273 return templ_7745c5c3_Err
270274 }
271275 }
272- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " data-state=\"")
276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " data-state=\"")
273277 if templ_7745c5c3_Err != nil {
274278 return templ_7745c5c3_Err
275279 }
276280 var templ_7745c5c3_Var10 string
277281 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.State)
278282 if templ_7745c5c3_Err != nil {
279- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 55}
283+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 55}
280284 }
281285 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
282286 if templ_7745c5c3_Err != nil {
283287 return templ_7745c5c3_Err
284288 }
285- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"><td class=\"c-n mute\">")
289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"><td class=\"c-n mute\">")
286290 if templ_7745c5c3_Err != nil {
287291 return templ_7745c5c3_Err
288292 }
289293 var templ_7745c5c3_Var11 string
290294 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number)
291295 if templ_7745c5c3_Err != nil {
292- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 37}
296+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 37}
293297 }
294298 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
295299 if templ_7745c5c3_Err != nil {
296300 return templ_7745c5c3_Err
297301 }
298- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</td><td class=\"c-state\">")
302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-state\">")
299303 if templ_7745c5c3_Err != nil {
300304 return templ_7745c5c3_Err
301305 }
302306 var templ_7745c5c3_Var12 string
303307 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(m.State)
304308 if templ_7745c5c3_Err != nil {
305- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 35}
309+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 35}
306310 }
307311 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
308312 if templ_7745c5c3_Err != nil {
309313 return templ_7745c5c3_Err
310314 }
311- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-subject\"><a href=\"")
315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"c-subject\"><a href=\"")
312316 if templ_7745c5c3_Err != nil {
313317 return templ_7745c5c3_Err
314318 }
315319 var templ_7745c5c3_Var13 templ.SafeURL
316320 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href))
317321 if templ_7745c5c3_Err != nil {
318- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 59}
322+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 59}
319323 }
320324 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
321325 if templ_7745c5c3_Err != nil {
322326 return templ_7745c5c3_Err
323327 }
324- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">")
328+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")
325329 if templ_7745c5c3_Err != nil {
326330 return templ_7745c5c3_Err
327331 }
328332 var templ_7745c5c3_Var14 string
329333 templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title)
330334 if templ_7745c5c3_Err != nil {
331- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 71}
335+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 71}
332336 }
333337 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
334338 if templ_7745c5c3_Err != nil {
335339 return templ_7745c5c3_Err
336340 }
337- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a> <span class=\"mute\">")
341+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</a> <span class=\"mute\">")
338342 if templ_7745c5c3_Err != nil {
339343 return templ_7745c5c3_Err
340344 }
341345 var templ_7745c5c3_Var15 string
342346 templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(m.Branches)
343347 if templ_7745c5c3_Err != nil {
344- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 109}
348+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 109}
345349 }
346350 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
347351 if templ_7745c5c3_Err != nil {
348352 return templ_7745c5c3_Err
349353 }
350- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</span></td><td class=\"c-who dim\">")
354+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span></td><td class=\"c-who dim\">")
351355 if templ_7745c5c3_Err != nil {
352356 return templ_7745c5c3_Err
353357 }
354358 var templ_7745c5c3_Var16 string
355359 templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author)
356360 if templ_7745c5c3_Err != nil {
357- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 69, Col: 38}
361+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 70, Col: 38}
358362 }
359363 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
360364 if templ_7745c5c3_Err != nil {
361365 return templ_7745c5c3_Err
362366 }
363- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</td><td class=\"c-when nowrap dim\">")
367+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</td><td class=\"c-when nowrap dim\">")
364368 if templ_7745c5c3_Err != nil {
365369 return templ_7745c5c3_Err
366370 }
367371 if m.WhenAt != "" {
368- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<time datetime=\"")
372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<time datetime=\"")
369373 if templ_7745c5c3_Err != nil {
370374 return templ_7745c5c3_Err
371375 }
372376 var templ_7745c5c3_Var17 string
373377 templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
374378 if templ_7745c5c3_Err != nil {
375- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 72, Col: 33}
379+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 73, Col: 33}
376380 }
377381 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
378382 if templ_7745c5c3_Err != nil {
379383 return templ_7745c5c3_Err
380384 }
381- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" title=\"")
385+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" title=\"")
382386 if templ_7745c5c3_Err != nil {
383387 return templ_7745c5c3_Err
384388 }
385389 var templ_7745c5c3_Var18 string
386390 templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt)
387391 if templ_7745c5c3_Err != nil {
388- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 72, Col: 52}
392+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 73, Col: 52}
389393 }
390394 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
391395 if templ_7745c5c3_Err != nil {
392396 return templ_7745c5c3_Err
393397 }
394- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">")
398+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\">")
395399 if templ_7745c5c3_Err != nil {
396400 return templ_7745c5c3_Err
397401 }
398402 var templ_7745c5c3_Var19 string
399403 templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.When)
400404 if templ_7745c5c3_Err != nil {
401- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 72, Col: 63}
405+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 73, Col: 63}
402406 }
403407 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
404408 if templ_7745c5c3_Err != nil {
405409 return templ_7745c5c3_Err
406410 }
407- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</time>")
411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</time>")
408412 if templ_7745c5c3_Err != nil {
409413 return templ_7745c5c3_Err
410414 }
411415 }
412- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td></tr>")
416+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</td></tr>")
413417 if templ_7745c5c3_Err != nil {
414418 return templ_7745c5c3_Err
415419 }
416420 }
417- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</tbody></table>")
421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</tbody></table>")
418422 if templ_7745c5c3_Err != nil {
419423 return templ_7745c5c3_Err
420424 }
@@ -444,66 +448,66 @@ func mrListRail(p MRListPage) templ.Component {
444448 templ_7745c5c3_Var20 = templ.NopComponent
445449 }
446450 ctx = templ.ClearChildren(ctx)
447- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>awaits review</dd><dt>merged</dt><dd>the target took the branch</dd><dt>closed</dt><dd>refused, branch kept</dd></dl></section><section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2>")
451+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">States</h2><dl class=\"kv\"><dt>open</dt><dd>awaits review</dd><dt>merged</dt><dd>the target took the branch</dd><dt>closed</dt><dd>refused, branch kept</dd></dl></section><section aria-labelledby=\"r-new\"><h2 id=\"r-new\">Open one</h2>")
448452 if templ_7745c5c3_Err != nil {
449453 return templ_7745c5c3_Err
450454 }
451455 if len(p.Branches) > 0 {
452- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<p class=\"dim\">compare a branch with ")
456+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<p class=\"dim\">compare a branch with ")
453457 if templ_7745c5c3_Err != nil {
454458 return templ_7745c5c3_Err
455459 }
456460 var templ_7745c5c3_Var21 string
457461 templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(p.Default)
458462 if templ_7745c5c3_Err != nil {
459- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 97, Col: 51}
463+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 98, Col: 51}
460464 }
461465 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
462466 if templ_7745c5c3_Err != nil {
463467 return templ_7745c5c3_Err
464468 }
465- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, ":</p><nav class=\"outline\" aria-label=\"Branches\">")
469+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ":</p><nav class=\"outline\" aria-label=\"Branches\">")
466470 if templ_7745c5c3_Err != nil {
467471 return templ_7745c5c3_Err
468472 }
469473 for _, b := range p.Branches {
470- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<a href=\"")
474+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<a href=\"")
471475 if templ_7745c5c3_Err != nil {
472476 return templ_7745c5c3_Err
473477 }
474478 var templ_7745c5c3_Var22 templ.SafeURL
475479 templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href))
476480 if templ_7745c5c3_Err != nil {
477- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 100, Col: 36}
481+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 101, Col: 36}
478482 }
479483 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
480484 if templ_7745c5c3_Err != nil {
481485 return templ_7745c5c3_Err
482486 }
483- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\">")
487+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">")
484488 if templ_7745c5c3_Err != nil {
485489 return templ_7745c5c3_Err
486490 }
487491 var templ_7745c5c3_Var23 string
488492 templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(b.Name)
489493 if templ_7745c5c3_Err != nil {
490- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 100, Col: 47}
494+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 101, Col: 47}
491495 }
492496 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
493497 if templ_7745c5c3_Err != nil {
494498 return templ_7745c5c3_Err
495499 }
496- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</a>")
500+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a>")
497501 if templ_7745c5c3_Err != nil {
498502 return templ_7745c5c3_Err
499503 }
500504 }
501- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</nav>")
505+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</nav>")
502506 if templ_7745c5c3_Err != nil {
503507 return templ_7745c5c3_Err
504508 }
505509 } else {
506- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<p class=\"dim\">there is no other branch to merge yet.</p>")
510+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<p class=\"dim\">there is no other branch to merge yet.</p>")
507511 if templ_7745c5c3_Err != nil {
508512 return templ_7745c5c3_Err
509513 }
@@ -512,7 +516,7 @@ func mrListRail(p MRListPage) templ.Component {
512516 if templ_7745c5c3_Err != nil {
513517 return templ_7745c5c3_Err
514518 }
515- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</section>")
519+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</section>")
516520 if templ_7745c5c3_Err != nil {
517521 return templ_7745c5c3_Err
518522 }
@@ -521,8 +525,9 @@ func mrListRail(p MRListPage) templ.Component {
521525 }
522526
523527 // MergeRequest is a merge request: its facts, its description, the diff with
524-// the notes under their lines, the general comments; and in the rail the merge
525-// form, the checks, the settings, the commits and the files.
528+// the notes under their lines, the general comments. The merge and More menus
529+// sit in the toolbar, the commits and checks in the card footer, and the rail
530+// holds the files and verdicts.
526531 func MergeRequest(l Layout, p MRPage) templ.Component {
527532 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
528533 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -556,59 +561,59 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
556561 }()
557562 }
558563 ctx = templ.InitializeContext(ctx)
559- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")
564+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<section class=\"mr-card\"><header class=\"show mr-h\"><h1><span class=\"mute\">")
560565 if templ_7745c5c3_Err != nil {
561566 return templ_7745c5c3_Err
562567 }
563568 var templ_7745c5c3_Var26 string
564569 templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number)
565570 if templ_7745c5c3_Err != nil {
566- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 37}
571+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 37}
567572 }
568573 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
569574 if templ_7745c5c3_Err != nil {
570575 return templ_7745c5c3_Err
571576 }
572- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span> ")
577+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span> ")
573578 if templ_7745c5c3_Err != nil {
574579 return templ_7745c5c3_Err
575580 }
576581 var templ_7745c5c3_Var27 string
577582 templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title)
578583 if templ_7745c5c3_Err != nil {
579- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 117, Col: 56}
584+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 56}
580585 }
581586 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
582587 if templ_7745c5c3_Err != nil {
583588 return templ_7745c5c3_Err
584589 }
585- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"")
590+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"")
586591 if templ_7745c5c3_Err != nil {
587592 return templ_7745c5c3_Err
588593 }
589594 var templ_7745c5c3_Var28 string
590595 templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State)
591596 if templ_7745c5c3_Err != nil {
592- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 120, Col: 46}
597+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 122, Col: 46}
593598 }
594599 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
595600 if templ_7745c5c3_Err != nil {
596601 return templ_7745c5c3_Err
597602 }
598- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">")
603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">")
599604 if templ_7745c5c3_Err != nil {
600605 return templ_7745c5c3_Err
601606 }
602607 var templ_7745c5c3_Var29 string
603608 templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.State)
604609 if templ_7745c5c3_Err != nil {
605- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 15}
610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 15}
606611 }
607612 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
608613 if templ_7745c5c3_Err != nil {
609614 return templ_7745c5c3_Err
610615 }
611- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " ")
616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " ")
612617 if templ_7745c5c3_Err != nil {
613618 return templ_7745c5c3_Err
614619 }
@@ -616,53 +621,53 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
616621 var templ_7745c5c3_Var30 string
617622 templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
618623 if templ_7745c5c3_Err != nil {
619- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 15}
624+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 125, Col: 15}
620625 }
621626 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
622627 if templ_7745c5c3_Err != nil {
623628 return templ_7745c5c3_Err
624629 }
625- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<a href=\"")
630+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<a href=\"")
626631 if templ_7745c5c3_Err != nil {
627632 return templ_7745c5c3_Err
628633 }
629634 var templ_7745c5c3_Var31 templ.SafeURL
630635 templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.MergedHref))
631636 if templ_7745c5c3_Err != nil {
632- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 54}
637+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 125, Col: 54}
633638 }
634639 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
635640 if templ_7745c5c3_Err != nil {
636641 return templ_7745c5c3_Err
637642 }
638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\">")
643+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\">")
639644 if templ_7745c5c3_Err != nil {
640645 return templ_7745c5c3_Err
641646 }
642647 var templ_7745c5c3_Var32 string
643648 templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergedShort)
644649 if templ_7745c5c3_Err != nil {
645- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 72}
650+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 125, Col: 72}
646651 }
647652 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
648653 if templ_7745c5c3_Err != nil {
649654 return templ_7745c5c3_Err
650655 }
651- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</a>")
656+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</a>")
652657 if templ_7745c5c3_Err != nil {
653658 return templ_7745c5c3_Err
654659 }
655660 var templ_7745c5c3_Var33 string
656661 templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + p.MergeMode)
657662 if templ_7745c5c3_Err != nil {
658- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 123, Col: 100}
663+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 125, Col: 100}
659664 }
660665 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
661666 if templ_7745c5c3_Err != nil {
662667 return templ_7745c5c3_Err
663668 }
664669 }
665- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</dd><dt>branch</dt><dd>")
670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</dd><dt>branch</dt><dd>")
666671 if templ_7745c5c3_Err != nil {
667672 return templ_7745c5c3_Err
668673 }
@@ -673,7 +678,7 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
673678 var templ_7745c5c3_Var34 string
674679 templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" → ")
675680 if templ_7745c5c3_Err != nil {
676- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 129, Col: 15}
681+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 131, Col: 15}
677682 }
678683 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
679684 if templ_7745c5c3_Err != nil {
@@ -683,20 +688,20 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
683688 if templ_7745c5c3_Err != nil {
684689 return templ_7745c5c3_Err
685690 }
686- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</dd><dt>opened</dt><dd>")
691+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</dd><dt>opened</dt><dd>")
687692 if templ_7745c5c3_Err != nil {
688693 return templ_7745c5c3_Err
689694 }
690695 var templ_7745c5c3_Var35 string
691696 templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name)
692697 if templ_7745c5c3_Err != nil {
693- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 21}
698+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 21}
694699 }
695700 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
696701 if templ_7745c5c3_Err != nil {
697702 return templ_7745c5c3_Err
698703 }
699- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " ")
704+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ")
700705 if templ_7745c5c3_Err != nil {
701706 return templ_7745c5c3_Err
702707 }
@@ -704,98 +709,98 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
704709 var templ_7745c5c3_Var36 string
705710 templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
706711 if templ_7745c5c3_Err != nil {
707- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 15}
712+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 15}
708713 }
709714 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
710715 if templ_7745c5c3_Err != nil {
711716 return templ_7745c5c3_Err
712717 }
713- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<time datetime=\"")
718+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<time datetime=\"")
714719 if templ_7745c5c3_Err != nil {
715720 return templ_7745c5c3_Err
716721 }
717722 var templ_7745c5c3_Var37 string
718723 templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt)
719724 if templ_7745c5c3_Err != nil {
720- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 49}
725+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 49}
721726 }
722727 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
723728 if templ_7745c5c3_Err != nil {
724729 return templ_7745c5c3_Err
725730 }
726- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" title=\"")
731+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" title=\"")
727732 if templ_7745c5c3_Err != nil {
728733 return templ_7745c5c3_Err
729734 }
730735 var templ_7745c5c3_Var38 string
731736 templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs)
732737 if templ_7745c5c3_Err != nil {
733- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 72}
738+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 72}
734739 }
735740 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
736741 if templ_7745c5c3_Err != nil {
737742 return templ_7745c5c3_Err
738743 }
739- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\">")
744+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">")
740745 if templ_7745c5c3_Err != nil {
741746 return templ_7745c5c3_Err
742747 }
743748 var templ_7745c5c3_Var39 string
744749 templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When)
745750 if templ_7745c5c3_Err != nil {
746- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 136, Col: 90}
751+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 138, Col: 90}
747752 }
748753 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
749754 if templ_7745c5c3_Err != nil {
750755 return templ_7745c5c3_Err
751756 }
752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</time>")
757+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</time>")
753758 if templ_7745c5c3_Err != nil {
754759 return templ_7745c5c3_Err
755760 }
756761 }
757- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</dd>")
762+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</dd>")
758763 if templ_7745c5c3_Err != nil {
759764 return templ_7745c5c3_Err
760765 }
761766 if p.UpdatedAt != "" {
762- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<dt>updated</dt><dd><time datetime=\"")
767+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<dt>updated</dt><dd><time datetime=\"")
763768 if templ_7745c5c3_Err != nil {
764769 return templ_7745c5c3_Err
765770 }
766771 var templ_7745c5c3_Var40 string
767772 templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt)
768773 if templ_7745c5c3_Err != nil {
769- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 141, Col: 38}
774+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 143, Col: 38}
770775 }
771776 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
772777 if templ_7745c5c3_Err != nil {
773778 return templ_7745c5c3_Err
774779 }
775- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\">")
780+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">")
776781 if templ_7745c5c3_Err != nil {
777782 return templ_7745c5c3_Err
778783 }
779784 var templ_7745c5c3_Var41 string
780785 templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated)
781786 if templ_7745c5c3_Err != nil {
782- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 141, Col: 52}
787+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 143, Col: 52}
783788 }
784789 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
785790 if templ_7745c5c3_Err != nil {
786791 return templ_7745c5c3_Err
787792 }
788- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</time></dd>")
793+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</time></dd>")
789794 if templ_7745c5c3_Err != nil {
790795 return templ_7745c5c3_Err
791796 }
792797 }
793- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</dl></header>")
798+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</dl></header>")
794799 if templ_7745c5c3_Err != nil {
795800 return templ_7745c5c3_Err
796801 }
797802 if p.Desc != "" {
798- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
803+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<article class=\"mr-desc prose\" aria-label=\"description\" hx-ignore>")
799804 if templ_7745c5c3_Err != nil {
800805 return templ_7745c5c3_Err
801806 }
@@ -803,58 +808,62 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
803808 if templ_7745c5c3_Err != nil {
804809 return templ_7745c5c3_Err
805810 }
806- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</article>")
811+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</article>")
807812 if templ_7745c5c3_Err != nil {
808813 return templ_7745c5c3_Err
809814 }
810815 }
811- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</section>")
816+ templ_7745c5c3_Err = mrFooter(p).Render(ctx, templ_7745c5c3_Buffer)
817+ if templ_7745c5c3_Err != nil {
818+ return templ_7745c5c3_Err
819+ }
820+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</section>")
812821 if templ_7745c5c3_Err != nil {
813822 return templ_7745c5c3_Err
814823 }
815824 if p.Deleting != "" {
816- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<p class=\"note\" role=\"status\">merged. the branch ")
825+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<p class=\"note\" role=\"status\">merged. the branch ")
817826 if templ_7745c5c3_Err != nil {
818827 return templ_7745c5c3_Err
819828 }
820829 var templ_7745c5c3_Var42 string
821830 templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(p.Deleting)
822831 if templ_7745c5c3_Err != nil {
823- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 152, Col: 64}
832+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 155, Col: 64}
824833 }
825834 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
826835 if templ_7745c5c3_Err != nil {
827836 return templ_7745c5c3_Err
828837 }
829- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " is being deleted.</p>")
838+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, " is being deleted.</p>")
830839 if templ_7745c5c3_Err != nil {
831840 return templ_7745c5c3_Err
832841 }
833842 }
834- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, " ")
843+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ")
835844 if templ_7745c5c3_Err != nil {
836845 return templ_7745c5c3_Err
837846 }
838847 if p.MergeError != "" {
839- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"d-err\" role=\"alert\">")
848+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<p class=\"d-err\" role=\"alert\">")
840849 if templ_7745c5c3_Err != nil {
841850 return templ_7745c5c3_Err
842851 }
843852 var templ_7745c5c3_Var43 string
844853 templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergeError)
845854 if templ_7745c5c3_Err != nil {
846- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 155, Col: 47}
855+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 158, Col: 47}
847856 }
848857 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
849858 if templ_7745c5c3_Err != nil {
850859 return templ_7745c5c3_Err
851860 }
852- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</p>")
861+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</p>")
853862 if templ_7745c5c3_Err != nil {
854863 return templ_7745c5c3_Err
855864 }
856865 }
857- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ")
866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " ")
858867 if templ_7745c5c3_Err != nil {
859868 return templ_7745c5c3_Err
860869 }
@@ -862,125 +871,131 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
862871 if templ_7745c5c3_Err != nil {
863872 return templ_7745c5c3_Err
864873 }
865- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " ")
874+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " <div id=\"mr-refresh-status\" role=\"status\" aria-live=\"polite\"></div><div id=\"mr-range\"")
875+ if templ_7745c5c3_Err != nil {
876+ return templ_7745c5c3_Err
877+ }
878+ if p.Since == nil && p.SinceNote == "" {
879+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, " hidden")
880+ if templ_7745c5c3_Err != nil {
881+ return templ_7745c5c3_Err
882+ }
883+ }
884+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, ">")
866885 if templ_7745c5c3_Err != nil {
867886 return templ_7745c5c3_Err
868887 }
869888 if p.Since != nil {
870- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<nav class=\"since\" aria-label=\"Diff range\"><a href=\"")
889+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<nav class=\"since\" aria-label=\"Diff range\"><a href=\"")
871890 if templ_7745c5c3_Err != nil {
872891 return templ_7745c5c3_Err
873892 }
874893 var templ_7745c5c3_Var44 templ.SafeURL
875894 templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref))
876895 if templ_7745c5c3_Err != nil {
877- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 161, Col: 42}
896+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 166, Col: 43}
878897 }
879898 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
880899 if templ_7745c5c3_Err != nil {
881900 return templ_7745c5c3_Err
882901 }
883- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\"")
902+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\"")
884903 if templ_7745c5c3_Err != nil {
885904 return templ_7745c5c3_Err
886905 }
887906 if !p.Since.Current {
888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " aria-current=\"page\"")
907+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, " aria-current=\"page\"")
889908 if templ_7745c5c3_Err != nil {
890909 return templ_7745c5c3_Err
891910 }
892911 }
893- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, ">all changes <span class=\"mute\">vs ")
912+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, ">all changes <span class=\"mute\">vs ")
894913 if templ_7745c5c3_Err != nil {
895914 return templ_7745c5c3_Err
896915 }
897916 var templ_7745c5c3_Var45 string
898917 templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
899918 if templ_7745c5c3_Err != nil {
900- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 165, Col: 49}
919+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 170, Col: 50}
901920 }
902921 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
903922 if templ_7745c5c3_Err != nil {
904923 return templ_7745c5c3_Err
905924 }
906- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</span></a> <a href=\"")
925+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</span></a> <a href=\"")
907926 if templ_7745c5c3_Err != nil {
908927 return templ_7745c5c3_Err
909928 }
910929 var templ_7745c5c3_Var46 templ.SafeURL
911930 templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref))
912931 if templ_7745c5c3_Err != nil {
913- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 44}
932+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 172, Col: 45}
914933 }
915934 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
916935 if templ_7745c5c3_Err != nil {
917936 return templ_7745c5c3_Err
918937 }
919- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "\"")
938+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\"")
920939 if templ_7745c5c3_Err != nil {
921940 return templ_7745c5c3_Err
922941 }
923942 if p.Since.Current {
924- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, " aria-current=\"page\"")
943+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, " aria-current=\"page\"")
925944 if templ_7745c5c3_Err != nil {
926945 return templ_7745c5c3_Err
927946 }
928947 }
929- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, ">since your last read <span class=\"mute\">")
948+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, ">since your last read <span class=\"mute\">")
930949 if templ_7745c5c3_Err != nil {
931950 return templ_7745c5c3_Err
932951 }
933952 var templ_7745c5c3_Var47 string
934953 templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort)
935954 if templ_7745c5c3_Err != nil {
936- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 171, Col: 64}
955+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 176, Col: 65}
937956 }
938957 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
939958 if templ_7745c5c3_Err != nil {
940959 return templ_7745c5c3_Err
941960 }
942- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " → ")
961+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, " → ")
943962 if templ_7745c5c3_Err != nil {
944963 return templ_7745c5c3_Err
945964 }
946965 var templ_7745c5c3_Var48 string
947966 templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort)
948967 if templ_7745c5c3_Err != nil {
949- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 171, Col: 90}
968+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 176, Col: 91}
950969 }
951970 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
952971 if templ_7745c5c3_Err != nil {
953972 return templ_7745c5c3_Err
954973 }
955- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span></a></nav>")
974+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</span></a></nav>")
956975 if templ_7745c5c3_Err != nil {
957976 return templ_7745c5c3_Err
958977 }
959978 }
960- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, " ")
961- if templ_7745c5c3_Err != nil {
962- return templ_7745c5c3_Err
963- }
964979 if p.SinceNote != "" {
965- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<p class=\"notice\">")
980+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<p class=\"notice\">")
966981 if templ_7745c5c3_Err != nil {
967982 return templ_7745c5c3_Err
968983 }
969984 var templ_7745c5c3_Var49 string
970985 templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote)
971986 if templ_7745c5c3_Err != nil {
972- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 175, Col: 34}
987+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 180, Col: 35}
973988 }
974989 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
975990 if templ_7745c5c3_Err != nil {
976991 return templ_7745c5c3_Err
977992 }
978- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</p>")
993+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "</p>")
979994 if templ_7745c5c3_Err != nil {
980995 return templ_7745c5c3_Err
981996 }
982997 }
983- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " ")
998+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</div>")
984999 if templ_7745c5c3_Err != nil {
9851000 return templ_7745c5c3_Err
9861001 }
@@ -988,106 +1003,112 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
9881003 if templ_7745c5c3_Err != nil {
9891004 return templ_7745c5c3_Err
9901005 }
991- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " ")
1006+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " <div id=\"mr-diff-notices\"")
9921007 if templ_7745c5c3_Err != nil {
9931008 return templ_7745c5c3_Err
9941009 }
995- if p.Invisible {
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>")
1010+ if !p.Invisible && p.Note == "" {
1011+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, " hidden")
9971012 if templ_7745c5c3_Err != nil {
9981013 return templ_7745c5c3_Err
9991014 }
10001015 }
1001- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " ")
1016+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, ">")
10021017 if templ_7745c5c3_Err != nil {
10031018 return templ_7745c5c3_Err
10041019 }
1020+ if p.Invisible {
1021+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<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>")
1022+ if templ_7745c5c3_Err != nil {
1023+ return templ_7745c5c3_Err
1024+ }
1025+ }
10051026 if p.Note != "" {
1006- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<p class=\"notice\">")
1027+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<p class=\"notice\">")
10071028 if templ_7745c5c3_Err != nil {
10081029 return templ_7745c5c3_Err
10091030 }
10101031 var templ_7745c5c3_Var50 string
10111032 templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
10121033 if templ_7745c5c3_Err != nil {
1013- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 185, Col: 29}
1034+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 192, Col: 30}
10141035 }
10151036 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
10161037 if templ_7745c5c3_Err != nil {
10171038 return templ_7745c5c3_Err
10181039 }
1019- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "</p>")
1040+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</p>")
10201041 if templ_7745c5c3_Err != nil {
10211042 return templ_7745c5c3_Err
10221043 }
10231044 }
1024- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, " <div id=\"diffs\" data-mr")
1045+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</div><div id=\"diffs\" data-mr")
10251046 if templ_7745c5c3_Err != nil {
10261047 return templ_7745c5c3_Err
10271048 }
10281049 if p.Canonical != "" {
1029- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, " data-canonical=\"")
1050+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, " data-canonical=\"")
10301051 if templ_7745c5c3_Err != nil {
10311052 return templ_7745c5c3_Err
10321053 }
10331054 var templ_7745c5c3_Var51 string
10341055 templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Canonical)
10351056 if templ_7745c5c3_Err != nil {
1036- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 191, Col: 32}
1057+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 199, Col: 32}
10371058 }
10381059 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
10391060 if templ_7745c5c3_Err != nil {
10401061 return templ_7745c5c3_Err
10411062 }
1042- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\"")
1063+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\"")
10431064 if templ_7745c5c3_Err != nil {
10441065 return templ_7745c5c3_Err
10451066 }
10461067 }
10471068 if p.Both {
1048- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " data-diff=\"")
1069+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " data-diff=\"")
10491070 if templ_7745c5c3_Err != nil {
10501071 return templ_7745c5c3_Err
10511072 }
10521073 var templ_7745c5c3_Var52 string
10531074 templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
10541075 if templ_7745c5c3_Err != nil {
1055- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 194, Col: 22}
1076+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 202, Col: 22}
10561077 }
10571078 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52)
10581079 if templ_7745c5c3_Err != nil {
10591080 return templ_7745c5c3_Err
10601081 }
1061- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "\"")
1082+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "\"")
10621083 if templ_7745c5c3_Err != nil {
10631084 return templ_7745c5c3_Err
10641085 }
10651086 } else {
1066- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " data-view=\"")
1087+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, " data-view=\"")
10671088 if templ_7745c5c3_Err != nil {
10681089 return templ_7745c5c3_Err
10691090 }
10701091 var templ_7745c5c3_Var53 string
10711092 templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
10721093 if templ_7745c5c3_Err != nil {
1073- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 196, Col: 22}
1094+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 204, Col: 22}
10741095 }
10751096 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53)
10761097 if templ_7745c5c3_Err != nil {
10771098 return templ_7745c5c3_Err
10781099 }
1079- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"")
1100+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "\"")
10801101 if templ_7745c5c3_Err != nil {
10811102 return templ_7745c5c3_Err
10821103 }
10831104 }
10841105 if p.Both && !p.Pinned {
1085- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
1106+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
10861107 if templ_7745c5c3_Err != nil {
10871108 return templ_7745c5c3_Err
10881109 }
10891110 }
1090- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, ">")
1111+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, ">")
10911112 if templ_7745c5c3_Err != nil {
10921113 return templ_7745c5c3_Err
10931114 }
@@ -1098,38 +1119,38 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
10981119 }
10991120 }
11001121 if len(p.Files) == 0 {
1101- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"note\">no changed files.</p>")
1122+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "<p class=\"note\">no changed files.</p>")
11021123 if templ_7745c5c3_Err != nil {
11031124 return templ_7745c5c3_Err
11041125 }
11051126 }
1106- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</div><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
1127+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</div><div id=\"mr-comments\"><h2 class=\"rule-h\" id=\"comments\"><span>Comments</span><span class=\"count\">")
11071128 if templ_7745c5c3_Err != nil {
11081129 return templ_7745c5c3_Err
11091130 }
11101131 var templ_7745c5c3_Var54 string
11111132 templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved))
11121133 if templ_7745c5c3_Err != nil {
1113- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 209, Col: 97}
1134+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 218, Col: 97}
11141135 }
11151136 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
11161137 if templ_7745c5c3_Err != nil {
11171138 return templ_7745c5c3_Err
11181139 }
1119- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, " open / ")
1140+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " open / ")
11201141 if templ_7745c5c3_Err != nil {
11211142 return templ_7745c5c3_Err
11221143 }
11231144 var templ_7745c5c3_Var55 string
11241145 templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments))
11251146 if templ_7745c5c3_Err != nil {
1126- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 209, Col: 126}
1147+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 218, Col: 126}
11271148 }
11281149 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
11291150 if templ_7745c5c3_Err != nil {
11301151 return templ_7745c5c3_Err
11311152 }
1132- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "</span></h2>")
1153+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</span></h2>")
11331154 if templ_7745c5c3_Err != nil {
11341155 return templ_7745c5c3_Err
11351156 }
@@ -1145,26 +1166,22 @@ func MergeRequest(l Layout, p MRPage) templ.Component {
11451166 return templ_7745c5c3_Err
11461167 }
11471168 }
1148- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, " ")
1149- if templ_7745c5c3_Err != nil {
1150- return templ_7745c5c3_Err
1151- }
11521169 if p.NComments == 0 {
1153- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<p class=\"dim\">no comments yet.</p>")
1170+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "<p class=\"dim\">no comments yet.</p>")
11541171 if templ_7745c5c3_Err != nil {
11551172 return templ_7745c5c3_Err
11561173 }
11571174 }
1158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, " ")
1159- if templ_7745c5c3_Err != nil {
1160- return templ_7745c5c3_Err
1161- }
11621175 if p.GeneralForm != nil {
11631176 templ_7745c5c3_Err = commentForm(*p.GeneralForm, "").Render(ctx, templ_7745c5c3_Buffer)
11641177 if templ_7745c5c3_Err != nil {
11651178 return templ_7745c5c3_Err
11661179 }
11671180 }
1181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</div>")
1182+ if templ_7745c5c3_Err != nil {
1183+ return templ_7745c5c3_Err
1184+ }
11681185 return nil
11691186 })
11701187 templ_7745c5c3_Err = Shell(l, mrRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var25), templ_7745c5c3_Buffer)
@@ -1197,33 +1214,33 @@ func branchLink(name, href string) templ.Component {
11971214 }
11981215 ctx = templ.ClearChildren(ctx)
11991216 if href != "" {
1200- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "<a href=\"")
1217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "<a href=\"")
12011218 if templ_7745c5c3_Err != nil {
12021219 return templ_7745c5c3_Err
12031220 }
12041221 var templ_7745c5c3_Var57 templ.SafeURL
12051222 templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
12061223 if templ_7745c5c3_Err != nil {
1207- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 227, Col: 31}
1224+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 237, Col: 31}
12081225 }
12091226 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
12101227 if templ_7745c5c3_Err != nil {
12111228 return templ_7745c5c3_Err
12121229 }
1213- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\">")
1230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\">")
12141231 if templ_7745c5c3_Err != nil {
12151232 return templ_7745c5c3_Err
12161233 }
12171234 var templ_7745c5c3_Var58 string
12181235 templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(name)
12191236 if templ_7745c5c3_Err != nil {
1220- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 227, Col: 40}
1237+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 237, Col: 40}
12211238 }
12221239 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
12231240 if templ_7745c5c3_Err != nil {
12241241 return templ_7745c5c3_Err
12251242 }
1226- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</a>")
1243+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "</a>")
12271244 if templ_7745c5c3_Err != nil {
12281245 return templ_7745c5c3_Err
12291246 }
@@ -1231,7 +1248,7 @@ func branchLink(name, href string) templ.Component {
12311248 var templ_7745c5c3_Var59 string
12321249 templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(name)
12331250 if templ_7745c5c3_Err != nil {
1234- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 229, Col: 8}
1251+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 8}
12351252 }
12361253 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
12371254 if templ_7745c5c3_Err != nil {
@@ -1305,56 +1322,56 @@ func commentView(c CommentView, suffix string) templ.Component {
13051322 templ_7745c5c3_Var61 = templ.NopComponent
13061323 }
13071324 ctx = templ.ClearChildren(ctx)
1308- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<div class=\"cmt\" id=\"")
1325+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<div class=\"cmt\" id=\"")
13091326 if templ_7745c5c3_Err != nil {
13101327 return templ_7745c5c3_Err
13111328 }
13121329 var templ_7745c5c3_Var62 string
13131330 templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)
13141331 if templ_7745c5c3_Err != nil {
1315- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 42}
1332+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 42}
13161333 }
13171334 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
13181335 if templ_7745c5c3_Err != nil {
13191336 return templ_7745c5c3_Err
13201337 }
1321- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" data-resolved=\"")
1338+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\" data-resolved=\"")
13221339 if templ_7745c5c3_Err != nil {
13231340 return templ_7745c5c3_Err
13241341 }
13251342 var templ_7745c5c3_Var63 string
13261343 templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))
13271344 if templ_7745c5c3_Err != nil {
1328- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 81}
1345+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 81}
13291346 }
13301347 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
13311348 if templ_7745c5c3_Err != nil {
13321349 return templ_7745c5c3_Err
13331350 }
1334- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\"")
1351+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\"")
13351352 if templ_7745c5c3_Err != nil {
13361353 return templ_7745c5c3_Err
13371354 }
13381355 if c.Reply != nil {
1339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, " data-thread")
1356+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, " data-thread")
13401357 if templ_7745c5c3_Err != nil {
13411358 return templ_7745c5c3_Err
13421359 }
13431360 }
1344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "><p class=\"c-head\"><span class=\"c-who\">")
1361+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "><p class=\"c-head\"><span class=\"c-who\">")
13451362 if templ_7745c5c3_Err != nil {
13461363 return templ_7745c5c3_Err
13471364 }
13481365 var templ_7745c5c3_Var64 string
13491366 templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
13501367 if templ_7745c5c3_Err != nil {
1351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 250, Col: 33}
1368+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 260, Col: 33}
13521369 }
13531370 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
13541371 if templ_7745c5c3_Err != nil {
13551372 return templ_7745c5c3_Err
13561373 }
1357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</span> ")
1374+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</span> ")
13581375 if templ_7745c5c3_Err != nil {
13591376 return templ_7745c5c3_Err
13601377 }
@@ -1362,52 +1379,52 @@ func commentView(c CommentView, suffix string) templ.Component {
13621379 var templ_7745c5c3_Var65 string
13631380 templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13641381 if templ_7745c5c3_Err != nil {
1365- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 9}
1382+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 262, Col: 9}
13661383 }
13671384 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
13681385 if templ_7745c5c3_Err != nil {
13691386 return templ_7745c5c3_Err
13701387 }
1371- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "<time class=\"mute\" datetime=\"")
1388+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<time class=\"mute\" datetime=\"")
13721389 if templ_7745c5c3_Err != nil {
13731390 return templ_7745c5c3_Err
13741391 }
13751392 var templ_7745c5c3_Var66 string
13761393 templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
13771394 if templ_7745c5c3_Err != nil {
1378- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 49}
1395+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 262, Col: 49}
13791396 }
13801397 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
13811398 if templ_7745c5c3_Err != nil {
13821399 return templ_7745c5c3_Err
13831400 }
1384- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\" title=\"")
1401+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" title=\"")
13851402 if templ_7745c5c3_Err != nil {
13861403 return templ_7745c5c3_Err
13871404 }
13881405 var templ_7745c5c3_Var67 string
13891406 templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)
13901407 if templ_7745c5c3_Err != nil {
1391- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 65}
1408+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 262, Col: 65}
13921409 }
13931410 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
13941411 if templ_7745c5c3_Err != nil {
13951412 return templ_7745c5c3_Err
13961413 }
1397- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\">")
1414+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\">")
13981415 if templ_7745c5c3_Err != nil {
13991416 return templ_7745c5c3_Err
14001417 }
14011418 var templ_7745c5c3_Var68 string
14021419 templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
14031420 if templ_7745c5c3_Err != nil {
1404- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 252, Col: 76}
1421+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 262, Col: 76}
14051422 }
14061423 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
14071424 if templ_7745c5c3_Err != nil {
14081425 return templ_7745c5c3_Err
14091426 }
1410- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "</time> ")
1427+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "</time> ")
14111428 if templ_7745c5c3_Err != nil {
14121429 return templ_7745c5c3_Err
14131430 }
@@ -1416,13 +1433,13 @@ func commentView(c CommentView, suffix string) templ.Component {
14161433 var templ_7745c5c3_Var69 string
14171434 templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
14181435 if templ_7745c5c3_Err != nil {
1419- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 255, Col: 9}
1436+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 265, Col: 9}
14201437 }
14211438 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
14221439 if templ_7745c5c3_Err != nil {
14231440 return templ_7745c5c3_Err
14241441 }
1425- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<span class=\"c-state\">resolved</span> ")
1442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<span class=\"c-state\">resolved</span> ")
14261443 if templ_7745c5c3_Err != nil {
14271444 return templ_7745c5c3_Err
14281445 }
@@ -1431,13 +1448,13 @@ func commentView(c CommentView, suffix string) templ.Component {
14311448 var templ_7745c5c3_Var70 string
14321449 templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
14331450 if templ_7745c5c3_Err != nil {
1434- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 9}
1451+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 268, Col: 9}
14351452 }
14361453 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
14371454 if templ_7745c5c3_Err != nil {
14381455 return templ_7745c5c3_Err
14391456 }
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> ")
1457+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")
14411458 if templ_7745c5c3_Err != nil {
14421459 return templ_7745c5c3_Err
14431460 }
@@ -1446,36 +1463,36 @@ func commentView(c CommentView, suffix string) templ.Component {
14461463 var templ_7745c5c3_Var71 string
14471464 templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
14481465 if templ_7745c5c3_Err != nil {
1449- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 261, Col: 9}
1466+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 271, Col: 9}
14501467 }
14511468 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
14521469 if templ_7745c5c3_Err != nil {
14531470 return templ_7745c5c3_Err
14541471 }
1455- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<span class=\"c-where mute\">")
1472+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<span class=\"c-where mute\">")
14561473 if templ_7745c5c3_Err != nil {
14571474 return templ_7745c5c3_Err
14581475 }
14591476 var templ_7745c5c3_Var72 string
14601477 templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)
14611478 if templ_7745c5c3_Err != nil {
1462- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 261, Col: 47}
1479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 271, Col: 47}
14631480 }
14641481 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72))
14651482 if templ_7745c5c3_Err != nil {
14661483 return templ_7745c5c3_Err
14671484 }
1468- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</span>")
1485+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</span>")
14691486 if templ_7745c5c3_Err != nil {
14701487 return templ_7745c5c3_Err
14711488 }
14721489 }
1473- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "</p>")
1490+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</p>")
14741491 if templ_7745c5c3_Err != nil {
14751492 return templ_7745c5c3_Err
14761493 }
14771494 if len(c.Parts) > 0 {
1478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "<p class=\"c-text\">")
1495+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "<p class=\"c-text\">")
14791496 if templ_7745c5c3_Err != nil {
14801497 return templ_7745c5c3_Err
14811498 }
@@ -1483,24 +1500,24 @@ func commentView(c CommentView, suffix string) templ.Component {
14831500 if templ_7745c5c3_Err != nil {
14841501 return templ_7745c5c3_Err
14851502 }
1486- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</p>")
1503+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "</p>")
14871504 if templ_7745c5c3_Err != nil {
14881505 return templ_7745c5c3_Err
14891506 }
14901507 } else if c.Deleted {
14911508 if c.Verdict != "" {
1492- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "<p class=\"mute\">text deleted</p>")
1509+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "<p class=\"mute\">text deleted</p>")
14931510 if templ_7745c5c3_Err != nil {
14941511 return templ_7745c5c3_Err
14951512 }
14961513 } else {
1497- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "<p class=\"mute\">deleted note</p>")
1514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "<p class=\"mute\">deleted note</p>")
14981515 if templ_7745c5c3_Err != nil {
14991516 return templ_7745c5c3_Err
15001517 }
15011518 }
15021519 } else {
1503- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "<div class=\"c-body prose\">")
1520+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<div class=\"c-body prose\">")
15041521 if templ_7745c5c3_Err != nil {
15051522 return templ_7745c5c3_Err
15061523 }
@@ -1515,147 +1532,147 @@ func commentView(c CommentView, suffix string) templ.Component {
15151532 return templ_7745c5c3_Err
15161533 }
15171534 }
1518- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "</div>")
1535+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "</div>")
15191536 if templ_7745c5c3_Err != nil {
15201537 return templ_7745c5c3_Err
15211538 }
15221539 }
15231540 if c.Edited {
1524- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<span class=\"mute\">edited</span> ")
1541+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "<span class=\"mute\">edited</span> ")
15251542 if templ_7745c5c3_Err != nil {
15261543 return templ_7745c5c3_Err
15271544 }
15281545 }
15291546 if c.Verdict != "" {
1530- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<span class=\"c-state\">")
1547+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<span class=\"c-state\">")
15311548 if templ_7745c5c3_Err != nil {
15321549 return templ_7745c5c3_Err
15331550 }
15341551 var templ_7745c5c3_Var73 string
15351552 templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict)
15361553 if templ_7745c5c3_Err != nil {
1537- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 285, Col: 36}
1554+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 295, Col: 36}
15381555 }
15391556 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
15401557 if templ_7745c5c3_Err != nil {
15411558 return templ_7745c5c3_Err
15421559 }
1543- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "</span> ")
1560+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "</span> ")
15441561 if templ_7745c5c3_Err != nil {
15451562 return templ_7745c5c3_Err
15461563 }
15471564 }
15481565 if c.Range != "" {
1549- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<span class=\"mute\">")
1566+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "<span class=\"mute\">")
15501567 if templ_7745c5c3_Err != nil {
15511568 return templ_7745c5c3_Err
15521569 }
15531570 var templ_7745c5c3_Var74 string
15541571 templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range)
15551572 if templ_7745c5c3_Err != nil {
1556- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 31}
1573+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 298, Col: 31}
15571574 }
15581575 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74))
15591576 if templ_7745c5c3_Err != nil {
15601577 return templ_7745c5c3_Err
15611578 }
1562- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "</span> ")
1579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "</span> ")
15631580 if templ_7745c5c3_Err != nil {
15641581 return templ_7745c5c3_Err
15651582 }
15661583 }
15671584 if c.Quote != "" {
1568- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
1585+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
15691586 if templ_7745c5c3_Err != nil {
15701587 return templ_7745c5c3_Err
15711588 }
15721589 var templ_7745c5c3_Var75 string
15731590 templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote)
15741591 if templ_7745c5c3_Err != nil {
1575- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 65}
1592+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 301, Col: 65}
15761593 }
15771594 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75)
15781595 if templ_7745c5c3_Err != nil {
15791596 return templ_7745c5c3_Err
15801597 }
1581- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "\">Quote</button> ")
1598+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\">Quote</button> ")
15821599 if templ_7745c5c3_Err != nil {
15831600 return templ_7745c5c3_Err
15841601 }
15851602 }
15861603 if c.Edit != nil {
1587- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<details><summary>Edit</summary><form method=\"post\" action=\"")
1604+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "<details><summary>Edit</summary><form method=\"post\" action=\"")
15881605 if templ_7745c5c3_Err != nil {
15891606 return templ_7745c5c3_Err
15901607 }
15911608 var templ_7745c5c3_Var76 templ.SafeURL
15921609 templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action))
15931610 if templ_7745c5c3_Err != nil {
1594- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 92}
1611+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 304, Col: 92}
15951612 }
15961613 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
15971614 if templ_7745c5c3_Err != nil {
15981615 return templ_7745c5c3_Err
15991616 }
1600- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\"><input type=\"hidden\" name=\"from\" value=\"")
1617+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "\"><input type=\"hidden\" name=\"from\" value=\"")
16011618 if templ_7745c5c3_Err != nil {
16021619 return templ_7745c5c3_Err
16031620 }
16041621 var templ_7745c5c3_Var77 string
16051622 templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From))
16061623 if templ_7745c5c3_Err != nil {
1607- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 153}
1624+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 304, Col: 153}
16081625 }
16091626 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
16101627 if templ_7745c5c3_Err != nil {
16111628 return templ_7745c5c3_Err
16121629 }
1613- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "\"><textarea name=\"text\" maxlength=\"8000\">")
1630+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "\"><textarea name=\"text\" maxlength=\"8000\">")
16141631 if templ_7745c5c3_Err != nil {
16151632 return templ_7745c5c3_Err
16161633 }
16171634 var templ_7745c5c3_Var78 string
16181635 templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text)
16191636 if templ_7745c5c3_Err != nil {
1620- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 294, Col: 209}
1637+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 304, Col: 209}
16211638 }
16221639 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
16231640 if templ_7745c5c3_Err != nil {
16241641 return templ_7745c5c3_Err
16251642 }
1626- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
1643+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
16271644 if templ_7745c5c3_Err != nil {
16281645 return templ_7745c5c3_Err
16291646 }
16301647 }
16311648 if c.Delete != nil {
1632- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<details><summary>Delete</summary><form method=\"post\" action=\"")
1649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "<details><summary>Delete</summary><form method=\"post\" action=\"")
16331650 if templ_7745c5c3_Err != nil {
16341651 return templ_7745c5c3_Err
16351652 }
16361653 var templ_7745c5c3_Var79 templ.SafeURL
16371654 templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action))
16381655 if templ_7745c5c3_Err != nil {
1639- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 96}
1656+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 307, Col: 96}
16401657 }
16411658 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79))
16421659 if templ_7745c5c3_Err != nil {
16431660 return templ_7745c5c3_Err
16441661 }
1645- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "\"><input type=\"hidden\" name=\"from\" value=\"")
1662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "\"><input type=\"hidden\" name=\"from\" value=\"")
16461663 if templ_7745c5c3_Err != nil {
16471664 return templ_7745c5c3_Err
16481665 }
16491666 var templ_7745c5c3_Var80 string
16501667 templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From))
16511668 if templ_7745c5c3_Err != nil {
1652- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 159}
1669+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 307, Col: 159}
16531670 }
16541671 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
16551672 if templ_7745c5c3_Err != nil {
16561673 return templ_7745c5c3_Err
16571674 }
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> ")
1675+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "\"><p>Delete this note?</p><button class=\"btn\" type=\"submit\">Confirm delete</button></form></details> ")
16591676 if templ_7745c5c3_Err != nil {
16601677 return templ_7745c5c3_Err
16611678 }
@@ -1667,7 +1684,7 @@ func commentView(c CommentView, suffix string) templ.Component {
16671684 }
16681685 }
16691686 if c.Reply != nil {
1670- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<details data-reply><summary>Reply</summary>")
1687+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "<details data-reply><summary>Reply</summary>")
16711688 if templ_7745c5c3_Err != nil {
16721689 return templ_7745c5c3_Err
16731690 }
@@ -1675,80 +1692,80 @@ func commentView(c CommentView, suffix string) templ.Component {
16751692 if templ_7745c5c3_Err != nil {
16761693 return templ_7745c5c3_Err
16771694 }
1678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "</details> ")
1695+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "</details> ")
16791696 if templ_7745c5c3_Err != nil {
16801697 return templ_7745c5c3_Err
16811698 }
16821699 }
16831700 if c.Resolve != nil {
1684- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "<form class=\"c-act\" method=\"post\" action=\"")
1701+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "<form class=\"c-act\" method=\"post\" action=\"")
16851702 if templ_7745c5c3_Err != nil {
16861703 return templ_7745c5c3_Err
16871704 }
16881705 var templ_7745c5c3_Var81 templ.SafeURL
16891706 templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
16901707 if templ_7745c5c3_Err != nil {
1691- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 308, Col: 77}
1708+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 318, Col: 77}
16921709 }
16931710 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
16941711 if templ_7745c5c3_Err != nil {
16951712 return templ_7745c5c3_Err
16961713 }
1697- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
1714+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
16981715 if templ_7745c5c3_Err != nil {
16991716 return templ_7745c5c3_Err
17001717 }
17011718 var templ_7745c5c3_Var82 string
17021719 templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
17031720 if templ_7745c5c3_Err != nil {
1704- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 309, Col: 74}
1721+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 74}
17051722 }
17061723 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)
17071724 if templ_7745c5c3_Err != nil {
17081725 return templ_7745c5c3_Err
17091726 }
1710- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\"> ")
1727+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "\"> ")
17111728 if templ_7745c5c3_Err != nil {
17121729 return templ_7745c5c3_Err
17131730 }
17141731 if c.Resolve.From > 0 {
1715- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "<input type=\"hidden\" name=\"from\" value=\"")
1732+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<input type=\"hidden\" name=\"from\" value=\"")
17161733 if templ_7745c5c3_Err != nil {
17171734 return templ_7745c5c3_Err
17181735 }
17191736 var templ_7745c5c3_Var83 string
17201737 templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
17211738 if templ_7745c5c3_Err != nil {
1722- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 311, Col: 66}
1739+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 321, Col: 66}
17231740 }
17241741 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83)
17251742 if templ_7745c5c3_Err != nil {
17261743 return templ_7745c5c3_Err
17271744 }
1728- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "\"> ")
1745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "\"> ")
17291746 if templ_7745c5c3_Err != nil {
17301747 return templ_7745c5c3_Err
17311748 }
17321749 }
1733- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<button type=\"submit\" class=\"btn\">")
1750+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "<button type=\"submit\" class=\"btn\">")
17341751 if templ_7745c5c3_Err != nil {
17351752 return templ_7745c5c3_Err
17361753 }
17371754 var templ_7745c5c3_Var84 string
17381755 templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
17391756 if templ_7745c5c3_Err != nil {
1740- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 55}
1757+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 323, Col: 55}
17411758 }
17421759 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84))
17431760 if templ_7745c5c3_Err != nil {
17441761 return templ_7745c5c3_Err
17451762 }
1746- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "</button></form>")
1763+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "</button></form>")
17471764 if templ_7745c5c3_Err != nil {
17481765 return templ_7745c5c3_Err
17491766 }
17501767 }
1751- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</div>")
1768+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</div>")
17521769 if templ_7745c5c3_Err != nil {
17531770 return templ_7745c5c3_Err
17541771 }
@@ -1785,209 +1802,209 @@ func commentForm(f CommentForm, suffix string) templ.Component {
17851802 templ_7745c5c3_Var85 = templ.NopComponent
17861803 }
17871804 ctx = templ.ClearChildren(ctx)
1788- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "<form class=\"cform\" method=\"post\" action=\"")
1805+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "<form class=\"cform\" method=\"post\" action=\"")
17891806 if templ_7745c5c3_Err != nil {
17901807 return templ_7745c5c3_Err
17911808 }
17921809 var templ_7745c5c3_Var86 templ.SafeURL
17931810 templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
17941811 if templ_7745c5c3_Err != nil {
1795- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 328, Col: 67}
1812+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 338, Col: 67}
17961813 }
17971814 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86))
17981815 if templ_7745c5c3_Err != nil {
17991816 return templ_7745c5c3_Err
18001817 }
1801- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "\"><label for=\"")
1818+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "\"><label for=\"")
18021819 if templ_7745c5c3_Err != nil {
18031820 return templ_7745c5c3_Err
18041821 }
18051822 var templ_7745c5c3_Var87 string
18061823 templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
18071824 if templ_7745c5c3_Err != nil {
1808- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 329, Col: 28}
1825+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 339, Col: 28}
18091826 }
18101827 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87)
18111828 if templ_7745c5c3_Err != nil {
18121829 return templ_7745c5c3_Err
18131830 }
1814- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "\">")
1831+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "\">")
18151832 if templ_7745c5c3_Err != nil {
18161833 return templ_7745c5c3_Err
18171834 }
18181835 var templ_7745c5c3_Var88 string
18191836 templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
18201837 if templ_7745c5c3_Err != nil {
1821- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 329, Col: 40}
1838+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 339, Col: 40}
18221839 }
18231840 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88))
18241841 if templ_7745c5c3_Err != nil {
18251842 return templ_7745c5c3_Err
18261843 }
1827- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "</label> <textarea id=\"")
1844+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "</label> <textarea id=\"")
18281845 if templ_7745c5c3_Err != nil {
18291846 return templ_7745c5c3_Err
18301847 }
18311848 var templ_7745c5c3_Var89 string
18321849 templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
18331850 if templ_7745c5c3_Err != nil {
1834- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 21}
1851+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 341, Col: 21}
18351852 }
18361853 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89)
18371854 if templ_7745c5c3_Err != nil {
18381855 return templ_7745c5c3_Err
18391856 }
1840- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "\" name=\"text\" rows=\"3\" maxlength=\"8000\"")
1857+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "\" name=\"text\" rows=\"3\" maxlength=\"8000\"")
18411858 if templ_7745c5c3_Err != nil {
18421859 return templ_7745c5c3_Err
18431860 }
18441861 if f.Plain {
1845- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, " required placeholder=\"plain text, 8000 characters at most\"")
1862+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, " required placeholder=\"plain text, 8000 characters at most\"")
18461863 if templ_7745c5c3_Err != nil {
18471864 return templ_7745c5c3_Err
18481865 }
18491866 } else {
1850- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, " placeholder=\"Markdown, 8000 characters at most\"")
1867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, " placeholder=\"Markdown, 8000 characters at most\"")
18511868 if templ_7745c5c3_Err != nil {
18521869 return templ_7745c5c3_Err
18531870 }
18541871 }
18551872 if f.Focus {
1856- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, " autofocus")
1873+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, " autofocus")
18571874 if templ_7745c5c3_Err != nil {
18581875 return templ_7745c5c3_Err
18591876 }
18601877 }
1861- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "></textarea> ")
1878+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "></textarea> ")
18621879 if templ_7745c5c3_Err != nil {
18631880 return templ_7745c5c3_Err
18641881 }
18651882 if f.Path != "" {
1866- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "<input type=\"hidden\" name=\"path\" value=\"")
1883+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "<input type=\"hidden\" name=\"path\" value=\"")
18671884 if templ_7745c5c3_Err != nil {
18681885 return templ_7745c5c3_Err
18691886 }
18701887 var templ_7745c5c3_Var90 string
18711888 templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
18721889 if templ_7745c5c3_Err != nil {
1873- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 346, Col: 50}
1890+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 356, Col: 50}
18741891 }
18751892 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
18761893 if templ_7745c5c3_Err != nil {
18771894 return templ_7745c5c3_Err
18781895 }
1879- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "\"> <input type=\"hidden\" name=\"line\" value=\"")
1896+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "\"> <input type=\"hidden\" name=\"line\" value=\"")
18801897 if templ_7745c5c3_Err != nil {
18811898 return templ_7745c5c3_Err
18821899 }
18831900 var templ_7745c5c3_Var91 string
18841901 templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
18851902 if templ_7745c5c3_Err != nil {
1886- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 347, Col: 50}
1903+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 357, Col: 50}
18871904 }
18881905 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91)
18891906 if templ_7745c5c3_Err != nil {
18901907 return templ_7745c5c3_Err
18911908 }
1892- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "\"> <input type=\"hidden\" name=\"side\" value=\"")
1909+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"> <input type=\"hidden\" name=\"side\" value=\"")
18931910 if templ_7745c5c3_Err != nil {
18941911 return templ_7745c5c3_Err
18951912 }
18961913 var templ_7745c5c3_Var92 string
18971914 templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
18981915 if templ_7745c5c3_Err != nil {
1899- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 348, Col: 50}
1916+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 358, Col: 50}
19001917 }
19011918 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
19021919 if templ_7745c5c3_Err != nil {
19031920 return templ_7745c5c3_Err
19041921 }
1905- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
1922+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
19061923 if templ_7745c5c3_Err != nil {
19071924 return templ_7745c5c3_Err
19081925 }
19091926 var templ_7745c5c3_Var93 string
19101927 templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine)
19111928 if templ_7745c5c3_Err != nil {
1912- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 349, Col: 86}
1929+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 359, Col: 86}
19131930 }
19141931 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var93)
19151932 if templ_7745c5c3_Err != nil {
19161933 return templ_7745c5c3_Err
19171934 }
1918- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"></label> ")
1935+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "\"></label> ")
19191936 if templ_7745c5c3_Err != nil {
19201937 return templ_7745c5c3_Err
19211938 }
19221939 }
19231940 if f.ReplyTo != "" {
1924- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<input type=\"hidden\" name=\"reply_to\" value=\"")
1941+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "<input type=\"hidden\" name=\"reply_to\" value=\"")
19251942 if templ_7745c5c3_Err != nil {
19261943 return templ_7745c5c3_Err
19271944 }
19281945 var templ_7745c5c3_Var94 string
19291946 templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo)
19301947 if templ_7745c5c3_Err != nil {
1931- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 352, Col: 57}
1948+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 362, Col: 57}
19321949 }
19331950 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var94)
19341951 if templ_7745c5c3_Err != nil {
19351952 return templ_7745c5c3_Err
19361953 }
1937- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "\"> ")
1954+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "\"> ")
19381955 if templ_7745c5c3_Err != nil {
19391956 return templ_7745c5c3_Err
19401957 }
19411958 }
19421959 if f.Path == "" && f.ReplyTo == "" && !f.Plain {
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>")
1960+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "<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>")
19441961 if templ_7745c5c3_Err != nil {
19451962 return templ_7745c5c3_Err
19461963 }
19471964 }
19481965 if f.From > 0 {
1949- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "<input type=\"hidden\" name=\"from\" value=\"")
1966+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<input type=\"hidden\" name=\"from\" value=\"")
19501967 if templ_7745c5c3_Err != nil {
19511968 return templ_7745c5c3_Err
19521969 }
19531970 var templ_7745c5c3_Var95 string
19541971 templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
19551972 if templ_7745c5c3_Err != nil {
1956- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 358, Col: 56}
1973+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 368, Col: 56}
19571974 }
19581975 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var95)
19591976 if templ_7745c5c3_Err != nil {
19601977 return templ_7745c5c3_Err
19611978 }
1962- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "\">")
1979+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "\">")
19631980 if templ_7745c5c3_Err != nil {
19641981 return templ_7745c5c3_Err
19651982 }
19661983 }
1967- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
1984+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
19681985 if templ_7745c5c3_Err != nil {
19691986 return templ_7745c5c3_Err
19701987 }
19711988 if f.Cancel != "" {
1972- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<a class=\"btn\" href=\"")
1989+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<a class=\"btn\" href=\"")
19731990 if templ_7745c5c3_Err != nil {
19741991 return templ_7745c5c3_Err
19751992 }
19761993 var templ_7745c5c3_Var96 templ.SafeURL
19771994 templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
19781995 if templ_7745c5c3_Err != nil {
1979- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 363, Col: 49}
1996+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 373, Col: 49}
19801997 }
19811998 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
19821999 if templ_7745c5c3_Err != nil {
19832000 return templ_7745c5c3_Err
19842001 }
1985- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "\">Cancel</a>")
2002+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "\">Cancel</a>")
19862003 if templ_7745c5c3_Err != nil {
19872004 return templ_7745c5c3_Err
19882005 }
19892006 }
1990- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "</div></form>")
2007+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "</div></form>")
19912008 if templ_7745c5c3_Err != nil {
19922009 return templ_7745c5c3_Err
19932010 }
@@ -1995,7 +2012,7 @@ func commentForm(f CommentForm, suffix string) templ.Component {
19952012 })
19962013 }
19972014
1998-func mrRail(p MRPage) templ.Component {
2015+func mrFooter(p MRPage) templ.Component {
19992016 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
20002017 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
20012018 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -2016,312 +2033,336 @@ func mrRail(p MRPage) templ.Component {
20162033 templ_7745c5c3_Var97 = templ.NopComponent
20172034 }
20182035 ctx = templ.ClearChildren(ctx)
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\">")
2036+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<footer class=\"mr-footer\"><nav data-mr-tabs aria-label=\"Branch details\"><a id=\"mr-tab-commits\" href=\"#mr-commits\">Commits <span class=\"count\">")
20202037 if templ_7745c5c3_Err != nil {
20212038 return templ_7745c5c3_Err
20222039 }
20232040 var templ_7745c5c3_Var98 string
2024- templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
2041+ templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitCount)
20252042 if templ_7745c5c3_Err != nil {
2026- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 372, Col: 49}
2043+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 88}
20272044 }
20282045 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
20292046 if templ_7745c5c3_Err != nil {
20302047 return templ_7745c5c3_Err
20312048 }
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>")
2049+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "</span></a> <a id=\"mr-tab-checks\" href=\"#mr-checks\">")
20332050 if templ_7745c5c3_Err != nil {
20342051 return templ_7745c5c3_Err
20352052 }
2036- templ_7745c5c3_Err = FileList(p.Files).Render(ctx, templ_7745c5c3_Buffer)
2053+ var templ_7745c5c3_Var99 string
2054+ templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs("Checks ")
2055+ if templ_7745c5c3_Err != nil {
2056+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 384, Col: 15}
2057+ }
2058+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
20372059 if templ_7745c5c3_Err != nil {
20382060 return templ_7745c5c3_Err
20392061 }
2040- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</section>")
2062+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, " <span data-checks-label>")
20412063 if templ_7745c5c3_Err != nil {
20422064 return templ_7745c5c3_Err
20432065 }
2044- if len(p.Verdicts) > 0 {
2045- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<section><h2>Verdicts</h2><ul>")
2066+ if p.ChecksSum.Total > 0 {
2067+ var templ_7745c5c3_Var100 string
2068+ templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(p.ChecksSum.Title)
20462069 if templ_7745c5c3_Err != nil {
2047- return templ_7745c5c3_Err
2070+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 25}
20482071 }
2049- for _, v := range p.Verdicts {
2050- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "<li><a href=\"")
2051- if templ_7745c5c3_Err != nil {
2052- return templ_7745c5c3_Err
2053- }
2054- var templ_7745c5c3_Var99 templ.SafeURL
2055- templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
2056- if templ_7745c5c3_Err != nil {
2057- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 37}
2058- }
2059- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
2060- if templ_7745c5c3_Err != nil {
2061- return templ_7745c5c3_Err
2062- }
2063- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "\">")
2064- if templ_7745c5c3_Err != nil {
2065- return templ_7745c5c3_Err
2066- }
2067- var templ_7745c5c3_Var100 string
2068- templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
2069- if templ_7745c5c3_Err != nil {
2070- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 50}
2071- }
2072- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
2073- if templ_7745c5c3_Err != nil {
2074- return templ_7745c5c3_Err
2075- }
2076- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, ": ")
2077- if templ_7745c5c3_Err != nil {
2078- return templ_7745c5c3_Err
2079- }
2080- var templ_7745c5c3_Var101 string
2081- templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2082- if templ_7745c5c3_Err != nil {
2083- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 63}
2084- }
2085- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
2086- if templ_7745c5c3_Err != nil {
2087- return templ_7745c5c3_Err
2088- }
2089- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "</a> ")
2090- if templ_7745c5c3_Err != nil {
2091- return templ_7745c5c3_Err
2092- }
2093- if v.Stale {
2094- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "<span class=\"mute\">(stale)</span>")
2095- if templ_7745c5c3_Err != nil {
2096- return templ_7745c5c3_Err
2097- }
2098- }
2099- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</li>")
2100- if templ_7745c5c3_Err != nil {
2101- return templ_7745c5c3_Err
2102- }
2072+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
2073+ if templ_7745c5c3_Err != nil {
2074+ return templ_7745c5c3_Err
21032075 }
2104- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</ul></section>")
2076+ } else {
2077+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "no checks")
21052078 if templ_7745c5c3_Err != nil {
21062079 return templ_7745c5c3_Err
21072080 }
21082081 }
2109- templ_7745c5c3_Err = MRChecks(p.Checks).Render(ctx, templ_7745c5c3_Buffer)
2110- if templ_7745c5c3_Err != nil {
2111- return templ_7745c5c3_Err
2112- }
2113- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
2114- if templ_7745c5c3_Err != nil {
2115- return templ_7745c5c3_Err
2116- }
2117- var templ_7745c5c3_Var102 string
2118- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
2119- if templ_7745c5c3_Err != nil {
2120- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 71}
2121- }
2122- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
2123- if templ_7745c5c3_Err != nil {
2124- return templ_7745c5c3_Err
2125- }
2126- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</span></h2><ul class=\"pcommits\">")
2082+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</span></a></nav><div id=\"mr-commits\" data-mr-panel role=\"region\" aria-labelledby=\"mr-tab-commits\"><table class=\"mr-commits\"><thead><tr><th>Commit</th><th>Subject</th><th>Author</th><th>Date</th><th>Signature</th><th>Status</th></tr></thead> <tbody>")
21272083 if templ_7745c5c3_Err != nil {
21282084 return templ_7745c5c3_Err
21292085 }
21302086 for _, c := range p.Commits {
2131- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "<li")
2087+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<tr")
21322088 if templ_7745c5c3_Err != nil {
21332089 return templ_7745c5c3_Err
21342090 }
21352091 if c.New {
2136- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, " data-new")
2092+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, " data-new")
21372093 if templ_7745c5c3_Err != nil {
21382094 return templ_7745c5c3_Err
21392095 }
21402096 }
2141- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "><a href=\"")
2097+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "><td><a href=\"")
21422098 if templ_7745c5c3_Err != nil {
21432099 return templ_7745c5c3_Err
21442100 }
2145- var templ_7745c5c3_Var103 templ.SafeURL
2146- templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
2101+ var templ_7745c5c3_Var101 templ.SafeURL
2102+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
21472103 if templ_7745c5c3_Err != nil {
2148- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 36}
2104+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 42}
21492105 }
2150- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
2106+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
21512107 if templ_7745c5c3_Err != nil {
21522108 return templ_7745c5c3_Err
21532109 }
2154- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\">")
2110+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\">")
21552111 if templ_7745c5c3_Err != nil {
21562112 return templ_7745c5c3_Err
21572113 }
2158- var templ_7745c5c3_Var104 string
2159- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
2114+ var templ_7745c5c3_Var102 string
2115+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
21602116 if templ_7745c5c3_Err != nil {
2161- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 48}
2117+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 54}
21622118 }
2163- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
2119+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
21642120 if templ_7745c5c3_Err != nil {
21652121 return templ_7745c5c3_Err
21662122 }
2167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</a> <span class=\"s\">")
2123+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</a></td><td>")
21682124 if templ_7745c5c3_Err != nil {
21692125 return templ_7745c5c3_Err
21702126 }
2171- var templ_7745c5c3_Var105 string
2172- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
2127+ var templ_7745c5c3_Var103 string
2128+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
21732129 if templ_7745c5c3_Err != nil {
2174- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 401, Col: 32}
2130+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 401, Col: 22}
21752131 }
2176- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
2177- if templ_7745c5c3_Err != nil {
2178- return templ_7745c5c3_Err
2179- }
2180- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "</span> ")
2132+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
21812133 if templ_7745c5c3_Err != nil {
21822134 return templ_7745c5c3_Err
21832135 }
2184- if c.Sig != nil {
2185- templ_7745c5c3_Err = signaturePill(c.Sig).Render(ctx, templ_7745c5c3_Buffer)
2186- if templ_7745c5c3_Err != nil {
2187- return templ_7745c5c3_Err
2188- }
2189- }
2190- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<span class=\"mute\">")
2136+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</td><td>")
21912137 if templ_7745c5c3_Err != nil {
21922138 return templ_7745c5c3_Err
21932139 }
2194- var templ_7745c5c3_Var106 string
2195- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
2140+ var templ_7745c5c3_Var104 string
2141+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
21962142 if templ_7745c5c3_Err != nil {
2197- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 406, Col: 16}
2143+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 402, Col: 21}
21982144 }
2199- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
2145+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
22002146 if templ_7745c5c3_Err != nil {
22012147 return templ_7745c5c3_Err
22022148 }
2203- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, " ")
2149+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</td><td>")
22042150 if templ_7745c5c3_Err != nil {
22052151 return templ_7745c5c3_Err
22062152 }
22072153 if c.WhenAt != "" {
2208- var templ_7745c5c3_Var107 string
2209- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
2210- if templ_7745c5c3_Err != nil {
2211- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 15}
2212- }
2213- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
2154+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "<time datetime=\"")
22142155 if templ_7745c5c3_Err != nil {
22152156 return templ_7745c5c3_Err
22162157 }
2217- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "<time datetime=\"")
2158+ var templ_7745c5c3_Var105 string
2159+ templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
22182160 if templ_7745c5c3_Err != nil {
2219- return templ_7745c5c3_Err
2220- }
2221- var templ_7745c5c3_Var108 string
2222- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
2223- if templ_7745c5c3_Err != nil {
2224- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 42}
2161+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 405, Col: 34}
22252162 }
2226- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var108)
2163+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105)
22272164 if templ_7745c5c3_Err != nil {
22282165 return templ_7745c5c3_Err
22292166 }
2230- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\">")
2167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "\">")
22312168 if templ_7745c5c3_Err != nil {
22322169 return templ_7745c5c3_Err
22332170 }
2234- var templ_7745c5c3_Var109 string
2235- templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
2171+ var templ_7745c5c3_Var106 string
2172+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
22362173 if templ_7745c5c3_Err != nil {
2237- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 408, Col: 53}
2174+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 405, Col: 45}
22382175 }
2239- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
2176+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
22402177 if templ_7745c5c3_Err != nil {
22412178 return templ_7745c5c3_Err
22422179 }
2243- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "</time> ")
2180+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</time>")
22442181 if templ_7745c5c3_Err != nil {
22452182 return templ_7745c5c3_Err
22462183 }
22472184 }
2248- if c.New {
2249- var templ_7745c5c3_Var110 string
2250- templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
2185+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</td><td>")
2186+ if templ_7745c5c3_Err != nil {
2187+ return templ_7745c5c3_Err
2188+ }
2189+ if c.Sig != nil {
2190+ templ_7745c5c3_Err = signaturePill(c.Sig).Render(ctx, templ_7745c5c3_Buffer)
22512191 if templ_7745c5c3_Err != nil {
2252- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 18}
2192+ return templ_7745c5c3_Err
22532193 }
2254- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))
2194+ }
2195+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "</td><td>")
2196+ if templ_7745c5c3_Err != nil {
2197+ return templ_7745c5c3_Err
2198+ }
2199+ if c.New {
2200+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<span>new</span>")
22552201 if templ_7745c5c3_Err != nil {
22562202 return templ_7745c5c3_Err
22572203 }
22582204 }
2259- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "</span></li>")
2205+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</td></tr>")
22602206 if templ_7745c5c3_Err != nil {
22612207 return templ_7745c5c3_Err
22622208 }
22632209 }
2264- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "</ul>")
2210+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</tbody></table>")
22652211 if templ_7745c5c3_Err != nil {
22662212 return templ_7745c5c3_Err
22672213 }
22682214 if len(p.Commits) == 0 {
2269- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "<p class=\"dim\">no commit to merge.</p>")
2215+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<p class=\"dim\">no commit to merge.</p>")
22702216 if templ_7745c5c3_Err != nil {
22712217 return templ_7745c5c3_Err
22722218 }
22732219 }
22742220 if p.CommitsNote != "" {
2275- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "<p class=\"note\">")
2221+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "<p class=\"note\">")
22762222 if templ_7745c5c3_Err != nil {
22772223 return templ_7745c5c3_Err
22782224 }
2279- var templ_7745c5c3_Var111 string
2280- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
2225+ var templ_7745c5c3_Var107 string
2226+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
22812227 if templ_7745c5c3_Err != nil {
2282- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 34}
2228+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 35}
22832229 }
2284- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2230+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
22852231 if templ_7745c5c3_Err != nil {
22862232 return templ_7745c5c3_Err
22872233 }
2288- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</p>")
2234+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "</p>")
22892235 if templ_7745c5c3_Err != nil {
22902236 return templ_7745c5c3_Err
22912237 }
22922238 }
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>")
2239+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "</div><div id=\"mr-checks-panel\" data-mr-panel role=\"region\" aria-labelledby=\"mr-tab-checks\">")
22942240 if templ_7745c5c3_Err != nil {
22952241 return templ_7745c5c3_Err
22962242 }
2297- var templ_7745c5c3_Var112 string
2298- templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
2243+ templ_7745c5c3_Err = MRChecks(p.Checks).Render(ctx, templ_7745c5c3_Buffer)
22992244 if templ_7745c5c3_Err != nil {
2300- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 428, Col: 36}
2245+ return templ_7745c5c3_Err
23012246 }
2302- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
2247+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "</div></footer>")
23032248 if templ_7745c5c3_Err != nil {
23042249 return templ_7745c5c3_Err
23052250 }
2306- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</dd><dt>checks</dt><dd>")
2251+ return nil
2252+ })
2253+}
2254+
2255+func mrRail(p MRPage) templ.Component {
2256+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2257+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
2258+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
2259+ return templ_7745c5c3_CtxErr
2260+ }
2261+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
2262+ if !templ_7745c5c3_IsBuffer {
2263+ defer func() {
2264+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
2265+ if templ_7745c5c3_Err == nil {
2266+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
2267+ }
2268+ }()
2269+ }
2270+ ctx = templ.InitializeContext(ctx)
2271+ templ_7745c5c3_Var108 := templ.GetChildren(ctx)
2272+ if templ_7745c5c3_Var108 == nil {
2273+ templ_7745c5c3_Var108 = templ.NopComponent
2274+ }
2275+ ctx = templ.ClearChildren(ctx)
2276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
23072277 if templ_7745c5c3_Err != nil {
23082278 return templ_7745c5c3_Err
23092279 }
2310- if p.ChecksBlock {
2311- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "failing ones block the merge")
2280+ var templ_7745c5c3_Var109 string
2281+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
2282+ if templ_7745c5c3_Err != nil {
2283+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 438, Col: 49}
2284+ }
2285+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
2286+ if templ_7745c5c3_Err != nil {
2287+ return templ_7745c5c3_Err
2288+ }
2289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</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>")
2290+ if templ_7745c5c3_Err != nil {
2291+ return templ_7745c5c3_Err
2292+ }
2293+ templ_7745c5c3_Err = FileList(p.Files).Render(ctx, templ_7745c5c3_Buffer)
2294+ if templ_7745c5c3_Err != nil {
2295+ return templ_7745c5c3_Err
2296+ }
2297+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "</section>")
2298+ if templ_7745c5c3_Err != nil {
2299+ return templ_7745c5c3_Err
2300+ }
2301+ if len(p.Verdicts) > 0 {
2302+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "<section><h2>Verdicts</h2><ul>")
23122303 if templ_7745c5c3_Err != nil {
23132304 return templ_7745c5c3_Err
23142305 }
2315- } else {
2316- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "do not block the merge")
2306+ for _, v := range p.Verdicts {
2307+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "<li><a href=\"")
2308+ if templ_7745c5c3_Err != nil {
2309+ return templ_7745c5c3_Err
2310+ }
2311+ var templ_7745c5c3_Var110 templ.SafeURL
2312+ templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
2313+ if templ_7745c5c3_Err != nil {
2314+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 451, Col: 37}
2315+ }
2316+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))
2317+ if templ_7745c5c3_Err != nil {
2318+ return templ_7745c5c3_Err
2319+ }
2320+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "\">")
2321+ if templ_7745c5c3_Err != nil {
2322+ return templ_7745c5c3_Err
2323+ }
2324+ var templ_7745c5c3_Var111 string
2325+ templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
2326+ if templ_7745c5c3_Err != nil {
2327+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 451, Col: 50}
2328+ }
2329+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2330+ if templ_7745c5c3_Err != nil {
2331+ return templ_7745c5c3_Err
2332+ }
2333+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, ": ")
2334+ if templ_7745c5c3_Err != nil {
2335+ return templ_7745c5c3_Err
2336+ }
2337+ var templ_7745c5c3_Var112 string
2338+ templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2339+ if templ_7745c5c3_Err != nil {
2340+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 451, Col: 63}
2341+ }
2342+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
2343+ if templ_7745c5c3_Err != nil {
2344+ return templ_7745c5c3_Err
2345+ }
2346+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</a> ")
2347+ if templ_7745c5c3_Err != nil {
2348+ return templ_7745c5c3_Err
2349+ }
2350+ if v.Stale {
2351+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "<span class=\"mute\">(stale)</span>")
2352+ if templ_7745c5c3_Err != nil {
2353+ return templ_7745c5c3_Err
2354+ }
2355+ }
2356+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "</li>")
2357+ if templ_7745c5c3_Err != nil {
2358+ return templ_7745c5c3_Err
2359+ }
2360+ }
2361+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "</ul></section>")
23172362 if templ_7745c5c3_Err != nil {
23182363 return templ_7745c5c3_Err
23192364 }
23202365 }
2321- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "</dd></dl></section>")
2322- if templ_7745c5c3_Err != nil {
2323- return templ_7745c5c3_Err
2324- }
23252366 return nil
23262367 })
23272368 }
@@ -2351,46 +2392,46 @@ func mrBar(p MRPage) templ.Component {
23512392 templ_7745c5c3_Var113 = templ.NopComponent
23522393 }
23532394 ctx = templ.ClearChildren(ctx)
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\">")
2395+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")
23552396 if templ_7745c5c3_Err != nil {
23562397 return templ_7745c5c3_Err
23572398 }
23582399 var templ_7745c5c3_Var114 string
23592400 templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
23602401 if templ_7745c5c3_Err != nil {
2361- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 55}
2402+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 468, Col: 55}
23622403 }
23632404 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114))
23642405 if templ_7745c5c3_Err != nil {
23652406 return templ_7745c5c3_Err
23662407 }
2367- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, " · <span class=\"plus\">+")
2408+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, " · <span class=\"plus\">+")
23682409 if templ_7745c5c3_Err != nil {
23692410 return templ_7745c5c3_Err
23702411 }
23712412 var templ_7745c5c3_Var115 string
23722413 templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
23732414 if templ_7745c5c3_Err != nil {
2374- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 95}
2415+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 468, Col: 95}
23752416 }
23762417 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
23772418 if templ_7745c5c3_Err != nil {
23782419 return templ_7745c5c3_Err
23792420 }
2380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "</span> <span class=\"minus\">-")
2421+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</span> <span class=\"minus\">-")
23812422 if templ_7745c5c3_Err != nil {
23822423 return templ_7745c5c3_Err
23832424 }
23842425 var templ_7745c5c3_Var116 string
23852426 templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
23862427 if templ_7745c5c3_Err != nil {
2387- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 140}
2428+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 468, Col: 140}
23882429 }
23892430 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
23902431 if templ_7745c5c3_Err != nil {
23912432 return templ_7745c5c3_Err
23922433 }
2393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "</span></span>")
2434+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "</span></span>")
23942435 if templ_7745c5c3_Err != nil {
23952436 return templ_7745c5c3_Err
23962437 }
@@ -2398,7 +2439,7 @@ func mrBar(p MRPage) templ.Component {
23982439 if templ_7745c5c3_Err != nil {
23992440 return templ_7745c5c3_Err
24002441 }
2401- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "<span class=\"grow\"></span>")
2442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<span class=\"grow\"></span>")
24022443 if templ_7745c5c3_Err != nil {
24032444 return templ_7745c5c3_Err
24042445 }
@@ -2411,16 +2452,12 @@ func mrBar(p MRPage) templ.Component {
24112452 if templ_7745c5c3_Err != nil {
24122453 return templ_7745c5c3_Err
24132454 }
2414- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, " ")
2415- if templ_7745c5c3_Err != nil {
2416- return templ_7745c5c3_Err
2417- }
2418- templ_7745c5c3_Err = moreMenu(p).Render(ctx, templ_7745c5c3_Buffer)
2419- if templ_7745c5c3_Err != nil {
2420- return templ_7745c5c3_Err
2421- }
24222455 }
2423- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</div>")
2456+ templ_7745c5c3_Err = moreMenu(p).Render(ctx, templ_7745c5c3_Buffer)
2457+ if templ_7745c5c3_Err != nil {
2458+ return templ_7745c5c3_Err
2459+ }
2460+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</div>")
24242461 if templ_7745c5c3_Err != nil {
24252462 return templ_7745c5c3_Err
24262463 }
@@ -2457,102 +2494,75 @@ func mergeMenu(p MRPage) templ.Component {
24572494 templ_7745c5c3_Var117 = templ.NopComponent
24582495 }
24592496 ctx = templ.ClearChildren(ctx)
2460- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"")
2497+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"")
24612498 if templ_7745c5c3_Err != nil {
24622499 return templ_7745c5c3_Err
24632500 }
24642501 if p.Box.Error != "" {
2465- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, " open")
2502+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " open")
24662503 if templ_7745c5c3_Err != nil {
24672504 return templ_7745c5c3_Err
24682505 }
24692506 }
2470- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
2507+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
24712508 if templ_7745c5c3_Err != nil {
24722509 return templ_7745c5c3_Err
24732510 }
24742511 if p.Box.ReadyAction != "" {
2475- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "Draft ")
2512+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "Draft ")
24762513 if templ_7745c5c3_Err != nil {
24772514 return templ_7745c5c3_Err
24782515 }
24792516 } else {
2480- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "Merge ")
2517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "Merge ")
24812518 if templ_7745c5c3_Err != nil {
24822519 return templ_7745c5c3_Err
24832520 }
24842521 }
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>")
2522+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\">")
24862523 if templ_7745c5c3_Err != nil {
24872524 return templ_7745c5c3_Err
24882525 }
24892526 if p.Box.HeadGone {
2490- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "gone: the source branch was deleted")
2527+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<p class=\"dim\">the source branch was deleted</p>")
24912528 if templ_7745c5c3_Err != nil {
24922529 return templ_7745c5c3_Err
24932530 }
2494- } else {
2495- var templ_7745c5c3_Var118 string
2496- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
2497- if templ_7745c5c3_Err != nil {
2498- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 483, Col: 19}
2499- }
2500- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2501- if templ_7745c5c3_Err != nil {
2502- return templ_7745c5c3_Err
2503- }
2504- }
2505- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "</dd><dt>into</dt><dd>")
2506- if templ_7745c5c3_Err != nil {
2507- return templ_7745c5c3_Err
2508- }
2509- var templ_7745c5c3_Var119 string
2510- templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2511- if templ_7745c5c3_Err != nil {
2512- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 18}
2513- }
2514- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
2515- if templ_7745c5c3_Err != nil {
2516- return templ_7745c5c3_Err
2517- }
2518- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</dd></dl>")
2519- if templ_7745c5c3_Err != nil {
2520- return templ_7745c5c3_Err
25212531 }
25222532 if p.Box.Error != "" {
2523- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<p class=\"d-err\" role=\"alert\">")
2533+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "<p class=\"d-err\" role=\"alert\">")
25242534 if templ_7745c5c3_Err != nil {
25252535 return templ_7745c5c3_Err
25262536 }
2527- var templ_7745c5c3_Var120 string
2528- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Error)
2537+ var templ_7745c5c3_Var118 string
2538+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Error)
25292539 if templ_7745c5c3_Err != nil {
2530- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 490, Col: 47}
2540+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 502, Col: 47}
25312541 }
2532- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2542+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
25332543 if templ_7745c5c3_Err != nil {
25342544 return templ_7745c5c3_Err
25352545 }
2536- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</p>")
2546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</p>")
25372547 if templ_7745c5c3_Err != nil {
25382548 return templ_7745c5c3_Err
25392549 }
25402550 }
25412551 if p.Box.ReadyAction != "" {
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=\"")
2552+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "<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=\"")
25432553 if templ_7745c5c3_Err != nil {
25442554 return templ_7745c5c3_Err
25452555 }
2546- var templ_7745c5c3_Var121 templ.SafeURL
2547- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
2556+ var templ_7745c5c3_Var119 templ.SafeURL
2557+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
25482558 if templ_7745c5c3_Err != nil {
2549- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 494, Col: 80}
2559+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 506, Col: 80}
25502560 }
2551- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2561+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
25522562 if templ_7745c5c3_Err != nil {
25532563 return templ_7745c5c3_Err
25542564 }
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>")
2565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")
25562566 if templ_7745c5c3_Err != nil {
25572567 return templ_7745c5c3_Err
25582568 }
@@ -2561,214 +2571,185 @@ func mergeMenu(p MRPage) templ.Component {
25612571 if templ_7745c5c3_Err != nil {
25622572 return templ_7745c5c3_Err
25632573 }
2564- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, " <form class=\"d-acts\" method=\"post\" action=\"")
2574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, " <form class=\"d-acts\" method=\"post\" action=\"")
25652575 if templ_7745c5c3_Err != nil {
25662576 return templ_7745c5c3_Err
25672577 }
2568- var templ_7745c5c3_Var122 templ.SafeURL
2569- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2578+ var templ_7745c5c3_Var120 templ.SafeURL
2579+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
25702580 if templ_7745c5c3_Err != nil {
2571- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 499, Col: 80}
2581+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 80}
25722582 }
2573- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2583+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
25742584 if templ_7745c5c3_Err != nil {
25752585 return templ_7745c5c3_Err
25762586 }
2577- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2587+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
25782588 if templ_7745c5c3_Err != nil {
25792589 return templ_7745c5c3_Err
25802590 }
2581- var templ_7745c5c3_Var123 string
2582- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2591+ var templ_7745c5c3_Var121 string
2592+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
25832593 if templ_7745c5c3_Err != nil {
2584- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 500, Col: 59}
2594+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 59}
25852595 }
2586- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
2596+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var121)
25872597 if templ_7745c5c3_Err != nil {
25882598 return templ_7745c5c3_Err
25892599 }
2590- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
2600+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
25912601 if templ_7745c5c3_Err != nil {
25922602 return templ_7745c5c3_Err
25932603 }
25942604 for i, m := range p.Box.Modes {
2595- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<label><input type=\"radio\" name=\"mode\" value=\"")
2605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "<label><input type=\"radio\" name=\"mode\" value=\"")
25962606 if templ_7745c5c3_Err != nil {
25972607 return templ_7745c5c3_Err
25982608 }
2599- var templ_7745c5c3_Var124 string
2600- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2609+ var templ_7745c5c3_Var122 string
2610+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
26012611 if templ_7745c5c3_Err != nil {
2602- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 508, Col: 24}
2612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 520, Col: 24}
26032613 }
2604- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
2614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122)
26052615 if templ_7745c5c3_Err != nil {
26062616 return templ_7745c5c3_Err
26072617 }
2608- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "\"")
2618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "\"")
26092619 if templ_7745c5c3_Err != nil {
26102620 return templ_7745c5c3_Err
26112621 }
26122622 if i == p.Box.Checked() {
2613- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, " checked")
2623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, " checked")
26142624 if templ_7745c5c3_Err != nil {
26152625 return templ_7745c5c3_Err
26162626 }
26172627 }
26182628 if m.Disabled {
2619- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, " disabled")
2629+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " disabled")
26202630 if templ_7745c5c3_Err != nil {
26212631 return templ_7745c5c3_Err
26222632 }
26232633 }
2624- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " title=\"")
2634+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, " title=\"")
26252635 if templ_7745c5c3_Err != nil {
26262636 return templ_7745c5c3_Err
26272637 }
2628- var templ_7745c5c3_Var125 string
2629- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)
2638+ var templ_7745c5c3_Var123 string
2639+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)
26302640 if templ_7745c5c3_Err != nil {
2631- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 25}
2641+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 523, Col: 25}
26322642 }
2633- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var125)
2643+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
26342644 if templ_7745c5c3_Err != nil {
26352645 return templ_7745c5c3_Err
26362646 }
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\"> ")
2647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "\" :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\"> ")
26382648 if templ_7745c5c3_Err != nil {
26392649 return templ_7745c5c3_Err
26402650 }
2641- var templ_7745c5c3_Var126 string
2642- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2651+ var templ_7745c5c3_Var124 string
2652+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
26432653 if templ_7745c5c3_Err != nil {
2644- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 515, Col: 17}
2654+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 527, Col: 17}
26452655 }
2646- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2656+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
26472657 if templ_7745c5c3_Err != nil {
26482658 return templ_7745c5c3_Err
26492659 }
2650- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, " ")
2660+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, " ")
26512661 if templ_7745c5c3_Err != nil {
26522662 return templ_7745c5c3_Err
26532663 }
26542664 if i < 9 {
2655- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "<kbd aria-hidden=\"true\">")
2665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "<kbd aria-hidden=\"true\">")
26562666 if templ_7745c5c3_Err != nil {
26572667 return templ_7745c5c3_Err
26582668 }
2659- var templ_7745c5c3_Var127 string
2660- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
2669+ var templ_7745c5c3_Var125 string
2670+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
26612671 if templ_7745c5c3_Err != nil {
2662- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 517, Col: 46}
2672+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 46}
26632673 }
2664- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2674+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
26652675 if templ_7745c5c3_Err != nil {
26662676 return templ_7745c5c3_Err
26672677 }
2668- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "</kbd>")
2678+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</kbd>")
26692679 if templ_7745c5c3_Err != nil {
26702680 return templ_7745c5c3_Err
26712681 }
26722682 }
2673- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</label>")
2683+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</label>")
26742684 if templ_7745c5c3_Err != nil {
26752685 return templ_7745c5c3_Err
26762686 }
26772687 }
2678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</fieldset>")
2688+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "</fieldset>")
26792689 if templ_7745c5c3_Err != nil {
26802690 return templ_7745c5c3_Err
26812691 }
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- }
27112692 if p.Box.CanDeleteBranch {
2712- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2693+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
27132694 if templ_7745c5c3_Err != nil {
27142695 return templ_7745c5c3_Err
27152696 }
2716- var templ_7745c5c3_Var129 string
2717- templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2697+ var templ_7745c5c3_Var126 string
2698+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
27182699 if templ_7745c5c3_Err != nil {
2719- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 102}
2700+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 535, Col: 102}
27202701 }
2721- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var129))
2702+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
27222703 if templ_7745c5c3_Err != nil {
27232704 return templ_7745c5c3_Err
27242705 }
2725- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2706+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
27262707 if templ_7745c5c3_Err != nil {
27272708 return templ_7745c5c3_Err
27282709 }
27292710 }
2730- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")
2711+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")
27312712 if templ_7745c5c3_Err != nil {
27322713 return templ_7745c5c3_Err
27332714 }
27342715 if !p.Box.CanMerge() {
2735- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, " disabled")
2716+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, " disabled")
27362717 if templ_7745c5c3_Err != nil {
27372718 return templ_7745c5c3_Err
27382719 }
27392720 }
2740- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, " title=\"")
2721+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, " title=\"")
27412722 if templ_7745c5c3_Err != nil {
27422723 return templ_7745c5c3_Err
27432724 }
2744- var templ_7745c5c3_Var130 string
2745- templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
2725+ var templ_7745c5c3_Var127 string
2726+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
27462727 if templ_7745c5c3_Err != nil {
2747- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 540, Col: 33}
2728+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 33}
27482729 }
2749- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
2730+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127)
27502731 if templ_7745c5c3_Err != nil {
27512732 return templ_7745c5c3_Err
27522733 }
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 ")
2734+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "\" :disabled=\"!this.form.querySelector('input[name=mode]:checked:not(:disabled)')\" :title=\"(this.form.querySelector('input[name=mode]:checked') || {}).title || ''\">Merge into ")
27542735 if templ_7745c5c3_Err != nil {
27552736 return templ_7745c5c3_Err
27562737 }
2757- var templ_7745c5c3_Var131 string
2758- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2738+ var templ_7745c5c3_Var128 string
2739+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
27592740 if templ_7745c5c3_Err != nil {
2760- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 28}
2741+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 28}
27612742 }
2762- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
2743+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
27632744 if templ_7745c5c3_Err != nil {
27642745 return templ_7745c5c3_Err
27652746 }
2766- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "</button></div></form>")
2747+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</button></div></form>")
27672748 if templ_7745c5c3_Err != nil {
27682749 return templ_7745c5c3_Err
27692750 }
27702751 }
2771- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "</div></details>")
2752+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</div></details>")
27722753 if templ_7745c5c3_Err != nil {
27732754 return templ_7745c5c3_Err
27742755 }
@@ -2794,182 +2775,182 @@ func MRMergeability(v MergeabilityView) templ.Component {
27942775 }()
27952776 }
27962777 ctx = templ.InitializeContext(ctx)
2797- templ_7745c5c3_Var132 := templ.GetChildren(ctx)
2798- if templ_7745c5c3_Var132 == nil {
2799- templ_7745c5c3_Var132 = templ.NopComponent
2778+ templ_7745c5c3_Var129 := templ.GetChildren(ctx)
2779+ if templ_7745c5c3_Var129 == nil {
2780+ templ_7745c5c3_Var129 = templ.NopComponent
28002781 }
28012782 ctx = templ.ClearChildren(ctx)
2802- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "<span data-mergeability data-state=\"")
2783+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "<span data-mergeability data-state=\"")
28032784 if templ_7745c5c3_Err != nil {
28042785 return templ_7745c5c3_Err
28052786 }
2806- var templ_7745c5c3_Var133 string
2807- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2787+ var templ_7745c5c3_Var130 string
2788+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
28082789 if templ_7745c5c3_Err != nil {
2809- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 22}
2790+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 559, Col: 22}
28102791 }
2811- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var133)
2792+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
28122793 if templ_7745c5c3_Err != nil {
28132794 return templ_7745c5c3_Err
28142795 }
2815- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\" data-ff-ok=\"")
2796+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "\" data-ff-ok=\"")
28162797 if templ_7745c5c3_Err != nil {
28172798 return templ_7745c5c3_Err
28182799 }
2819- var templ_7745c5c3_Var134 string
2820- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
2800+ var templ_7745c5c3_Var131 string
2801+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
28212802 if templ_7745c5c3_Err != nil {
2822- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 557, Col: 41}
2803+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 41}
28232804 }
2824- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var134)
2805+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)
28252806 if templ_7745c5c3_Err != nil {
28262807 return templ_7745c5c3_Err
28272808 }
2828- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\" data-merge-ok=\"")
2809+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "\" data-merge-ok=\"")
28292810 if templ_7745c5c3_Err != nil {
28302811 return templ_7745c5c3_Err
28312812 }
2832- var templ_7745c5c3_Var135 string
2833- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
2813+ var templ_7745c5c3_Var132 string
2814+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
28342815 if templ_7745c5c3_Err != nil {
2835- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 558, Col: 47}
2816+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 561, Col: 47}
28362817 }
2837- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
2818+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var132)
28382819 if templ_7745c5c3_Err != nil {
28392820 return templ_7745c5c3_Err
28402821 }
2841- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\" data-ff-reason=\"")
2822+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\" data-ff-reason=\"")
28422823 if templ_7745c5c3_Err != nil {
28432824 return templ_7745c5c3_Err
28442825 }
2845- var templ_7745c5c3_Var136 string
2846- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
2826+ var templ_7745c5c3_Var133 string
2827+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
28472828 if templ_7745c5c3_Err != nil {
2848- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 559, Col: 29}
2829+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 562, Col: 29}
28492830 }
2850- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136)
2831+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var133)
28512832 if templ_7745c5c3_Err != nil {
28522833 return templ_7745c5c3_Err
28532834 }
2854- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "\" data-merge-reason=\"")
2835+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\" data-merge-reason=\"")
28552836 if templ_7745c5c3_Err != nil {
28562837 return templ_7745c5c3_Err
28572838 }
2858- var templ_7745c5c3_Var137 string
2859- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
2839+ var templ_7745c5c3_Var134 string
2840+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
28602841 if templ_7745c5c3_Err != nil {
2861- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 35}
2842+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 35}
28622843 }
2863- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137)
2844+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var134)
28642845 if templ_7745c5c3_Err != nil {
28652846 return templ_7745c5c3_Err
28662847 }
2867- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\"><span class=\"badge\" data-state=\"")
2848+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\"><span class=\"badge\" data-state=\"")
28682849 if templ_7745c5c3_Err != nil {
28692850 return templ_7745c5c3_Err
28702851 }
2871- var templ_7745c5c3_Var138 string
2872- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2852+ var templ_7745c5c3_Var135 string
2853+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
28732854 if templ_7745c5c3_Err != nil {
2874- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 562, Col: 42}
2855+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 42}
28752856 }
2876- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2857+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
28772858 if templ_7745c5c3_Err != nil {
28782859 return templ_7745c5c3_Err
28792860 }
2880- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\">")
2861+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\">")
28812862 if templ_7745c5c3_Err != nil {
28822863 return templ_7745c5c3_Err
28832864 }
2884- var templ_7745c5c3_Var139 string
2885- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2865+ var templ_7745c5c3_Var136 string
2866+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
28862867 if templ_7745c5c3_Err != nil {
2887- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 562, Col: 54}
2868+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 54}
28882869 }
2889- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
2870+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
28902871 if templ_7745c5c3_Err != nil {
28912872 return templ_7745c5c3_Err
28922873 }
2893- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span> ")
2874+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "</span> ")
28942875 if templ_7745c5c3_Err != nil {
28952876 return templ_7745c5c3_Err
28962877 }
28972878 if v.Detail != "" {
2898- var templ_7745c5c3_Var140 string
2899- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2879+ var templ_7745c5c3_Var137 string
2880+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
29002881 if templ_7745c5c3_Err != nil {
2901- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 8}
2882+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 567, Col: 8}
29022883 }
2903- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2884+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
29042885 if templ_7745c5c3_Err != nil {
29052886 return templ_7745c5c3_Err
29062887 }
2907- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<span class=\"dim\">")
2888+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<span class=\"dim\">")
29082889 if templ_7745c5c3_Err != nil {
29092890 return templ_7745c5c3_Err
29102891 }
2911- var templ_7745c5c3_Var141 string
2912- templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
2892+ var templ_7745c5c3_Var138 string
2893+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
29132894 if templ_7745c5c3_Err != nil {
2914- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 38}
2895+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 567, Col: 38}
29152896 }
2916- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
2897+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
29172898 if templ_7745c5c3_Err != nil {
29182899 return templ_7745c5c3_Err
29192900 }
2920- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</span>")
2901+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</span>")
29212902 if templ_7745c5c3_Err != nil {
29222903 return templ_7745c5c3_Err
29232904 }
29242905 }
2925- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</span> ")
2906+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span> ")
29262907 if templ_7745c5c3_Err != nil {
29272908 return templ_7745c5c3_Err
29282909 }
29292910 if len(v.Paths) > 0 {
2930- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")
2911+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")
29312912 if templ_7745c5c3_Err != nil {
29322913 return templ_7745c5c3_Err
29332914 }
29342915 for _, path := range v.Paths {
2935- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<li><code>")
2916+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "<li><code>")
29362917 if templ_7745c5c3_Err != nil {
29372918 return templ_7745c5c3_Err
29382919 }
2939- var templ_7745c5c3_Var142 string
2940- templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(path)
2920+ var templ_7745c5c3_Var139 string
2921+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(path)
29412922 if templ_7745c5c3_Err != nil {
2942- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 570, Col: 20}
2923+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 573, Col: 20}
29432924 }
2944- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142))
2925+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
29452926 if templ_7745c5c3_Err != nil {
29462927 return templ_7745c5c3_Err
29472928 }
2948- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</code></li>")
2929+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</code></li>")
29492930 if templ_7745c5c3_Err != nil {
29502931 return templ_7745c5c3_Err
29512932 }
29522933 }
2953- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</ul>")
2934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</ul>")
29542935 if templ_7745c5c3_Err != nil {
29552936 return templ_7745c5c3_Err
29562937 }
29572938 }
29582939 if v.PathsNote != "" {
2959- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<p class=\"dim\">")
2940+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<p class=\"dim\">")
29602941 if templ_7745c5c3_Err != nil {
29612942 return templ_7745c5c3_Err
29622943 }
2963- var templ_7745c5c3_Var143 string
2964- templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
2944+ var templ_7745c5c3_Var140 string
2945+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
29652946 if templ_7745c5c3_Err != nil {
2966- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 575, Col: 30}
2947+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 30}
29672948 }
2968- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143))
2949+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
29692950 if templ_7745c5c3_Err != nil {
29702951 return templ_7745c5c3_Err
29712952 }
2972- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</p>")
2953+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</p>")
29732954 if templ_7745c5c3_Err != nil {
29742955 return templ_7745c5c3_Err
29752956 }
@@ -2996,12 +2977,12 @@ func mrMergeabilitySlot(v MergeabilityView) templ.Component {
29962977 }()
29972978 }
29982979 ctx = templ.InitializeContext(ctx)
2999- templ_7745c5c3_Var144 := templ.GetChildren(ctx)
3000- if templ_7745c5c3_Var144 == nil {
3001- templ_7745c5c3_Var144 = templ.NopComponent
2980+ templ_7745c5c3_Var141 := templ.GetChildren(ctx)
2981+ if templ_7745c5c3_Var141 == nil {
2982+ templ_7745c5c3_Var141 = templ.NopComponent
30022983 }
30032984 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>")
2985+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "<div id=\"mr-mergeability\" class=\"live-slot\" role=\"status\" data-slot>")
30052986 if templ_7745c5c3_Err != nil {
30062987 return templ_7745c5c3_Err
30072988 }
@@ -3009,7 +2990,7 @@ func mrMergeabilitySlot(v MergeabilityView) templ.Component {
30092990 if templ_7745c5c3_Err != nil {
30102991 return templ_7745c5c3_Err
30112992 }
3012- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "</div>")
2993+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "</div>")
30132994 if templ_7745c5c3_Err != nil {
30142995 return templ_7745c5c3_Err
30152996 }
@@ -3034,9 +3015,9 @@ func MRMergeabilityFragment(v MergeabilityView) templ.Component {
30343015 }()
30353016 }
30363017 ctx = templ.InitializeContext(ctx)
3037- templ_7745c5c3_Var145 := templ.GetChildren(ctx)
3038- if templ_7745c5c3_Var145 == nil {
3039- templ_7745c5c3_Var145 = templ.NopComponent
3018+ templ_7745c5c3_Var142 := templ.GetChildren(ctx)
3019+ if templ_7745c5c3_Var142 == nil {
3020+ templ_7745c5c3_Var142 = templ.NopComponent
30403021 }
30413022 ctx = templ.ClearChildren(ctx)
30423023 templ_7745c5c3_Err = mrMergeabilitySlot(v).Render(ctx, templ_7745c5c3_Buffer)
@@ -3047,8 +3028,8 @@ func MRMergeabilityFragment(v MergeabilityView) templ.Component {
30473028 })
30483029 }
30493030
3050-// moreMenu is what else the owner does with an open request: mark the head
3051-// read, close it, merge it from a terminal.
3031+// moreMenu shows read-only settings to every viewer, and keeps owner actions
3032+// behind the same merge-box permission as before.
30523033 func moreMenu(p MRPage) templ.Component {
30533034 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
30543035 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -3065,79 +3046,142 @@ func moreMenu(p MRPage) templ.Component {
30653046 }()
30663047 }
30673048 ctx = templ.InitializeContext(ctx)
3068- templ_7745c5c3_Var146 := templ.GetChildren(ctx)
3069- if templ_7745c5c3_Var146 == nil {
3070- templ_7745c5c3_Var146 = templ.NopComponent
3049+ templ_7745c5c3_Var143 := templ.GetChildren(ctx)
3050+ if templ_7745c5c3_Var143 == nil {
3051+ templ_7745c5c3_Var143 = templ.NopComponent
30713052 }
30723053 ctx = templ.ClearChildren(ctx)
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\">")
3054+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "<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\"><section class=\"merge-settings\"><h2>Merge settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
30743055 if templ_7745c5c3_Err != nil {
30753056 return templ_7745c5c3_Err
30763057 }
3077- if p.Box.CanMarkRead {
3078- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "<form method=\"post\" action=\"")
3058+ var templ_7745c5c3_Var144 string
3059+ templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
3060+ if templ_7745c5c3_Err != nil {
3061+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 38}
3062+ }
3063+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var144))
3064+ if templ_7745c5c3_Err != nil {
3065+ return templ_7745c5c3_Err
3066+ }
3067+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</dd><dt>checks</dt><dd>")
3068+ if templ_7745c5c3_Err != nil {
3069+ return templ_7745c5c3_Err
3070+ }
3071+ if p.ChecksBlock {
3072+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "failing ones block the merge")
30793073 if templ_7745c5c3_Err != nil {
30803074 return templ_7745c5c3_Err
30813075 }
3082- var templ_7745c5c3_Var147 templ.SafeURL
3083- templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
3084- if templ_7745c5c3_Err != nil {
3085- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 599, Col: 64}
3086- }
3087- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
3076+ } else {
3077+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "do not block the merge")
30883078 if templ_7745c5c3_Err != nil {
30893079 return templ_7745c5c3_Err
30903080 }
3091- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
3081+ }
3082+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</dd></dl></section>")
3083+ if templ_7745c5c3_Err != nil {
3084+ return templ_7745c5c3_Err
3085+ }
3086+ if p.Box != nil {
3087+ if p.Box.Signed {
3088+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "<p class=\"d-note\" id=\"mr-signing\">squash and merge commits are signed with ermite's own key (generated on first start). ")
3089+ if templ_7745c5c3_Err != nil {
3090+ return templ_7745c5c3_Err
3091+ }
3092+ if p.Box.Fingerprint != "" {
3093+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "fingerprint <code>")
3094+ if templ_7745c5c3_Err != nil {
3095+ return templ_7745c5c3_Err
3096+ }
3097+ var templ_7745c5c3_Var145 string
3098+ templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
3099+ if templ_7745c5c3_Err != nil {
3100+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 621, Col: 44}
3101+ }
3102+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
3103+ if templ_7745c5c3_Err != nil {
3104+ return templ_7745c5c3_Err
3105+ }
3106+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "</code> ")
3107+ if templ_7745c5c3_Err != nil {
3108+ return templ_7745c5c3_Err
3109+ }
3110+ }
3111+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "<code>ermite signing-key show</code> prints it.</p>")
3112+ if templ_7745c5c3_Err != nil {
3113+ return templ_7745c5c3_Err
3114+ }
3115+ }
3116+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, " ")
30923117 if templ_7745c5c3_Err != nil {
30933118 return templ_7745c5c3_Err
30943119 }
3095- var templ_7745c5c3_Var148 string
3096- templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
3097- if templ_7745c5c3_Err != nil {
3098- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 600, Col: 59}
3120+ if p.Box.CanMarkRead {
3121+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<form method=\"post\" action=\"")
3122+ if templ_7745c5c3_Err != nil {
3123+ return templ_7745c5c3_Err
3124+ }
3125+ var templ_7745c5c3_Var146 templ.SafeURL
3126+ templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
3127+ if templ_7745c5c3_Err != nil {
3128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 627, Col: 65}
3129+ }
3130+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
3131+ if templ_7745c5c3_Err != nil {
3132+ return templ_7745c5c3_Err
3133+ }
3134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
3135+ if templ_7745c5c3_Err != nil {
3136+ return templ_7745c5c3_Err
3137+ }
3138+ var templ_7745c5c3_Var147 string
3139+ templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
3140+ if templ_7745c5c3_Err != nil {
3141+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 628, Col: 60}
3142+ }
3143+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)
3144+ if templ_7745c5c3_Err != nil {
3145+ return templ_7745c5c3_Err
3146+ }
3147+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "\"> <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>")
3148+ if templ_7745c5c3_Err != nil {
3149+ return templ_7745c5c3_Err
3150+ }
30993151 }
3100- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148)
3152+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, " <form method=\"post\" action=\"")
31013153 if templ_7745c5c3_Err != nil {
31023154 return templ_7745c5c3_Err
31033155 }
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>")
3156+ var templ_7745c5c3_Var148 templ.SafeURL
3157+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
31053158 if templ_7745c5c3_Err != nil {
3106- return templ_7745c5c3_Err
3159+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 632, Col: 65}
31073160 }
3108- }
3109- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "<form method=\"post\" action=\"")
3110- if templ_7745c5c3_Err != nil {
3111- return templ_7745c5c3_Err
3112- }
3113- var templ_7745c5c3_Var149 templ.SafeURL
3114- templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
3115- if templ_7745c5c3_Err != nil {
3116- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 604, Col: 64}
3117- }
3118- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))
3119- if templ_7745c5c3_Err != nil {
3120- return templ_7745c5c3_Err
3121- }
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>")
3123- if templ_7745c5c3_Err != nil {
3124- return templ_7745c5c3_Err
3125- }
3126- if !p.Box.HeadGone && p.Box.ReadyAction == "" {
3127- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
3161+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
31283162 if templ_7745c5c3_Err != nil {
31293163 return templ_7745c5c3_Err
31303164 }
3131- templ_7745c5c3_Err = CloneBlock(p.Box.CLI).Render(ctx, templ_7745c5c3_Buffer)
3165+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
31323166 if templ_7745c5c3_Err != nil {
31333167 return templ_7745c5c3_Err
31343168 }
3135- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "</details>")
3136- if templ_7745c5c3_Err != nil {
3137- return templ_7745c5c3_Err
3169+ if !p.Box.HeadGone && p.Box.ReadyAction == "" {
3170+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
3171+ if templ_7745c5c3_Err != nil {
3172+ return templ_7745c5c3_Err
3173+ }
3174+ templ_7745c5c3_Err = CloneBlock(p.Box.CLI).Render(ctx, templ_7745c5c3_Buffer)
3175+ if templ_7745c5c3_Err != nil {
3176+ return templ_7745c5c3_Err
3177+ }
3178+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</details>")
3179+ if templ_7745c5c3_Err != nil {
3180+ return templ_7745c5c3_Err
3181+ }
31383182 }
31393183 }
3140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "</div></details>")
3184+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</div></details>")
31413185 if templ_7745c5c3_Err != nil {
31423186 return templ_7745c5c3_Err
31433187 }
@@ -3161,12 +3205,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
31613205 }()
31623206 }
31633207 ctx = templ.InitializeContext(ctx)
3164- templ_7745c5c3_Var150 := templ.GetChildren(ctx)
3165- if templ_7745c5c3_Var150 == nil {
3166- templ_7745c5c3_Var150 = templ.NopComponent
3208+ templ_7745c5c3_Var149 := templ.GetChildren(ctx)
3209+ if templ_7745c5c3_Var149 == nil {
3210+ templ_7745c5c3_Var149 = templ.NopComponent
31673211 }
31683212 ctx = templ.ClearChildren(ctx)
3169- templ_7745c5c3_Var151 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3213+ templ_7745c5c3_Var150 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
31703214 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
31713215 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
31723216 if !templ_7745c5c3_IsBuffer {
@@ -3178,263 +3222,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
31783222 }()
31793223 }
31803224 ctx = templ.InitializeContext(ctx)
3181- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "<header class=\"show mr-h\"><h1>")
3225+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "<header class=\"show mr-h\"><h1>")
31823226 if templ_7745c5c3_Err != nil {
31833227 return templ_7745c5c3_Err
31843228 }
3185- var templ_7745c5c3_Var152 string
3186- templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3229+ var templ_7745c5c3_Var151 string
3230+ templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
31873231 if templ_7745c5c3_Err != nil {
3188- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 620, Col: 17}
3232+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 17}
31893233 }
3190- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
3234+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
31913235 if templ_7745c5c3_Err != nil {
31923236 return templ_7745c5c3_Err
31933237 }
3194- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, " → ")
3238+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, " → ")
31953239 if templ_7745c5c3_Err != nil {
31963240 return templ_7745c5c3_Err
31973241 }
3198- var templ_7745c5c3_Var153 string
3199- templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3242+ var templ_7745c5c3_Var152 string
3243+ templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
32003244 if templ_7745c5c3_Err != nil {
3201- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 620, Col: 34}
3245+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 34}
32023246 }
3203- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
3247+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
32043248 if templ_7745c5c3_Err != nil {
32053249 return templ_7745c5c3_Err
32063250 }
3207- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
3251+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
32083252 if templ_7745c5c3_Err != nil {
32093253 return templ_7745c5c3_Err
32103254 }
3211- var templ_7745c5c3_Var154 templ.SafeURL
3212- templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
3255+ var templ_7745c5c3_Var153 templ.SafeURL
3256+ templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
32133257 if templ_7745c5c3_Err != nil {
3214- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 623, Col: 45}
3258+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 652, Col: 45}
32153259 }
3216- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
3260+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
32173261 if templ_7745c5c3_Err != nil {
32183262 return templ_7745c5c3_Err
32193263 }
3220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "\">")
3264+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "\">")
32213265 if templ_7745c5c3_Err != nil {
32223266 return templ_7745c5c3_Err
32233267 }
3224- var templ_7745c5c3_Var155 string
3225- templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3268+ var templ_7745c5c3_Var154 string
3269+ templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
32263270 if templ_7745c5c3_Err != nil {
3227- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 623, Col: 58}
3271+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 652, Col: 58}
32283272 }
3229- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
3273+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
32303274 if templ_7745c5c3_Err != nil {
32313275 return templ_7745c5c3_Err
32323276 }
3233- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</a></dd><dt>target</dt><dd><a href=\"")
3277+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "</a></dd><dt>target</dt><dd><a href=\"")
32343278 if templ_7745c5c3_Err != nil {
32353279 return templ_7745c5c3_Err
32363280 }
3237- var templ_7745c5c3_Var156 templ.SafeURL
3238- templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
3281+ var templ_7745c5c3_Var155 templ.SafeURL
3282+ templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
32393283 if templ_7745c5c3_Err != nil {
3240- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 625, Col: 45}
3284+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 654, Col: 45}
32413285 }
3242- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
3286+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
32433287 if templ_7745c5c3_Err != nil {
32443288 return templ_7745c5c3_Err
32453289 }
3246- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "\">")
3290+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "\">")
32473291 if templ_7745c5c3_Err != nil {
32483292 return templ_7745c5c3_Err
32493293 }
3250- var templ_7745c5c3_Var157 string
3251- templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3294+ var templ_7745c5c3_Var156 string
3295+ templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
32523296 if templ_7745c5c3_Err != nil {
3253- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 625, Col: 58}
3297+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 654, Col: 58}
32543298 }
3255- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
3299+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
32563300 if templ_7745c5c3_Err != nil {
32573301 return templ_7745c5c3_Err
32583302 }
3259- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "</a></dd><dt>ahead</dt><dd>")
3303+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "</a></dd><dt>ahead</dt><dd>")
32603304 if templ_7745c5c3_Err != nil {
32613305 return templ_7745c5c3_Err
32623306 }
3263- var templ_7745c5c3_Var158 string
3264- templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
3307+ var templ_7745c5c3_Var157 string
3308+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
32653309 if templ_7745c5c3_Err != nil {
3266- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 627, Col: 21}
3310+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 21}
32673311 }
3268- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
3312+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
32693313 if templ_7745c5c3_Err != nil {
32703314 return templ_7745c5c3_Err
32713315 }
3272- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "</dd></dl></header>")
3316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "</dd></dl></header>")
32733317 if templ_7745c5c3_Err != nil {
32743318 return templ_7745c5c3_Err
32753319 }
32763320 if p.Note != "" {
3277- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "<p class=\"notice\">")
3321+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "<p class=\"notice\">")
32783322 if templ_7745c5c3_Err != nil {
32793323 return templ_7745c5c3_Err
32803324 }
3281- var templ_7745c5c3_Var159 string
3282- templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
3325+ var templ_7745c5c3_Var158 string
3326+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
32833327 if templ_7745c5c3_Err != nil {
3284- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 631, Col: 29}
3328+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 660, Col: 29}
32853329 }
3286- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
3330+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
32873331 if templ_7745c5c3_Err != nil {
32883332 return templ_7745c5c3_Err
32893333 }
3290- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "</p>")
3334+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "</p>")
32913335 if templ_7745c5c3_Err != nil {
32923336 return templ_7745c5c3_Err
32933337 }
32943338 }
3295- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, " ")
3339+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, " ")
32963340 if templ_7745c5c3_Err != nil {
32973341 return templ_7745c5c3_Err
32983342 }
32993343 if p.Existing != nil {
3300- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
3344+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
33013345 if templ_7745c5c3_Err != nil {
33023346 return templ_7745c5c3_Err
33033347 }
3304- var templ_7745c5c3_Var160 templ.SafeURL
3305- templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
3348+ var templ_7745c5c3_Var159 templ.SafeURL
3349+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
33063350 if templ_7745c5c3_Err != nil {
3307- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 634, Col: 114}
3351+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 663, Col: 114}
33083352 }
3309- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
3353+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
33103354 if templ_7745c5c3_Err != nil {
33113355 return templ_7745c5c3_Err
33123356 }
3313- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "\">")
3357+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 311, "\">")
33143358 if templ_7745c5c3_Err != nil {
33153359 return templ_7745c5c3_Err
33163360 }
3317- var templ_7745c5c3_Var161 string
3318- templ_7745c5c3_Var161, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
3361+ var templ_7745c5c3_Var160 string
3362+ templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
33193363 if templ_7745c5c3_Err != nil {
3320- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 634, Col: 135}
3364+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 663, Col: 135}
33213365 }
3322- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var161))
3366+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
33233367 if templ_7745c5c3_Err != nil {
33243368 return templ_7745c5c3_Err
33253369 }
3326- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "</a></p>")
3370+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 312, "</a></p>")
33273371 if templ_7745c5c3_Err != nil {
33283372 return templ_7745c5c3_Err
33293373 }
33303374 }
3331- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, " ")
3375+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, " ")
33323376 if templ_7745c5c3_Err != nil {
33333377 return templ_7745c5c3_Err
33343378 }
33353379 if p.Create != nil {
3336- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "<form class=\"cform create\" method=\"post\" action=\"")
3380+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "<form class=\"cform create\" method=\"post\" action=\"")
33373381 if templ_7745c5c3_Err != nil {
33383382 return templ_7745c5c3_Err
33393383 }
3340- var templ_7745c5c3_Var162 templ.SafeURL
3341- templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
3384+ var templ_7745c5c3_Var161 templ.SafeURL
3385+ templ_7745c5c3_Var161, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
33423386 if templ_7745c5c3_Err != nil {
3343- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 637, Col: 83}
3387+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 666, Col: 83}
33443388 }
3345- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var162))
3389+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var161))
33463390 if templ_7745c5c3_Err != nil {
33473391 return templ_7745c5c3_Err
33483392 }
3349- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, "\"><input type=\"hidden\" name=\"source\" value=\"")
3393+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, "\"><input type=\"hidden\" name=\"source\" value=\"")
33503394 if templ_7745c5c3_Err != nil {
33513395 return templ_7745c5c3_Err
33523396 }
3353- var templ_7745c5c3_Var163 string
3354- templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
3397+ var templ_7745c5c3_Var162 string
3398+ templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
33553399 if templ_7745c5c3_Err != nil {
3356- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 638, Col: 62}
3400+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 667, Col: 62}
33573401 }
3358- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var163)
3402+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var162)
33593403 if templ_7745c5c3_Err != nil {
33603404 return templ_7745c5c3_Err
33613405 }
3362- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "\"> <input type=\"hidden\" name=\"target\" value=\"")
3406+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "\"> <input type=\"hidden\" name=\"target\" value=\"")
33633407 if templ_7745c5c3_Err != nil {
33643408 return templ_7745c5c3_Err
33653409 }
3366- var templ_7745c5c3_Var164 string
3367- templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
3410+ var templ_7745c5c3_Var163 string
3411+ templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
33683412 if templ_7745c5c3_Err != nil {
3369- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 639, Col: 62}
3413+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 668, Col: 62}
33703414 }
3371- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var164)
3415+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var163)
33723416 if templ_7745c5c3_Err != nil {
33733417 return templ_7745c5c3_Err
33743418 }
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=\"")
3419+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
33763420 if templ_7745c5c3_Err != nil {
33773421 return templ_7745c5c3_Err
33783422 }
3379- var templ_7745c5c3_Var165 string
3380- templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
3423+ var templ_7745c5c3_Var164 string
3424+ templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
33813425 if templ_7745c5c3_Err != nil {
3382- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 641, Col: 97}
3426+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 670, Col: 97}
33833427 }
3384- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var165)
3428+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var164)
33853429 if templ_7745c5c3_Err != nil {
33863430 return templ_7745c5c3_Err
33873431 }
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>")
3432+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, "\" 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>")
33893433 if templ_7745c5c3_Err != nil {
33903434 return templ_7745c5c3_Err
33913435 }
33923436 }
3393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, " ")
3437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 319, " ")
33943438 if templ_7745c5c3_Err != nil {
33953439 return templ_7745c5c3_Err
33963440 }
33973441 if p.HasDiff {
3398- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
3442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
33993443 if templ_7745c5c3_Err != nil {
34003444 return templ_7745c5c3_Err
34013445 }
3402- var templ_7745c5c3_Var166 string
3403- templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
3446+ var templ_7745c5c3_Var165 string
3447+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
34043448 if templ_7745c5c3_Err != nil {
3405- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 651, Col: 57}
3449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 57}
34063450 }
3407- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
34083452 if templ_7745c5c3_Err != nil {
34093453 return templ_7745c5c3_Err
34103454 }
3411- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, " · <span class=\"plus\">+")
3455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " · <span class=\"plus\">+")
34123456 if templ_7745c5c3_Err != nil {
34133457 return templ_7745c5c3_Err
34143458 }
3415- var templ_7745c5c3_Var167 string
3416- templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
3459+ var templ_7745c5c3_Var166 string
3460+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
34173461 if templ_7745c5c3_Err != nil {
3418- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 651, Col: 97}
3462+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 97}
34193463 }
3420- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
3464+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
34213465 if templ_7745c5c3_Err != nil {
34223466 return templ_7745c5c3_Err
34233467 }
3424- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "</span> <span class=\"minus\">-")
3468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "</span> <span class=\"minus\">-")
34253469 if templ_7745c5c3_Err != nil {
34263470 return templ_7745c5c3_Err
34273471 }
3428- var templ_7745c5c3_Var168 string
3429- templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
3472+ var templ_7745c5c3_Var167 string
3473+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
34303474 if templ_7745c5c3_Err != nil {
3431- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 651, Col: 142}
3475+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 142}
34323476 }
3433- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
3477+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
34343478 if templ_7745c5c3_Err != nil {
34353479 return templ_7745c5c3_Err
34363480 }
3437- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "</span></span> <span class=\"grow\"></span>")
3481+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, "</span></span> <span class=\"grow\"></span>")
34383482 if templ_7745c5c3_Err != nil {
34393483 return templ_7745c5c3_Err
34403484 }
@@ -3442,64 +3486,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
34423486 if templ_7745c5c3_Err != nil {
34433487 return templ_7745c5c3_Err
34443488 }
3445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, "</div>")
3489+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, "</div>")
34463490 if templ_7745c5c3_Err != nil {
34473491 return templ_7745c5c3_Err
34483492 }
34493493 if p.Invisible {
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>")
3494+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, "<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>")
34513495 if templ_7745c5c3_Err != nil {
34523496 return templ_7745c5c3_Err
34533497 }
34543498 }
3455- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, " <div id=\"diffs\"")
3499+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, " <div id=\"diffs\"")
34563500 if templ_7745c5c3_Err != nil {
34573501 return templ_7745c5c3_Err
34583502 }
34593503 if p.Both {
3460- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " data-diff=\"")
3504+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, " data-diff=\"")
34613505 if templ_7745c5c3_Err != nil {
34623506 return templ_7745c5c3_Err
34633507 }
3464- var templ_7745c5c3_Var169 string
3465- templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3508+ var templ_7745c5c3_Var168 string
3509+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
34663510 if templ_7745c5c3_Err != nil {
3467- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 664, Col: 23}
3511+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 693, Col: 23}
34683512 }
3469- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var169)
3513+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var168)
34703514 if templ_7745c5c3_Err != nil {
34713515 return templ_7745c5c3_Err
34723516 }
3473- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "\"")
3517+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "\"")
34743518 if templ_7745c5c3_Err != nil {
34753519 return templ_7745c5c3_Err
34763520 }
34773521 } else {
3478- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, " data-view=\"")
3522+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, " data-view=\"")
34793523 if templ_7745c5c3_Err != nil {
34803524 return templ_7745c5c3_Err
34813525 }
3482- var templ_7745c5c3_Var170 string
3483- templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3526+ var templ_7745c5c3_Var169 string
3527+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
34843528 if templ_7745c5c3_Err != nil {
3485- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 666, Col: 23}
3529+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 23}
34863530 }
3487- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var170)
3531+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var169)
34883532 if templ_7745c5c3_Err != nil {
34893533 return templ_7745c5c3_Err
34903534 }
3491- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, "\"")
3535+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, "\"")
34923536 if templ_7745c5c3_Err != nil {
34933537 return templ_7745c5c3_Err
34943538 }
34953539 }
34963540 if p.Both && !p.Pinned {
3497- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
3541+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
34983542 if templ_7745c5c3_Err != nil {
34993543 return templ_7745c5c3_Err
35003544 }
35013545 }
3502- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, ">")
3546+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, ">")
35033547 if templ_7745c5c3_Err != nil {
35043548 return templ_7745c5c3_Err
35053549 }
@@ -3509,14 +3553,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
35093553 return templ_7745c5c3_Err
35103554 }
35113555 }
3512- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, "</div>")
3556+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "</div>")
35133557 if templ_7745c5c3_Err != nil {
35143558 return templ_7745c5c3_Err
35153559 }
35163560 }
35173561 return nil
35183562 })
3519- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var151), templ_7745c5c3_Buffer)
3563+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var150), templ_7745c5c3_Buffer)
35203564 if templ_7745c5c3_Err != nil {
35213565 return templ_7745c5c3_Err
35223566 }
@@ -3540,173 +3584,173 @@ func compareRail(p ComparePage) templ.Component {
35403584 }()
35413585 }
35423586 ctx = templ.InitializeContext(ctx)
3543- templ_7745c5c3_Var171 := templ.GetChildren(ctx)
3544- if templ_7745c5c3_Var171 == nil {
3545- templ_7745c5c3_Var171 = templ.NopComponent
3587+ templ_7745c5c3_Var170 := templ.GetChildren(ctx)
3588+ if templ_7745c5c3_Var170 == nil {
3589+ templ_7745c5c3_Var170 = templ.NopComponent
35463590 }
35473591 ctx = templ.ClearChildren(ctx)
3548- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
3592+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
35493593 if templ_7745c5c3_Err != nil {
35503594 return templ_7745c5c3_Err
35513595 }
3552- var templ_7745c5c3_Var172 string
3553- templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
3596+ var templ_7745c5c3_Var171 string
3597+ templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
35543598 if templ_7745c5c3_Err != nil {
3555- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 67}
3599+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 711, Col: 67}
35563600 }
3557- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
3601+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
35583602 if templ_7745c5c3_Err != nil {
35593603 return templ_7745c5c3_Err
35603604 }
3561- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, "</span></h2><ul class=\"pcommits\">")
3605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "</span></h2><ul class=\"pcommits\">")
35623606 if templ_7745c5c3_Err != nil {
35633607 return templ_7745c5c3_Err
35643608 }
35653609 for _, c := range p.Ahead {
3566- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, "<li><a href=\"")
3610+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "<li><a href=\"")
35673611 if templ_7745c5c3_Err != nil {
35683612 return templ_7745c5c3_Err
35693613 }
3570- var templ_7745c5c3_Var173 templ.SafeURL
3571- templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
3614+ var templ_7745c5c3_Var172 templ.SafeURL
3615+ templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
35723616 if templ_7745c5c3_Err != nil {
3573- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 686, Col: 37}
3617+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 715, Col: 37}
35743618 }
3575- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
3619+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
35763620 if templ_7745c5c3_Err != nil {
35773621 return templ_7745c5c3_Err
35783622 }
3579- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, "\">")
3623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "\">")
35803624 if templ_7745c5c3_Err != nil {
35813625 return templ_7745c5c3_Err
35823626 }
3583- var templ_7745c5c3_Var174 string
3584- templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
3627+ var templ_7745c5c3_Var173 string
3628+ templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
35853629 if templ_7745c5c3_Err != nil {
3586- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 686, Col: 49}
3630+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 715, Col: 49}
35873631 }
3588- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var174))
3632+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
35893633 if templ_7745c5c3_Err != nil {
35903634 return templ_7745c5c3_Err
35913635 }
3592- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, "</a> <span class=\"s\">")
3636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "</a> <span class=\"s\">")
35933637 if templ_7745c5c3_Err != nil {
35943638 return templ_7745c5c3_Err
35953639 }
3596- var templ_7745c5c3_Var175 string
3597- templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
3640+ var templ_7745c5c3_Var174 string
3641+ templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
35983642 if templ_7745c5c3_Err != nil {
3599- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 687, Col: 32}
3643+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 716, Col: 32}
36003644 }
3601- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var175))
3645+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var174))
36023646 if templ_7745c5c3_Err != nil {
36033647 return templ_7745c5c3_Err
36043648 }
3605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "</span> <span class=\"mute\">")
3649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, "</span> <span class=\"mute\">")
36063650 if templ_7745c5c3_Err != nil {
36073651 return templ_7745c5c3_Err
36083652 }
3609- var templ_7745c5c3_Var176 string
3610- templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
3653+ var templ_7745c5c3_Var175 string
3654+ templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
36113655 if templ_7745c5c3_Err != nil {
3612- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 689, Col: 16}
3656+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 718, Col: 16}
36133657 }
3614- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var176))
3658+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var175))
36153659 if templ_7745c5c3_Err != nil {
36163660 return templ_7745c5c3_Err
36173661 }
3618- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, " ")
3662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, " ")
36193663 if templ_7745c5c3_Err != nil {
36203664 return templ_7745c5c3_Err
36213665 }
36223666 if c.WhenAt != "" {
3623- var templ_7745c5c3_Var177 string
3624- templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
3667+ var templ_7745c5c3_Var176 string
3668+ templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
36253669 if templ_7745c5c3_Err != nil {
3626- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 15}
3670+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 15}
36273671 }
3628- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))
3672+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var176))
36293673 if templ_7745c5c3_Err != nil {
36303674 return templ_7745c5c3_Err
36313675 }
3632- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "<time datetime=\"")
3676+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, "<time datetime=\"")
36333677 if templ_7745c5c3_Err != nil {
36343678 return templ_7745c5c3_Err
36353679 }
3636- var templ_7745c5c3_Var178 string
3637- templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
3680+ var templ_7745c5c3_Var177 string
3681+ templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
36383682 if templ_7745c5c3_Err != nil {
3639- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 42}
3683+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 42}
36403684 }
3641- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var178)
3685+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var177)
36423686 if templ_7745c5c3_Err != nil {
36433687 return templ_7745c5c3_Err
36443688 }
3645- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "\">")
3689+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, "\">")
36463690 if templ_7745c5c3_Err != nil {
36473691 return templ_7745c5c3_Err
36483692 }
3649- var templ_7745c5c3_Var179 string
3650- templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
3693+ var templ_7745c5c3_Var178 string
3694+ templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
36513695 if templ_7745c5c3_Err != nil {
3652- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 53}
3696+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 53}
36533697 }
3654- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
3698+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))
36553699 if templ_7745c5c3_Err != nil {
36563700 return templ_7745c5c3_Err
36573701 }
3658- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "</time>")
3702+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "</time>")
36593703 if templ_7745c5c3_Err != nil {
36603704 return templ_7745c5c3_Err
36613705 }
36623706 }
3663- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "</span></li>")
3707+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "</span></li>")
36643708 if templ_7745c5c3_Err != nil {
36653709 return templ_7745c5c3_Err
36663710 }
36673711 }
3668- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, "</ul>")
3712+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</ul>")
36693713 if templ_7745c5c3_Err != nil {
36703714 return templ_7745c5c3_Err
36713715 }
36723716 if len(p.Ahead) == 0 {
3673- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, "<p class=\"dim\">no commit ahead of ")
3717+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 346, "<p class=\"dim\">no commit ahead of ")
36743718 if templ_7745c5c3_Err != nil {
36753719 return templ_7745c5c3_Err
36763720 }
3677- var templ_7745c5c3_Var180 string
3678- templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3721+ var templ_7745c5c3_Var179 string
3722+ templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
36793723 if templ_7745c5c3_Err != nil {
3680- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 698, Col: 47}
3724+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 727, Col: 47}
36813725 }
3682- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var180))
3726+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
36833727 if templ_7745c5c3_Err != nil {
36843728 return templ_7745c5c3_Err
36853729 }
3686- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, ".</p>")
3730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 347, ".</p>")
36873731 if templ_7745c5c3_Err != nil {
36883732 return templ_7745c5c3_Err
36893733 }
36903734 }
3691- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, "</section>")
3735+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 348, "</section>")
36923736 if templ_7745c5c3_Err != nil {
36933737 return templ_7745c5c3_Err
36943738 }
36953739 if p.HasDiff {
3696- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
3740+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 349, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
36973741 if templ_7745c5c3_Err != nil {
36983742 return templ_7745c5c3_Err
36993743 }
3700- var templ_7745c5c3_Var181 string
3701- templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
3744+ var templ_7745c5c3_Var180 string
3745+ templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
37023746 if templ_7745c5c3_Err != nil {
3703- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 703, Col: 66}
3747+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 732, Col: 66}
37043748 }
3705- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var181))
3749+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var180))
37063750 if templ_7745c5c3_Err != nil {
37073751 return templ_7745c5c3_Err
37083752 }
3709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "</span></h2>")
3753+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 350, "</span></h2>")
37103754 if templ_7745c5c3_Err != nil {
37113755 return templ_7745c5c3_Err
37123756 }
@@ -3714,12 +3758,12 @@ func compareRail(p ComparePage) templ.Component {
37143758 if templ_7745c5c3_Err != nil {
37153759 return templ_7745c5c3_Err
37163760 }
3717- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</section>")
3761+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 351, "</section>")
37183762 if templ_7745c5c3_Err != nil {
37193763 return templ_7745c5c3_Err
37203764 }
37213765 }
3722- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 346, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
3766+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 352, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
37233767 if templ_7745c5c3_Err != nil {
37243768 return templ_7745c5c3_Err
37253769 }
@@ -3727,7 +3771,7 @@ func compareRail(p ComparePage) templ.Component {
37273771 if templ_7745c5c3_Err != nil {
37283772 return templ_7745c5c3_Err
37293773 }
3730- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 347, "</section>")
3774+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 353, "</section>")
37313775 if templ_7745c5c3_Err != nil {
37323776 return templ_7745c5c3_Err
37333777 }
M internal/adapters/web/mr_test.go +67 -0
@@ -4,6 +4,7 @@ package web
44
55 import (
66 "context"
7+ "time"
78 "testing"
89
910 "github.com/stretchr/testify/assert"
@@ -128,3 +129,69 @@ func TestMergeabilityLiveEvents(t *testing.T) {
128129 require.NoError(t, err)
129130 assert.Empty(t, fs)
130131 }
132+
133+func TestMRPanelsAndMenus(t *testing.T) {
134+ p := MRPage{CommitCount: "500+", CommitsNote: "more commits", Modes: []string{"squash", "merge"}, ChecksBlock: true,
135+ ChecksSum: ChecksSummary{Total: 2, Label: "1/2", Title: "1/2 failed"}, Box: &MergeBox{Signed: true, Fingerprint: "fingerprint", HeadGone: true}}
136+ footer := renderToString(t, mrFooter(p))
137+ assert.Contains(t, footer, `<footer class="mr-footer"><nav data-mr-tabs`)
138+ assert.NotContains(t, footer, `<details`)
139+ assert.NotContains(t, footer, `<summary`)
140+ // No hidden attribute: the CSS shows the panel of the URL fragment until
141+ // mr.js takes over, so the tabs work as anchors without scripts.
142+ assert.Contains(t, footer, `id="mr-commits" data-mr-panel role="region"`)
143+ assert.Contains(t, footer, `id="mr-checks-panel" data-mr-panel role="region"`)
144+ assert.NotContains(t, footer, `data-mr-panel hidden`)
145+ css, err := staticFS.ReadFile("static/css/60-mr.css")
146+ require.NoError(t, err)
147+ assert.Contains(t, string(css), `&:not([data-panels]) [data-mr-panel]:not(:target, :has(:target)) { display: none; }`)
148+ assert.Contains(t, footer, `data-mr-tabs`)
149+ assert.Contains(t, footer, `id="mr-commits"`)
150+ assert.Contains(t, footer, `id="mr-checks"`)
151+ assert.Contains(t, footer, `<table class="mr-commits">`)
152+ assert.Contains(t, footer, "500+")
153+ assert.Contains(t, footer, "more commits")
154+ assert.Contains(t, footer, "1/2 failed")
155+ rail := renderToString(t, mrRail(p))
156+ assert.Contains(t, rail, "g f")
157+ for _, text := range []string{"Checks", "Commits", "Settings", "mr-checks"} {
158+ assert.NotContains(t, rail, text)
159+ }
160+ more := renderToString(t, moreMenu(p))
161+ for _, text := range []string{"Merge settings", "squash, merge", "failing ones block", `id="mr-signing"`, "fingerprint", "ermite signing-key show"} {
162+ assert.Contains(t, more, text)
163+ }
164+ merge := renderToString(t, mergeMenu(p))
165+ assert.NotContains(t, merge, `<dl`)
166+ assert.NotContains(t, merge, `mr-signing`)
167+ assert.Contains(t, merge, "the source branch was deleted")
168+ bar := renderToString(t, mrBar(MRPage{}))
169+ assert.Contains(t, bar, `id="mr-ck"`)
170+ assert.Contains(t, bar, `href="#mr-checks"`)
171+ assert.Contains(t, bar, "Merge settings")
172+ assert.NotContains(t, bar, ">Close</button>")
173+}
174+
175+func TestMRReadPresentationCommitCount(t *testing.T) {
176+ commits := func(n int) []domain.Commit { return make([]domain.Commit, n) }
177+ for _, tc := range []struct {
178+ name string
179+ commits []domain.Commit
180+ truncated bool
181+ count string
182+ note string
183+ }{
184+ {"empty", nil, false, "0", ""},
185+ {"complete", commits(2), false, "2", ""},
186+ {"truncated", commits(3), true, "3+", "the branch has more commits than the 3 listed."},
187+ } {
188+ t.Run(tc.name, func(t *testing.T) {
189+ s := &Server{cfg: Config{Now: time.Now}}
190+ p := &MRPage{}
191+ s.mrReadPresentation(p, "demo", 1, ports.MergeRequestDetail{Commits: tc.commits, CommitsTruncated: tc.truncated}, false, false)
192+ assert.Equal(t, tc.count, p.CommitCount)
193+ assert.Equal(t, tc.note, p.CommitsNote)
194+ assert.Len(t, p.Commits, len(tc.commits))
195+ })
196+ }
197+}
M internal/adapters/web/signature_test.go +1 -1
@@ -145,7 +145,7 @@ func TestCommitPageShowsTheSignature(t *testing.T) {
145145 func TestMergeRequestCommitsShowTheSignatureBadge(t *testing.T) {
146146 t.Parallel()
147147 signed := signatureBadge(verification(domain.FormatGPG, domain.StatusVerified, gpgFP, ""), false)
148- got := renderToString(t, mrRail(MRPage{Commits: []MRCommit{
148+ got := renderToString(t, mrFooter(MRPage{Commits: []MRCommit{
149149 {Short: "aaaaaaa", Href: "/r/commit/a", Subject: "signed", Sig: signed},
150150 {Short: "bbbbbbb", Href: "/r/commit/b", Subject: "unsigned"},
151151 }}))
M internal/adapters/web/static/css/60-mr.css +34 -2
@@ -8,6 +8,38 @@
88 & > .mr-desc.prose { border: 0; border-top: 1px solid var(--border); border-radius: 0; }
99 }
1010
11+ .mr-footer {
12+ border-top: 1px solid var(--border); padding: var(--s2) var(--s4);
13+ /* the shade of the card header; the card's overflow clips the corners */
14+ background: var(--bg-raised);
15+ font-size: var(--fs-s);
16+ /* the tabs are buttons: a thin border, the count in a darker badge;
17+ the open one in the teal accent */
18+ & nav { display: flex; flex-wrap: wrap; gap: var(--s2); }
19+ & [data-mr-tabs] a {
20+ display: inline-flex; align-items: center; gap: var(--s1);
21+ padding: 2px var(--s2); border: 1px solid var(--border-strong); border-radius: var(--r-s);
22+ color: var(--ink-dim); text-decoration: none; cursor: pointer;
23+ }
24+ & [data-checks-label] { color: var(--ink-dim); font-size: var(--fs-xs); }
25+ & [data-mr-tabs] .count { margin-left: 0; color: var(--ink-hi); background: var(--bg-canvas); box-shadow: inset 0 0 0 1px var(--border); }
26+ & [data-mr-tabs] a:hover { background: var(--bg-strong); color: var(--ink-hi); }
27+ & [data-mr-tabs] a[aria-expanded="true"] { border-color: var(--teal); color: var(--teal); background: var(--bg-surface); }
28+ & [data-mr-panel] { padding-block-start: var(--s2); overflow-x: auto; }
29+ & [hidden] { display: none; }
30+ /* Until mr.js drives the panels (data-panels, then [hidden]), only the
31+ panel of the URL fragment shows: the tabs work as anchors without
32+ scripts, and no panel flashes open before the script runs. */
33+ &:not([data-panels]) [data-mr-panel]:not(:target, :has(:target)) { display: none; }
34+ & section h2 { font-size: var(--fs-s); color: var(--ink-dim); }
35+ & table { width: 100%; border-collapse: collapse; }
36+ & th, & td { text-align: left; padding: var(--s2); border-bottom: 1px solid var(--border); }
37+ & th { color: var(--ink-dim); font-weight: 500; }
38+ & tbody tr:last-child > :is(td, th) { border-bottom: 0; }
39+ }
40+ .merge-settings { font-size: var(--fs-s); color: var(--ink-dim); }
41+ .merge-settings h2 { font-size: var(--fs-s); }
42+
1143 .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); }
1244 .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); }
1345 .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); }
@@ -112,11 +144,11 @@
112144 & .d-or > summary:hover { color: var(--ink-hi); }
113145 & .d-or[open] > .clone { margin-top: var(--s2); }
114146 & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; }
147+ & #mr-signing code { overflow-wrap: anywhere; }
115148 }
116149
117150 /* the rail of a merge request follows the review: it sticks under the
118- header, one screen high, and scrolls on its own to its checks, commits
119- and settings. The list of files keeps its own scroll, under a height, so
151+ header, one screen high, and scrolls on its own to its verdicts. The list of files keeps its own scroll, under a height, so
120152 that the blocks below it stay in reach. Only on a two-column layout. */
121153 @media (min-width: 1081px) {
122154 .rail:has(.r-files) {
M internal/adapters/web/static/css/80-live.css +5 -0
@@ -8,5 +8,10 @@
88 padding: var(--s3) var(--s4); font-weight: 700;
99 background: var(--bg-surface); border: 1px solid var(--border-strong); border-left: 3px solid var(--teal); border-radius: var(--r-s);
1010 }
11+ /* A deferred MR refresh must not insert height above a long diff. */
12+ #mr-refresh-status:has(a) {
13+ position: fixed; bottom: var(--s4); left: var(--s4); z-index: 24;
14+ max-width: min(42rem, calc(100vw - 2 * var(--s4))); font-size: var(--fs-s);
15+ }
1116 .live-slot a { color: var(--ink-hi); }
1217 }
A internal/adapters/web/static/css/85-mobile.css +53 -0
@@ -0,0 +1,53 @@
1+@layer components {
2+ .mr-touch-range { display: none; }
3+
4+ /* MR only: retain the shell's desktop layout and other pages' spacing. */
5+ @media (max-width: 720px) {
6+ body:has(#diffs[data-mr]) {
7+ --pad: 0px;
8+ --hdr-h: 97px;
9+ overflow-x: clip;
10+ }
11+ body:has(#diffs[data-mr]) .work { padding-inline: 0; margin-inline: 0; gap: var(--s3); }
12+ body:has(#diffs[data-mr]) main { min-width: 0; padding-inline: 0; }
13+ body:has(#diffs[data-mr]) :is(.card, .mr-card, .mr-desc, .dfile, .dscroll, .code, .rail section) {
14+ margin-inline: 0; border-inline: 0; border-radius: 0; min-width: 0; max-width: 100%;
15+ }
16+ body:has(#diffs[data-mr]) :is(.mr-h, .mr-desc.prose, .card-h, .mr-footer) { padding-inline: 4px; }
17+ body:has(#diffs[data-mr]) .mr-footer :is(th, td) { padding-inline: var(--s1); }
18+ body:has(#diffs[data-mr]) .mr-desc.prose > pre { margin-inline: -4px; padding-inline: 0; max-width: calc(100% + 8px); }
19+ #diffs[data-mr] .dfile > summary { padding-inline: 0; border-inline: 0; border-radius: 0; }
20+ #diffs[data-mr] :is(.dscroll, .code) { overflow-x: auto; overscroll-behavior-x: contain; padding-inline: 0; }
21+ #diffs[data-mr] .dt .ln { width: calc(var(--lnw) + .5ch); min-width: calc(var(--lnw) + .5ch); padding-inline: .25ch; }
22+ #diffs[data-mr] .dt .m { width: 1ch; }
23+ #diffs[data-mr][data-diff] .uni { display: table; }
24+ #diffs[data-mr][data-diff] .spl { display: none; }
25+ #diffs[data-mr] .fnote td { padding-inline: 0; }
26+ #diffs[data-mr] :is(.cform, .cmt) { width: min(65ch, calc(100vw - 5rem)); max-width: calc(100vw - 5rem); overflow-wrap: anywhere; }
27+ body:has(#diffs[data-mr]) :is(input, textarea, select) { font-size: 16px; }
28+ body:has(#diffs[data-mr]) .f-btns { flex-wrap: wrap; }
29+ body:has(#diffs[data-mr]) .menu .menu-pop {
30+ position: fixed; inset: auto 0 0; width: auto; max-width: 100%;
31+ max-height: 75dvh; overflow-y: auto; border-radius: var(--r) var(--r) 0 0;
32+ padding-bottom: max(var(--s4), env(safe-area-inset-bottom));
33+ }
34+ body:has(#diffs[data-mr]) .mr-bar { position: static; }
35+ /* A wrapping toolbar has no reliable fixed sticky height. */
36+ body:has(#diffs[data-mr]) { --bar-h: 0px; }
37+ }
38+
39+ @media (pointer: coarse), (hover: none) {
40+ .mr-touch-range { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); }
41+ /* Real, disjoint cells: never expand invisible targets into other lines
42+ or into the neighbouring old/new gutter. */
43+ #diffs[data-mr] .dt { line-height: max(var(--lh-code), 40px); }
44+ #diffs[data-mr] .dt .ln { width: max(calc(var(--lnw) + .5ch), 40px); min-width: 40px; padding: 0; }
45+ #diffs[data-mr] .spl .c-ln { width: max(calc(var(--lnw) + .5ch), 40px); }
46+ #diffs[data-mr] .ln a { display: block; width: 100%; min-height: 40px; text-align: center; text-decoration: underline; }
47+ .mr-touch-range[hidden] { display: none; }
48+ #diffs[data-mr] .ln a[data-range-start] { outline: 2px solid var(--focus); background: var(--sel); }
49+ body:has(#diffs[data-mr]) :is(.cform .btn, .menu .btn, .mr-touch-range .btn, .c-act button) { min-height: 40px; height: auto; padding-block: var(--s2); }
50+ #diffs[data-mr] .cform { scroll-margin-block: calc(var(--hdr-h) + 12px); }
51+ .mr-footer [data-mr-tabs] a { display: inline-flex; align-items: center; min-height: 40px; }
52+ }
53+}
M internal/adapters/web/static/js/ermite.js +36 -2
@@ -183,6 +183,7 @@
183183 layout(mode) {
184184 const el = $('[data-diff]');
185185 if (!el) return;
186+ if (el.hasAttribute('data-mr') && el.dataset.mobile === 'true') return;
186187 el.dataset.diff = mode;
187188 try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ }
188189 for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true;
@@ -730,13 +731,21 @@
730731 mark();
731732 }, { rootMargin: '0px 0px -67% 0px' });
732733 for (const f of files) io.observe(f);
734+ // A refreshed MR replaced the rail and the windowed diff. Rebuild once,
735+ // not once per file (which would make a large file list quadratic).
736+ ermite.refreshFiles = () => {
737+ io.disconnect(); inBand.clear(); current = null;
738+ files = $$('.dfile'); links.clear();
739+ for (const a of $$('.files a[href^="#"]')) links.set(a.hash.slice(1), a);
740+ for (const f of files) io.observe(f);
741+ };
733742 // A window of files of a merge request came in (mr.js): its files replace
734743 // placeholders, which the observer and the list knew by element.
735744 ermite.watchFile = (f, old) => {
736745 if (old) { io.unobserve(old); inBand.delete(old); }
737746 files = $$('.dfile');
738747 for (const x of [...inBand]) if (!x.isConnected) inBand.delete(x);
739- if (current && !current.isConnected) current = document.getElementById(current.id);
748+ if (current && !current.isConnected) current = null;
740749 io.observe(f);
741750 };
742751 }
@@ -844,6 +853,10 @@
844853 // reloads the page. A stream the server refuses on reconnection (the
845854 // repository is gone, the session ended) makes the page reload.
846855 window.ermiteLive = (() => {
856+ let connection = null;
857+ window.addEventListener?.('htmx:sse:after:connection', (event) => {
858+ if (event.target.matches?.('.hint-live')) connection = event.detail.connection;
859+ });
847860 let reloading = false;
848861 let stopped = false;
849862 // A stream that drops and comes back within LOST_AFTER (a proxy or the
@@ -898,6 +911,24 @@ window.ermiteLive = (() => {
898911 // fitIn lays out the one-line chip lists of a fragment now in the page.
899912 const fitIn = (el) => { for (const ul of el.querySelectorAll('[data-fit]')) window.ermiteFit?.fit(ul); };
900913 return {
914+ // teardown runs remove, which deletes the stream element. hx-sse has no
915+ // public close API: its cleanup aborts the fetch, then calls
916+ // connection.reader.cancel() without catching the AbortError it rejects
917+ // with. Wrapping that one reader handles exactly this rejection, with no
918+ // timing window and no global filter.
919+ async teardown(remove) {
920+ const reader = connection?.reader;
921+ if (reader) connection.reader = { cancel: () => reader.cancel().catch(() => {}) };
922+ return remove();
923+ },
924+ resumeStream(element) {
925+ // The public connection includes id-only messages as well as named ones.
926+ const id = connection?.lastEventId;
927+ if (!id) return;
928+ const url = new URL(element.getAttribute('hx-sse:connect'), location.href);
929+ url.searchParams.set('after', id);
930+ element.setAttribute('hx-sse:connect', url.pathname + url.search);
931+ },
901932 // lost is htmx:sse:close and htmx:sse:error: "live updates paused" shows
902933 // only if the stream is still down LOST_AFTER later.
903934 lost() {
@@ -956,7 +987,10 @@ window.ermiteLive = (() => {
956987 reload() {
957988 if (reloading) return;
958989 reloading = true;
959- setTimeout(() => location.reload(), 300);
990+ setTimeout(() => {
991+ reloading = false;
992+ if (window.ermiteLive.canReload?.() !== false) location.reload();
993+ }, 300);
960994 },
961995 // error handles htmx:sse:error (detail.status is the status of a refused
962996 // reconnection). hx-sse retries for ever; a 404 or a 401 will not change by
M internal/adapters/web/static/js/mr.js +408 -2
@@ -27,6 +27,20 @@ window.ermiteMR = (() => {
2727 b.searchParams.set('on', `${y[1]}:${nums[0]}-${nums[1]}:${y[3]}`);
2828 return b.href;
2929 },
30+ // Range mode is explicit: ordinary taps still open single-line forms.
31+ touchRange(previous, current, base) {
32+ if (!previous) return { start: current, url: null };
33+ const url = core.rangeURL(previous, current, base);
34+ if (url) return { start: null, url };
35+ if (previous === current) return { start: null, url: current };
36+ return { start: current, url: null }; // another file/side starts afresh
37+ },
38+ mobileURL(href, view, narrow) {
39+ if (!narrow || view !== 'split') return null;
40+ const url = new URL(href);
41+ url.searchParams.set('view', 'unified');
42+ return url.href;
43+ },
3044 quote(button) {
3145 const thread = button.closest('.cmt').closest('[data-thread]');
3246 const details = thread?.querySelector(':scope > details[data-reply]');
@@ -49,6 +63,13 @@ window.ermiteMR = (() => {
4963 if (/^[1-9]$/.test(key) && Number(key) <= modes) return { mode: Number(key) - 1 };
5064 return null;
5165 },
66+ tabIndex(key, index, count) {
67+ if (key === 'Home') return 0;
68+ if (key === 'End') return count - 1;
69+ if (key === 'ArrowRight') return (index + 1) % count;
70+ if (key === 'ArrowLeft') return (index + count - 1) % count;
71+ return null;
72+ },
5273 // step moves an index within [0, n).
5374 step(i, d, n) {
5475 if (n === 0) return -1;
@@ -60,13 +81,16 @@ window.ermiteMR = (() => {
6081 const $ = (sel, root = document) => root.querySelector(sel);
6182 const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
6283 const loading = new Map(); // src -> promise of the swap
84+ let refreshTouchComments = () => {};
6385 let moved = false;
6486 let lazyIO = null; // the observer of the placeholders (watch)
87+ let diffGeneration = 0;
6588 let revealGen = 0; // the last reveal wins the scroll
6689
6790 // load fetches the window of a placeholder and swaps its files in. The
6891 // promise resolves once they are in the page (or the page said to reload).
6992 function load(ph) {
93+ const generation = diffGeneration;
7094 const src = ph?.dataset.src;
7195 if (!src || moved) return Promise.resolve();
7296 if (loading.has(src)) return loading.get(src);
@@ -80,12 +104,14 @@ window.ermiteMR = (() => {
80104 const p = fetch(url, { headers: { Accept: 'text/html' }, credentials: 'same-origin' })
81105 .then(async (res) => {
82106 const html = await res.text();
107+ if (generation !== diffGeneration) return;
83108 if (res.status === 409) return stale(html);
84109 if (!res.ok) throw new Error('loading files: ' + res.status);
85110 // A window of the old diff after the page went stale stays out.
86111 if (!moved) swap(html);
87112 })
88113 .catch((err) => {
114+ if (generation !== diffGeneration) return;
89115 loading.delete(src);
90116 for (const p of $$('.dlazy[data-loading]')) if (p.dataset.src === src) p.removeAttribute('data-loading');
91117 window.ermite?.flash?.(err.message, 4000);
@@ -108,6 +134,7 @@ window.ermiteMR = (() => {
108134 window.htmx?.process(f);
109135 window.ermite?.watchFile?.(f, old);
110136 }
137+ refreshTouchComments();
111138 }
112139
113140 // stale: the diff changed under the page. The placeholders say so; no file
@@ -130,7 +157,7 @@ window.ermiteMR = (() => {
130157 // A page with both layouts says which shows in data-diff; a large diff
131158 // rendered in one layout only says it in data-view.
132159 const diffs = $('#diffs')?.dataset;
133- if (/^c\d+$/.test(id) && (diffs?.diff || diffs?.view) === 'split') {
160+ if (/^c\d+$/.test(id) && !window.matchMedia?.('(max-width: 720px)').matches && (diffs?.diff || diffs?.view) === 'split') {
134161 const split = document.getElementById(id + '-s');
135162 if (split) return split;
136163 }
@@ -172,6 +199,7 @@ window.ermiteMR = (() => {
172199 // that come in are marked in the list of files (ermite.js marks the one in
173200 // view).
174201 function watch() {
202+ lazyIO?.disconnect();
175203 if (!('IntersectionObserver' in window)) {
176204 for (const p of $$('.dlazy[data-src]')) load(p);
177205 return;
@@ -285,8 +313,386 @@ window.ermiteMR = (() => {
285313 reveal(id);
286314 }
287315
316+ // Enhance the ordinary anchor navigation only when scripts are available.
317+ let disposePanels = () => {};
318+ // The tabs are disclosure buttons (aria-expanded + aria-controls), not a
319+ // tablist: a tablist always has a selected tab, and here every panel can be
320+ // folded. State: the index of the open panel, or -1 for none.
321+ function panels(initial = -1) {
322+ disposePanels();
323+ const footer = $('.mr-footer'), nav = $('[data-mr-tabs]');
324+ if (!footer || !nav) return;
325+ const tabs = $$('a', nav), panels = $$('[data-mr-panel]', footer);
326+ const ids = new Set(['#mr-commits', '#mr-checks', '#mr-checks-panel']);
327+ let current = -1;
328+ // show applies the state; fromUser also keeps the URL fragment in step.
329+ function show(index, fromUser = false) {
330+ current = index;
331+ tabs.forEach((tab, i) => {
332+ tab.setAttribute('aria-controls', panels[i].id);
333+ tab.setAttribute('aria-expanded', String(i === index));
334+ panels[i].hidden = i !== index;
335+ });
336+ if (!fromUser) return;
337+ if (index >= 0) {
338+ if (location.hash !== '#' + panels[index].id) history.replaceState(null, '', '#' + panels[index].id);
339+ } else if (ids.has(location.hash)) {
340+ history.replaceState(null, '', location.pathname + location.search);
341+ }
342+ }
343+ const toggle = (i) => show(i === current ? -1 : i, true);
344+ tabs.forEach((tab, i) => {
345+ tab.addEventListener('click', (e) => { e.preventDefault(); toggle(i); });
346+ tab.addEventListener('keydown', (e) => {
347+ const next = core.tabIndex(e.key, i, tabs.length);
348+ if (next === null) return;
349+ e.preventDefault(); tabs[next].focus();
350+ });
351+ });
352+ // hash opens the panel of the URL fragment. Its target was hidden when the
353+ // browser scrolled, so the panel is brought in; never on a refresh, which
354+ // keeps the scroll.
355+ function hash() {
356+ const h = location.hash;
357+ const index = h === '#mr-checks' || h === '#mr-checks-panel' ? 1 : h === '#mr-commits' ? 0 : -1;
358+ if (index >= 0) { show(index); panels[index].scrollIntoView?.({ block: 'nearest' }); }
359+ }
360+ // The checks badge always opens Checks; it never folds it.
361+ const checksClick = (e) => {
362+ if (e.target.closest?.('#mr-ck')) show(1);
363+ };
364+ document.addEventListener('click', checksClick);
365+ let observer;
366+ const summary = $('#mr-ck');
367+ if (summary) {
368+ // The live stream replaces #mr-ck as a whole: watch only the children of
369+ // its parent, and write the label only when it changes. hx-live rewrites
370+ // other text in the same bar on every mutation of the document; a write
371+ // here on each of them would wake hx-live again, a loop that never yields.
372+ const sync = () => {
373+ const label = $('[data-checks-label]', nav);
374+ const text = $('#mr-ck')?.title || $('#mr-ck')?.textContent.trim() || 'no checks';
375+ if (label && label.textContent !== text) label.textContent = text;
376+ };
377+ observer = new MutationObserver(sync);
378+ observer.observe(summary.parentElement, { childList: true });
379+ sync();
380+ }
381+ show(initial);
382+ footer.setAttribute('data-panels', '');
383+ if (initial < 0) hash();
384+ addEventListener('hashchange', hash);
385+ disposePanels = () => {
386+ document.removeEventListener?.('click', checksClick);
387+ window.removeEventListener?.('hashchange', hash);
388+ observer?.disconnect?.();
389+ };
390+ }
391+
392+ let disposeTouch = () => {};
393+ function touchComments(scroll = true) {
394+ disposeTouch();
395+ const diffs = $('#diffs');
396+ const bar = document.createElement('div');
397+ bar.className = 'mr-touch-range';
398+ const button = document.createElement('button');
399+ button.type = 'button';
400+ button.className = 'btn';
401+ button.textContent = 'Select line range';
402+ button.setAttribute('aria-pressed', 'false');
403+ const status = document.createElement('span');
404+ status.setAttribute('role', 'status');
405+ bar.append(button, status);
406+ diffs.prepend(bar);
407+ refreshTouchComments = () => { bar.hidden = !$('.ln a[href*="on="]', diffs); };
408+ refreshTouchComments();
409+ const touch = window.matchMedia?.('(pointer: coarse), (hover: none)');
410+ let enabled = false, start = null, marker = null;
411+ const clear = () => { marker?.removeAttribute('data-range-start'); marker = null; start = null; };
412+ button.addEventListener('click', () => {
413+ enabled = !enabled;
414+ clear();
415+ button.setAttribute('aria-pressed', String(enabled));
416+ status.textContent = enabled ? 'Tap the first line number, then the last. Tap the same line for one line.' : '';
417+ });
418+ diffs.addEventListener('click', (e) => {
419+ const link = e.target.closest?.('.ln a[href*="on="]');
420+ if (!touch?.matches || !enabled || !link || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
421+ e.preventDefault();
422+ e.stopPropagation(); // do not alter the desktop shift-click anchor
423+ const next = core.touchRange(start, link.href, location.href);
424+ clear();
425+ if (next.url) { location.assign(next.url); return; }
426+ start = next.start;
427+ marker = link;
428+ marker.setAttribute('data-range-start', '');
429+ status.textContent = `Range starts at line ${link.textContent.trim()}. Tap the last line number (same file and side).`;
430+ });
431+ const changed = () => {
432+ if (touch.matches) return;
433+ enabled = false;
434+ clear();
435+ button.setAttribute('aria-pressed', 'false');
436+ status.textContent = '';
437+ };
438+ touch?.addEventListener('change', changed);
439+ disposeTouch = () => { touch?.removeEventListener?.('change', changed); bar.remove?.(); };
440+ // The server opens the line form. Scroll after layout without stealing
441+ // focus or forcing a mobile keyboard to appear.
442+ if (scroll && new URL(location.href).searchParams.has('on')) requestAnimationFrame(() => {
443+ const form = $$('.fnote .cform', diffs).find((f) => f.getClientRects().length);
444+ form?.scrollIntoView({ block: 'center' });
445+ });
446+ }
447+
448+ // A refresh scheduler has one request in flight, a trailing debounce, and
449+ // a dirty bit for pushes received during that request. Drafts are checked
450+ // again after the fetch: typing while the network is slow is safe too.
451+ function refresher({ blocked, fetchPage, apply, notice, failed, delay = 300 }) {
452+ let timer, busy = false, pending = false;
453+ const schedule = () => { clearTimeout(timer); timer = setTimeout(run, delay); };
454+ async function run() {
455+ if (busy || !pending) return;
456+ if (blocked()) { notice(true); return; }
457+ busy = true;
458+ pending = false;
459+ try {
460+ const page = await fetchPage();
461+ if (blocked()) { pending = true; notice(true); return; }
462+ if (await apply(page) === false) { pending = true; notice(true); }
463+ else notice(false);
464+ } catch { if (blocked()) { pending = true; notice(true); } else failed(); }
465+ finally {
466+ busy = false;
467+ if (pending && !blocked()) schedule();
468+ }
469+ }
470+ return {
471+ request() { pending = true; schedule(); },
472+ resume() { if (pending) schedule(); },
473+ pending() { return pending || busy; },
474+ };
475+ }
476+
477+ function hasDraft() {
478+ // Only form controls, never diff rows. Inline edits live in .cmt forms; the
479+ // merge form (d-acts) holds a chosen mode and the delete-branch box.
480+ return $$(':is(form.cform, .cmt form, form.d-acts) :is(textarea, input, select)').some((t) => {
481+ if (['hidden', 'submit', 'button', 'reset', 'image'].includes(t.type)) return false;
482+ if (document.activeElement === t) return true;
483+ if (t.type === 'radio' || t.type === 'checkbox') return t.checked !== t.defaultChecked;
484+ if (t.tagName === 'SELECT') return [...t.options].some((o) => o.selected !== o.defaultSelected);
485+ return t.value !== t.defaultValue;
486+ }) ||
487+ !!document.activeElement?.closest?.('.cform');
488+ }
489+
490+ // Path + side + line is stable when files are inserted before this one;
491+ // fN is only a positional anchor. Hidden unified/split rows are ignored.
492+ function scrollAnchor() {
493+ const top = Math.max(0, $('.hdr')?.getBoundingClientRect().bottom || 0,
494+ $('.mr-bar')?.getBoundingClientRect().bottom || 0);
495+ const diffs = $('#diffs');
496+ const bounds = diffs.getBoundingClientRect();
497+ const x = Math.max(1, Math.min(innerWidth - 1, bounds.left + Math.min(100, bounds.width / 2)));
498+ // A bounded number of probes, independent of the number of diff lines.
499+ let y = top + 2, fallback;
500+ for (let probe = 0; probe < 6 && y < innerHeight; probe++) {
501+ const hit = document.elementFromPoint(x, y);
502+ const file = hit?.closest('details.dfile');
503+ if (!file || !diffs.contains(file)) { y += 32; continue; }
504+ const row = hit.closest('tr');
505+ const link = row && $('.ln a[href*="on="]', row);
506+ const header = $(':scope > summary', file);
507+ const headerOffset = file.getBoundingClientRect().top;
508+ fallback = { file: file.dataset.file, offset: headerOffset, headerOffset, y: scrollY };
509+ if (link) return { ...fallback,
510+ on: new URL(link.href, location.href).searchParams.get('on'),
511+ suffix: link.getAttribute('href').split('on=')[1].split('#')[0],
512+ offset: row.getBoundingClientRect().top };
513+ if (file.open && header) y = Math.max(y + 24, header.getBoundingClientRect().bottom + 2);
514+ else return fallback;
515+ }
516+ if (fallback) return fallback;
517+ return { y: scrollY };
518+ }
519+
520+ function restoreAnchor(anchor) {
521+ const file = $$('#diffs details.dfile').find((f) => f.dataset.file === anchor.file);
522+ let el, offset = anchor.offset;
523+ if (file && anchor.on) {
524+ const suffix = anchor.suffix && 'on=' + anchor.suffix + '#' + file.id;
525+ const layout = $('#diffs').dataset.diff || $('#diffs').dataset.view;
526+ const table = $(layout === 'split' && !window.matchMedia?.('(max-width: 720px)').matches ? '.dt.spl' : '.dt.uni', file) || file;
527+ if (suffix) el = $('a[href$="' + CSS.escape(suffix) + '"]', table)?.closest('tr');
528+ }
529+ if (!el && file) { el = file; offset = anchor.headerOffset ?? anchor.offset; }
530+ if (el) window.scrollBy({ top: el.getBoundingClientRect().top - offset, behavior: 'instant' });
531+ else window.scrollTo({ top: Math.max(0, Math.min(anchor.y, document.documentElement.scrollHeight - innerHeight)), behavior: 'instant' });
532+ }
533+
534+ async function fetchPage(url = location.href, mobileRetry = false) {
535+ const res = await fetch(url, { credentials: 'same-origin', headers: { Accept: 'text/html' } });
536+ if (!res.ok || res.redirected) throw new Error('MR refresh refused');
537+ const page = new DOMParser().parseFromString(await res.text(), 'text/html');
538+ if (!$('#diffs[data-mr]', page) || !$('.mr-bar', page) || !$('.hint-live', page)) throw new Error('not an MR page');
539+ const unified = core.mobileURL(url, $('#diffs', page).dataset.view,
540+ !!window.matchMedia?.('(max-width: 720px)').matches);
541+ if (unified && !mobileRetry) return fetchPage(unified, true);
542+ return page;
543+ }
544+
545+ // htmx 4's public delete swap invokes recursive before:cleanup (including
546+ // hx-sse's abort/reader.cancel and hx-live's binding disposal). Only an empty
547+ // string is parsed; the fetched nodes themselves are adopted, never serialized.
548+ async function replaceRegion(old, fresh) {
549+ if (!old || !fresh) throw new Error('missing MR refresh region');
550+ for (const el of $$('[autofocus]', fresh)) el.removeAttribute('autofocus');
551+ const marker = document.createComment('MR region');
552+ old.before(marker);
553+ const remove = () => window.htmx.swap({ sourceElement: old, target: old, text: '',
554+ swap: 'delete settle:0 transition:false', push: false, replace: false });
555+ if (old.matches('.hint-live')) {
556+ window.ermiteLive.resumeStream(fresh);
557+ await window.ermiteLive.teardown(remove);
558+ } else await remove();
559+ const node = document.adoptNode(fresh);
560+ marker.replaceWith(node);
561+ window.htmx.process(node);
562+ }
563+
564+ async function refreshPage(page) {
565+ const regions = ['.mr-footer', '#mr-range', '.mr-bar', '#mr-diff-notices', '#diffs', '#mr-comments', '.rail', '.hint-live'];
566+ for (const sel of regions) if (!$(sel) || !$(sel, page)) throw new Error('missing MR region');
567+ let anchor = scrollAnchor();
568+ const nextDiffs = $('#diffs', page);
569+ const mobile = !!window.matchMedia?.('(max-width: 720px)').matches;
570+ const chosen = mobile ? 'unified' : ($('#diffs').dataset.diff || $('#diffs').dataset.view);
571+ if (nextDiffs.dataset.diff) nextDiffs.dataset.diff = chosen;
572+ // A one-layout response must use its own contract, never the old page's.
573+ if (mobile && nextDiffs.dataset.view === 'split') throw new Error('mobile needs unified');
574+ const anchorFile = $$('#diffs details.dfile', page).find((f) => f.dataset.file === anchor.file);
575+ const placeholder = anchorFile && $('.dlazy[data-src]', anchorFile);
576+ if (placeholder) {
577+ const d = nextDiffs.dataset;
578+ const url = placeholder.dataset.src + '&view=' + encodeURIComponent(d.diff || d.view || 'unified') + '&layouts=' + (d.diff ? 'both' : 'one');
579+ const res = await fetch(url, { credentials: 'same-origin', headers: { Accept: 'text/html' } });
580+ if (!res.ok || res.redirected) throw new Error('anchor window changed');
581+ let windowPage = new DOMParser().parseFromString(await res.text(), 'text/html');
582+ if (hasDraft()) return false;
583+ anchor = scrollAnchor();
584+ const windowFiles = $$('details.dfile', windowPage);
585+ // If the reader moved outside this window, discard it; lazy loading will
586+ // fetch the new position rather than committing an unrelated window.
587+ if (windowFiles.some((f) => f.dataset.file === anchor.file)) {
588+ const byID = new Map($$('#diffs details.dfile', page).map((f) => [f.id, f]));
589+ for (const f of windowFiles) byID.get(f.id)?.replaceWith(f);
590+ }
591+ windowPage = null;
592+ }
593+ if (hasDraft()) return false;
594+ const files = new Map($$('#diffs details.dfile').map((f) => [f.dataset.file, f.open]));
595+ const folded = $('.r-files')?.hasAttribute('data-folded');
596+ const railY = $('.rail')?.scrollTop, filesY = $('.r-files .files')?.scrollTop;
597+ const openPanel = $$('[data-mr-tabs] a').findIndex((t) => t.getAttribute('aria-expanded') === 'true');
598+ const active = document.activeElement;
599+ const focus = active && { file: active.closest?.('.files a')?.getAttribute('title'), region: regions.find((sel) => $(sel)?.contains(active)), menu: active.closest?.('details.menu')?.id, id: active.id, tag: active.tagName, name: active.getAttribute('name'), href: active.getAttribute('href') };
600+ for (const f of $$('#diffs details.dfile', page)) if (files.has(f.dataset.file)) f.open = files.get(f.dataset.file);
601+ ++diffGeneration; ++revealGen; moved = false; loading.clear(); lazyIO?.disconnect();
602+ $('#diffs', page).dataset.mrStarted = 'true';
603+ // The fresh footer comes in with the panels as they are, so that an open
604+ // panel does not fold and open again while the other regions swap.
605+ const freshFooter = $('.mr-footer', page);
606+ if (freshFooter) {
607+ freshFooter.setAttribute('data-panels', '');
608+ $$('[data-mr-panel]', freshFooter).forEach((p, i) => { p.hidden = i !== openPanel; });
609+ }
610+ for (const sel of regions) {
611+ const fresh = $(sel, page);
612+ await replaceRegion($(sel), fresh);
613+ }
614+ for (const key of ['ref', 'sha', 'paths', 'blob']) {
615+ if (page.body.dataset[key] !== undefined) document.body.dataset[key] = page.body.dataset[key];
616+ }
617+ const oldSHA = $('.hdr .ref .sha'), newSHA = $('.hdr .ref .sha', page);
618+ if (oldSHA && newSHA) oldSHA.textContent = newSHA.textContent;
619+ page = null;
620+ panels(openPanel);
621+ foldFiles(!!folded);
622+ if ($('.rail')) $('.rail').scrollTop = railY;
623+ if ($('.r-files .files')) $('.r-files .files').scrollTop = filesY;
624+ touchComments(false);
625+ $('#diffs').dataset.mobile = String(!!window.matchMedia?.('(max-width: 720px)').matches);
626+ window.ermite?.refreshFiles?.();
627+ watch();
628+ if (focus && active !== document.body) {
629+ const region = focus.region ? $(focus.region) : document;
630+ const menu = focus.menu && document.getElementById(focus.menu);
631+ if (menu) menu.open = true;
632+ const el = focus.file ? $('a[title="' + CSS.escape(focus.file) + '"]', region) :
633+ focus.id ? document.getElementById(focus.id) :
634+ focus.href ? $('a[href="' + CSS.escape(focus.href) + '"]', region) :
635+ focus.name ? $('[name="' + CSS.escape(focus.name) + '"]', region) :
636+ menu?.querySelector(':scope > summary');
637+ el?.focus({ preventScroll: true });
638+ }
639+ restoreAnchor(anchor);
640+ }
641+
642+ function refreshNotice(deferred) {
643+ const slot = $('#mr-refresh-status');
644+ if (!slot) return;
645+ slot.replaceChildren();
646+ if (!deferred) {
647+ const text = document.createElement('span'); text.className = 'sr'; text.textContent = 'branch updated'; slot.append(text);
648+ return;
649+ }
650+ slot.append('branch updated — refreshing after your comment. ');
651+
652+ }
653+ const headRefresh = refresher({ blocked: hasDraft, fetchPage, apply: refreshPage, notice: refreshNotice,
654+ failed: () => window.ermiteLive?.reload() });
655+ function headChanged() { if ($('#diffs[data-mr]')) headRefresh.request(); }
656+
288657 function start() {
289658 if (!$('#diffs[data-mr]')) return;
659+ if (window.ermiteLive) window.ermiteLive.canReload = () => {
660+ if (!hasDraft()) return true;
661+ headRefresh.request(); refreshNotice(true); return false;
662+ };
663+ // Global delegation is installed once; replacement regions have their own
664+ // disposable panel/touch hooks below.
665+ if ($('#diffs').dataset.mrStarted) return;
666+ $('#diffs').dataset.mrStarted = 'true';
667+ for (const type of ['input', 'focusout', 'reset', 'submit']) document.addEventListener?.(type, () => headRefresh.resume());
668+ document.addEventListener?.('click', (e) => {
669+ const cancel = e.target.closest?.('.cform .f-btns a');
670+ if (!cancel || !headRefresh.pending() || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
671+ e.preventDefault();
672+ const form = cancel.closest('.cform');
673+ form.reset();
674+ if (form.contains(document.activeElement)) document.activeElement.blur();
675+ headRefresh.resume();
676+ });
677+ panels();
678+ // Large diffs may contain only the split table. Ask the existing server
679+ // route for unified rather than writing the saved session preference.
680+ const narrow = window.matchMedia?.('(max-width: 720px)');
681+ const unify = () => {
682+ $('#diffs').dataset.mobile = String(!!narrow?.matches);
683+ const url = core.mobileURL(location.href, $('#diffs')?.dataset.view, narrow?.matches);
684+ if (url) { location.replace(url); return true; }
685+ return false;
686+ };
687+ if (unify()) return;
688+ narrow?.addEventListener('change', unify);
689+ addEventListener('click', (e) => {
690+ if (narrow?.matches && e.target.closest?.('.vbtn[data-view="split"]')) {
691+ e.preventDefault();
692+ e.stopImmediatePropagation();
693+ }
694+ }, true);
695+ touchComments();
290696 let folded = false;
291697 try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ }
292698 if (folded) foldFiles(true);
@@ -316,7 +722,7 @@ window.ermiteMR = (() => {
316722 });
317723 }
318724
319- return { core, start, load, reveal, openMerge, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };
725+ return { core, refresher, hasDraft, fetchPage, scrollAnchor, restoreAnchor, headChanged, panels, start, load, reveal, openMerge, menuKey, markRead, focusFiles, stepFiles, leaveFiles, foldFiles, openFile };
320726 })();
321727
322728 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => window.ermiteMR.start());
M internal/app/testdata/web/commit-big.html +3 -3
@@ -127,12 +127,12 @@
127127 </span>
128128 <details class="menu vmenu">
129129 <summary class="btn" aria-label="Diff view">
130-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
130+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
131131 </summary>
132132 <div class="menu-pop" role="group" aria-label="Diff view">
133-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
133+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
134134 </a>
135-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
135+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
136136 </a>
137137 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
138138 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commit-binary.html +3 -3
@@ -123,12 +123,12 @@
123123 </span>
124124 <details class="menu vmenu">
125125 <summary class="btn" aria-label="Diff view">
126-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
126+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
127127 </summary>
128128 <div class="menu-pop" role="group" aria-label="Diff view">
129-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
129+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
130130 </a>
131-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
131+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
132132 </a>
133133 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
134134 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commit-merge.html +3 -3
@@ -122,12 +122,12 @@
122122 </span>
123123 <details class="menu vmenu">
124124 <summary class="btn" aria-label="Diff view">
125-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
125+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
126126 </summary>
127127 <div class="menu-pop" role="group" aria-label="Diff view">
128-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
128+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
129129 </a>
130-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
130+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
131131 </a>
132132 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
133133 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commit-modify-split.html +3 -3
@@ -129,12 +129,12 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
129129 </span>
130130 <details class="menu vmenu">
131131 <summary class="btn" aria-label="Diff view">
132-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span>
132+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span>
133133 </summary>
134134 <div class="menu-pop" role="group" aria-label="Diff view">
135-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
135+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
136136 </a>
137-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
137+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" aria-current="true" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
138138 </a>
139139 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
140140 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commit-modify.html +3 -3
@@ -129,12 +129,12 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
129129 </span>
130130 <details class="menu vmenu">
131131 <summary class="btn" aria-label="Diff view">
132-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
132+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
133133 </summary>
134134 <div class="menu-pop" role="group" aria-label="Diff view">
135-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
135+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
136136 </a>
137-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
137+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
138138 </a>
139139 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
140140 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commit-rename.html +3 -3
@@ -123,12 +123,12 @@
123123 </span>
124124 <details class="menu vmenu">
125125 <summary class="btn" aria-label="Diff view">
126-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
126+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
127127 </summary>
128128 <div class="menu-pop" role="group" aria-label="Diff view">
129-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
129+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
130130 </a>
131-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
131+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
132132 </a>
133133 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
134134 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commit-root.html +3 -3
@@ -121,12 +121,12 @@
121121 </span>
122122 <details class="menu vmenu">
123123 <summary class="btn" aria-label="Diff view">
124-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
124+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
125125 </summary>
126126 <div class="menu-pop" role="group" aria-label="Diff view">
127-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
127+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
128128 </a>
129-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
129+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
130130 </a>
131131 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
132132 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commit-trojan.html +3 -3
@@ -124,12 +124,12 @@
124124 </span>
125125 <details class="menu vmenu">
126126 <summary class="btn" aria-label="Diff view">
127-<span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
127+<span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span>
128128 </summary>
129129 <div class="menu-pop" role="group" aria-label="Diff view">
130-<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
130+<a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd>
131131 </a>
132-<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
132+<a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd>
133133 </a>
134134 <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn">
135135 <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span>
M internal/app/testdata/web/commits-root.html +1 -0
@@ -973,6 +973,7 @@
973973 htmx:sse:error -> ermiteLive.error(event.detail.status);
974974 htmx:sse:before:connection -> ermiteLive.gate(event);
975975 home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
976+ mr-head -> ermiteMR.headChanged();
976977 gap -> ermiteLive.reload()">
977978 </span>
978979 </div>
M internal/app/testdata/web/commits-small.html +1 -0
@@ -318,6 +318,7 @@
318318 htmx:sse:error -> ermiteLive.error(event.detail.status);
319319 htmx:sse:before:connection -> ermiteLive.gate(event);
320320 home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
321+ mr-head -> ermiteMR.headChanged();
321322 gap -> ermiteLive.reload()">
322323 </span>
323324 </div>
M internal/app/testdata/web/home.html +1 -0
@@ -194,6 +194,7 @@
194194 htmx:sse:error -> ermiteLive.error(event.detail.status);
195195 htmx:sse:before:connection -> ermiteLive.gate(event);
196196 home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
197+ mr-head -> ermiteMR.headChanged();
197198 gap -> ermiteLive.reload()">
198199 </span>
199200 </div>
M internal/app/testdata/web/refs.html +1 -0
@@ -200,6 +200,7 @@
200200 htmx:sse:error -> ermiteLive.error(event.detail.status);
201201 htmx:sse:before:connection -> ermiteLive.gate(event);
202202 home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
203+ mr-head -> ermiteMR.headChanged();
203204 gap -> ermiteLive.reload()">
204205 </span>
205206 </div>
M internal/app/testdata/web/summary.html +1 -0
@@ -226,6 +226,7 @@
226226 htmx:sse:error -> ermiteLive.error(event.detail.status);
227227 htmx:sse:before:connection -> ermiteLive.gate(event);
228228 home-list, repo-last-push, refs-list, commits-head, mr-notice, mr-mergeability, ci-run, ci-job, inbox -> ermiteLive.swap(event.detail.data);
229+ mr-head -> ermiteMR.headChanged();
229230 gap -> ermiteLive.reload()">
230231 </span>
231232 </div>
M internal/app/web_events_test.go +12 -5
@@ -245,12 +245,19 @@ func TestWebEventsResumeAndGap(t *testing.T) {
245245 }
246246 s.cancel()
247247
248- r := h.sse("topic=home", ids[0])
249- for _, want := range ids[1:] {
250- ev := r.next(t, watchWait)
251- assert.Equal(t, want, ev.id)
252- assert.Equal(t, "home-list", ev.name)
248+ // A replacement stream resumes via the URL; reconnect headers still win.
249+ for _, r := range []*sseStream{
250+ h.sse("topic=home&after="+ids[0], ""),
251+ h.sse("topic=home&after=invalid", ids[0]),
252+ } {
253+ for _, want := range ids[1:] {
254+ ev := r.next(t, watchWait)
255+ assert.Equal(t, want, ev.id)
256+ assert.Equal(t, "home-list", ev.name)
257+ }
258+ r.cancel()
253259 }
260+ r := h.sse("topic=home", ids[2])
254261 h.writeFile(wt, "live.txt", []byte("x\n"))
255262 h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/pub.git"), "main")
256263 live := r.next(t, watchWait)
M internal/app/web_mr_test.go +4 -3
@@ -335,7 +335,7 @@ func TestWebMergeRequestReviewAndFastForward(t *testing.T) {
335335 all := b.get(base, as(token))
336336 assert.Contains(t, all.body, `href="/proj/merge-requests/1/since"`)
337337 assert.Contains(t, all.body, "since your last read")
338- assert.Contains(t, all.body, "<li data-new>", "the commit pushed since is marked")
338+ assert.Contains(t, all.body, "<tr data-new>", "the commit pushed since is marked")
339339 assert.Contains(t, all.body, "data-read>Mark read")
340340 assert.Contains(t, all.body, "outdated", "the notes were written on the earlier head")
341341 since := b.get(base+"/since", as(token))
@@ -595,8 +595,9 @@ func TestWebMergeRequestEvents(t *testing.T) {
595595
596596 r.writeFile(r.wt, "more.txt", []byte("more\n"))
597597 r.mustGit(r.wt, "push", "-q", r.url, "feat/search")
598- ev = nextNamed(t, one, "mr-notice")
599- assert.Contains(t, ev.data, "the branch changed")
598+ ev = nextNamed(t, one, "mr-head")
599+ assert.Equal(t, `<div id="mr-head"></div>`, ev.data)
600+ assert.NotContains(t, ev.data, "the branch changed")
600601
601602 r.cli(t, adminToken, "mr", "close", "proj", "1").ok(t)
602603 assert.Contains(t, nextNamed(t, one, "mr-notice").data, "was closed")

Keyboard shortcuts

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

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

Branches 3

Tags 43

↑↓ moveenter openesc close