detached 86ac875

#1 feat(mr): notes with threads, line ranges, edits and verdicts

state
merged · 86ac875 · fast-forward
branch
feat/mr-notes → main
opened
bnema ·
updated

Merge request notes (ADR 0032): one uniform review object, published at once, no draft review.

  • Threads (one level, resolution on the root), line ranges on one side, author edit and delete.
  • Notes and descriptions rendered as safe Markdown (no raw HTML, link policy), ^N links to note N.
  • Optional verdict on a root note (approve, changes requested), needs the new mr:review scope; informative only; latest per author; approvals stale after a push; changes requested stay until the author's next verdict; immutable.
  • Events comment_edited, comment_deleted, comment_resolved.
  • RPC, CLI (mr comment --reply/--line A-B/--approve/--request-changes, mr edit-comment, mr delete-comment, show and watch), web (threads, quote, Shift+click ranges with a no-JS fallback, verdict list), docs and the ermite skill.

Depends on the ermite-api MR (feat/mr-notes). Until ermite-api v0.8.0 is tagged this branch builds only with a go.work that uses that branch; a last commit will require v0.8.0.

Upgrade: mr:review is a new scope, so a token that held every scope loses the owner's rights until it is recreated (--preset cli) with the root token or from an owner web session.

Checks (with the API branch in a go.work): build, go test -race, go test ./internal/app, golangci-lint --new-from-rev=main, hexcheck, templ drift, buf lint/breaking/generate, mocks drift, node tests (145). Reviewed by three read-only reviewers plus a targeted re-review; all findings fixed. Not verified yet: manual browser check of the merge request page.

CommitSubjectAuthorDateSignatureStatus
86ac875Merge remote-tracking branch 'origin/main' into feat/mr-notesbriceGPG Verified
77b3426build(deps): ermite-api v0.8.0briceGPG Verified
c064166fix(web): split-only note links and disjoint heading ids of split note copiesbriceGPG Verified
06682affix(mr): deleted verdict notes, split-layout note links and verdict labelsbriceGPG Verified
3872b20test(mr): stronger note retry and range testsbriceGPG Verified
31c9416docs(mr): notes, threads, ranges and verdictsbriceGPG Verified
f8d5320merge: web for merge request notesbriceGPG Verified
aec37bffeat(web): threads, line ranges, Markdown notes, edits and verdicts on merge requestsbriceGPG Verified
945925cmerge: RPC and CLI for merge request notesbriceGPG Verified
8def6defeat(cli): replies, ranges, verdicts, edit and delete for merge request notesbriceGPG Verified
aa64b62fix(mr): recheck the reply target when a note save is retriedbriceGPG Verified
b31a354feat(mr): replies, line ranges, edits, deletes and verdicts in the merge request use casebriceGPG Verified
3a784d4feat(mr): note model with threads, ranges, verdicts and the mr:review scopebriceGPG Verified

Checks 8

62 files · +3086 -650
7/8
M docs/cli.md +2 -1
@@ -55,7 +55,8 @@ ermite token create <label> [--preset cli|agent|ci|runner] [--scope …] [--repo
5555 ermite token list | revoke <id>
5656
5757 ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff]
58-ermite mr comment <repo> <n> -m … [--path … --line … --side old|new] | resolve <repo> <n> <comment>
58+ermite mr comment <repo> <n> -m … [--path … --line N|A-B --side old|new] [--reply <id>] [--approve|--request-changes]
59+ermite mr resolve <repo> <n> <comment> | edit-comment <repo> <n> <comment> -m … | delete-comment <repo> <n> <comment>
5960 ermite mr merge <repo> <n> --head <id> [--squash] [--delete-branch] | close <repo> <n>
6061
6162 ermite run list <repo> [--workflow …] [--branch …] [--status …]
@@ -55,7 +55,8 @@ ermite token create <label> [--preset cli|agent|ci|runner] [--scope …] [--repo
55 ermite token list | revoke <id>55 ermite token list | revoke <id>
56 56
57 ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff]57 ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff]
58-ermite mr comment <repo> <n> -m … [--path … --line … --side old|new] | resolve <repo> <n> <comment>58+ermite mr comment <repo> <n> -m … [--path … --line N|A-B --side old|new] [--reply <id>] [--approve|--request-changes]
59+ermite mr resolve <repo> <n> <comment> | edit-comment <repo> <n> <comment> -m … | delete-comment <repo> <n> <comment>
59 ermite mr merge <repo> <n> --head <id> [--squash] [--delete-branch] | close <repo> <n>60 ermite mr merge <repo> <n> --head <id> [--squash] [--delete-branch] | close <repo> <n>
60 61
61 ermite run list <repo> [--workflow …] [--branch …] [--status …]62 ermite run list <repo> [--workflow …] [--branch …] [--status …]
M docs/merge-requests.md +30 -1
@@ -17,10 +17,39 @@ merge request web URL on its own line. With `--json`, merge request objects
1717 - With `failing_checks_block` on (default), the merge is refused while a check of the head failed or is still running.
1818 - `--delete-branch` deletes the source branch, unless it is the default branch or another open request uses it.
1919 - `ermite repo edit --merge-modes` limits the modes per repository.
20-- Scopes: `mr:write` to open and comment, `mr:merge` to merge.
20+- Scopes: `mr:write` to open and comment, `mr:review` to give a verdict, `mr:merge` to merge.
2121
2222 Verify squash commits with the `allowed_signers` line from `ermite signing-key show`:
2323
2424 ```sh
2525 git verify-commit <commit>
2626 ```
27+
28+## Notes
29+
30+A note is the only review object. It is published as soon as it is written: there is no draft and no submit step.
31+
32+```sh
33+ermite mr comment myrepo 1 -m "General remark"
34+ermite mr comment myrepo 1 -m "Why?" --path main.go --line 10-14 --side new
35+ermite mr comment myrepo 1 -m "Fixed in ^3" --reply 2
36+ermite mr resolve myrepo 1 2
37+ermite mr edit-comment myrepo 1 2 -m "New text"
38+ermite mr delete-comment myrepo 1 2
39+```
40+
41+- Notes are Markdown, up to 8000 characters, rendered without raw HTML. Quote with `> `.
42+- `^N` in a note or a description links to note N of the same merge request. A reference to a missing note stays plain text.
43+- A note is general, on one line, or on a range `A-B` of lines on one side (`old` or `new`) of the diff. The lines must be in the diff.
44+- Threads have one level: a reply (`--reply <id>`) takes the anchor of its root and cannot carry a verdict. Resolving or reopening is done on the root and covers its replies.
45+- Only the author edits or deletes a note. An edited note shows "edited". A deleted note loses its text; a deleted root with replies stays as "deleted note".
46+
47+## Verdicts
48+
49+A root note can carry a verdict: `--approve` or `--request-changes`. The text may then be empty. A verdict needs the `mr:review` scope (or the owner); `mr:write` comments but cannot give one.
50+
51+- A verdict ends a review: agents and `ermite watch` act on it, not on each note.
52+- Verdicts are informative: none blocks or allows a merge.
53+- `mr show` lists the latest verdict of each author (`verdicts` in `--json`).
54+- An approval is stale once the branch head differs from the head it was given on. Changes requested stay current until their author gives a new verdict, whatever is pushed.
55+- Verdicts are immutable: editing or deleting the note keeps its verdict. Only a new verdict of the same author replaces it.
@@ -17,10 +17,39 @@ merge request web URL on its own line. With `--json`, merge request objects
17 - With `failing_checks_block` on (default), the merge is refused while a check of the head failed or is still running.17 - With `failing_checks_block` on (default), the merge is refused while a check of the head failed or is still running.
18 - `--delete-branch` deletes the source branch, unless it is the default branch or another open request uses it.18 - `--delete-branch` deletes the source branch, unless it is the default branch or another open request uses it.
19 - `ermite repo edit --merge-modes` limits the modes per repository.19 - `ermite repo edit --merge-modes` limits the modes per repository.
20-- Scopes: `mr:write` to open and comment, `mr:merge` to merge.20+- Scopes: `mr:write` to open and comment, `mr:review` to give a verdict, `mr:merge` to merge.
21 21
22 Verify squash commits with the `allowed_signers` line from `ermite signing-key show`:22 Verify squash commits with the `allowed_signers` line from `ermite signing-key show`:
23 23
24 ```sh24 ```sh
25 git verify-commit <commit>25 git verify-commit <commit>
26 ```26 ```
27+
28+## Notes
29+
30+A note is the only review object. It is published as soon as it is written: there is no draft and no submit step.
31+
32+```sh
33+ermite mr comment myrepo 1 -m "General remark"
34+ermite mr comment myrepo 1 -m "Why?" --path main.go --line 10-14 --side new
35+ermite mr comment myrepo 1 -m "Fixed in ^3" --reply 2
36+ermite mr resolve myrepo 1 2
37+ermite mr edit-comment myrepo 1 2 -m "New text"
38+ermite mr delete-comment myrepo 1 2
39+```
40+
41+- Notes are Markdown, up to 8000 characters, rendered without raw HTML. Quote with `> `.
42+- `^N` in a note or a description links to note N of the same merge request. A reference to a missing note stays plain text.
43+- A note is general, on one line, or on a range `A-B` of lines on one side (`old` or `new`) of the diff. The lines must be in the diff.
44+- Threads have one level: a reply (`--reply <id>`) takes the anchor of its root and cannot carry a verdict. Resolving or reopening is done on the root and covers its replies.
45+- Only the author edits or deletes a note. An edited note shows "edited". A deleted note loses its text; a deleted root with replies stays as "deleted note".
46+
47+## Verdicts
48+
49+A root note can carry a verdict: `--approve` or `--request-changes`. The text may then be empty. A verdict needs the `mr:review` scope (or the owner); `mr:write` comments but cannot give one.
50+
51+- A verdict ends a review: agents and `ermite watch` act on it, not on each note.
52+- Verdicts are informative: none blocks or allows a merge.
53+- `mr show` lists the latest verdict of each author (`verdicts` in `--json`).
54+- An approval is stale once the branch head differs from the head it was given on. Changes requested stay current until their author gives a new verdict, whatever is pushed.
55+- Verdicts are immutable: editing or deleting the note keeps its verdict. Only a new verdict of the same author replaces it.
M docs/tokens.md +2 -2
@@ -6,7 +6,7 @@ ermite token list
66 ermite token revoke <id>
77 ```
88
9-The secret is printed once. Presets and `--scope` add up. The full scope list is in [`skills/ermite/CLI.md`](../skills/ermite/CLI.md).
9+The secret is printed once. Presets and `--scope` add up. The full scope list is in [`skills/ermite/CLI.md`](../skills/ermite/CLI.md). `mr:review` lets a token give a merge request verdict (`--approve`, `--request-changes`); the `cli` preset includes it, the `agent` preset does not. A token created before `mr:review` existed lacks it: recreate it with `--preset cli` using the root token or from a signed-in owner web session (an old token lacking `mr:review` cannot grant it), then replace the stored CLI secret and revoke the old token to keep the owner's rights.
1010
1111 | Preset | For |
1212 |---|---|
@@ -28,7 +28,7 @@ ermite token create claude --preset agent --scope repo:read --repo myrepo --bran
2828 The token **can**:
2929
3030 - push, force-push and delete branches under its prefixes;
31-- open, comment on and close merge requests;
31+- open, comment on and close merge requests; with `mr:review`, give verdicts;
3232 - with `mr:merge`, merge into a branch under its prefixes;
3333 - read CI runs, logs and checks; with `ci:write`, cancel and re-run runs of its own branches;
3434 - create tokens whose prefixes lie within its own.
@@ -6,7 +6,7 @@ ermite token list
6 ermite token revoke <id>6 ermite token revoke <id>
7 ```7 ```
8 8
9-The secret is printed once. Presets and `--scope` add up. The full scope list is in [`skills/ermite/CLI.md`](../skills/ermite/CLI.md).9+The secret is printed once. Presets and `--scope` add up. The full scope list is in [`skills/ermite/CLI.md`](../skills/ermite/CLI.md). `mr:review` lets a token give a merge request verdict (`--approve`, `--request-changes`); the `cli` preset includes it, the `agent` preset does not. A token created before `mr:review` existed lacks it: recreate it with `--preset cli` using the root token or from a signed-in owner web session (an old token lacking `mr:review` cannot grant it), then replace the stored CLI secret and revoke the old token to keep the owner's rights.
10 10
11 | Preset | For |11 | Preset | For |
12 |---|---|12 |---|---|
@@ -28,7 +28,7 @@ ermite token create claude --preset agent --scope repo:read --repo myrepo --bran
28 The token **can**:28 The token **can**:
29 29
30 - push, force-push and delete branches under its prefixes;30 - push, force-push and delete branches under its prefixes;
31-- open, comment on and close merge requests;31+- open, comment on and close merge requests; with `mr:review`, give verdicts;
32 - with `mr:merge`, merge into a branch under its prefixes;32 - with `mr:merge`, merge into a branch under its prefixes;
33 - read CI runs, logs and checks; with `ci:write`, cancel and re-run runs of its own branches;33 - read CI runs, logs and checks; with `ci:write`, cancel and re-run runs of its own branches;
34 - create tokens whose prefixes lie within its own.34 - create tokens whose prefixes lie within its own.
M gen/ermite/storage/merge_request.pb.go +122 -18 generated file: collapsed
M go.mod +1 -1
@@ -13,7 +13,7 @@ tool (
1313
1414 require (
1515 bnema.dev/ermite-actions v0.2.0
16- bnema.dev/ermite-api v0.7.0
16+ bnema.dev/ermite-api v0.8.0
1717 bnema.dev/ermite-git v0.1.0
1818 bnema.dev/ermite-kv v0.1.0
1919 connectrpc.com/connect v1.21.0
@@ -13,7 +13,7 @@ tool (
13 13
14 require (14 require (
15 bnema.dev/ermite-actions v0.2.015 bnema.dev/ermite-actions v0.2.0
16- bnema.dev/ermite-api v0.7.016+ bnema.dev/ermite-api v0.8.0
17 bnema.dev/ermite-git v0.1.017 bnema.dev/ermite-git v0.1.0
18 bnema.dev/ermite-kv v0.1.018 bnema.dev/ermite-kv v0.1.0
19 connectrpc.com/connect v1.21.019 connectrpc.com/connect v1.21.0
M go.sum +2 -2 generated file: collapsed
M internal/adapters/client/scopes_test.go +1 -0
@@ -16,6 +16,7 @@ import (
1616
1717 func TestScopeWireNames(t *testing.T) {
1818 want := map[ermite.Scope]string{
19+ ermite.Scope_SCOPE_MR_REVIEW: "mr:review",
1920 ermite.Scope_SCOPE_REPO_READ: "repo:read",
2021 ermite.Scope_SCOPE_REPO_WRITE: "repo:write",
2122 ermite.Scope_SCOPE_REPO_ADMIN: "repo:admin",
@@ -16,6 +16,7 @@ import (
16 16
17 func TestScopeWireNames(t *testing.T) {17 func TestScopeWireNames(t *testing.T) {
18 want := map[ermite.Scope]string{18 want := map[ermite.Scope]string{
19+ ermite.Scope_SCOPE_MR_REVIEW: "mr:review",
19 ermite.Scope_SCOPE_REPO_READ: "repo:read",20 ermite.Scope_SCOPE_REPO_READ: "repo:read",
20 ermite.Scope_SCOPE_REPO_WRITE: "repo:write",21 ermite.Scope_SCOPE_REPO_WRITE: "repo:write",
21 ermite.Scope_SCOPE_REPO_ADMIN: "repo:admin",22 ermite.Scope_SCOPE_REPO_ADMIN: "repo:admin",
M internal/adapters/kvstore/mergerequests.go +6 -0
@@ -570,6 +570,11 @@ func encodeComment(c domain.Comment) ([]byte, error) {
570570 return nil, err
571571 }
572572 return mustMarshal(&storagepb.Comment{
573+ ReplyTo: c.ReplyTo,
574+ StartLine: int32(c.StartLine), //nolint:gosec // validated against the diff
575+ Verdict: storagepb.Verdict(c.Verdict),
576+ EditedAt: encodeTime(c.EditedAt),
577+ Deleted: c.Deleted,
573578 Id: c.ID,
574579 Author: c.Author,
575580 Path: c.Path,
@@ -593,6 +598,7 @@ func decodeComment(item kv.Item) (domain.Comment, error) {
593598 return domain.Comment{}, fmt.Errorf("decode comment %d: %w", pb.GetId(), err)
594599 }
595600 c := domain.Comment{
601+ ReplyTo: pb.GetReplyTo(), StartLine: int(pb.GetStartLine()), Verdict: domain.Verdict(pb.GetVerdict()), EditedAt: decodeTime(pb.GetEditedAt()), Deleted: pb.GetDeleted(),
596602 ID: pb.GetId(), Author: pb.GetAuthor(), Path: pb.GetPath(), Line: int(pb.GetLine()),
597603 Side: decodeSide(pb.GetSide()), Text: text, Resolved: pb.GetResolved(),
598604 CreatedAt: decodeTime(pb.GetCreatedAt()), Version: item.Version,
@@ -570,6 +570,11 @@ func encodeComment(c domain.Comment) ([]byte, error) {
570 return nil, err570 return nil, err
571 }571 }
572 return mustMarshal(&storagepb.Comment{572 return mustMarshal(&storagepb.Comment{
573+ ReplyTo: c.ReplyTo,
574+ StartLine: int32(c.StartLine), //nolint:gosec // validated against the diff
575+ Verdict: storagepb.Verdict(c.Verdict),
576+ EditedAt: encodeTime(c.EditedAt),
577+ Deleted: c.Deleted,
573 Id: c.ID,578 Id: c.ID,
574 Author: c.Author,579 Author: c.Author,
575 Path: c.Path,580 Path: c.Path,
@@ -593,6 +598,7 @@ func decodeComment(item kv.Item) (domain.Comment, error) {
593 return domain.Comment{}, fmt.Errorf("decode comment %d: %w", pb.GetId(), err)598 return domain.Comment{}, fmt.Errorf("decode comment %d: %w", pb.GetId(), err)
594 }599 }
595 c := domain.Comment{600 c := domain.Comment{
601+ ReplyTo: pb.GetReplyTo(), StartLine: int(pb.GetStartLine()), Verdict: domain.Verdict(pb.GetVerdict()), EditedAt: decodeTime(pb.GetEditedAt()), Deleted: pb.GetDeleted(),
596 ID: pb.GetId(), Author: pb.GetAuthor(), Path: pb.GetPath(), Line: int(pb.GetLine()),602 ID: pb.GetId(), Author: pb.GetAuthor(), Path: pb.GetPath(), Line: int(pb.GetLine()),
597 Side: decodeSide(pb.GetSide()), Text: text, Resolved: pb.GetResolved(),603 Side: decodeSide(pb.GetSide()), Text: text, Resolved: pb.GetResolved(),
598 CreatedAt: decodeTime(pb.GetCreatedAt()), Version: item.Version,604 CreatedAt: decodeTime(pb.GetCreatedAt()), Version: item.Version,
M internal/adapters/kvstore/mergerequests_test.go +25 -0
@@ -547,3 +547,28 @@ func TestSigningSeedRejectsBadGenerators(t *testing.T) {
547547 _, err = s.SigningSeed(ctx, func() ([]byte, error) { return nil, assert.AnError }, created)
548548 require.ErrorIs(t, err, assert.AnError)
549549 }
550+
551+func TestNoteFieldsRoundTrip(t *testing.T) {
552+ ctx := context.Background()
553+ _, store, name := repoWithMRs(t)
554+ mr, err := store.Create(ctx, newMR(t, name, "notes"))
555+ require.NoError(t, err)
556+ for id := int64(1); id <= 2; id++ {
557+ mr, err = store.Get(ctx, name, mr.Number)
558+ require.NoError(t, err)
559+ c := newComment(t, id, "note")
560+ if id == 2 {
561+ c.ReplyTo = 1
562+ c.StartLine = 1
563+ c.Verdict = domain.VerdictApprove
564+ c.EditedAt = created
565+ c.Deleted = true
566+ }
567+ mr.CommentSeq = id
568+ require.NoError(t, store.SaveComment(ctx, mr, c))
569+ got, err := store.GetComment(ctx, name, mr.Number, id)
570+ require.NoError(t, err)
571+ c.Version = got.Version
572+ assert.Equal(t, c, got)
573+ }
574+}
@@ -547,3 +547,28 @@ func TestSigningSeedRejectsBadGenerators(t *testing.T) {
547 _, err = s.SigningSeed(ctx, func() ([]byte, error) { return nil, assert.AnError }, created)547 _, err = s.SigningSeed(ctx, func() ([]byte, error) { return nil, assert.AnError }, created)
548 require.ErrorIs(t, err, assert.AnError)548 require.ErrorIs(t, err, assert.AnError)
549 }549 }
550+
551+func TestNoteFieldsRoundTrip(t *testing.T) {
552+ ctx := context.Background()
553+ _, store, name := repoWithMRs(t)
554+ mr, err := store.Create(ctx, newMR(t, name, "notes"))
555+ require.NoError(t, err)
556+ for id := int64(1); id <= 2; id++ {
557+ mr, err = store.Get(ctx, name, mr.Number)
558+ require.NoError(t, err)
559+ c := newComment(t, id, "note")
560+ if id == 2 {
561+ c.ReplyTo = 1
562+ c.StartLine = 1
563+ c.Verdict = domain.VerdictApprove
564+ c.EditedAt = created
565+ c.Deleted = true
566+ }
567+ mr.CommentSeq = id
568+ require.NoError(t, store.SaveComment(ctx, mr, c))
569+ got, err := store.GetComment(ctx, name, mr.Number, id)
570+ require.NoError(t, err)
571+ c.Version = got.Version
572+ assert.Equal(t, c, got)
573+ }
574+}
M internal/adapters/rpc/convert.go +10 -0
@@ -124,6 +124,7 @@ func mergeModesFromPB(in []pb.MergeMode) ([]domain.MergeMode, error) {
124124 }
125125
126126 var scopeToPB = map[domain.Scope]pb.Scope{
127+ domain.ScopeMRReview: pb.Scope_SCOPE_MR_REVIEW,
127128 domain.ScopeRepoRead: pb.Scope_SCOPE_REPO_READ,
128129 domain.ScopeRepoWrite: pb.Scope_SCOPE_REPO_WRITE,
129130 domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN,
@@ -471,6 +472,15 @@ func watchEventToPB(we ports.WatchEvent) *pb.Event {
471472 case domain.CommentAdded:
472473 out.Time = timestamp(ev.At)
473474 out.Kind = &pb.Event_CommentAdded{CommentAdded: &pb.CommentAdded{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
475+ case domain.CommentEdited:
476+ out.Time = timestamp(ev.At)
477+ out.Kind = &pb.Event_CommentEdited{CommentEdited: &pb.CommentEdited{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
478+ case domain.CommentDeleted:
479+ out.Time = timestamp(ev.At)
480+ out.Kind = &pb.Event_CommentDeleted{CommentDeleted: &pb.CommentDeleted{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
481+ case domain.CommentResolved:
482+ out.Time = timestamp(ev.At)
483+ out.Kind = &pb.Event_CommentResolved{CommentResolved: &pb.CommentResolved{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
474484 case domain.RunUpdated:
475485 out.Time = timestamp(ev.At)
476486 out.Kind = runEventToPB(ev)
@@ -124,6 +124,7 @@ func mergeModesFromPB(in []pb.MergeMode) ([]domain.MergeMode, error) {
124 }124 }
125 125
126 var scopeToPB = map[domain.Scope]pb.Scope{126 var scopeToPB = map[domain.Scope]pb.Scope{
127+ domain.ScopeMRReview: pb.Scope_SCOPE_MR_REVIEW,
127 domain.ScopeRepoRead: pb.Scope_SCOPE_REPO_READ,128 domain.ScopeRepoRead: pb.Scope_SCOPE_REPO_READ,
128 domain.ScopeRepoWrite: pb.Scope_SCOPE_REPO_WRITE,129 domain.ScopeRepoWrite: pb.Scope_SCOPE_REPO_WRITE,
129 domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN,130 domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN,
@@ -471,6 +472,15 @@ func watchEventToPB(we ports.WatchEvent) *pb.Event {
471 case domain.CommentAdded:472 case domain.CommentAdded:
472 out.Time = timestamp(ev.At)473 out.Time = timestamp(ev.At)
473 out.Kind = &pb.Event_CommentAdded{CommentAdded: &pb.CommentAdded{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}474 out.Kind = &pb.Event_CommentAdded{CommentAdded: &pb.CommentAdded{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
475+ case domain.CommentEdited:
476+ out.Time = timestamp(ev.At)
477+ out.Kind = &pb.Event_CommentEdited{CommentEdited: &pb.CommentEdited{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
478+ case domain.CommentDeleted:
479+ out.Time = timestamp(ev.At)
480+ out.Kind = &pb.Event_CommentDeleted{CommentDeleted: &pb.CommentDeleted{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
481+ case domain.CommentResolved:
482+ out.Time = timestamp(ev.At)
483+ out.Kind = &pb.Event_CommentResolved{CommentResolved: &pb.CommentResolved{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
474 case domain.RunUpdated:484 case domain.RunUpdated:
475 out.Time = timestamp(ev.At)485 out.Time = timestamp(ev.At)
476 out.Kind = runEventToPB(ev)486 out.Kind = runEventToPB(ev)
M internal/adapters/rpc/mergerequest.go +70 -0
@@ -17,6 +17,52 @@ import (
1717 // mergeRequestService implements ermiteconnect.MergeRequestServiceHandler.
1818 type mergeRequestService struct{ mrs ports.MergeRequestService }
1919
20+func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {
21+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
22+ if err != nil {
23+ return nil, connectError(ctx, err)
24+ }
25+ c, err := s.mrs.EditComment(ctx, caller(ctx), name, req.Msg.GetNumber(), commentID(req.Msg.GetCommentId()), req.Msg.GetText())
26+ if err != nil {
27+ return nil, connectError(ctx, err)
28+ }
29+ return connect.NewResponse(&pb.EditCommentResponse{Comment: commentToPB(c)}), nil
30+}
31+
32+func (s *mergeRequestService) DeleteComment(ctx context.Context, req *connect.Request[pb.DeleteCommentRequest]) (*connect.Response[pb.DeleteCommentResponse], error) {
33+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
34+ if err != nil {
35+ return nil, connectError(ctx, err)
36+ }
37+ c, err := s.mrs.DeleteComment(ctx, caller(ctx), name, req.Msg.GetNumber(), commentID(req.Msg.GetCommentId()))
38+ if err != nil {
39+ return nil, connectError(ctx, err)
40+ }
41+ return connect.NewResponse(&pb.DeleteCommentResponse{Comment: commentToPB(c)}), nil
42+}
43+
44+func verdictToPB(v domain.Verdict) pb.Verdict {
45+ switch v {
46+ case domain.VerdictApprove:
47+ return pb.Verdict_VERDICT_APPROVE
48+ case domain.VerdictChangesRequested:
49+ return pb.Verdict_VERDICT_CHANGES_REQUESTED
50+ }
51+ return pb.Verdict_VERDICT_UNSPECIFIED
52+}
53+
54+func verdictFromPB(v pb.Verdict) (domain.Verdict, error) {
55+ switch v {
56+ case pb.Verdict_VERDICT_UNSPECIFIED:
57+ return domain.VerdictNone, nil
58+ case pb.Verdict_VERDICT_APPROVE:
59+ return domain.VerdictApprove, nil
60+ case pb.Verdict_VERDICT_CHANGES_REQUESTED:
61+ return domain.VerdictChangesRequested, nil
62+ }
63+ return domain.VerdictNone, fmt.Errorf("%w: verdict %v", domain.ErrInvalid, v)
64+}
65+
2066 func mergeRequestStateToPB(s domain.MRState) pb.MergeRequestState {
2167 switch s {
2268 case domain.MRStateOpen:
@@ -96,9 +142,19 @@ func commentToPB(c domain.Comment) *pb.Comment {
96142 Text: c.Text.String(),
97143 Resolved: c.Resolved,
98144 CreatedAt: timestamp(c.CreatedAt),
145+ ReplyTo: optionalCommentID(c.ReplyTo),
146+ StartLine: int32(c.StartLine), //nolint:gosec // validated diff line
147+ Verdict: verdictToPB(c.Verdict), EditedAt: timestamp(c.EditedAt), Deleted: c.Deleted,
99148 }
100149 }
101150
151+func optionalCommentID(id int64) string {
152+ if id == 0 {
153+ return ""
154+ }
155+ return strconv.FormatInt(id, 10)
156+}
157+
102158 // commentID parses the opaque id of a comment. One that this server never
103159 // issued is 0, which names no comment.
104160 func commentID(s string) int64 {
@@ -185,6 +241,9 @@ func (s *mergeRequestService) GetMergeRequest(ctx context.Context, req *connect.
185241 for _, c := range d.Comments {
186242 out.Comments = append(out.Comments, commentToPB(c))
187243 }
244+ for _, v := range d.Verdicts {
245+ out.Verdicts = append(out.Verdicts, &pb.AuthorVerdict{Author: v.Author, Verdict: verdictToPB(v.Verdict), CommentId: strconv.FormatInt(v.CommentID, 10), CommitId: v.CommitID.String(), CreatedAt: timestamp(v.At), Stale: v.Stale})
246+ }
188247 return connect.NewResponse(out), nil
189248 }
190249
@@ -197,7 +256,18 @@ func (s *mergeRequestService) AddComment(ctx context.Context, req *connect.Reque
197256 if err != nil {
198257 return nil, connectError(ctx, err)
199258 }
259+ verdict, err := verdictFromPB(req.Msg.GetVerdict())
260+ if err != nil {
261+ return nil, connectError(ctx, err)
262+ }
263+ replyTo := commentID(req.Msg.GetReplyTo())
264+ if req.Msg.GetReplyTo() != "" && replyTo == 0 {
265+ var v domain.Violations
266+ v.Add("reply_to", "is not a note id")
267+ return nil, connectError(ctx, v.Err())
268+ }
200269 c, err := s.mrs.AddComment(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.AddCommentParams{
270+ ReplyTo: replyTo, StartLine: int(req.Msg.GetStartLine()), Verdict: verdict,
201271 Path: req.Msg.GetPath(), Line: int(req.Msg.GetLine()), Side: side, Text: req.Msg.GetText(),
202272 })
203273 if err != nil {
@@ -17,6 +17,52 @@ import (
17 // mergeRequestService implements ermiteconnect.MergeRequestServiceHandler.17 // mergeRequestService implements ermiteconnect.MergeRequestServiceHandler.
18 type mergeRequestService struct{ mrs ports.MergeRequestService }18 type mergeRequestService struct{ mrs ports.MergeRequestService }
19 19
20+func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {
21+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
22+ if err != nil {
23+ return nil, connectError(ctx, err)
24+ }
25+ c, err := s.mrs.EditComment(ctx, caller(ctx), name, req.Msg.GetNumber(), commentID(req.Msg.GetCommentId()), req.Msg.GetText())
26+ if err != nil {
27+ return nil, connectError(ctx, err)
28+ }
29+ return connect.NewResponse(&pb.EditCommentResponse{Comment: commentToPB(c)}), nil
30+}
31+
32+func (s *mergeRequestService) DeleteComment(ctx context.Context, req *connect.Request[pb.DeleteCommentRequest]) (*connect.Response[pb.DeleteCommentResponse], error) {
33+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
34+ if err != nil {
35+ return nil, connectError(ctx, err)
36+ }
37+ c, err := s.mrs.DeleteComment(ctx, caller(ctx), name, req.Msg.GetNumber(), commentID(req.Msg.GetCommentId()))
38+ if err != nil {
39+ return nil, connectError(ctx, err)
40+ }
41+ return connect.NewResponse(&pb.DeleteCommentResponse{Comment: commentToPB(c)}), nil
42+}
43+
44+func verdictToPB(v domain.Verdict) pb.Verdict {
45+ switch v {
46+ case domain.VerdictApprove:
47+ return pb.Verdict_VERDICT_APPROVE
48+ case domain.VerdictChangesRequested:
49+ return pb.Verdict_VERDICT_CHANGES_REQUESTED
50+ }
51+ return pb.Verdict_VERDICT_UNSPECIFIED
52+}
53+
54+func verdictFromPB(v pb.Verdict) (domain.Verdict, error) {
55+ switch v {
56+ case pb.Verdict_VERDICT_UNSPECIFIED:
57+ return domain.VerdictNone, nil
58+ case pb.Verdict_VERDICT_APPROVE:
59+ return domain.VerdictApprove, nil
60+ case pb.Verdict_VERDICT_CHANGES_REQUESTED:
61+ return domain.VerdictChangesRequested, nil
62+ }
63+ return domain.VerdictNone, fmt.Errorf("%w: verdict %v", domain.ErrInvalid, v)
64+}
65+
20 func mergeRequestStateToPB(s domain.MRState) pb.MergeRequestState {66 func mergeRequestStateToPB(s domain.MRState) pb.MergeRequestState {
21 switch s {67 switch s {
22 case domain.MRStateOpen:68 case domain.MRStateOpen:
@@ -96,9 +142,19 @@ func commentToPB(c domain.Comment) *pb.Comment {
96 Text: c.Text.String(),142 Text: c.Text.String(),
97 Resolved: c.Resolved,143 Resolved: c.Resolved,
98 CreatedAt: timestamp(c.CreatedAt),144 CreatedAt: timestamp(c.CreatedAt),
145+ ReplyTo: optionalCommentID(c.ReplyTo),
146+ StartLine: int32(c.StartLine), //nolint:gosec // validated diff line
147+ Verdict: verdictToPB(c.Verdict), EditedAt: timestamp(c.EditedAt), Deleted: c.Deleted,
99 }148 }
100 }149 }
101 150
151+func optionalCommentID(id int64) string {
152+ if id == 0 {
153+ return ""
154+ }
155+ return strconv.FormatInt(id, 10)
156+}
157+
102 // commentID parses the opaque id of a comment. One that this server never158 // commentID parses the opaque id of a comment. One that this server never
103 // issued is 0, which names no comment.159 // issued is 0, which names no comment.
104 func commentID(s string) int64 {160 func commentID(s string) int64 {
@@ -185,6 +241,9 @@ func (s *mergeRequestService) GetMergeRequest(ctx context.Context, req *connect.
185 for _, c := range d.Comments {241 for _, c := range d.Comments {
186 out.Comments = append(out.Comments, commentToPB(c))242 out.Comments = append(out.Comments, commentToPB(c))
187 }243 }
244+ for _, v := range d.Verdicts {
245+ out.Verdicts = append(out.Verdicts, &pb.AuthorVerdict{Author: v.Author, Verdict: verdictToPB(v.Verdict), CommentId: strconv.FormatInt(v.CommentID, 10), CommitId: v.CommitID.String(), CreatedAt: timestamp(v.At), Stale: v.Stale})
246+ }
188 return connect.NewResponse(out), nil247 return connect.NewResponse(out), nil
189 }248 }
190 249
@@ -197,7 +256,18 @@ func (s *mergeRequestService) AddComment(ctx context.Context, req *connect.Reque
197 if err != nil {256 if err != nil {
198 return nil, connectError(ctx, err)257 return nil, connectError(ctx, err)
199 }258 }
259+ verdict, err := verdictFromPB(req.Msg.GetVerdict())
260+ if err != nil {
261+ return nil, connectError(ctx, err)
262+ }
263+ replyTo := commentID(req.Msg.GetReplyTo())
264+ if req.Msg.GetReplyTo() != "" && replyTo == 0 {
265+ var v domain.Violations
266+ v.Add("reply_to", "is not a note id")
267+ return nil, connectError(ctx, v.Err())
268+ }
200 c, err := s.mrs.AddComment(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.AddCommentParams{269 c, err := s.mrs.AddComment(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.AddCommentParams{
270+ ReplyTo: replyTo, StartLine: int(req.Msg.GetStartLine()), Verdict: verdict,
201 Path: req.Msg.GetPath(), Line: int(req.Msg.GetLine()), Side: side, Text: req.Msg.GetText(),271 Path: req.Msg.GetPath(), Line: int(req.Msg.GetLine()), Side: side, Text: req.Msg.GetText(),
202 })272 })
203 if err != nil {273 if err != nil {
A internal/adapters/rpc/mr_api_test.go +124 -0
@@ -0,0 +1,124 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package rpc
4+
5+import (
6+ "testing"
7+ "time"
8+
9+ pb "bnema.dev/ermite-api/gen/ermite"
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ "bnema.dev/ermite/internal/ports/mocks"
13+ "connectrpc.com/connect"
14+ "github.com/stretchr/testify/assert"
15+ "github.com/stretchr/testify/mock"
16+ "github.com/stretchr/testify/require"
17+)
18+
19+func TestNoteRPCFields(t *testing.T) {
20+ now := time.Now().UTC()
21+ note := domain.Comment{ID: 4, ReplyTo: 2, StartLine: 10, Line: 24, Verdict: domain.VerdictApprove, EditedAt: now, Deleted: true}
22+ got := commentToPB(note)
23+ assert.Equal(t, "2", got.GetReplyTo())
24+ assert.EqualValues(t, 10, got.GetStartLine())
25+ assert.Equal(t, pb.Verdict_VERDICT_APPROVE, got.GetVerdict())
26+ assert.Equal(t, now, got.GetEditedAt().AsTime())
27+ assert.True(t, got.GetDeleted())
28+ assert.Empty(t, commentToPB(domain.Comment{}).GetReplyTo())
29+ assert.Nil(t, commentToPB(domain.Comment{}).GetEditedAt())
30+ for _, v := range []domain.Verdict{domain.VerdictNone, domain.VerdictApprove, domain.VerdictChangesRequested} {
31+ back, err := verdictFromPB(verdictToPB(v))
32+ require.NoError(t, err)
33+ assert.Equal(t, v, back)
34+ }
35+ m := mocks.NewMergeRequestService(t)
36+ m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{Text: "note", StartLine: 10, Line: 24, Verdict: domain.VerdictApprove}).Return(note, nil).Once()
37+ res, err := (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Text: "note", StartLine: 10, Line: 24, Verdict: pb.Verdict_VERDICT_APPROVE}))
38+ require.NoError(t, err)
39+ assert.Equal(t, got, res.Msg.GetComment())
40+ _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Verdict: pb.Verdict(99)}))
41+ requireCode(t, connect.CodeInvalidArgument, err)
42+}
43+
44+func TestNoteMutationRPCCodes(t *testing.T) {
45+ for _, edit := range []bool{false, true} {
46+ for _, failure := range []error{nil, domain.ErrForbidden, domain.ErrPreconditionFailed, domain.ErrNotFound} {
47+ m := mocks.NewMergeRequestService(t)
48+ note := domain.Comment{ID: 2, Deleted: !edit}
49+ var err error
50+ if edit {
51+ m.EXPECT().EditComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(2), "new").Return(note, failure).Once()
52+ _, err = (&mergeRequestService{m}).EditComment(ownerCtx(), connect.NewRequest(&pb.EditCommentRequest{Repo: "demo", Number: 1, CommentId: "2", Text: "new"}))
53+ } else {
54+ m.EXPECT().DeleteComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(2)).Return(note, failure).Once()
55+ _, err = (&mergeRequestService{m}).DeleteComment(ownerCtx(), connect.NewRequest(&pb.DeleteCommentRequest{Repo: "demo", Number: 1, CommentId: "2"}))
56+ }
57+ switch failure {
58+ case nil:
59+ require.NoError(t, err)
60+ case domain.ErrForbidden:
61+ requireCode(t, connect.CodePermissionDenied, err)
62+ case domain.ErrPreconditionFailed:
63+ requireCode(t, connect.CodeFailedPrecondition, err)
64+ case domain.ErrNotFound:
65+ requireCode(t, connect.CodeNotFound, err)
66+ }
67+ }
68+ }
69+}
70+
71+func TestNoteEventsRPC(t *testing.T) {
72+ repo := mustRepoName(t, "demo")
73+ note := domain.Comment{ID: 3, Verdict: domain.VerdictApprove, Deleted: true}
74+ at := time.Now().UTC()
75+ events := []domain.Event{
76+ domain.CommentEdited{Repo: repo, Number: 1, Comment: note, At: at},
77+ domain.CommentDeleted{Repo: repo, Number: 1, Comment: note, At: at},
78+ domain.CommentResolved{Repo: repo, Number: 1, Comment: note, At: at},
79+ }
80+ for i, ev := range events {
81+ got := watchEventToPB(ports.WatchEvent{ID: "e", Event: ev})
82+ require.NotNil(t, got)
83+ assert.Equal(t, at, got.GetTime().AsTime())
84+ var c *pb.Comment
85+ switch i {
86+ case 0:
87+ c = got.GetCommentEdited().GetComment()
88+ case 1:
89+ c = got.GetCommentDeleted().GetComment()
90+ case 2:
91+ c = got.GetCommentResolved().GetComment()
92+ }
93+ assert.Equal(t, "3", c.GetId())
94+ assert.True(t, c.GetDeleted())
95+ assert.Equal(t, pb.Verdict_VERDICT_APPROVE, c.GetVerdict())
96+ }
97+}
98+
99+func TestNoteReplyIDValidation(t *testing.T) {
100+ for _, id := range []string{"abc", "0", "-1", "01", "9223372036854775808"} {
101+ _, err := (&mergeRequestService{mocks.NewMergeRequestService(t)}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, ReplyTo: id, Text: "reply"}))
102+ requireCode(t, connect.CodeInvalidArgument, err)
103+ assert.Contains(t, err.Error(), "reply_to")
104+ assert.Contains(t, err.Error(), "is not a note id")
105+ }
106+ m := mocks.NewMergeRequestService(t)
107+ m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{ReplyTo: 2, Text: "reply"}).Return(domain.Comment{ID: 3, ReplyTo: 2}, nil).Once()
108+ res, err := (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, ReplyTo: "2", Text: "reply"}))
109+ require.NoError(t, err)
110+ assert.Equal(t, "2", res.Msg.GetComment().GetReplyTo())
111+}
112+
113+func TestNoteVerdictsDetailAndScope(t *testing.T) {
114+ m := mocks.NewMergeRequestService(t)
115+ m.EXPECT().Get(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.GetMergeRequestOptions{}).Return(ports.MergeRequestDetail{Verdicts: []domain.AuthorVerdict{{Author: "owner", Verdict: domain.VerdictApprove, CommentID: 4, Stale: true}}}, nil).Once()
116+ res, err := (&mergeRequestService{m}).GetMergeRequest(ownerCtx(), connect.NewRequest(&pb.GetMergeRequestRequest{Repo: "demo", Number: 1}))
117+ require.NoError(t, err)
118+ require.Len(t, res.Msg.GetVerdicts(), 1)
119+ assert.Equal(t, "4", res.Msg.GetVerdicts()[0].GetCommentId())
120+ assert.True(t, res.Msg.GetVerdicts()[0].GetStale())
121+ m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{Verdict: domain.VerdictApprove}).Return(domain.Comment{}, domain.ErrForbidden).Once()
122+ _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))
123+ requireCode(t, connect.CodePermissionDenied, err)
124+}
@@ -0,0 +1,124 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package rpc
4+
5+import (
6+ "testing"
7+ "time"
8+
9+ pb "bnema.dev/ermite-api/gen/ermite"
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ "bnema.dev/ermite/internal/ports/mocks"
13+ "connectrpc.com/connect"
14+ "github.com/stretchr/testify/assert"
15+ "github.com/stretchr/testify/mock"
16+ "github.com/stretchr/testify/require"
17+)
18+
19+func TestNoteRPCFields(t *testing.T) {
20+ now := time.Now().UTC()
21+ note := domain.Comment{ID: 4, ReplyTo: 2, StartLine: 10, Line: 24, Verdict: domain.VerdictApprove, EditedAt: now, Deleted: true}
22+ got := commentToPB(note)
23+ assert.Equal(t, "2", got.GetReplyTo())
24+ assert.EqualValues(t, 10, got.GetStartLine())
25+ assert.Equal(t, pb.Verdict_VERDICT_APPROVE, got.GetVerdict())
26+ assert.Equal(t, now, got.GetEditedAt().AsTime())
27+ assert.True(t, got.GetDeleted())
28+ assert.Empty(t, commentToPB(domain.Comment{}).GetReplyTo())
29+ assert.Nil(t, commentToPB(domain.Comment{}).GetEditedAt())
30+ for _, v := range []domain.Verdict{domain.VerdictNone, domain.VerdictApprove, domain.VerdictChangesRequested} {
31+ back, err := verdictFromPB(verdictToPB(v))
32+ require.NoError(t, err)
33+ assert.Equal(t, v, back)
34+ }
35+ m := mocks.NewMergeRequestService(t)
36+ m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{Text: "note", StartLine: 10, Line: 24, Verdict: domain.VerdictApprove}).Return(note, nil).Once()
37+ res, err := (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Text: "note", StartLine: 10, Line: 24, Verdict: pb.Verdict_VERDICT_APPROVE}))
38+ require.NoError(t, err)
39+ assert.Equal(t, got, res.Msg.GetComment())
40+ _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Verdict: pb.Verdict(99)}))
41+ requireCode(t, connect.CodeInvalidArgument, err)
42+}
43+
44+func TestNoteMutationRPCCodes(t *testing.T) {
45+ for _, edit := range []bool{false, true} {
46+ for _, failure := range []error{nil, domain.ErrForbidden, domain.ErrPreconditionFailed, domain.ErrNotFound} {
47+ m := mocks.NewMergeRequestService(t)
48+ note := domain.Comment{ID: 2, Deleted: !edit}
49+ var err error
50+ if edit {
51+ m.EXPECT().EditComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(2), "new").Return(note, failure).Once()
52+ _, err = (&mergeRequestService{m}).EditComment(ownerCtx(), connect.NewRequest(&pb.EditCommentRequest{Repo: "demo", Number: 1, CommentId: "2", Text: "new"}))
53+ } else {
54+ m.EXPECT().DeleteComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), int64(2)).Return(note, failure).Once()
55+ _, err = (&mergeRequestService{m}).DeleteComment(ownerCtx(), connect.NewRequest(&pb.DeleteCommentRequest{Repo: "demo", Number: 1, CommentId: "2"}))
56+ }
57+ switch failure {
58+ case nil:
59+ require.NoError(t, err)
60+ case domain.ErrForbidden:
61+ requireCode(t, connect.CodePermissionDenied, err)
62+ case domain.ErrPreconditionFailed:
63+ requireCode(t, connect.CodeFailedPrecondition, err)
64+ case domain.ErrNotFound:
65+ requireCode(t, connect.CodeNotFound, err)
66+ }
67+ }
68+ }
69+}
70+
71+func TestNoteEventsRPC(t *testing.T) {
72+ repo := mustRepoName(t, "demo")
73+ note := domain.Comment{ID: 3, Verdict: domain.VerdictApprove, Deleted: true}
74+ at := time.Now().UTC()
75+ events := []domain.Event{
76+ domain.CommentEdited{Repo: repo, Number: 1, Comment: note, At: at},
77+ domain.CommentDeleted{Repo: repo, Number: 1, Comment: note, At: at},
78+ domain.CommentResolved{Repo: repo, Number: 1, Comment: note, At: at},
79+ }
80+ for i, ev := range events {
81+ got := watchEventToPB(ports.WatchEvent{ID: "e", Event: ev})
82+ require.NotNil(t, got)
83+ assert.Equal(t, at, got.GetTime().AsTime())
84+ var c *pb.Comment
85+ switch i {
86+ case 0:
87+ c = got.GetCommentEdited().GetComment()
88+ case 1:
89+ c = got.GetCommentDeleted().GetComment()
90+ case 2:
91+ c = got.GetCommentResolved().GetComment()
92+ }
93+ assert.Equal(t, "3", c.GetId())
94+ assert.True(t, c.GetDeleted())
95+ assert.Equal(t, pb.Verdict_VERDICT_APPROVE, c.GetVerdict())
96+ }
97+}
98+
99+func TestNoteReplyIDValidation(t *testing.T) {
100+ for _, id := range []string{"abc", "0", "-1", "01", "9223372036854775808"} {
101+ _, err := (&mergeRequestService{mocks.NewMergeRequestService(t)}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, ReplyTo: id, Text: "reply"}))
102+ requireCode(t, connect.CodeInvalidArgument, err)
103+ assert.Contains(t, err.Error(), "reply_to")
104+ assert.Contains(t, err.Error(), "is not a note id")
105+ }
106+ m := mocks.NewMergeRequestService(t)
107+ m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{ReplyTo: 2, Text: "reply"}).Return(domain.Comment{ID: 3, ReplyTo: 2}, nil).Once()
108+ res, err := (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, ReplyTo: "2", Text: "reply"}))
109+ require.NoError(t, err)
110+ assert.Equal(t, "2", res.Msg.GetComment().GetReplyTo())
111+}
112+
113+func TestNoteVerdictsDetailAndScope(t *testing.T) {
114+ m := mocks.NewMergeRequestService(t)
115+ m.EXPECT().Get(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.GetMergeRequestOptions{}).Return(ports.MergeRequestDetail{Verdicts: []domain.AuthorVerdict{{Author: "owner", Verdict: domain.VerdictApprove, CommentID: 4, Stale: true}}}, nil).Once()
116+ res, err := (&mergeRequestService{m}).GetMergeRequest(ownerCtx(), connect.NewRequest(&pb.GetMergeRequestRequest{Repo: "demo", Number: 1}))
117+ require.NoError(t, err)
118+ require.Len(t, res.Msg.GetVerdicts(), 1)
119+ assert.Equal(t, "4", res.Msg.GetVerdicts()[0].GetCommentId())
120+ assert.True(t, res.Msg.GetVerdicts()[0].GetStale())
121+ m.EXPECT().AddComment(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.AddCommentParams{Verdict: domain.VerdictApprove}).Return(domain.Comment{}, domain.ErrForbidden).Once()
122+ _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))
123+ requireCode(t, connect.CodePermissionDenied, err)
124+}
M internal/adapters/web/diff.go +1 -0
@@ -30,6 +30,7 @@ type Seg struct {
3030 // DiffLine is one line of a hunk, in the unified view and, paired, in the
3131 // split view.
3232 type DiffLine struct {
33+ Selected bool
3334 Kind string // "ctx", "add", "del"
3435 Mark string // " ", "+", "-": every change keeps its marker, not only a colour
3536 Old, New string // line numbers, "" when the side has none
@@ -30,6 +30,7 @@ type Seg struct {
30 // DiffLine is one line of a hunk, in the unified view and, paired, in the30 // DiffLine is one line of a hunk, in the unified view and, paired, in the
31 // split view.31 // split view.
32 type DiffLine struct {32 type DiffLine struct {
33+ Selected bool
33 Kind string // "ctx", "add", "del"34 Kind string // "ctx", "add", "del"
34 Mark string // " ", "+", "-": every change keeps its marker, not only a colour35 Mark string // " ", "+", "-": every change keeps its marker, not only a colour
35 Old, New string // line numbers, "" when the side has none36 Old, New string // line numbers, "" when the side has none
M internal/adapters/web/diff.templ +4 -4
@@ -232,7 +232,7 @@ templ DiffFileView(f DiffFile, p DiffView) {
232232 }
233233 <tr class="hunk"><td colspan="4">{ h.Header }</td></tr>
234234 for _, ln := range h.Lines {
235- <tr class={ ln.Kind }>
235+ <tr class={ ln.Kind, templ.KV("sel", ln.Selected) }>
236236 <td class="ln">@gutter(ln.Old, ln.OldHref)</td>
237237 <td class="ln">@gutter(ln.New, ln.NewHref)</td>
238238 <td class="m">{ ln.Mark }</td>
@@ -369,15 +369,15 @@ templ splitSide(ln *DiffLine, old bool) {
369369 <td class="m none"></td>
370370 <td class="tx none"></td>
371371 } else {
372- <td class={ "ln", ln.Kind }>
372+ <td class={ "ln", ln.Kind, templ.KV("sel", ln.Selected) }>
373373 if old {
374374 @gutter(ln.Old, ln.OldHref)
375375 } else {
376376 @gutter(ln.New, ln.NewHref)
377377 }
378378 </td>
379- <td class={ "m", ln.Kind }>{ ln.Mark }</td>
380- <td class={ "tx", "chroma", ln.Kind }>@codeLine(*ln)</td>
379+ <td class={ "m", ln.Kind, templ.KV("sel", ln.Selected) }>{ ln.Mark }</td>
380+ <td class={ "tx", "chroma", ln.Kind, templ.KV("sel", ln.Selected) }>@codeLine(*ln)</td>
381381 }
382382 }
383383
@@ -232,7 +232,7 @@ templ DiffFileView(f DiffFile, p DiffView) {
232 }232 }
233 <tr class="hunk"><td colspan="4">{ h.Header }</td></tr>233 <tr class="hunk"><td colspan="4">{ h.Header }</td></tr>
234 for _, ln := range h.Lines {234 for _, ln := range h.Lines {
235- <tr class={ ln.Kind }>235+ <tr class={ ln.Kind, templ.KV("sel", ln.Selected) }>
236 <td class="ln">@gutter(ln.Old, ln.OldHref)</td>236 <td class="ln">@gutter(ln.Old, ln.OldHref)</td>
237 <td class="ln">@gutter(ln.New, ln.NewHref)</td>237 <td class="ln">@gutter(ln.New, ln.NewHref)</td>
238 <td class="m">{ ln.Mark }</td>238 <td class="m">{ ln.Mark }</td>
@@ -369,15 +369,15 @@ templ splitSide(ln *DiffLine, old bool) {
369 <td class="m none"></td>369 <td class="m none"></td>
370 <td class="tx none"></td>370 <td class="tx none"></td>
371 } else {371 } else {
372- <td class={ "ln", ln.Kind }>372+ <td class={ "ln", ln.Kind, templ.KV("sel", ln.Selected) }>
373 if old {373 if old {
374 @gutter(ln.Old, ln.OldHref)374 @gutter(ln.Old, ln.OldHref)
375 } else {375 } else {
376 @gutter(ln.New, ln.NewHref)376 @gutter(ln.New, ln.NewHref)
377 }377 }
378 </td>378 </td>
379- <td class={ "m", ln.Kind }>{ ln.Mark }</td>379+ <td class={ "m", ln.Kind, templ.KV("sel", ln.Selected) }>{ ln.Mark }</td>
380- <td class={ "tx", "chroma", ln.Kind }>@codeLine(*ln)</td>380+ <td class={ "tx", "chroma", ln.Kind, templ.KV("sel", ln.Selected) }>@codeLine(*ln)</td>
381 }381 }
382 }382 }
383 383
M internal/adapters/web/diff_templ.go +5 -5 generated file: collapsed
M internal/adapters/web/fragments.go +7 -1
@@ -58,7 +58,7 @@ func liveEvent(we ports.WatchEvent) bool {
5858 }
5959 switch we.Event.(type) {
6060 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
61- domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded,
61+ domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
6262 domain.RunUpdated, domain.JobUpdated:
6363 return true
6464 }
@@ -164,6 +164,12 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
164164 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
165165 case domain.MergeRequestReady:
166166 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")
167+ case domain.CommentEdited:
168+ return l.mrNotice(ctx, ev.Number, "a note was edited — reload")
169+ case domain.CommentDeleted:
170+ return l.mrNotice(ctx, ev.Number, "a note was deleted — reload")
171+ case domain.CommentResolved:
172+ return l.mrNotice(ctx, ev.Number, "a thread was resolved — reload")
167173 case domain.CommentAdded:
168174 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")
169175 case domain.PushEvent:
@@ -58,7 +58,7 @@ func liveEvent(we ports.WatchEvent) bool {
58 }58 }
59 switch we.Event.(type) {59 switch we.Event.(type) {
60 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,60 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
61- domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded,61+ domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
62 domain.RunUpdated, domain.JobUpdated:62 domain.RunUpdated, domain.JobUpdated:
63 return true63 return true
64 }64 }
@@ -164,6 +164,12 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
164 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")164 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
165 case domain.MergeRequestReady:165 case domain.MergeRequestReady:
166 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")166 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")
167+ case domain.CommentEdited:
168+ return l.mrNotice(ctx, ev.Number, "a note was edited — reload")
169+ case domain.CommentDeleted:
170+ return l.mrNotice(ctx, ev.Number, "a note was deleted — reload")
171+ case domain.CommentResolved:
172+ return l.mrNotice(ctx, ev.Number, "a thread was resolved — reload")
167 case domain.CommentAdded:173 case domain.CommentAdded:
168 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")174 return l.mrNotice(ctx, ev.Number, "a comment was added — reload")
169 case domain.PushEvent:175 case domain.PushEvent:
M internal/adapters/web/jstest/mr.test.mjs +56 -0
@@ -36,3 +36,59 @@ test('step: stays in the list', () => {
3636 assert.equal(C.step(-1, -1, 3), 2);
3737 assert.equal(C.step(0, 1, 0), -1);
3838 });
39+
40+test('rangeURL requires the same file, side and MR and sorts endpoints', () => {
41+ const base = 'https://forge.test/demo/merge-requests/1';
42+ const a = base+'?on=new:24:dir%2Fa.go#f1';
43+ const b = base+'?from=25&on=new:10:dir%2Fa.go#f1';
44+ const u = new URL(C.rangeURL(a,b,base));
45+ assert.equal(u.searchParams.get('on'),'new:10-24:dir/a.go');
46+ assert.equal(u.searchParams.get('from'),'25');
47+ assert.equal(u.hash,'#f1');
48+ assert.equal(C.rangeURL(a,a,base),null);
49+ assert.equal(C.rangeURL(a,b.replace('new:','old:'),base),null);
50+ assert.equal(C.rangeURL(a,b.replace('a.go','b.go'),base),null);
51+ assert.equal(C.rangeURL(a,b.replace('/1?','/2?'),base),null);
52+});
53+
54+test('quote opens the thread reply and prefills and focuses its textarea', () => {
55+ let focused=false;
56+ const field={value:'',focus(){focused=true;}};
57+ const details={open:false,querySelector(){return field;}};
58+ const thread={querySelector(){return details;}};
59+ const button={dataset:{quote:'> source\n\n^2 '},closest(){return {closest(){return thread;}};}};
60+ assert.equal(C.quote(button),true);
61+ assert.equal(details.open,true);
62+ assert.equal(field.value,'> source\n\n^2 ');
63+ assert.equal(focused,true);
64+});
65+
66+for (const dataset of [{diff:'split'}, {view:'split'}]) test(`reveal selects the split note (${JSON.stringify(dataset)}), including after lazy loading`, async () => {
67+ const saved = globalThis.document;
68+ const savedRAF = globalThis.requestAnimationFrame;
69+ let scrolled = '';
70+ const details = {open:false};
71+ const node = id => ({id, querySelector(){return null;}, closest(){return details;}, scrollIntoView(){scrolled=id;}});
72+ const nodes = new Map([['c12',node('c12')],['c12-s',node('c12-s')]]);
73+ const ph = {dataset:{notes:'c12'}};
74+ globalThis.document = {
75+ querySelector: sel => sel === '#diffs' ? {dataset} : null,
76+ querySelectorAll: () => [ph],
77+ getElementById: id => nodes.get(id),
78+ };
79+ globalThis.requestAnimationFrame = fn => fn();
80+ try {
81+ assert.equal(window.ermiteMR.reveal('c12'),false);
82+ assert.equal(scrolled,'c12-s');
83+ assert.equal(details.open,true);
84+ nodes.clear(); scrolled=''; details.open=false;
85+ assert.equal(window.ermiteMR.reveal('c12'),true);
86+ nodes.set('c12',node('c12')); nodes.set('c12-s',node('c12-s'));
87+ await Promise.resolve();
88+ assert.equal(scrolled,'c12-s');
89+ assert.equal(details.open,true);
90+ } finally {
91+ globalThis.document=saved;
92+ globalThis.requestAnimationFrame=savedRAF;
93+ }
94+});
@@ -36,3 +36,59 @@ test('step: stays in the list', () => {
36 assert.equal(C.step(-1, -1, 3), 2);36 assert.equal(C.step(-1, -1, 3), 2);
37 assert.equal(C.step(0, 1, 0), -1);37 assert.equal(C.step(0, 1, 0), -1);
38 });38 });
39+
40+test('rangeURL requires the same file, side and MR and sorts endpoints', () => {
41+ const base = 'https://forge.test/demo/merge-requests/1';
42+ const a = base+'?on=new:24:dir%2Fa.go#f1';
43+ const b = base+'?from=25&on=new:10:dir%2Fa.go#f1';
44+ const u = new URL(C.rangeURL(a,b,base));
45+ assert.equal(u.searchParams.get('on'),'new:10-24:dir/a.go');
46+ assert.equal(u.searchParams.get('from'),'25');
47+ assert.equal(u.hash,'#f1');
48+ assert.equal(C.rangeURL(a,a,base),null);
49+ assert.equal(C.rangeURL(a,b.replace('new:','old:'),base),null);
50+ assert.equal(C.rangeURL(a,b.replace('a.go','b.go'),base),null);
51+ assert.equal(C.rangeURL(a,b.replace('/1?','/2?'),base),null);
52+});
53+
54+test('quote opens the thread reply and prefills and focuses its textarea', () => {
55+ let focused=false;
56+ const field={value:'',focus(){focused=true;}};
57+ const details={open:false,querySelector(){return field;}};
58+ const thread={querySelector(){return details;}};
59+ const button={dataset:{quote:'> source\n\n^2 '},closest(){return {closest(){return thread;}};}};
60+ assert.equal(C.quote(button),true);
61+ assert.equal(details.open,true);
62+ assert.equal(field.value,'> source\n\n^2 ');
63+ assert.equal(focused,true);
64+});
65+
66+for (const dataset of [{diff:'split'}, {view:'split'}]) test(`reveal selects the split note (${JSON.stringify(dataset)}), including after lazy loading`, async () => {
67+ const saved = globalThis.document;
68+ const savedRAF = globalThis.requestAnimationFrame;
69+ let scrolled = '';
70+ const details = {open:false};
71+ const node = id => ({id, querySelector(){return null;}, closest(){return details;}, scrollIntoView(){scrolled=id;}});
72+ const nodes = new Map([['c12',node('c12')],['c12-s',node('c12-s')]]);
73+ const ph = {dataset:{notes:'c12'}};
74+ globalThis.document = {
75+ querySelector: sel => sel === '#diffs' ? {dataset} : null,
76+ querySelectorAll: () => [ph],
77+ getElementById: id => nodes.get(id),
78+ };
79+ globalThis.requestAnimationFrame = fn => fn();
80+ try {
81+ assert.equal(window.ermiteMR.reveal('c12'),false);
82+ assert.equal(scrolled,'c12-s');
83+ assert.equal(details.open,true);
84+ nodes.clear(); scrolled=''; details.open=false;
85+ assert.equal(window.ermiteMR.reveal('c12'),true);
86+ nodes.set('c12',node('c12')); nodes.set('c12-s',node('c12-s'));
87+ await Promise.resolve();
88+ assert.equal(scrolled,'c12-s');
89+ assert.equal(details.open,true);
90+ } finally {
91+ globalThis.document=saved;
92+ globalThis.requestAnimationFrame=savedRAF;
93+ }
94+});
M internal/adapters/web/markdown.go +79 -0
@@ -7,6 +7,8 @@ import (
77 "html/template"
88 "net/url"
99 "path"
10+ "regexp"
11+ "strconv"
1012 "strings"
1113
1214 "github.com/yuin/goldmark"
@@ -62,10 +64,40 @@ func (m *markdown) render(source []byte, repo, ref, dir string) (template.HTML,
6264 // #fragment links chosen, so that several documents on one page (the summaries
6365 // of a run) do not share ids.
6466 func (m *markdown) renderWithIDs(source []byte, repo, ref, dir, idPrefix string) (template.HTML, []Heading, error) {
67+ return m.renderDocument(source, repo, ref, dir, idPrefix, nil)
68+}
69+
70+// renderNote uses the repository Markdown policy, then adds local note links.
71+func (m *markdown) renderNote(source []byte, repo, ref, idPrefix string, notes func(int64) bool) (template.HTML, error) {
72+ h, _, err := m.renderDocument(source, repo, ref, "", idPrefix, notes)
73+ return h, err
74+}
75+
76+func (m *markdown) renderDocument(source []byte, repo, ref, dir, idPrefix string, notes func(int64) bool) (template.HTML, []Heading, error) {
6577 pc := parser.NewContext(parser.WithIDs(&prefixedIDs{prefix: idPrefix, inner: parser.NewContext().IDs()}))
6678 doc := m.md.Parser().Parse(text.NewReader(source), parser.WithContext(pc))
6779 pol := linkPolicy{repo: repo, ref: ref, dir: dir, source: source, idPrefix: idPrefix}
80+ // Collect eligible text before the policy unwraps rejected links: their
81+ // labels must not accidentally become references either.
82+ var refs []*ast.Text
83+ if notes != nil {
84+ _ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
85+ if !entering {
86+ return ast.WalkContinue, nil
87+ }
88+ switch n := n.(type) {
89+ case *ast.CodeSpan, *ast.Link, *ast.AutoLink, *ast.Image, *ast.CodeBlock, *ast.FencedCodeBlock:
90+ return ast.WalkSkipChildren, nil
91+ case *ast.Text:
92+ refs = append(refs, n)
93+ }
94+ return ast.WalkContinue, nil
95+ })
96+ }
6897 pol.apply(doc)
98+ for _, t := range refs {
99+ noteReferences(t, source, notes)
100+ }
69101
70102 var buf bytes.Buffer
71103 if err := m.md.Renderer().Render(&buf, source, doc); err != nil {
@@ -291,3 +323,50 @@ func (altRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
291323 return ast.WalkContinue, nil
292324 })
293325 }
326+
327+var noteReferencePattern = regexp.MustCompile(`(^|[^0-9A-Za-z_^])\^([1-9][0-9]{0,5})\b`)
328+
329+// noteReferences changes only text segments; code and link labels never enter
330+// this function. New fragment links deliberately bypass heading-prefix rewriting.
331+func noteReferences(t *ast.Text, source []byte, exists func(int64) bool) {
332+ parent := t.Parent()
333+ if parent == nil {
334+ return
335+ }
336+ value := t.Value(source)
337+ offset := 0
338+ for _, match := range noteReferencePattern.FindAllSubmatchIndex(value, -1) {
339+ id := string(value[match[4]:match[5]])
340+ n, err := strconv.ParseInt(id, 10, 64)
341+ if err != nil || !exists(n) {
342+ continue
343+ }
344+ start := match[4] - 1
345+ // Markdown punctuation may split adjacent text into separate AST nodes.
346+ // Check the original preceding byte as well as the segment-local regex.
347+ if at := t.Segment.Start + start; at > 0 {
348+ prev := source[at-1]
349+ if prev >= '0' && prev <= '9' || prev >= 'A' && prev <= 'Z' || prev >= 'a' && prev <= 'z' || prev == '_' || prev == '^' {
350+ continue
351+ }
352+ }
353+ if start > offset {
354+ parent.InsertBefore(parent, t, ast.NewTextSegment(text.NewSegment(t.Segment.Start+offset, t.Segment.Start+start)))
355+ }
356+ link := ast.NewLink()
357+ link.Destination = []byte("#c" + id)
358+ link.Title = []byte("note " + id)
359+ link.SetAttributeString("class", []byte("ref"))
360+ link.AppendChild(link, ast.NewString(value[start:match[5]]))
361+ parent.InsertBefore(parent, t, link)
362+ offset = match[5]
363+ }
364+ if offset == 0 {
365+ return
366+ }
367+ tail := ast.NewTextSegment(text.NewSegment(t.Segment.Start+offset, t.Segment.Stop))
368+ tail.SetSoftLineBreak(t.SoftLineBreak())
369+ tail.SetHardLineBreak(t.HardLineBreak())
370+ parent.InsertBefore(parent, t, tail)
371+ parent.RemoveChild(parent, t)
372+}
@@ -7,6 +7,8 @@ import (
7 "html/template"7 "html/template"
8 "net/url"8 "net/url"
9 "path"9 "path"
10+ "regexp"
11+ "strconv"
10 "strings"12 "strings"
11 13
12 "github.com/yuin/goldmark"14 "github.com/yuin/goldmark"
@@ -62,10 +64,40 @@ func (m *markdown) render(source []byte, repo, ref, dir string) (template.HTML,
62 // #fragment links chosen, so that several documents on one page (the summaries64 // #fragment links chosen, so that several documents on one page (the summaries
63 // of a run) do not share ids.65 // of a run) do not share ids.
64 func (m *markdown) renderWithIDs(source []byte, repo, ref, dir, idPrefix string) (template.HTML, []Heading, error) {66 func (m *markdown) renderWithIDs(source []byte, repo, ref, dir, idPrefix string) (template.HTML, []Heading, error) {
67+ return m.renderDocument(source, repo, ref, dir, idPrefix, nil)
68+}
69+
70+// renderNote uses the repository Markdown policy, then adds local note links.
71+func (m *markdown) renderNote(source []byte, repo, ref, idPrefix string, notes func(int64) bool) (template.HTML, error) {
72+ h, _, err := m.renderDocument(source, repo, ref, "", idPrefix, notes)
73+ return h, err
74+}
75+
76+func (m *markdown) renderDocument(source []byte, repo, ref, dir, idPrefix string, notes func(int64) bool) (template.HTML, []Heading, error) {
65 pc := parser.NewContext(parser.WithIDs(&prefixedIDs{prefix: idPrefix, inner: parser.NewContext().IDs()}))77 pc := parser.NewContext(parser.WithIDs(&prefixedIDs{prefix: idPrefix, inner: parser.NewContext().IDs()}))
66 doc := m.md.Parser().Parse(text.NewReader(source), parser.WithContext(pc))78 doc := m.md.Parser().Parse(text.NewReader(source), parser.WithContext(pc))
67 pol := linkPolicy{repo: repo, ref: ref, dir: dir, source: source, idPrefix: idPrefix}79 pol := linkPolicy{repo: repo, ref: ref, dir: dir, source: source, idPrefix: idPrefix}
80+ // Collect eligible text before the policy unwraps rejected links: their
81+ // labels must not accidentally become references either.
82+ var refs []*ast.Text
83+ if notes != nil {
84+ _ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
85+ if !entering {
86+ return ast.WalkContinue, nil
87+ }
88+ switch n := n.(type) {
89+ case *ast.CodeSpan, *ast.Link, *ast.AutoLink, *ast.Image, *ast.CodeBlock, *ast.FencedCodeBlock:
90+ return ast.WalkSkipChildren, nil
91+ case *ast.Text:
92+ refs = append(refs, n)
93+ }
94+ return ast.WalkContinue, nil
95+ })
96+ }
68 pol.apply(doc)97 pol.apply(doc)
98+ for _, t := range refs {
99+ noteReferences(t, source, notes)
100+ }
69 101
70 var buf bytes.Buffer102 var buf bytes.Buffer
71 if err := m.md.Renderer().Render(&buf, source, doc); err != nil {103 if err := m.md.Renderer().Render(&buf, source, doc); err != nil {
@@ -291,3 +323,50 @@ func (altRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
291 return ast.WalkContinue, nil323 return ast.WalkContinue, nil
292 })324 })
293 }325 }
326+
327+var noteReferencePattern = regexp.MustCompile(`(^|[^0-9A-Za-z_^])\^([1-9][0-9]{0,5})\b`)
328+
329+// noteReferences changes only text segments; code and link labels never enter
330+// this function. New fragment links deliberately bypass heading-prefix rewriting.
331+func noteReferences(t *ast.Text, source []byte, exists func(int64) bool) {
332+ parent := t.Parent()
333+ if parent == nil {
334+ return
335+ }
336+ value := t.Value(source)
337+ offset := 0
338+ for _, match := range noteReferencePattern.FindAllSubmatchIndex(value, -1) {
339+ id := string(value[match[4]:match[5]])
340+ n, err := strconv.ParseInt(id, 10, 64)
341+ if err != nil || !exists(n) {
342+ continue
343+ }
344+ start := match[4] - 1
345+ // Markdown punctuation may split adjacent text into separate AST nodes.
346+ // Check the original preceding byte as well as the segment-local regex.
347+ if at := t.Segment.Start + start; at > 0 {
348+ prev := source[at-1]
349+ if prev >= '0' && prev <= '9' || prev >= 'A' && prev <= 'Z' || prev >= 'a' && prev <= 'z' || prev == '_' || prev == '^' {
350+ continue
351+ }
352+ }
353+ if start > offset {
354+ parent.InsertBefore(parent, t, ast.NewTextSegment(text.NewSegment(t.Segment.Start+offset, t.Segment.Start+start)))
355+ }
356+ link := ast.NewLink()
357+ link.Destination = []byte("#c" + id)
358+ link.Title = []byte("note " + id)
359+ link.SetAttributeString("class", []byte("ref"))
360+ link.AppendChild(link, ast.NewString(value[start:match[5]]))
361+ parent.InsertBefore(parent, t, link)
362+ offset = match[5]
363+ }
364+ if offset == 0 {
365+ return
366+ }
367+ tail := ast.NewTextSegment(text.NewSegment(t.Segment.Start+offset, t.Segment.Stop))
368+ tail.SetSoftLineBreak(t.SoftLineBreak())
369+ tail.SetHardLineBreak(t.HardLineBreak())
370+ parent.InsertBefore(parent, t, tail)
371+ parent.RemoveChild(parent, t)
372+}
M internal/adapters/web/markdown_test.go +29 -0
@@ -136,3 +136,32 @@ func TestMarkdownGFM(t *testing.T) {
136136 assert.Contains(t, out, "<del>gone</del>")
137137 assert.Contains(t, out, `type="checkbox"`)
138138 }
139+
140+func TestNoteMarkdown(t *testing.T) {
141+ t.Parallel()
142+ tests := []struct {
143+ source string
144+ links int
145+ }{
146+ {"^2 ^99 a^2 _^2 ^^2 1^2", 1},
147+ {"`^2` [^2](https://example.org) [^2](javascript:alert(1))", 0},
148+ {"```\n^2\n```", 0},
149+ {"^2, ^2\n^2", 3},
150+ }
151+ for _, tt := range tests {
152+ t.Run(tt.source, func(t *testing.T) {
153+ t.Parallel()
154+ h, err := newMarkdown().renderNote([]byte(tt.source), "demo", "main", "c1-", func(n int64) bool { return n == 2 })
155+ require.NoError(t, err)
156+ assert.Equal(t, tt.links, strings.Count(string(h), `href="#c2"`), string(h))
157+ assert.NotContains(t, string(h), `href="#c99"`)
158+ assert.NotContains(t, string(h), "javascript:")
159+ })
160+ }
161+ h, err := newMarkdown().renderNote([]byte("# Heading\n\n<script>alert(1)</script>\n\n**bold** ^2"), "demo", "main", "c1-", func(n int64) bool { return n == 2 })
162+ require.NoError(t, err)
163+ assert.Contains(t, string(h), `id="c1-heading"`)
164+ assert.Contains(t, string(h), "<strong>bold</strong>")
165+ assert.NotContains(t, string(h), "<script")
166+ assert.Contains(t, string(h), `title="note 2"`)
167+}
@@ -136,3 +136,32 @@ func TestMarkdownGFM(t *testing.T) {
136 assert.Contains(t, out, "<del>gone</del>")136 assert.Contains(t, out, "<del>gone</del>")
137 assert.Contains(t, out, `type="checkbox"`)137 assert.Contains(t, out, `type="checkbox"`)
138 }138 }
139+
140+func TestNoteMarkdown(t *testing.T) {
141+ t.Parallel()
142+ tests := []struct {
143+ source string
144+ links int
145+ }{
146+ {"^2 ^99 a^2 _^2 ^^2 1^2", 1},
147+ {"`^2` [^2](https://example.org) [^2](javascript:alert(1))", 0},
148+ {"```\n^2\n```", 0},
149+ {"^2, ^2\n^2", 3},
150+ }
151+ for _, tt := range tests {
152+ t.Run(tt.source, func(t *testing.T) {
153+ t.Parallel()
154+ h, err := newMarkdown().renderNote([]byte(tt.source), "demo", "main", "c1-", func(n int64) bool { return n == 2 })
155+ require.NoError(t, err)
156+ assert.Equal(t, tt.links, strings.Count(string(h), `href="#c2"`), string(h))
157+ assert.NotContains(t, string(h), `href="#c99"`)
158+ assert.NotContains(t, string(h), "javascript:")
159+ })
160+ }
161+ h, err := newMarkdown().renderNote([]byte("# Heading\n\n<script>alert(1)</script>\n\n**bold** ^2"), "demo", "main", "c1-", func(n int64) bool { return n == 2 })
162+ require.NoError(t, err)
163+ assert.Contains(t, string(h), `id="c1-heading"`)
164+ assert.Contains(t, string(h), "<strong>bold</strong>")
165+ assert.NotContains(t, string(h), "<script")
166+ assert.Contains(t, string(h), `title="note 2"`)
167+}
M internal/adapters/web/mr.go +129 -25
@@ -101,13 +101,19 @@ type MRListPage struct {
101101
102102 // CommentView is a comment of a merge request, ready to show.
103103 type CommentView struct {
104- ID string
105- Author string
106- When string
107- WhenAt string
108- Abs string
109- Parts []TextPart
110- Resolved bool
104+ ID string
105+ Author string
106+ When string
107+ WhenAt string
108+ Abs string
109+ Body, BodySplit template.HTML
110+ Replies []CommentView
111+ Edited, Deleted bool
112+ Verdict, Range, Quote string
113+ Edit *EditForm
114+ Delete *DeleteForm
115+ Reply *CommentForm
116+ Resolved bool
111117 // Outdated is true when the head moved since the comment was written, so
112118 // its line may have moved too.
113119 Outdated bool
@@ -119,6 +125,19 @@ type CommentView struct {
119125 Resolve *ResolveForm
120126 }
121127
128+type EditForm struct {
129+ Action, Text string
130+ From int
131+}
132+type DeleteForm struct {
133+ Action string
134+ From int
135+}
136+type VerdictView struct {
137+ Author, Label, Href, Short string
138+ Stale bool
139+}
140+
122141 // ResolveForm is the POST that changes the resolved state of a comment.
123142 type ResolveForm struct {
124143 Action string
@@ -135,9 +154,10 @@ type CommentForm struct {
135154 Action string
136155 Title string
137156 // Path, Line and Side designate the line; empty for a general comment.
138- Path, Line, Side string
139- Cancel string
140- Focus bool
157+ Path, Line, Side string
158+ StartLine, ReplyTo string
159+ Cancel string
160+ Focus bool
141161 // From is the window of the page the form is on (see mrWindow), sent
142162 // back so that the answer returns to it; 0 for the first.
143163 From int
@@ -192,6 +212,7 @@ type MRPage struct {
192212 State string
193213 Open bool
194214 Signed bool
215+ NoteAuthor string
195216 Source, Target string
196217 SourceHref string
197218 TargetHref string
@@ -226,6 +247,7 @@ type MRPage struct {
226247 NComments int
227248 NUnresolved int
228249 GeneralForm *CommentForm
250+ Verdicts []VerdictView
229251
230252 Commits []MRCommit
231253 CommitsNote string
@@ -471,7 +493,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
471493
472494 p := MRPage{
473495 Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(m.Title.String()),
474- State: mrStateName(m), Open: open, Signed: signed,
496+ State: mrStateName(m), Open: open, Signed: signed, NoteAuthor: domain.AuthorName(principal(ctx)),
475497 Source: visibleText(m.Source), Target: visibleText(m.Target),
476498 Author: s.who(m.Author),
477499 Head: head, HeadShort: short(head),
@@ -485,12 +507,15 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
485507 // Relative links point into the source branch as it is now (its head
486508 // commit); the heading ids carry their own prefix, apart from the
487509 // ids of the diff.
488- h, _, err := s.md.renderWithIDs([]byte(desc), d.name(), head, "", "mr-desc-")
510+ h, err := s.md.renderNote([]byte(desc), d.name(), head, "mr-desc-", noteIDs(res.Comments))
489511 if err != nil {
490512 return nil, 0, err
491513 }
492514 p.Desc = h
493515 }
516+ for _, v := range res.Verdicts {
517+ p.Verdicts = append(p.Verdicts, VerdictView{Author: s.who(v.Author), Label: v.Verdict.String(), Stale: v.Stale, Href: "#c" + strconv.FormatInt(v.CommentID, 10), Short: short(v.CommitID.String())})
518+ }
494519 p.Opened.Name = p.Author
495520 p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt)
496521 if !m.CreatedAt.IsZero() {
@@ -684,9 +709,10 @@ func shellWord(s string) string {
684709 // lineRef designates a line of the diff in the query of the comment form:
685710 // "?on=new:12:path/to/file".
686711 type lineRef struct {
687- side string
688- line int
689- path string
712+ side string
713+ line int
714+ start int
715+ path string
690716 }
691717
692718 func parseOn(s string) *lineRef {
@@ -695,11 +721,21 @@ func parseOn(s string) *lineRef {
695721 return nil
696722 }
697723 num, path, ok := strings.Cut(rest, ":")
724+ start := 0
725+ if a, b, rangeOK := strings.Cut(num, "-"); rangeOK {
726+ var e error
727+ start, e = strconv.Atoi(a)
728+ end, e2 := strconv.Atoi(b)
729+ if e != nil || e2 != nil || start < 1 || start >= end {
730+ return nil
731+ }
732+ num = b
733+ }
698734 line, err := strconv.Atoi(num)
699735 if !ok || err != nil || line < 1 || path == "" {
700736 return nil
701737 }
702- return &lineRef{side: side, line: line, path: path}
738+ return &lineRef{side: side, line: line, start: start, path: path}
703739 }
704740
705741 // placeComments puts every comment under its line when the diff shown has it,
@@ -757,21 +793,66 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
757793 return index[lineKey{path, side, line}]
758794 }
759795
796+ exists := noteIDs(comments)
797+ children := map[int64][]domain.Comment{}
760798 for _, c := range comments {
799+ if c.ReplyTo != 0 && !c.Deleted {
800+ children[c.ReplyTo] = append(children[c.ReplyTo], c)
801+ }
802+ if !c.Deleted {
803+ p.NComments++
804+ }
805+ if c.ReplyTo == 0 && !c.Deleted && !c.Resolved {
806+ p.NUnresolved++
807+ }
808+ }
809+ var view func(domain.Comment, bool) CommentView
810+ view = func(c domain.Comment, replyable bool) CommentView {
761811 cid := strconv.FormatInt(c.ID, 10)
762- v := CommentView{
763- ID: cid, Author: s.who(c.Author),
764- Parts: plainParts(c.Text.String()), Resolved: c.Resolved,
765- Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head,
812+ body, err := s.md.renderNote([]byte(c.Text.String()), repo, head, "c"+cid+"-", exists)
813+ if err != nil {
814+ body = ""
766815 }
816+ bodySplit, err := s.md.renderNote([]byte(c.Text.String()), repo, head, "c"+cid+"s-", exists)
817+ if err != nil {
818+ bodySplit = ""
819+ }
820+ v := CommentView{BodySplit: bodySplit, ID: cid, Author: s.who(c.Author), Body: body, Resolved: c.Resolved, Deleted: c.Deleted, Edited: !c.EditedAt.IsZero(), Verdict: c.Verdict.String(), Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head}
767821 v.When, v.WhenAt = whenOf(now, c.CreatedAt)
768822 if !c.CreatedAt.IsZero() {
769823 v.Abs = absolute(c.CreatedAt)
770824 }
771- p.NComments++
772- if !c.Resolved {
773- p.NUnresolved++
825+ if c.StartLine > 0 {
826+ v.Range = "lines " + itoa(c.StartLine) + "–" + itoa(c.Line)
774827 }
828+ if canWrite && !c.Deleted {
829+ from := windowStart(fileAt[c.Path])
830+ root := cid
831+ if c.ReplyTo != 0 {
832+ root = strconv.FormatInt(c.ReplyTo, 10)
833+ }
834+ if replyable {
835+ v.Quote = "> " + strings.ReplaceAll(c.Text.String(), "\n", "\n> ") + "\n\n^" + cid + " "
836+ }
837+ if c.ReplyTo == 0 {
838+ v.Reply = &CommentForm{ID: "reply-" + cid, Action: mrHref(repo, n, "comments"), Title: "Reply", ReplyTo: root, From: from}
839+ }
840+ if c.Author == p.NoteAuthor {
841+ v.Edit = &EditForm{Action: mrHref(repo, n, "comments", cid, "edit"), Text: c.Text.String(), From: from}
842+ v.Delete = &DeleteForm{Action: mrHref(repo, n, "comments", cid, "delete"), From: from}
843+ }
844+ }
845+ for _, reply := range children[c.ID] {
846+ v.Replies = append(v.Replies, view(reply, replyable))
847+ }
848+ return v
849+ }
850+ for _, c := range comments {
851+ if c.ReplyTo != 0 || c.Deleted && c.Verdict == domain.VerdictNone && len(children[c.ID]) == 0 {
852+ continue
853+ }
854+ cid := strconv.FormatInt(c.ID, 10)
855+ v := view(c, canWrite && !c.Deleted)
775856 general := c.Path == ""
776857 fi, listed := fileAt[c.Path]
777858 if !general && listed && p.Files[fi].Lazy {
@@ -789,7 +870,7 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
789870 v.Where = visibleText(c.Path) + ":" + itoa(c.Line) + " (" + side + ")"
790871 }
791872 }
792- if canWrite {
873+ if canWrite && !c.Deleted {
793874 v.Resolve = &ResolveForm{Action: resolveAction(cid), Value: !c.Resolved, Label: "Resolve"}
794875 if !general && listed {
795876 v.Resolve.From = windowStart(fi)
@@ -815,9 +896,18 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
815896 if on.side == "old" {
816897 side = domain.SideOld
817898 }
899+ start := on.start
900+ if start == 0 {
901+ start = on.line
902+ }
903+ for key, ln := range index {
904+ if key.path == on.path && key.side == side && key.line >= start && key.line <= on.line {
905+ ln.Selected = true
906+ }
907+ }
818908 if ln := find(on.path, side, on.line); ln != nil {
819909 ln.Form = &CommentForm{
820- ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side,
910+ StartLine: optionalLine(on.start), ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side,
821911 Title: "comment on " + visibleText(on.path) + ":" + itoa(on.line) + " (" + on.side + ")",
822912 Cancel: withFrom(self, windowStart(fileAt[on.path])) + "#" + fileIDs[on.path], Focus: true,
823913 From: windowStart(fileAt[on.path]),
@@ -842,3 +932,17 @@ func userText(parts []TextPart) templ.Component {
842932 return err
843933 })
844934 }
935+
936+func noteIDs(comments []domain.Comment) func(int64) bool {
937+ ids := make(map[int64]bool, len(comments))
938+ for _, c := range comments {
939+ ids[c.ID] = true
940+ }
941+ return func(n int64) bool { return ids[n] }
942+}
943+func optionalLine(n int) string {
944+ if n == 0 {
945+ return ""
946+ }
947+ return itoa(n)
948+}
@@ -101,13 +101,19 @@ type MRListPage struct {
101 101
102 // CommentView is a comment of a merge request, ready to show.102 // CommentView is a comment of a merge request, ready to show.
103 type CommentView struct {103 type CommentView struct {
104- ID string104+ ID string
105- Author string105+ Author string
106- When string106+ When string
107- WhenAt string107+ WhenAt string
108- Abs string108+ Abs string
109- Parts []TextPart109+ Body, BodySplit template.HTML
110- Resolved bool110+ Replies []CommentView
111+ Edited, Deleted bool
112+ Verdict, Range, Quote string
113+ Edit *EditForm
114+ Delete *DeleteForm
115+ Reply *CommentForm
116+ Resolved bool
111 // Outdated is true when the head moved since the comment was written, so117 // Outdated is true when the head moved since the comment was written, so
112 // its line may have moved too.118 // its line may have moved too.
113 Outdated bool119 Outdated bool
@@ -119,6 +125,19 @@ type CommentView struct {
119 Resolve *ResolveForm125 Resolve *ResolveForm
120 }126 }
121 127
128+type EditForm struct {
129+ Action, Text string
130+ From int
131+}
132+type DeleteForm struct {
133+ Action string
134+ From int
135+}
136+type VerdictView struct {
137+ Author, Label, Href, Short string
138+ Stale bool
139+}
140+
122 // ResolveForm is the POST that changes the resolved state of a comment.141 // ResolveForm is the POST that changes the resolved state of a comment.
123 type ResolveForm struct {142 type ResolveForm struct {
124 Action string143 Action string
@@ -135,9 +154,10 @@ type CommentForm struct {
135 Action string154 Action string
136 Title string155 Title string
137 // Path, Line and Side designate the line; empty for a general comment.156 // Path, Line and Side designate the line; empty for a general comment.
138- Path, Line, Side string157+ Path, Line, Side string
139- Cancel string158+ StartLine, ReplyTo string
140- Focus bool159+ Cancel string
160+ Focus bool
141 // From is the window of the page the form is on (see mrWindow), sent161 // From is the window of the page the form is on (see mrWindow), sent
142 // back so that the answer returns to it; 0 for the first.162 // back so that the answer returns to it; 0 for the first.
143 From int163 From int
@@ -192,6 +212,7 @@ type MRPage struct {
192 State string212 State string
193 Open bool213 Open bool
194 Signed bool214 Signed bool
215+ NoteAuthor string
195 Source, Target string216 Source, Target string
196 SourceHref string217 SourceHref string
197 TargetHref string218 TargetHref string
@@ -226,6 +247,7 @@ type MRPage struct {
226 NComments int247 NComments int
227 NUnresolved int248 NUnresolved int
228 GeneralForm *CommentForm249 GeneralForm *CommentForm
250+ Verdicts []VerdictView
229 251
230 Commits []MRCommit252 Commits []MRCommit
231 CommitsNote string253 CommitsNote string
@@ -471,7 +493,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
471 493
472 p := MRPage{494 p := MRPage{
473 Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(m.Title.String()),495 Number: "#" + strconv.FormatInt(n, 10), Title: visibleText(m.Title.String()),
474- State: mrStateName(m), Open: open, Signed: signed,496+ State: mrStateName(m), Open: open, Signed: signed, NoteAuthor: domain.AuthorName(principal(ctx)),
475 Source: visibleText(m.Source), Target: visibleText(m.Target),497 Source: visibleText(m.Source), Target: visibleText(m.Target),
476 Author: s.who(m.Author),498 Author: s.who(m.Author),
477 Head: head, HeadShort: short(head),499 Head: head, HeadShort: short(head),
@@ -485,12 +507,15 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ
485 // Relative links point into the source branch as it is now (its head507 // Relative links point into the source branch as it is now (its head
486 // commit); the heading ids carry their own prefix, apart from the508 // commit); the heading ids carry their own prefix, apart from the
487 // ids of the diff.509 // ids of the diff.
488- h, _, err := s.md.renderWithIDs([]byte(desc), d.name(), head, "", "mr-desc-")510+ h, err := s.md.renderNote([]byte(desc), d.name(), head, "mr-desc-", noteIDs(res.Comments))
489 if err != nil {511 if err != nil {
490 return nil, 0, err512 return nil, 0, err
491 }513 }
492 p.Desc = h514 p.Desc = h
493 }515 }
516+ for _, v := range res.Verdicts {
517+ p.Verdicts = append(p.Verdicts, VerdictView{Author: s.who(v.Author), Label: v.Verdict.String(), Stale: v.Stale, Href: "#c" + strconv.FormatInt(v.CommentID, 10), Short: short(v.CommitID.String())})
518+ }
494 p.Opened.Name = p.Author519 p.Opened.Name = p.Author
495 p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt)520 p.Opened.When, p.Opened.WhenAt = whenOf(now, m.CreatedAt)
496 if !m.CreatedAt.IsZero() {521 if !m.CreatedAt.IsZero() {
@@ -684,9 +709,10 @@ func shellWord(s string) string {
684 // lineRef designates a line of the diff in the query of the comment form:709 // lineRef designates a line of the diff in the query of the comment form:
685 // "?on=new:12:path/to/file".710 // "?on=new:12:path/to/file".
686 type lineRef struct {711 type lineRef struct {
687- side string712+ side string
688- line int713+ line int
689- path string714+ start int
715+ path string
690 }716 }
691 717
692 func parseOn(s string) *lineRef {718 func parseOn(s string) *lineRef {
@@ -695,11 +721,21 @@ func parseOn(s string) *lineRef {
695 return nil721 return nil
696 }722 }
697 num, path, ok := strings.Cut(rest, ":")723 num, path, ok := strings.Cut(rest, ":")
724+ start := 0
725+ if a, b, rangeOK := strings.Cut(num, "-"); rangeOK {
726+ var e error
727+ start, e = strconv.Atoi(a)
728+ end, e2 := strconv.Atoi(b)
729+ if e != nil || e2 != nil || start < 1 || start >= end {
730+ return nil
731+ }
732+ num = b
733+ }
698 line, err := strconv.Atoi(num)734 line, err := strconv.Atoi(num)
699 if !ok || err != nil || line < 1 || path == "" {735 if !ok || err != nil || line < 1 || path == "" {
700 return nil736 return nil
701 }737 }
702- return &lineRef{side: side, line: line, path: path}738+ return &lineRef{side: side, line: line, start: start, path: path}
703 }739 }
704 740
705 // placeComments puts every comment under its line when the diff shown has it,741 // placeComments puts every comment under its line when the diff shown has it,
@@ -757,21 +793,66 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
757 return index[lineKey{path, side, line}]793 return index[lineKey{path, side, line}]
758 }794 }
759 795
796+ exists := noteIDs(comments)
797+ children := map[int64][]domain.Comment{}
760 for _, c := range comments {798 for _, c := range comments {
799+ if c.ReplyTo != 0 && !c.Deleted {
800+ children[c.ReplyTo] = append(children[c.ReplyTo], c)
801+ }
802+ if !c.Deleted {
803+ p.NComments++
804+ }
805+ if c.ReplyTo == 0 && !c.Deleted && !c.Resolved {
806+ p.NUnresolved++
807+ }
808+ }
809+ var view func(domain.Comment, bool) CommentView
810+ view = func(c domain.Comment, replyable bool) CommentView {
761 cid := strconv.FormatInt(c.ID, 10)811 cid := strconv.FormatInt(c.ID, 10)
762- v := CommentView{812+ body, err := s.md.renderNote([]byte(c.Text.String()), repo, head, "c"+cid+"-", exists)
763- ID: cid, Author: s.who(c.Author),813+ if err != nil {
764- Parts: plainParts(c.Text.String()), Resolved: c.Resolved,814+ body = ""
765- Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head,
766 }815 }
816+ bodySplit, err := s.md.renderNote([]byte(c.Text.String()), repo, head, "c"+cid+"s-", exists)
817+ if err != nil {
818+ bodySplit = ""
819+ }
820+ v := CommentView{BodySplit: bodySplit, ID: cid, Author: s.who(c.Author), Body: body, Resolved: c.Resolved, Deleted: c.Deleted, Edited: !c.EditedAt.IsZero(), Verdict: c.Verdict.String(), Outdated: !c.CommitID.IsZero() && c.CommitID.String() != head}
767 v.When, v.WhenAt = whenOf(now, c.CreatedAt)821 v.When, v.WhenAt = whenOf(now, c.CreatedAt)
768 if !c.CreatedAt.IsZero() {822 if !c.CreatedAt.IsZero() {
769 v.Abs = absolute(c.CreatedAt)823 v.Abs = absolute(c.CreatedAt)
770 }824 }
771- p.NComments++825+ if c.StartLine > 0 {
772- if !c.Resolved {826+ v.Range = "lines " + itoa(c.StartLine) + "–" + itoa(c.Line)
773- p.NUnresolved++
774 }827 }
828+ if canWrite && !c.Deleted {
829+ from := windowStart(fileAt[c.Path])
830+ root := cid
831+ if c.ReplyTo != 0 {
832+ root = strconv.FormatInt(c.ReplyTo, 10)
833+ }
834+ if replyable {
835+ v.Quote = "> " + strings.ReplaceAll(c.Text.String(), "\n", "\n> ") + "\n\n^" + cid + " "
836+ }
837+ if c.ReplyTo == 0 {
838+ v.Reply = &CommentForm{ID: "reply-" + cid, Action: mrHref(repo, n, "comments"), Title: "Reply", ReplyTo: root, From: from}
839+ }
840+ if c.Author == p.NoteAuthor {
841+ v.Edit = &EditForm{Action: mrHref(repo, n, "comments", cid, "edit"), Text: c.Text.String(), From: from}
842+ v.Delete = &DeleteForm{Action: mrHref(repo, n, "comments", cid, "delete"), From: from}
843+ }
844+ }
845+ for _, reply := range children[c.ID] {
846+ v.Replies = append(v.Replies, view(reply, replyable))
847+ }
848+ return v
849+ }
850+ for _, c := range comments {
851+ if c.ReplyTo != 0 || c.Deleted && c.Verdict == domain.VerdictNone && len(children[c.ID]) == 0 {
852+ continue
853+ }
854+ cid := strconv.FormatInt(c.ID, 10)
855+ v := view(c, canWrite && !c.Deleted)
775 general := c.Path == ""856 general := c.Path == ""
776 fi, listed := fileAt[c.Path]857 fi, listed := fileAt[c.Path]
777 if !general && listed && p.Files[fi].Lazy {858 if !general && listed && p.Files[fi].Lazy {
@@ -789,7 +870,7 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
789 v.Where = visibleText(c.Path) + ":" + itoa(c.Line) + " (" + side + ")"870 v.Where = visibleText(c.Path) + ":" + itoa(c.Line) + " (" + side + ")"
790 }871 }
791 }872 }
792- if canWrite {873+ if canWrite && !c.Deleted {
793 v.Resolve = &ResolveForm{Action: resolveAction(cid), Value: !c.Resolved, Label: "Resolve"}874 v.Resolve = &ResolveForm{Action: resolveAction(cid), Value: !c.Resolved, Label: "Resolve"}
794 if !general && listed {875 if !general && listed {
795 v.Resolve.From = windowStart(fi)876 v.Resolve.From = windowStart(fi)
@@ -815,9 +896,18 @@ func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []domai
815 if on.side == "old" {896 if on.side == "old" {
816 side = domain.SideOld897 side = domain.SideOld
817 }898 }
899+ start := on.start
900+ if start == 0 {
901+ start = on.line
902+ }
903+ for key, ln := range index {
904+ if key.path == on.path && key.side == side && key.line >= start && key.line <= on.line {
905+ ln.Selected = true
906+ }
907+ }
818 if ln := find(on.path, side, on.line); ln != nil {908 if ln := find(on.path, side, on.line); ln != nil {
819 ln.Form = &CommentForm{909 ln.Form = &CommentForm{
820- ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side,910+ StartLine: optionalLine(on.start), ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side,
821 Title: "comment on " + visibleText(on.path) + ":" + itoa(on.line) + " (" + on.side + ")",911 Title: "comment on " + visibleText(on.path) + ":" + itoa(on.line) + " (" + on.side + ")",
822 Cancel: withFrom(self, windowStart(fileAt[on.path])) + "#" + fileIDs[on.path], Focus: true,912 Cancel: withFrom(self, windowStart(fileAt[on.path])) + "#" + fileIDs[on.path], Focus: true,
823 From: windowStart(fileAt[on.path]),913 From: windowStart(fileAt[on.path]),
@@ -842,3 +932,17 @@ func userText(parts []TextPart) templ.Component {
842 return err932 return err
843 })933 })
844 }934 }
935+
936+func noteIDs(comments []domain.Comment) func(int64) bool {
937+ ids := make(map[int64]bool, len(comments))
938+ for _, c := range comments {
939+ ids[c.ID] = true
940+ }
941+ return func(n int64) bool { return ids[n] }
942+}
943+func optionalLine(n int) string {
944+ if n == 0 {
945+ return ""
946+ }
947+ return itoa(n)
948+}
M internal/adapters/web/mr.templ +66 -6
@@ -229,10 +229,9 @@ templ lineNotes(notes []CommentView, form *CommentForm, suffix string) {
229229 }
230230
231231 // commentView is a note: a ruled block, under its line in the diff or in the
232-// list of comments. The text is plain: escaped, its line breaks kept, its
233-// http(s) addresses linked.
232+// list of comments. Body comes only from the safe Markdown renderer.
234233 templ commentView(c CommentView, suffix string) {
235- <div class="cmt" id={ "c" + c.ID + suffix } data-resolved={ boolText(c.Resolved) }>
234+ <div class="cmt" id={ "c" + c.ID + suffix } data-resolved={ boolText(c.Resolved) } data-thread?={ c.Reply != nil }>
236235 <p class="c-head">
237236 <span class="c-who">{ c.Author }</span>
238237 if c.WhenAt != "" {
@@ -248,7 +247,47 @@ templ commentView(c CommentView, suffix string) {
248247 { " " }<span class="c-where mute">{ c.Where }</span>
249248 }
250249 </p>
251- <p class="c-text">@userText(c.Parts)</p>
250+ if c.Deleted {
251+ if c.Verdict != "" {
252+ <p class="mute">text deleted</p>
253+ } else {
254+ <p class="mute">deleted note</p>
255+ }
256+ } else {
257+ <div class="c-body prose">
258+ if suffix == "-s" {
259+ @templ.Raw(string(c.BodySplit))
260+ } else {
261+ @templ.Raw(string(c.Body))
262+ }
263+ </div>
264+ }
265+ if c.Edited {
266+ <span class="mute">edited</span>
267+ }
268+ if c.Verdict != "" {
269+ <span class="c-state">{ c.Verdict }</span>
270+ }
271+ if c.Range != "" {
272+ <span class="mute">{ c.Range }</span>
273+ }
274+ if c.Quote != "" {
275+ <button type="button" class="btn" data-js data-quote={ c.Quote }>Quote</button>
276+ }
277+ if c.Edit != nil {
278+ <details><summary>Edit</summary><form method="post" action={ templ.SafeURL(c.Edit.Action) }><input type="hidden" name="from" value={ itoa(c.Edit.From) }/><textarea name="text" maxlength="8000">{ c.Edit.Text }</textarea><button class="btn" type="submit">Save</button></form></details>
279+ }
280+ if c.Delete != nil {
281+ <details><summary>Delete</summary><form method="post" action={ templ.SafeURL(c.Delete.Action) }><input type="hidden" name="from" value={ itoa(c.Delete.From) }/><p>Delete this note?</p><button class="btn" type="submit">Confirm delete</button></form></details>
282+ }
283+ for _, reply := range c.Replies {
284+ @commentView(reply, suffix)
285+ }
286+ if c.Reply != nil {
287+ <details data-reply>
288+ <summary>Reply</summary>@commentForm(*c.Reply, suffix)
289+ </details>
290+ }
252291 if c.Resolve != nil {
253292 <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }>
254293 <input type="hidden" name="resolved" value={ boolText(c.Resolve.Value) }/>
@@ -277,8 +316,7 @@ templ commentForm(f CommentForm, suffix string) {
277316 name="text"
278317 rows="3"
279318 maxlength="8000"
280- required
281- placeholder="plain text, 8000 characters at most"
319+ placeholder="Markdown, 8000 characters at most"
282320 if f.Focus {
283321 autofocus
284322 }
@@ -287,6 +325,13 @@ templ commentForm(f CommentForm, suffix string) {
287325 <input type="hidden" name="path" value={ f.Path }/>
288326 <input type="hidden" name="line" value={ f.Line }/>
289327 <input type="hidden" name="side" value={ f.Side }/>
328+ <label>from line <input type="number" name="start_line" min="1" value={ f.StartLine }/></label>
329+ }
330+ if f.ReplyTo != "" {
331+ <input type="hidden" name="reply_to" value={ f.ReplyTo }/>
332+ }
333+ if f.Path == "" && f.ReplyTo == "" {
334+ <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>
290335 }
291336 if f.From > 0 {
292337 <input type="hidden" name="from" value={ itoa(f.From) }/>
@@ -310,6 +355,21 @@ templ mrRail(p MRPage) {
310355 </h2>
311356 @FileList(p.Files)
312357 </section>
358+ if len(p.Verdicts) > 0 {
359+ <section>
360+ <h2>Verdicts</h2>
361+ <ul>
362+ for _, v := range p.Verdicts {
363+ <li>
364+ <a href={ templ.SafeURL(v.Href) }>{ v.Author }: { v.Label }</a>
365+ if v.Stale {
366+ <span class="mute">(stale)</span>
367+ }
368+ </li>
369+ }
370+ </ul>
371+ </section>
372+ }
313373 @MRChecks(p.Checks)
314374 <section aria-labelledby="r-commits">
315375 <h2 id="r-commits">Commits <span class="count">{ itoa(len(p.Commits)) }</span></h2>
@@ -229,10 +229,9 @@ templ lineNotes(notes []CommentView, form *CommentForm, suffix string) {
229 }229 }
230 230
231 // commentView is a note: a ruled block, under its line in the diff or in the231 // commentView is a note: a ruled block, under its line in the diff or in the
232-// list of comments. The text is plain: escaped, its line breaks kept, its232+// list of comments. Body comes only from the safe Markdown renderer.
233-// http(s) addresses linked.
234 templ commentView(c CommentView, suffix string) {233 templ commentView(c CommentView, suffix string) {
235- <div class="cmt" id={ "c" + c.ID + suffix } data-resolved={ boolText(c.Resolved) }>234+ <div class="cmt" id={ "c" + c.ID + suffix } data-resolved={ boolText(c.Resolved) } data-thread?={ c.Reply != nil }>
236 <p class="c-head">235 <p class="c-head">
237 <span class="c-who">{ c.Author }</span>236 <span class="c-who">{ c.Author }</span>
238 if c.WhenAt != "" {237 if c.WhenAt != "" {
@@ -248,7 +247,47 @@ templ commentView(c CommentView, suffix string) {
248 { " " }<span class="c-where mute">{ c.Where }</span>247 { " " }<span class="c-where mute">{ c.Where }</span>
249 }248 }
250 </p>249 </p>
251- <p class="c-text">@userText(c.Parts)</p>250+ if c.Deleted {
251+ if c.Verdict != "" {
252+ <p class="mute">text deleted</p>
253+ } else {
254+ <p class="mute">deleted note</p>
255+ }
256+ } else {
257+ <div class="c-body prose">
258+ if suffix == "-s" {
259+ @templ.Raw(string(c.BodySplit))
260+ } else {
261+ @templ.Raw(string(c.Body))
262+ }
263+ </div>
264+ }
265+ if c.Edited {
266+ <span class="mute">edited</span>
267+ }
268+ if c.Verdict != "" {
269+ <span class="c-state">{ c.Verdict }</span>
270+ }
271+ if c.Range != "" {
272+ <span class="mute">{ c.Range }</span>
273+ }
274+ if c.Quote != "" {
275+ <button type="button" class="btn" data-js data-quote={ c.Quote }>Quote</button>
276+ }
277+ if c.Edit != nil {
278+ <details><summary>Edit</summary><form method="post" action={ templ.SafeURL(c.Edit.Action) }><input type="hidden" name="from" value={ itoa(c.Edit.From) }/><textarea name="text" maxlength="8000">{ c.Edit.Text }</textarea><button class="btn" type="submit">Save</button></form></details>
279+ }
280+ if c.Delete != nil {
281+ <details><summary>Delete</summary><form method="post" action={ templ.SafeURL(c.Delete.Action) }><input type="hidden" name="from" value={ itoa(c.Delete.From) }/><p>Delete this note?</p><button class="btn" type="submit">Confirm delete</button></form></details>
282+ }
283+ for _, reply := range c.Replies {
284+ @commentView(reply, suffix)
285+ }
286+ if c.Reply != nil {
287+ <details data-reply>
288+ <summary>Reply</summary>@commentForm(*c.Reply, suffix)
289+ </details>
290+ }
252 if c.Resolve != nil {291 if c.Resolve != nil {
253 <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }>292 <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }>
254 <input type="hidden" name="resolved" value={ boolText(c.Resolve.Value) }/>293 <input type="hidden" name="resolved" value={ boolText(c.Resolve.Value) }/>
@@ -277,8 +316,7 @@ templ commentForm(f CommentForm, suffix string) {
277 name="text"316 name="text"
278 rows="3"317 rows="3"
279 maxlength="8000"318 maxlength="8000"
280- required319+ placeholder="Markdown, 8000 characters at most"
281- placeholder="plain text, 8000 characters at most"
282 if f.Focus {320 if f.Focus {
283 autofocus321 autofocus
284 }322 }
@@ -287,6 +325,13 @@ templ commentForm(f CommentForm, suffix string) {
287 <input type="hidden" name="path" value={ f.Path }/>325 <input type="hidden" name="path" value={ f.Path }/>
288 <input type="hidden" name="line" value={ f.Line }/>326 <input type="hidden" name="line" value={ f.Line }/>
289 <input type="hidden" name="side" value={ f.Side }/>327 <input type="hidden" name="side" value={ f.Side }/>
328+ <label>from line <input type="number" name="start_line" min="1" value={ f.StartLine }/></label>
329+ }
330+ if f.ReplyTo != "" {
331+ <input type="hidden" name="reply_to" value={ f.ReplyTo }/>
332+ }
333+ if f.Path == "" && f.ReplyTo == "" {
334+ <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>
290 }335 }
291 if f.From > 0 {336 if f.From > 0 {
292 <input type="hidden" name="from" value={ itoa(f.From) }/>337 <input type="hidden" name="from" value={ itoa(f.From) }/>
@@ -310,6 +355,21 @@ templ mrRail(p MRPage) {
310 </h2>355 </h2>
311 @FileList(p.Files)356 @FileList(p.Files)
312 </section>357 </section>
358+ if len(p.Verdicts) > 0 {
359+ <section>
360+ <h2>Verdicts</h2>
361+ <ul>
362+ for _, v := range p.Verdicts {
363+ <li>
364+ <a href={ templ.SafeURL(v.Href) }>{ v.Author }: { v.Label }</a>
365+ if v.Stale {
366+ <span class="mute">(stale)</span>
367+ }
368+ </li>
369+ }
370+ </ul>
371+ </section>
372+ }
313 @MRChecks(p.Checks)373 @MRChecks(p.Checks)
314 <section aria-labelledby="r-commits">374 <section aria-labelledby="r-commits">
315 <h2 id="r-commits">Commits <span class="count">{ itoa(len(p.Commits)) }</span></h2>375 <h2 id="r-commits">Commits <span class="count">{ itoa(len(p.Commits)) }</span></h2>
M internal/adapters/web/mr_files.go +5 -3
@@ -131,8 +131,10 @@ func countNotes(files []DiffFile, comments []domain.Comment) {
131131 if c.Path == "" || !ok {
132132 continue
133133 }
134- files[i].Notes++
135- if !c.Resolved {
134+ if c.ReplyTo == 0 && !c.Deleted {
135+ files[i].Notes++
136+ }
137+ if c.ReplyTo == 0 && !c.Deleted && !c.Resolved {
136138 files[i].OpenNotes++
137139 }
138140 ids[i] = append(ids[i], "c"+strconv.FormatInt(c.ID, 10))
@@ -196,7 +198,7 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component,
196198 m := res.MergeRequest
197199 signed := SignedIn(ctx)
198200 canWrite := signed && m.State == domain.MRStateOpen && !since
199- p := MRPage{Files: s.hl.files(ctx, res.Diff.Files), LooseInFiles: true}
201+ p := MRPage{Files: s.hl.files(ctx, res.Diff.Files), LooseInFiles: true, NoteAuthor: domain.AuthorName(principal(ctx))}
200202 contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String()))
201203 s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil)
202204 window := p.Files[from:min(from+mrWindow, len(p.Files))]
@@ -131,8 +131,10 @@ func countNotes(files []DiffFile, comments []domain.Comment) {
131 if c.Path == "" || !ok {131 if c.Path == "" || !ok {
132 continue132 continue
133 }133 }
134- files[i].Notes++134+ if c.ReplyTo == 0 && !c.Deleted {
135- if !c.Resolved {135+ files[i].Notes++
136+ }
137+ if c.ReplyTo == 0 && !c.Deleted && !c.Resolved {
136 files[i].OpenNotes++138 files[i].OpenNotes++
137 }139 }
138 ids[i] = append(ids[i], "c"+strconv.FormatInt(c.ID, 10))140 ids[i] = append(ids[i], "c"+strconv.FormatInt(c.ID, 10))
@@ -196,7 +198,7 @@ func (s *Server) mrFiles(ctx context.Context, r *http.Request) (templ.Component,
196 m := res.MergeRequest198 m := res.MergeRequest
197 signed := SignedIn(ctx)199 signed := SignedIn(ctx)
198 canWrite := signed && m.State == domain.MRStateOpen && !since200 canWrite := signed && m.State == domain.MRStateOpen && !since
199- p := MRPage{Files: s.hl.files(ctx, res.Diff.Files), LooseInFiles: true}201+ p := MRPage{Files: s.hl.files(ctx, res.Diff.Files), LooseInFiles: true, NoteAuthor: domain.AuthorName(principal(ctx))}
200 contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String()))202 contextGaps(p.Files, res.Diff.Files, repoHref(d.name(), "context", head.String()))
201 s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil)203 s.placeComments(&p, d.name(), n, res.Comments, m.HeadID.String(), !since, canWrite, nil)
202 window := p.Files[from:min(from+mrWindow, len(p.Files))]204 window := p.Files[from:min(from+mrWindow, len(p.Files))]
A internal/adapters/web/mr_notes_test.go +162 -0
@@ -0,0 +1,162 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "bnema.dev/ermite/internal/ports/mocks"
7+ "bytes"
8+ "context"
9+ "github.com/stretchr/testify/mock"
10+ "net/http"
11+ "net/http/httptest"
12+ "net/url"
13+ "strings"
14+ "testing"
15+ "time"
16+
17+ "bnema.dev/ermite/internal/domain"
18+ "bnema.dev/ermite/internal/ports"
19+ "github.com/stretchr/testify/assert"
20+ "github.com/stretchr/testify/require"
21+)
22+
23+func TestParseOnRange(t *testing.T) {
24+ t.Parallel()
25+ for _, raw := range []string{"new:0-2:a", "new:2-2:a", "new:3-2:a", "new:1-x:a", "new:1-2:", "other:1-2:a"} {
26+ assert.Nil(t, parseOn(raw), raw)
27+ }
28+ assert.Equal(t, &lineRef{side: "new", start: 10, line: 24, path: "dir:a.go"}, parseOn("new:10-24:dir:a.go"))
29+ assert.Equal(t, &lineRef{side: "old", line: 10, path: "a"}, parseOn("old:10:a"))
30+}
31+
32+func TestNoteThreads(t *testing.T) {
33+ t.Parallel()
34+ text, err := domain.ParsePlainText("**hello** ^2")
35+ require.NoError(t, err)
36+ s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()}
37+ comments := []domain.Comment{
38+ {ID: 1, Author: "alice", Text: text},
39+ {ID: 2, Author: "bob", ReplyTo: 1, Text: text},
40+ {ID: 3, Author: "alice", Deleted: true},
41+ {ID: 4, Author: "alice", Deleted: true},
42+ {ID: 5, Author: "bob", ReplyTo: 4, Text: text},
43+ {ID: 6, Author: "alice", Text: text, Verdict: domain.VerdictApprove, Resolved: true},
44+ }
45+ p := MRPage{NoteAuthor: "alice"}
46+ s.placeComments(&p, "demo", 1, comments, "", true, true, nil)
47+ require.Len(t, p.General, 3)
48+ assert.Equal(t, 4, p.NComments)
49+ assert.Equal(t, 1, p.NUnresolved)
50+ require.Len(t, p.General[0].Replies, 1)
51+ assert.NotNil(t, p.General[0].Edit)
52+ assert.Nil(t, p.General[0].Replies[0].Edit)
53+ assert.Nil(t, p.General[0].Replies[0].Resolve)
54+ assert.Contains(t, string(p.General[0].Body), "<strong>hello</strong>")
55+ assert.Contains(t, string(p.General[0].Body), `href="#c2"`)
56+ assert.True(t, p.General[1].Deleted)
57+ var b bytes.Buffer
58+ require.NoError(t, commentView(p.General[0], "").Render(context.Background(), &b))
59+ assert.Contains(t, b.String(), `name="reply_to" value="1"`)
60+ assert.Contains(t, b.String(), `data-js data-quote=`)
61+ assert.Contains(t, b.String(), `/comments/1/edit`)
62+ assert.NotContains(t, b.String(), `/comments/2/edit`)
63+}
64+
65+func TestNoteLiveEvents(t *testing.T) {
66+ t.Parallel()
67+ l := liveStream{view: "mr", n: "1"}
68+ events := []domain.Event{domain.CommentEdited{Number: 1}, domain.CommentDeleted{Number: 1}, domain.CommentResolved{Number: 1}}
69+ for _, ev := range events {
70+ we := ports.WatchEvent{Event: ev}
71+ assert.True(t, liveEvent(we))
72+ fs, err := l.repoPage(context.Background(), we)
73+ require.NoError(t, err)
74+ require.Len(t, fs, 1)
75+ assert.Contains(t, fs[0].html, "reload")
76+ }
77+}
78+
79+func TestNotePosts(t *testing.T) {
80+ t.Parallel()
81+ svc := mocks.NewMergeRequestService(t)
82+ repo := repoName(t, "demo")
83+ s := &Server{cfg: Config{MergeRequests: svc}}
84+ ctx := domain.ContextWithPrincipal(context.Background(), domain.Owner())
85+ request := func(values url.Values) *http.Request {
86+ r := httptest.NewRequest("POST", "/demo/merge-requests/1/comments", strings.NewReader(values.Encode())).WithContext(ctx)
87+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
88+ r.SetPathValue("repo", "demo")
89+ r.SetPathValue("n", "1")
90+ r.SetPathValue("id", "2")
91+ require.NoError(t, r.ParseForm())
92+ return r
93+ }
94+ svc.EXPECT().AddComment(mock.Anything, mock.Anything, repo, int64(1), mock.MatchedBy(func(p ports.AddCommentParams) bool { return p.ReplyTo == 2 && p.Text == "reply" })).Return(domain.Comment{ID: 7}, nil).Once()
95+ to, _, err := s.mrComment(ctx, request(url.Values{"reply_to": {"2"}, "text": {"reply"}}))
96+ require.NoError(t, err)
97+ assert.Equal(t, "/demo/merge-requests/1#c7", to)
98+ svc.EXPECT().AddComment(mock.Anything, mock.Anything, repo, int64(1), mock.MatchedBy(func(p ports.AddCommentParams) bool {
99+ return p.StartLine == 10 && p.Line == 24 && p.Path == "a.go" && p.Verdict == domain.VerdictApprove
100+ })).Return(domain.Comment{ID: 8}, nil).Once()
101+ to, _, err = s.mrComment(ctx, request(url.Values{"path": {"a.go"}, "line": {"24"}, "start_line": {"10"}, "side": {"new"}, "verdict": {"approve"}}))
102+ require.NoError(t, err)
103+ assert.Equal(t, "/demo/merge-requests/1#c8", to)
104+ svc.EXPECT().EditComment(mock.Anything, mock.Anything, repo, int64(1), int64(2), "new\ntext").Return(domain.Comment{ID: 2}, nil).Once()
105+ to, _, err = s.mrEditComment(ctx, request(url.Values{"text": {"new\r\ntext"}}))
106+ require.NoError(t, err)
107+ assert.Equal(t, "/demo/merge-requests/1#c2", to)
108+ svc.EXPECT().DeleteComment(mock.Anything, mock.Anything, repo, int64(1), int64(2)).Return(domain.Comment{ID: 2}, nil).Once()
109+ to, _, err = s.mrDeleteComment(ctx, request(nil))
110+ require.NoError(t, err)
111+ assert.Equal(t, "/demo/merge-requests/1#c2", to)
112+}
113+
114+func TestDeletedNoteRendering(t *testing.T) {
115+ t.Parallel()
116+ text, err := domain.ParsePlainText("surviving reply")
117+ require.NoError(t, err)
118+ s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()}
119+ p := MRPage{}
120+ s.placeComments(&p, "demo", 1, []domain.Comment{
121+ {ID: 1, Author: "reviewer", Deleted: true, Verdict: domain.VerdictApprove},
122+ {ID: 2, Deleted: true},
123+ {ID: 3, ReplyTo: 2, Deleted: true},
124+ {ID: 4, Deleted: true},
125+ {ID: 5, ReplyTo: 4, Text: text},
126+ }, "", true, true, nil)
127+ require.Len(t, p.General, 2)
128+ assert.Equal(t, "1", p.General[0].ID)
129+ assert.Equal(t, "4", p.General[1].ID)
130+ var b bytes.Buffer
131+ require.NoError(t, commentView(p.General[0], "").Render(context.Background(), &b))
132+ assert.Contains(t, b.String(), `<span class="c-who">owner</span>`)
133+ assert.Contains(t, b.String(), "approve")
134+ assert.Contains(t, b.String(), "text deleted")
135+ b.Reset()
136+ require.NoError(t, commentView(p.General[1], "").Render(context.Background(), &b))
137+ assert.Contains(t, b.String(), "surviving reply")
138+ assert.NotContains(t, b.String(), "Quote")
139+ assert.NotContains(t, b.String(), "data-quote")
140+}
141+
142+func TestInlineNoteHeadingIDs(t *testing.T) {
143+ t.Parallel()
144+ // "S H" would get the id the split copy of "H" gets if the prefixes of the
145+ // two copies overlapped.
146+ text, err := domain.ParsePlainText("# H\n\n# S H\n\n[jump](#h)")
147+ require.NoError(t, err)
148+ s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()}
149+ p := MRPage{Files: []DiffFile{{Key: "a", ID: "f1", Hunks: []DiffHunk{{Lines: []DiffLine{{Kind: "add", newN: 1}}}}}}}
150+ s.placeComments(&p, "demo", 1, []domain.Comment{{ID: 12, Path: "a", Side: domain.SideNew, Line: 1, Text: text}}, "", true, true, nil)
151+ notes := p.Files[0].Hunks[0].Lines[0].Notes
152+ require.Len(t, notes, 1)
153+ var b bytes.Buffer
154+ require.NoError(t, commentView(notes[0], "").Render(context.Background(), &b))
155+ require.NoError(t, commentView(notes[0], "-s").Render(context.Background(), &b))
156+ html := b.String()
157+ for _, id := range []string{"c12-h", "c12-s-h", "c12s-h", "c12s-s-h"} {
158+ assert.Equal(t, 1, strings.Count(html, `id="`+id+`"`), id)
159+ }
160+ assert.Contains(t, html, `href="#c12-h"`)
161+ assert.Contains(t, html, `href="#c12s-h"`)
162+}
@@ -0,0 +1,162 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package web
4+
5+import (
6+ "bnema.dev/ermite/internal/ports/mocks"
7+ "bytes"
8+ "context"
9+ "github.com/stretchr/testify/mock"
10+ "net/http"
11+ "net/http/httptest"
12+ "net/url"
13+ "strings"
14+ "testing"
15+ "time"
16+
17+ "bnema.dev/ermite/internal/domain"
18+ "bnema.dev/ermite/internal/ports"
19+ "github.com/stretchr/testify/assert"
20+ "github.com/stretchr/testify/require"
21+)
22+
23+func TestParseOnRange(t *testing.T) {
24+ t.Parallel()
25+ for _, raw := range []string{"new:0-2:a", "new:2-2:a", "new:3-2:a", "new:1-x:a", "new:1-2:", "other:1-2:a"} {
26+ assert.Nil(t, parseOn(raw), raw)
27+ }
28+ assert.Equal(t, &lineRef{side: "new", start: 10, line: 24, path: "dir:a.go"}, parseOn("new:10-24:dir:a.go"))
29+ assert.Equal(t, &lineRef{side: "old", line: 10, path: "a"}, parseOn("old:10:a"))
30+}
31+
32+func TestNoteThreads(t *testing.T) {
33+ t.Parallel()
34+ text, err := domain.ParsePlainText("**hello** ^2")
35+ require.NoError(t, err)
36+ s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()}
37+ comments := []domain.Comment{
38+ {ID: 1, Author: "alice", Text: text},
39+ {ID: 2, Author: "bob", ReplyTo: 1, Text: text},
40+ {ID: 3, Author: "alice", Deleted: true},
41+ {ID: 4, Author: "alice", Deleted: true},
42+ {ID: 5, Author: "bob", ReplyTo: 4, Text: text},
43+ {ID: 6, Author: "alice", Text: text, Verdict: domain.VerdictApprove, Resolved: true},
44+ }
45+ p := MRPage{NoteAuthor: "alice"}
46+ s.placeComments(&p, "demo", 1, comments, "", true, true, nil)
47+ require.Len(t, p.General, 3)
48+ assert.Equal(t, 4, p.NComments)
49+ assert.Equal(t, 1, p.NUnresolved)
50+ require.Len(t, p.General[0].Replies, 1)
51+ assert.NotNil(t, p.General[0].Edit)
52+ assert.Nil(t, p.General[0].Replies[0].Edit)
53+ assert.Nil(t, p.General[0].Replies[0].Resolve)
54+ assert.Contains(t, string(p.General[0].Body), "<strong>hello</strong>")
55+ assert.Contains(t, string(p.General[0].Body), `href="#c2"`)
56+ assert.True(t, p.General[1].Deleted)
57+ var b bytes.Buffer
58+ require.NoError(t, commentView(p.General[0], "").Render(context.Background(), &b))
59+ assert.Contains(t, b.String(), `name="reply_to" value="1"`)
60+ assert.Contains(t, b.String(), `data-js data-quote=`)
61+ assert.Contains(t, b.String(), `/comments/1/edit`)
62+ assert.NotContains(t, b.String(), `/comments/2/edit`)
63+}
64+
65+func TestNoteLiveEvents(t *testing.T) {
66+ t.Parallel()
67+ l := liveStream{view: "mr", n: "1"}
68+ events := []domain.Event{domain.CommentEdited{Number: 1}, domain.CommentDeleted{Number: 1}, domain.CommentResolved{Number: 1}}
69+ for _, ev := range events {
70+ we := ports.WatchEvent{Event: ev}
71+ assert.True(t, liveEvent(we))
72+ fs, err := l.repoPage(context.Background(), we)
73+ require.NoError(t, err)
74+ require.Len(t, fs, 1)
75+ assert.Contains(t, fs[0].html, "reload")
76+ }
77+}
78+
79+func TestNotePosts(t *testing.T) {
80+ t.Parallel()
81+ svc := mocks.NewMergeRequestService(t)
82+ repo := repoName(t, "demo")
83+ s := &Server{cfg: Config{MergeRequests: svc}}
84+ ctx := domain.ContextWithPrincipal(context.Background(), domain.Owner())
85+ request := func(values url.Values) *http.Request {
86+ r := httptest.NewRequest("POST", "/demo/merge-requests/1/comments", strings.NewReader(values.Encode())).WithContext(ctx)
87+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
88+ r.SetPathValue("repo", "demo")
89+ r.SetPathValue("n", "1")
90+ r.SetPathValue("id", "2")
91+ require.NoError(t, r.ParseForm())
92+ return r
93+ }
94+ svc.EXPECT().AddComment(mock.Anything, mock.Anything, repo, int64(1), mock.MatchedBy(func(p ports.AddCommentParams) bool { return p.ReplyTo == 2 && p.Text == "reply" })).Return(domain.Comment{ID: 7}, nil).Once()
95+ to, _, err := s.mrComment(ctx, request(url.Values{"reply_to": {"2"}, "text": {"reply"}}))
96+ require.NoError(t, err)
97+ assert.Equal(t, "/demo/merge-requests/1#c7", to)
98+ svc.EXPECT().AddComment(mock.Anything, mock.Anything, repo, int64(1), mock.MatchedBy(func(p ports.AddCommentParams) bool {
99+ return p.StartLine == 10 && p.Line == 24 && p.Path == "a.go" && p.Verdict == domain.VerdictApprove
100+ })).Return(domain.Comment{ID: 8}, nil).Once()
101+ to, _, err = s.mrComment(ctx, request(url.Values{"path": {"a.go"}, "line": {"24"}, "start_line": {"10"}, "side": {"new"}, "verdict": {"approve"}}))
102+ require.NoError(t, err)
103+ assert.Equal(t, "/demo/merge-requests/1#c8", to)
104+ svc.EXPECT().EditComment(mock.Anything, mock.Anything, repo, int64(1), int64(2), "new\ntext").Return(domain.Comment{ID: 2}, nil).Once()
105+ to, _, err = s.mrEditComment(ctx, request(url.Values{"text": {"new\r\ntext"}}))
106+ require.NoError(t, err)
107+ assert.Equal(t, "/demo/merge-requests/1#c2", to)
108+ svc.EXPECT().DeleteComment(mock.Anything, mock.Anything, repo, int64(1), int64(2)).Return(domain.Comment{ID: 2}, nil).Once()
109+ to, _, err = s.mrDeleteComment(ctx, request(nil))
110+ require.NoError(t, err)
111+ assert.Equal(t, "/demo/merge-requests/1#c2", to)
112+}
113+
114+func TestDeletedNoteRendering(t *testing.T) {
115+ t.Parallel()
116+ text, err := domain.ParsePlainText("surviving reply")
117+ require.NoError(t, err)
118+ s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()}
119+ p := MRPage{}
120+ s.placeComments(&p, "demo", 1, []domain.Comment{
121+ {ID: 1, Author: "reviewer", Deleted: true, Verdict: domain.VerdictApprove},
122+ {ID: 2, Deleted: true},
123+ {ID: 3, ReplyTo: 2, Deleted: true},
124+ {ID: 4, Deleted: true},
125+ {ID: 5, ReplyTo: 4, Text: text},
126+ }, "", true, true, nil)
127+ require.Len(t, p.General, 2)
128+ assert.Equal(t, "1", p.General[0].ID)
129+ assert.Equal(t, "4", p.General[1].ID)
130+ var b bytes.Buffer
131+ require.NoError(t, commentView(p.General[0], "").Render(context.Background(), &b))
132+ assert.Contains(t, b.String(), `<span class="c-who">owner</span>`)
133+ assert.Contains(t, b.String(), "approve")
134+ assert.Contains(t, b.String(), "text deleted")
135+ b.Reset()
136+ require.NoError(t, commentView(p.General[1], "").Render(context.Background(), &b))
137+ assert.Contains(t, b.String(), "surviving reply")
138+ assert.NotContains(t, b.String(), "Quote")
139+ assert.NotContains(t, b.String(), "data-quote")
140+}
141+
142+func TestInlineNoteHeadingIDs(t *testing.T) {
143+ t.Parallel()
144+ // "S H" would get the id the split copy of "H" gets if the prefixes of the
145+ // two copies overlapped.
146+ text, err := domain.ParsePlainText("# H\n\n# S H\n\n[jump](#h)")
147+ require.NoError(t, err)
148+ s := &Server{cfg: Config{Now: time.Now}, md: newMarkdown()}
149+ p := MRPage{Files: []DiffFile{{Key: "a", ID: "f1", Hunks: []DiffHunk{{Lines: []DiffLine{{Kind: "add", newN: 1}}}}}}}
150+ s.placeComments(&p, "demo", 1, []domain.Comment{{ID: 12, Path: "a", Side: domain.SideNew, Line: 1, Text: text}}, "", true, true, nil)
151+ notes := p.Files[0].Hunks[0].Lines[0].Notes
152+ require.Len(t, notes, 1)
153+ var b bytes.Buffer
154+ require.NoError(t, commentView(notes[0], "").Render(context.Background(), &b))
155+ require.NoError(t, commentView(notes[0], "-s").Render(context.Background(), &b))
156+ html := b.String()
157+ for _, id := range []string{"c12-h", "c12-s-h", "c12s-h", "c12s-s-h"} {
158+ assert.Equal(t, 1, strings.Count(html, `id="`+id+`"`), id)
159+ }
160+ assert.Contains(t, html, `href="#c12-h"`)
161+ assert.Contains(t, html, `href="#c12s-h"`)
162+}
M internal/adapters/web/mr_post.go +47 -0
@@ -177,6 +177,23 @@ func (s *Server) mrComment(ctx context.Context, r *http.Request) (string, string
177177 return "", back, formError("side must be new or old")
178178 }
179179 }
180+ if raw := r.PostFormValue("reply_to"); raw != "" {
181+ params.ReplyTo = commentID(raw)
182+ if params.ReplyTo == 0 {
183+ return "", back, formError("reply_to is not a note id")
184+ }
185+ }
186+ if raw := r.PostFormValue("start_line"); raw != "" {
187+ v, e := strconv.ParseInt(raw, 10, 32)
188+ if e != nil {
189+ return "", back, formError("start_line is not a number")
190+ }
191+ params.StartLine = int(v)
192+ }
193+ params.Verdict, err = domain.ParseVerdict(r.PostFormValue("verdict"))
194+ if err != nil {
195+ return "", back, err
196+ }
180197 name, err := domain.ParseRepoName(repo)
181198 if err != nil {
182199 return "", back, err
@@ -356,3 +373,33 @@ func formBool(s string) (bool, error) {
356373 func normalizeNewlines(s string) string {
357374 return strings.ReplaceAll(s, "\r\n", "\n")
358375 }
376+
377+// mrEditComment passes source text to the author-checked use case.
378+func (s *Server) mrEditComment(ctx context.Context, r *http.Request) (string, string, error) {
379+ n, err := number(r)
380+ if err != nil {
381+ return "", "", err
382+ }
383+ back := withFrom(mrHref(r.PathValue("repo"), n), formFrom(r))
384+ name, err := domain.ParseRepoName(r.PathValue("repo"))
385+ if err != nil {
386+ return "", back, err
387+ }
388+ c, err := s.cfg.MergeRequests.EditComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")), normalizeNewlines(r.PostFormValue("text")))
389+ return back + "#c" + strconv.FormatInt(c.ID, 10), back, err
390+}
391+
392+// mrDeleteComment passes the note id to the author-checked use case.
393+func (s *Server) mrDeleteComment(ctx context.Context, r *http.Request) (string, string, error) {
394+ n, err := number(r)
395+ if err != nil {
396+ return "", "", err
397+ }
398+ back := withFrom(mrHref(r.PathValue("repo"), n), formFrom(r))
399+ name, err := domain.ParseRepoName(r.PathValue("repo"))
400+ if err != nil {
401+ return "", back, err
402+ }
403+ c, err := s.cfg.MergeRequests.DeleteComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")))
404+ return back + "#c" + strconv.FormatInt(c.ID, 10), back, err
405+}
@@ -177,6 +177,23 @@ func (s *Server) mrComment(ctx context.Context, r *http.Request) (string, string
177 return "", back, formError("side must be new or old")177 return "", back, formError("side must be new or old")
178 }178 }
179 }179 }
180+ if raw := r.PostFormValue("reply_to"); raw != "" {
181+ params.ReplyTo = commentID(raw)
182+ if params.ReplyTo == 0 {
183+ return "", back, formError("reply_to is not a note id")
184+ }
185+ }
186+ if raw := r.PostFormValue("start_line"); raw != "" {
187+ v, e := strconv.ParseInt(raw, 10, 32)
188+ if e != nil {
189+ return "", back, formError("start_line is not a number")
190+ }
191+ params.StartLine = int(v)
192+ }
193+ params.Verdict, err = domain.ParseVerdict(r.PostFormValue("verdict"))
194+ if err != nil {
195+ return "", back, err
196+ }
180 name, err := domain.ParseRepoName(repo)197 name, err := domain.ParseRepoName(repo)
181 if err != nil {198 if err != nil {
182 return "", back, err199 return "", back, err
@@ -356,3 +373,33 @@ func formBool(s string) (bool, error) {
356 func normalizeNewlines(s string) string {373 func normalizeNewlines(s string) string {
357 return strings.ReplaceAll(s, "\r\n", "\n")374 return strings.ReplaceAll(s, "\r\n", "\n")
358 }375 }
376+
377+// mrEditComment passes source text to the author-checked use case.
378+func (s *Server) mrEditComment(ctx context.Context, r *http.Request) (string, string, error) {
379+ n, err := number(r)
380+ if err != nil {
381+ return "", "", err
382+ }
383+ back := withFrom(mrHref(r.PathValue("repo"), n), formFrom(r))
384+ name, err := domain.ParseRepoName(r.PathValue("repo"))
385+ if err != nil {
386+ return "", back, err
387+ }
388+ c, err := s.cfg.MergeRequests.EditComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")), normalizeNewlines(r.PostFormValue("text")))
389+ return back + "#c" + strconv.FormatInt(c.ID, 10), back, err
390+}
391+
392+// mrDeleteComment passes the note id to the author-checked use case.
393+func (s *Server) mrDeleteComment(ctx context.Context, r *http.Request) (string, string, error) {
394+ n, err := number(r)
395+ if err != nil {
396+ return "", "", err
397+ }
398+ back := withFrom(mrHref(r.PathValue("repo"), n), formFrom(r))
399+ name, err := domain.ParseRepoName(r.PathValue("repo"))
400+ if err != nil {
401+ return "", back, err
402+ }
403+ c, err := s.cfg.MergeRequests.DeleteComment(ctx, principal(ctx), name, n, commentID(r.PathValue("id")))
404+ return back + "#c" + strconv.FormatInt(c.ID, 10), back, err
405+}
M internal/adapters/web/mr_templ.go +777 -480
@@ -1215,8 +1215,7 @@ func lineNotes(notes []CommentView, form *CommentForm, suffix string) templ.Comp
12151215 }
12161216
12171217 // commentView is a note: a ruled block, under its line in the diff or in the
1218-// list of comments. The text is plain: escaped, its line breaks kept, its
1219-// http(s) addresses linked.
1218+// list of comments. Body comes only from the safe Markdown renderer.
12201219 func commentView(c CommentView, suffix string) templ.Component {
12211220 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
12221221 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -1245,7 +1244,7 @@ func commentView(c CommentView, suffix string) templ.Component {
12451244 var templ_7745c5c3_Var59 string
12461245 templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)
12471246 if templ_7745c5c3_Err != nil {
1248- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 235, Col: 42}
1247+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 42}
12491248 }
12501249 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
12511250 if templ_7745c5c3_Err != nil {
@@ -1258,26 +1257,36 @@ func commentView(c CommentView, suffix string) templ.Component {
12581257 var templ_7745c5c3_Var60 string
12591258 templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))
12601259 if templ_7745c5c3_Err != nil {
1261- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 235, Col: 81}
1260+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 81}
12621261 }
12631262 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
12641263 if templ_7745c5c3_Err != nil {
12651264 return templ_7745c5c3_Err
12661265 }
1267- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"><p class=\"c-head\"><span class=\"c-who\">")
1266+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"")
1267+ if templ_7745c5c3_Err != nil {
1268+ return templ_7745c5c3_Err
1269+ }
1270+ if c.Reply != nil {
1271+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " data-thread")
1272+ if templ_7745c5c3_Err != nil {
1273+ return templ_7745c5c3_Err
1274+ }
1275+ }
1276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "><p class=\"c-head\"><span class=\"c-who\">")
12681277 if templ_7745c5c3_Err != nil {
12691278 return templ_7745c5c3_Err
12701279 }
12711280 var templ_7745c5c3_Var61 string
12721281 templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
12731282 if templ_7745c5c3_Err != nil {
1274- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 237, Col: 33}
1283+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 33}
12751284 }
12761285 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
12771286 if templ_7745c5c3_Err != nil {
12781287 return templ_7745c5c3_Err
12791288 }
1280- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span> ")
1289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span> ")
12811290 if templ_7745c5c3_Err != nil {
12821291 return templ_7745c5c3_Err
12831292 }
@@ -1285,52 +1294,52 @@ func commentView(c CommentView, suffix string) templ.Component {
12851294 var templ_7745c5c3_Var62 string
12861295 templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
12871296 if templ_7745c5c3_Err != nil {
1288- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 9}
1297+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 9}
12891298 }
12901299 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
12911300 if templ_7745c5c3_Err != nil {
12921301 return templ_7745c5c3_Err
12931302 }
1294- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<time class=\"mute\" datetime=\"")
1303+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<time class=\"mute\" datetime=\"")
12951304 if templ_7745c5c3_Err != nil {
12961305 return templ_7745c5c3_Err
12971306 }
12981307 var templ_7745c5c3_Var63 string
12991308 templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
13001309 if templ_7745c5c3_Err != nil {
1301- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 49}
1310+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 49}
13021311 }
13031312 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
13041313 if templ_7745c5c3_Err != nil {
13051314 return templ_7745c5c3_Err
13061315 }
1307- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\" title=\"")
1316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "\" title=\"")
13081317 if templ_7745c5c3_Err != nil {
13091318 return templ_7745c5c3_Err
13101319 }
13111320 var templ_7745c5c3_Var64 string
13121321 templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)
13131322 if templ_7745c5c3_Err != nil {
1314- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 65}
1323+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 65}
13151324 }
13161325 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
13171326 if templ_7745c5c3_Err != nil {
13181327 return templ_7745c5c3_Err
13191328 }
1320- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\">")
1329+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\">")
13211330 if templ_7745c5c3_Err != nil {
13221331 return templ_7745c5c3_Err
13231332 }
13241333 var templ_7745c5c3_Var65 string
13251334 templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
13261335 if templ_7745c5c3_Err != nil {
1327- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 76}
1336+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 76}
13281337 }
13291338 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
13301339 if templ_7745c5c3_Err != nil {
13311340 return templ_7745c5c3_Err
13321341 }
1333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</time> ")
1342+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "</time> ")
13341343 if templ_7745c5c3_Err != nil {
13351344 return templ_7745c5c3_Err
13361345 }
@@ -1339,13 +1348,13 @@ func commentView(c CommentView, suffix string) templ.Component {
13391348 var templ_7745c5c3_Var66 string
13401349 templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13411350 if templ_7745c5c3_Err != nil {
1342- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 242, Col: 9}
1351+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 241, Col: 9}
13431352 }
13441353 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
13451354 if templ_7745c5c3_Err != nil {
13461355 return templ_7745c5c3_Err
13471356 }
1348- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<span class=\"c-state\">resolved</span> ")
1357+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<span class=\"c-state\">resolved</span> ")
13491358 if templ_7745c5c3_Err != nil {
13501359 return templ_7745c5c3_Err
13511360 }
@@ -1354,13 +1363,13 @@ func commentView(c CommentView, suffix string) templ.Component {
13541363 var templ_7745c5c3_Var67 string
13551364 templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13561365 if templ_7745c5c3_Err != nil {
1357- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 245, Col: 9}
1366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 9}
13581367 }
13591368 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
13601369 if templ_7745c5c3_Err != nil {
13611370 return templ_7745c5c3_Err
13621371 }
1363- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")
1372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")
13641373 if templ_7745c5c3_Err != nil {
13651374 return templ_7745c5c3_Err
13661375 }
@@ -1369,111 +1378,296 @@ func commentView(c CommentView, suffix string) templ.Component {
13691378 var templ_7745c5c3_Var68 string
13701379 templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
13711380 if templ_7745c5c3_Err != nil {
1372- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 9}
1381+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 9}
13731382 }
13741383 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
13751384 if templ_7745c5c3_Err != nil {
13761385 return templ_7745c5c3_Err
13771386 }
1378- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<span class=\"c-where mute\">")
1387+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"c-where mute\">")
13791388 if templ_7745c5c3_Err != nil {
13801389 return templ_7745c5c3_Err
13811390 }
13821391 var templ_7745c5c3_Var69 string
13831392 templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)
13841393 if templ_7745c5c3_Err != nil {
1385- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 47}
1394+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 47}
13861395 }
13871396 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
13881397 if templ_7745c5c3_Err != nil {
13891398 return templ_7745c5c3_Err
13901399 }
1391- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</span>")
1400+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</span>")
13921401 if templ_7745c5c3_Err != nil {
13931402 return templ_7745c5c3_Err
13941403 }
13951404 }
1396- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</p><p class=\"c-text\">")
1405+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</p>")
13971406 if templ_7745c5c3_Err != nil {
13981407 return templ_7745c5c3_Err
13991408 }
1400- templ_7745c5c3_Err = userText(c.Parts).Render(ctx, templ_7745c5c3_Buffer)
1401- if templ_7745c5c3_Err != nil {
1402- return templ_7745c5c3_Err
1409+ if c.Deleted {
1410+ if c.Verdict != "" {
1411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"mute\">text deleted</p>")
1412+ if templ_7745c5c3_Err != nil {
1413+ return templ_7745c5c3_Err
1414+ }
1415+ } else {
1416+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<p class=\"mute\">deleted note</p>")
1417+ if templ_7745c5c3_Err != nil {
1418+ return templ_7745c5c3_Err
1419+ }
1420+ }
1421+ } else {
1422+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<div class=\"c-body prose\">")
1423+ if templ_7745c5c3_Err != nil {
1424+ return templ_7745c5c3_Err
1425+ }
1426+ if suffix == "-s" {
1427+ templ_7745c5c3_Err = templ.Raw(string(c.BodySplit)).Render(ctx, templ_7745c5c3_Buffer)
1428+ if templ_7745c5c3_Err != nil {
1429+ return templ_7745c5c3_Err
1430+ }
1431+ } else {
1432+ templ_7745c5c3_Err = templ.Raw(string(c.Body)).Render(ctx, templ_7745c5c3_Buffer)
1433+ if templ_7745c5c3_Err != nil {
1434+ return templ_7745c5c3_Err
1435+ }
1436+ }
1437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "</div>")
1438+ if templ_7745c5c3_Err != nil {
1439+ return templ_7745c5c3_Err
1440+ }
14031441 }
1404- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</p>")
1405- if templ_7745c5c3_Err != nil {
1406- return templ_7745c5c3_Err
1442+ if c.Edited {
1443+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<span class=\"mute\">edited</span> ")
1444+ if templ_7745c5c3_Err != nil {
1445+ return templ_7745c5c3_Err
1446+ }
14071447 }
1408- if c.Resolve != nil {
1409- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "<form class=\"c-act\" method=\"post\" action=\"")
1448+ if c.Verdict != "" {
1449+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<span class=\"c-state\">")
14101450 if templ_7745c5c3_Err != nil {
14111451 return templ_7745c5c3_Err
14121452 }
1413- var templ_7745c5c3_Var70 templ.SafeURL
1414- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
1453+ var templ_7745c5c3_Var70 string
1454+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict)
14151455 if templ_7745c5c3_Err != nil {
1416- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 253, Col: 77}
1456+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 269, Col: 36}
14171457 }
14181458 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
14191459 if templ_7745c5c3_Err != nil {
14201460 return templ_7745c5c3_Err
14211461 }
1422- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
1462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</span> ")
1463+ if templ_7745c5c3_Err != nil {
1464+ return templ_7745c5c3_Err
1465+ }
1466+ }
1467+ if c.Range != "" {
1468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<span class=\"mute\">")
14231469 if templ_7745c5c3_Err != nil {
14241470 return templ_7745c5c3_Err
14251471 }
14261472 var templ_7745c5c3_Var71 string
1427- templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
1473+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range)
1474+ if templ_7745c5c3_Err != nil {
1475+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 272, Col: 31}
1476+ }
1477+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
1478+ if templ_7745c5c3_Err != nil {
1479+ return templ_7745c5c3_Err
1480+ }
1481+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</span> ")
1482+ if templ_7745c5c3_Err != nil {
1483+ return templ_7745c5c3_Err
1484+ }
1485+ }
1486+ if c.Quote != "" {
1487+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
1488+ if templ_7745c5c3_Err != nil {
1489+ return templ_7745c5c3_Err
1490+ }
1491+ var templ_7745c5c3_Var72 string
1492+ templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote)
1493+ if templ_7745c5c3_Err != nil {
1494+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 65}
1495+ }
1496+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
14281497 if templ_7745c5c3_Err != nil {
1429- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 254, Col: 74}
1498+ return templ_7745c5c3_Err
1499+ }
1500+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "\">Quote</button> ")
1501+ if templ_7745c5c3_Err != nil {
1502+ return templ_7745c5c3_Err
14301503 }
1431- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)
1504+ }
1505+ if c.Edit != nil {
1506+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "<details><summary>Edit</summary><form method=\"post\" action=\"")
14321507 if templ_7745c5c3_Err != nil {
14331508 return templ_7745c5c3_Err
14341509 }
1435- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\"> ")
1510+ var templ_7745c5c3_Var73 templ.SafeURL
1511+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action))
1512+ if templ_7745c5c3_Err != nil {
1513+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 92}
1514+ }
1515+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1516+ if templ_7745c5c3_Err != nil {
1517+ return templ_7745c5c3_Err
1518+ }
1519+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "\"><input type=\"hidden\" name=\"from\" value=\"")
1520+ if templ_7745c5c3_Err != nil {
1521+ return templ_7745c5c3_Err
1522+ }
1523+ var templ_7745c5c3_Var74 string
1524+ templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From))
1525+ if templ_7745c5c3_Err != nil {
1526+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 153}
1527+ }
1528+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)
1529+ if templ_7745c5c3_Err != nil {
1530+ return templ_7745c5c3_Err
1531+ }
1532+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "\"><textarea name=\"text\" maxlength=\"8000\">")
1533+ if templ_7745c5c3_Err != nil {
1534+ return templ_7745c5c3_Err
1535+ }
1536+ var templ_7745c5c3_Var75 string
1537+ templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text)
1538+ if templ_7745c5c3_Err != nil {
1539+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 209}
1540+ }
1541+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
1542+ if templ_7745c5c3_Err != nil {
1543+ return templ_7745c5c3_Err
1544+ }
1545+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
1546+ if templ_7745c5c3_Err != nil {
1547+ return templ_7745c5c3_Err
1548+ }
1549+ }
1550+ if c.Delete != nil {
1551+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<details><summary>Delete</summary><form method=\"post\" action=\"")
1552+ if templ_7745c5c3_Err != nil {
1553+ return templ_7745c5c3_Err
1554+ }
1555+ var templ_7745c5c3_Var76 templ.SafeURL
1556+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action))
1557+ if templ_7745c5c3_Err != nil {
1558+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 281, Col: 96}
1559+ }
1560+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
1561+ if templ_7745c5c3_Err != nil {
1562+ return templ_7745c5c3_Err
1563+ }
1564+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "\"><input type=\"hidden\" name=\"from\" value=\"")
1565+ if templ_7745c5c3_Err != nil {
1566+ return templ_7745c5c3_Err
1567+ }
1568+ var templ_7745c5c3_Var77 string
1569+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From))
1570+ if templ_7745c5c3_Err != nil {
1571+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 281, Col: 159}
1572+ }
1573+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
1574+ if templ_7745c5c3_Err != nil {
1575+ return templ_7745c5c3_Err
1576+ }
1577+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\"><p>Delete this note?</p><button class=\"btn\" type=\"submit\">Confirm delete</button></form></details> ")
1578+ if templ_7745c5c3_Err != nil {
1579+ return templ_7745c5c3_Err
1580+ }
1581+ }
1582+ for _, reply := range c.Replies {
1583+ templ_7745c5c3_Err = commentView(reply, suffix).Render(ctx, templ_7745c5c3_Buffer)
1584+ if templ_7745c5c3_Err != nil {
1585+ return templ_7745c5c3_Err
1586+ }
1587+ }
1588+ if c.Reply != nil {
1589+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<details data-reply><summary>Reply</summary>")
1590+ if templ_7745c5c3_Err != nil {
1591+ return templ_7745c5c3_Err
1592+ }
1593+ templ_7745c5c3_Err = commentForm(*c.Reply, suffix).Render(ctx, templ_7745c5c3_Buffer)
1594+ if templ_7745c5c3_Err != nil {
1595+ return templ_7745c5c3_Err
1596+ }
1597+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "</details> ")
1598+ if templ_7745c5c3_Err != nil {
1599+ return templ_7745c5c3_Err
1600+ }
1601+ }
1602+ if c.Resolve != nil {
1603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<form class=\"c-act\" method=\"post\" action=\"")
1604+ if templ_7745c5c3_Err != nil {
1605+ return templ_7745c5c3_Err
1606+ }
1607+ var templ_7745c5c3_Var78 templ.SafeURL
1608+ templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
1609+ if templ_7745c5c3_Err != nil {
1610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 292, Col: 77}
1611+ }
1612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
1613+ if templ_7745c5c3_Err != nil {
1614+ return templ_7745c5c3_Err
1615+ }
1616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
1617+ if templ_7745c5c3_Err != nil {
1618+ return templ_7745c5c3_Err
1619+ }
1620+ var templ_7745c5c3_Var79 string
1621+ templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
1622+ if templ_7745c5c3_Err != nil {
1623+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 293, Col: 74}
1624+ }
1625+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79)
1626+ if templ_7745c5c3_Err != nil {
1627+ return templ_7745c5c3_Err
1628+ }
1629+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\"> ")
14361630 if templ_7745c5c3_Err != nil {
14371631 return templ_7745c5c3_Err
14381632 }
14391633 if c.Resolve.From > 0 {
1440- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<input type=\"hidden\" name=\"from\" value=\"")
1634+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "<input type=\"hidden\" name=\"from\" value=\"")
14411635 if templ_7745c5c3_Err != nil {
14421636 return templ_7745c5c3_Err
14431637 }
1444- var templ_7745c5c3_Var72 string
1445- templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
1638+ var templ_7745c5c3_Var80 string
1639+ templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
14461640 if templ_7745c5c3_Err != nil {
1447- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 256, Col: 66}
1641+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 295, Col: 66}
14481642 }
1449- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
1643+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
14501644 if templ_7745c5c3_Err != nil {
14511645 return templ_7745c5c3_Err
14521646 }
1453- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\"> ")
1647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "\"> ")
14541648 if templ_7745c5c3_Err != nil {
14551649 return templ_7745c5c3_Err
14561650 }
14571651 }
1458- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<button type=\"submit\" class=\"btn\">")
1652+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<button type=\"submit\" class=\"btn\">")
14591653 if templ_7745c5c3_Err != nil {
14601654 return templ_7745c5c3_Err
14611655 }
1462- var templ_7745c5c3_Var73 string
1463- templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
1656+ var templ_7745c5c3_Var81 string
1657+ templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
14641658 if templ_7745c5c3_Err != nil {
1465- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 55}
1659+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 55}
14661660 }
1467- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1661+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
14681662 if templ_7745c5c3_Err != nil {
14691663 return templ_7745c5c3_Err
14701664 }
1471- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</button></form>")
1665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</button></form>")
14721666 if templ_7745c5c3_Err != nil {
14731667 return templ_7745c5c3_Err
14741668 }
14751669 }
1476- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</div>")
1670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</div>")
14771671 if templ_7745c5c3_Err != nil {
14781672 return templ_7745c5c3_Err
14791673 }
@@ -1505,165 +1699,203 @@ func commentForm(f CommentForm, suffix string) templ.Component {
15051699 }()
15061700 }
15071701 ctx = templ.InitializeContext(ctx)
1508- templ_7745c5c3_Var74 := templ.GetChildren(ctx)
1509- if templ_7745c5c3_Var74 == nil {
1510- templ_7745c5c3_Var74 = templ.NopComponent
1702+ templ_7745c5c3_Var82 := templ.GetChildren(ctx)
1703+ if templ_7745c5c3_Var82 == nil {
1704+ templ_7745c5c3_Var82 = templ.NopComponent
15111705 }
15121706 ctx = templ.ClearChildren(ctx)
1513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<form class=\"cform\" method=\"post\" action=\"")
1707+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<form class=\"cform\" method=\"post\" action=\"")
15141708 if templ_7745c5c3_Err != nil {
15151709 return templ_7745c5c3_Err
15161710 }
1517- var templ_7745c5c3_Var75 templ.SafeURL
1518- templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
1711+ var templ_7745c5c3_Var83 templ.SafeURL
1712+ templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
15191713 if templ_7745c5c3_Err != nil {
1520- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 67}
1714+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 312, Col: 67}
15211715 }
1522- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
1716+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
15231717 if templ_7745c5c3_Err != nil {
15241718 return templ_7745c5c3_Err
15251719 }
1526- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "\"><label for=\"")
1720+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "\"><label for=\"")
15271721 if templ_7745c5c3_Err != nil {
15281722 return templ_7745c5c3_Err
15291723 }
1530- var templ_7745c5c3_Var76 string
1531- templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1724+ var templ_7745c5c3_Var84 string
1725+ templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
15321726 if templ_7745c5c3_Err != nil {
1533- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 28}
1727+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 28}
15341728 }
1535- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76)
1729+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84)
15361730 if templ_7745c5c3_Err != nil {
15371731 return templ_7745c5c3_Err
15381732 }
1539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "\">")
1733+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "\">")
15401734 if templ_7745c5c3_Err != nil {
15411735 return templ_7745c5c3_Err
15421736 }
1543- var templ_7745c5c3_Var77 string
1544- templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
1737+ var templ_7745c5c3_Var85 string
1738+ templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
15451739 if templ_7745c5c3_Err != nil {
1546- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 40}
1740+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 40}
15471741 }
1548- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77))
1742+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
15491743 if templ_7745c5c3_Err != nil {
15501744 return templ_7745c5c3_Err
15511745 }
1552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</label> <textarea id=\"")
1746+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "</label> <textarea id=\"")
15531747 if templ_7745c5c3_Err != nil {
15541748 return templ_7745c5c3_Err
15551749 }
1556- var templ_7745c5c3_Var78 string
1557- templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1750+ var templ_7745c5c3_Var86 string
1751+ templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
15581752 if templ_7745c5c3_Err != nil {
1559- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 21}
1753+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 21}
15601754 }
1561- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78)
1755+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86)
15621756 if templ_7745c5c3_Err != nil {
15631757 return templ_7745c5c3_Err
15641758 }
1565- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "\" name=\"text\" rows=\"3\" maxlength=\"8000\" required placeholder=\"plain text, 8000 characters at most\"")
1759+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\" name=\"text\" rows=\"3\" maxlength=\"8000\" placeholder=\"Markdown, 8000 characters at most\"")
15661760 if templ_7745c5c3_Err != nil {
15671761 return templ_7745c5c3_Err
15681762 }
15691763 if f.Focus {
1570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, " autofocus")
1764+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, " autofocus")
15711765 if templ_7745c5c3_Err != nil {
15721766 return templ_7745c5c3_Err
15731767 }
15741768 }
1575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "></textarea> ")
1769+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "></textarea> ")
15761770 if templ_7745c5c3_Err != nil {
15771771 return templ_7745c5c3_Err
15781772 }
15791773 if f.Path != "" {
1580- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<input type=\"hidden\" name=\"path\" value=\"")
1774+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<input type=\"hidden\" name=\"path\" value=\"")
15811775 if templ_7745c5c3_Err != nil {
15821776 return templ_7745c5c3_Err
15831777 }
1584- var templ_7745c5c3_Var79 string
1585- templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
1778+ var templ_7745c5c3_Var87 string
1779+ templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
15861780 if templ_7745c5c3_Err != nil {
1587- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 287, Col: 50}
1781+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 325, Col: 50}
15881782 }
1589- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79)
1783+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87)
15901784 if templ_7745c5c3_Err != nil {
15911785 return templ_7745c5c3_Err
15921786 }
1593- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "\"> <input type=\"hidden\" name=\"line\" value=\"")
1787+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "\"> <input type=\"hidden\" name=\"line\" value=\"")
15941788 if templ_7745c5c3_Err != nil {
15951789 return templ_7745c5c3_Err
15961790 }
1597- var templ_7745c5c3_Var80 string
1598- templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
1791+ var templ_7745c5c3_Var88 string
1792+ templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
15991793 if templ_7745c5c3_Err != nil {
1600- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 50}
1794+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 326, Col: 50}
16011795 }
1602- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
1796+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88)
16031797 if templ_7745c5c3_Err != nil {
16041798 return templ_7745c5c3_Err
16051799 }
1606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "\"> <input type=\"hidden\" name=\"side\" value=\"")
1800+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "\"> <input type=\"hidden\" name=\"side\" value=\"")
16071801 if templ_7745c5c3_Err != nil {
16081802 return templ_7745c5c3_Err
16091803 }
1610- var templ_7745c5c3_Var81 string
1611- templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
1804+ var templ_7745c5c3_Var89 string
1805+ templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
1806+ if templ_7745c5c3_Err != nil {
1807+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 50}
1808+ }
1809+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89)
1810+ if templ_7745c5c3_Err != nil {
1811+ return templ_7745c5c3_Err
1812+ }
1813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
1814+ if templ_7745c5c3_Err != nil {
1815+ return templ_7745c5c3_Err
1816+ }
1817+ var templ_7745c5c3_Var90 string
1818+ templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine)
16121819 if templ_7745c5c3_Err != nil {
1613- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 289, Col: 50}
1820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 328, Col: 86}
16141821 }
1615- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var81)
1822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
16161823 if templ_7745c5c3_Err != nil {
16171824 return templ_7745c5c3_Err
16181825 }
1619- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\"> ")
1826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "\"></label> ")
1827+ if templ_7745c5c3_Err != nil {
1828+ return templ_7745c5c3_Err
1829+ }
1830+ }
1831+ if f.ReplyTo != "" {
1832+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "<input type=\"hidden\" name=\"reply_to\" value=\"")
1833+ if templ_7745c5c3_Err != nil {
1834+ return templ_7745c5c3_Err
1835+ }
1836+ var templ_7745c5c3_Var91 string
1837+ templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo)
1838+ if templ_7745c5c3_Err != nil {
1839+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 57}
1840+ }
1841+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91)
1842+ if templ_7745c5c3_Err != nil {
1843+ return templ_7745c5c3_Err
1844+ }
1845+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "\"> ")
1846+ if templ_7745c5c3_Err != nil {
1847+ return templ_7745c5c3_Err
1848+ }
1849+ }
1850+ if f.Path == "" && f.ReplyTo == "" {
1851+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "<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>")
16201852 if templ_7745c5c3_Err != nil {
16211853 return templ_7745c5c3_Err
16221854 }
16231855 }
16241856 if f.From > 0 {
1625- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<input type=\"hidden\" name=\"from\" value=\"")
1857+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "<input type=\"hidden\" name=\"from\" value=\"")
16261858 if templ_7745c5c3_Err != nil {
16271859 return templ_7745c5c3_Err
16281860 }
1629- var templ_7745c5c3_Var82 string
1630- templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
1861+ var templ_7745c5c3_Var92 string
1862+ templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
16311863 if templ_7745c5c3_Err != nil {
1632- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 292, Col: 56}
1864+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 337, Col: 56}
16331865 }
1634- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)
1866+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
16351867 if templ_7745c5c3_Err != nil {
16361868 return templ_7745c5c3_Err
16371869 }
1638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "\">")
1870+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "\">")
16391871 if templ_7745c5c3_Err != nil {
16401872 return templ_7745c5c3_Err
16411873 }
16421874 }
1643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
1875+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
16441876 if templ_7745c5c3_Err != nil {
16451877 return templ_7745c5c3_Err
16461878 }
16471879 if f.Cancel != "" {
1648- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<a class=\"btn\" href=\"")
1880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "<a class=\"btn\" href=\"")
16491881 if templ_7745c5c3_Err != nil {
16501882 return templ_7745c5c3_Err
16511883 }
1652- var templ_7745c5c3_Var83 templ.SafeURL
1653- templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
1884+ var templ_7745c5c3_Var93 templ.SafeURL
1885+ templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
16541886 if templ_7745c5c3_Err != nil {
1655- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 49}
1887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 342, Col: 49}
16561888 }
1657- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
1889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))
16581890 if templ_7745c5c3_Err != nil {
16591891 return templ_7745c5c3_Err
16601892 }
1661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\">Cancel</a>")
1893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "\">Cancel</a>")
16621894 if templ_7745c5c3_Err != nil {
16631895 return templ_7745c5c3_Err
16641896 }
16651897 }
1666- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "</div></form>")
1898+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "</div></form>")
16671899 if templ_7745c5c3_Err != nil {
16681900 return templ_7745c5c3_Err
16691901 }
@@ -1687,25 +1919,25 @@ func mrRail(p MRPage) templ.Component {
16871919 }()
16881920 }
16891921 ctx = templ.InitializeContext(ctx)
1690- templ_7745c5c3_Var84 := templ.GetChildren(ctx)
1691- if templ_7745c5c3_Var84 == nil {
1692- templ_7745c5c3_Var84 = templ.NopComponent
1922+ templ_7745c5c3_Var94 := templ.GetChildren(ctx)
1923+ if templ_7745c5c3_Var94 == nil {
1924+ templ_7745c5c3_Var94 = templ.NopComponent
16931925 }
16941926 ctx = templ.ClearChildren(ctx)
1695- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
1927+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
16961928 if templ_7745c5c3_Err != nil {
16971929 return templ_7745c5c3_Err
16981930 }
1699- var templ_7745c5c3_Var85 string
1700- templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
1931+ var templ_7745c5c3_Var95 string
1932+ templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
17011933 if templ_7745c5c3_Err != nil {
1702- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 306, Col: 49}
1934+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 351, Col: 49}
17031935 }
1704- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
1936+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))
17051937 if templ_7745c5c3_Err != nil {
17061938 return templ_7745c5c3_Err
17071939 }
1708- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "</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>")
1940+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "</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>")
17091941 if templ_7745c5c3_Err != nil {
17101942 return templ_7745c5c3_Err
17111943 }
@@ -1713,82 +1945,147 @@ func mrRail(p MRPage) templ.Component {
17131945 if templ_7745c5c3_Err != nil {
17141946 return templ_7745c5c3_Err
17151947 }
1716- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</section>")
1948+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "</section>")
17171949 if templ_7745c5c3_Err != nil {
17181950 return templ_7745c5c3_Err
17191951 }
1952+ if len(p.Verdicts) > 0 {
1953+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<section><h2>Verdicts</h2><ul>")
1954+ if templ_7745c5c3_Err != nil {
1955+ return templ_7745c5c3_Err
1956+ }
1957+ for _, v := range p.Verdicts {
1958+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<li><a href=\"")
1959+ if templ_7745c5c3_Err != nil {
1960+ return templ_7745c5c3_Err
1961+ }
1962+ var templ_7745c5c3_Var96 templ.SafeURL
1963+ templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
1964+ if templ_7745c5c3_Err != nil {
1965+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 364, Col: 37}
1966+ }
1967+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
1968+ if templ_7745c5c3_Err != nil {
1969+ return templ_7745c5c3_Err
1970+ }
1971+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\">")
1972+ if templ_7745c5c3_Err != nil {
1973+ return templ_7745c5c3_Err
1974+ }
1975+ var templ_7745c5c3_Var97 string
1976+ templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
1977+ if templ_7745c5c3_Err != nil {
1978+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 364, Col: 50}
1979+ }
1980+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97))
1981+ if templ_7745c5c3_Err != nil {
1982+ return templ_7745c5c3_Err
1983+ }
1984+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, ": ")
1985+ if templ_7745c5c3_Err != nil {
1986+ return templ_7745c5c3_Err
1987+ }
1988+ var templ_7745c5c3_Var98 string
1989+ templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
1990+ if templ_7745c5c3_Err != nil {
1991+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 364, Col: 63}
1992+ }
1993+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
1994+ if templ_7745c5c3_Err != nil {
1995+ return templ_7745c5c3_Err
1996+ }
1997+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</a> ")
1998+ if templ_7745c5c3_Err != nil {
1999+ return templ_7745c5c3_Err
2000+ }
2001+ if v.Stale {
2002+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<span class=\"mute\">(stale)</span>")
2003+ if templ_7745c5c3_Err != nil {
2004+ return templ_7745c5c3_Err
2005+ }
2006+ }
2007+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "</li>")
2008+ if templ_7745c5c3_Err != nil {
2009+ return templ_7745c5c3_Err
2010+ }
2011+ }
2012+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "</ul></section>")
2013+ if templ_7745c5c3_Err != nil {
2014+ return templ_7745c5c3_Err
2015+ }
2016+ }
17202017 templ_7745c5c3_Err = MRChecks(p.Checks).Render(ctx, templ_7745c5c3_Buffer)
17212018 if templ_7745c5c3_Err != nil {
17222019 return templ_7745c5c3_Err
17232020 }
1724- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
2021+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
17252022 if templ_7745c5c3_Err != nil {
17262023 return templ_7745c5c3_Err
17272024 }
1728- var templ_7745c5c3_Var86 string
1729- templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
2025+ var templ_7745c5c3_Var99 string
2026+ templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
17302027 if templ_7745c5c3_Err != nil {
1731- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 71}
2028+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 375, Col: 71}
17322029 }
1733- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86))
2030+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
17342031 if templ_7745c5c3_Err != nil {
17352032 return templ_7745c5c3_Err
17362033 }
1737- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "</span></h2><ul class=\"pcommits\">")
2034+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "</span></h2><ul class=\"pcommits\">")
17382035 if templ_7745c5c3_Err != nil {
17392036 return templ_7745c5c3_Err
17402037 }
17412038 for _, c := range p.Commits {
1742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "<li")
2039+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "<li")
17432040 if templ_7745c5c3_Err != nil {
17442041 return templ_7745c5c3_Err
17452042 }
17462043 if c.New {
1747- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, " data-new")
2044+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, " data-new")
17482045 if templ_7745c5c3_Err != nil {
17492046 return templ_7745c5c3_Err
17502047 }
17512048 }
1752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "><a href=\"")
2049+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "><a href=\"")
17532050 if templ_7745c5c3_Err != nil {
17542051 return templ_7745c5c3_Err
17552052 }
1756- var templ_7745c5c3_Var87 templ.SafeURL
1757- templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
2053+ var templ_7745c5c3_Var100 templ.SafeURL
2054+ templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
17582055 if templ_7745c5c3_Err != nil {
1759- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 36}
2056+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 379, Col: 36}
17602057 }
1761- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87))
2058+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
17622059 if templ_7745c5c3_Err != nil {
17632060 return templ_7745c5c3_Err
17642061 }
1765- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\">")
2062+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\">")
17662063 if templ_7745c5c3_Err != nil {
17672064 return templ_7745c5c3_Err
17682065 }
1769- var templ_7745c5c3_Var88 string
1770- templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
2066+ var templ_7745c5c3_Var101 string
2067+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
17712068 if templ_7745c5c3_Err != nil {
1772- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 48}
2069+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 379, Col: 48}
17732070 }
1774- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88))
2071+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
17752072 if templ_7745c5c3_Err != nil {
17762073 return templ_7745c5c3_Err
17772074 }
1778- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "</a> <span class=\"s\">")
2075+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "</a> <span class=\"s\">")
17792076 if templ_7745c5c3_Err != nil {
17802077 return templ_7745c5c3_Err
17812078 }
1782- var templ_7745c5c3_Var89 string
1783- templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
2079+ var templ_7745c5c3_Var102 string
2080+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
17842081 if templ_7745c5c3_Err != nil {
1785- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 320, Col: 32}
2082+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 380, Col: 32}
17862083 }
1787- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89))
2084+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
17882085 if templ_7745c5c3_Err != nil {
17892086 return templ_7745c5c3_Err
17902087 }
1791- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</span> ")
2088+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</span> ")
17922089 if templ_7745c5c3_Err != nil {
17932090 return templ_7745c5c3_Err
17942091 }
@@ -1798,138 +2095,138 @@ func mrRail(p MRPage) templ.Component {
17982095 return templ_7745c5c3_Err
17992096 }
18002097 }
1801- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<span class=\"mute\">")
2098+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<span class=\"mute\">")
18022099 if templ_7745c5c3_Err != nil {
18032100 return templ_7745c5c3_Err
18042101 }
1805- var templ_7745c5c3_Var90 string
1806- templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
2102+ var templ_7745c5c3_Var103 string
2103+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
18072104 if templ_7745c5c3_Err != nil {
1808- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 325, Col: 16}
2105+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 16}
18092106 }
1810- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90))
2107+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
18112108 if templ_7745c5c3_Err != nil {
18122109 return templ_7745c5c3_Err
18132110 }
1814- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, " ")
2111+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, " ")
18152112 if templ_7745c5c3_Err != nil {
18162113 return templ_7745c5c3_Err
18172114 }
18182115 if c.WhenAt != "" {
1819- var templ_7745c5c3_Var91 string
1820- templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
2116+ var templ_7745c5c3_Var104 string
2117+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
18212118 if templ_7745c5c3_Err != nil {
1822- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 15}
2119+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 15}
18232120 }
1824- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91))
2121+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
18252122 if templ_7745c5c3_Err != nil {
18262123 return templ_7745c5c3_Err
18272124 }
1828- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "<time datetime=\"")
2125+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "<time datetime=\"")
18292126 if templ_7745c5c3_Err != nil {
18302127 return templ_7745c5c3_Err
18312128 }
1832- var templ_7745c5c3_Var92 string
1833- templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
2129+ var templ_7745c5c3_Var105 string
2130+ templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
18342131 if templ_7745c5c3_Err != nil {
1835- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 42}
2132+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 42}
18362133 }
1837- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
2134+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105)
18382135 if templ_7745c5c3_Err != nil {
18392136 return templ_7745c5c3_Err
18402137 }
1841- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "\">")
2138+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\">")
18422139 if templ_7745c5c3_Err != nil {
18432140 return templ_7745c5c3_Err
18442141 }
1845- var templ_7745c5c3_Var93 string
1846- templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
2142+ var templ_7745c5c3_Var106 string
2143+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
18472144 if templ_7745c5c3_Err != nil {
1848- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 53}
2145+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 53}
18492146 }
1850- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))
2147+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
18512148 if templ_7745c5c3_Err != nil {
18522149 return templ_7745c5c3_Err
18532150 }
1854- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</time> ")
2151+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</time> ")
18552152 if templ_7745c5c3_Err != nil {
18562153 return templ_7745c5c3_Err
18572154 }
18582155 }
18592156 if c.New {
1860- var templ_7745c5c3_Var94 string
1861- templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
2157+ var templ_7745c5c3_Var107 string
2158+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
18622159 if templ_7745c5c3_Err != nil {
1863- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 330, Col: 18}
2160+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 390, Col: 18}
18642161 }
1865- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94))
2162+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
18662163 if templ_7745c5c3_Err != nil {
18672164 return templ_7745c5c3_Err
18682165 }
18692166 }
1870- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "</span></li>")
2167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</span></li>")
18712168 if templ_7745c5c3_Err != nil {
18722169 return templ_7745c5c3_Err
18732170 }
18742171 }
1875- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "</ul>")
2172+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</ul>")
18762173 if templ_7745c5c3_Err != nil {
18772174 return templ_7745c5c3_Err
18782175 }
18792176 if len(p.Commits) == 0 {
1880- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "<p class=\"dim\">no commit to merge.</p>")
2177+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "<p class=\"dim\">no commit to merge.</p>")
18812178 if templ_7745c5c3_Err != nil {
18822179 return templ_7745c5c3_Err
18832180 }
18842181 }
18852182 if p.CommitsNote != "" {
1886- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "<p class=\"note\">")
2183+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"note\">")
18872184 if templ_7745c5c3_Err != nil {
18882185 return templ_7745c5c3_Err
18892186 }
1890- var templ_7745c5c3_Var95 string
1891- templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
2187+ var templ_7745c5c3_Var108 string
2188+ templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
18922189 if templ_7745c5c3_Err != nil {
1893- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 340, Col: 34}
2190+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 34}
18942191 }
1895- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))
2192+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
18962193 if templ_7745c5c3_Err != nil {
18972194 return templ_7745c5c3_Err
18982195 }
1899- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</p>")
2196+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</p>")
19002197 if templ_7745c5c3_Err != nil {
19012198 return templ_7745c5c3_Err
19022199 }
19032200 }
1904- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
2201+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
19052202 if templ_7745c5c3_Err != nil {
19062203 return templ_7745c5c3_Err
19072204 }
1908- var templ_7745c5c3_Var96 string
1909- templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
2205+ var templ_7745c5c3_Var109 string
2206+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
19102207 if templ_7745c5c3_Err != nil {
1911- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 347, Col: 36}
2208+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 407, Col: 36}
19122209 }
1913- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
2210+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
19142211 if templ_7745c5c3_Err != nil {
19152212 return templ_7745c5c3_Err
19162213 }
1917- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "</dd><dt>checks</dt><dd>")
2214+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "</dd><dt>checks</dt><dd>")
19182215 if templ_7745c5c3_Err != nil {
19192216 return templ_7745c5c3_Err
19202217 }
19212218 if p.ChecksBlock {
1922- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "failing ones block the merge")
2219+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "failing ones block the merge")
19232220 if templ_7745c5c3_Err != nil {
19242221 return templ_7745c5c3_Err
19252222 }
19262223 } else {
1927- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "do not block the merge")
2224+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "do not block the merge")
19282225 if templ_7745c5c3_Err != nil {
19292226 return templ_7745c5c3_Err
19302227 }
19312228 }
1932- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "</dd></dl></section>")
2229+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</dd></dl></section>")
19332230 if templ_7745c5c3_Err != nil {
19342231 return templ_7745c5c3_Err
19352232 }
@@ -1957,51 +2254,51 @@ func mrBar(p MRPage) templ.Component {
19572254 }()
19582255 }
19592256 ctx = templ.InitializeContext(ctx)
1960- templ_7745c5c3_Var97 := templ.GetChildren(ctx)
1961- if templ_7745c5c3_Var97 == nil {
1962- templ_7745c5c3_Var97 = templ.NopComponent
2257+ templ_7745c5c3_Var110 := templ.GetChildren(ctx)
2258+ if templ_7745c5c3_Var110 == nil {
2259+ templ_7745c5c3_Var110 = templ.NopComponent
19632260 }
19642261 ctx = templ.ClearChildren(ctx)
1965- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")
2262+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")
19662263 if templ_7745c5c3_Err != nil {
19672264 return templ_7745c5c3_Err
19682265 }
1969- var templ_7745c5c3_Var98 string
1970- templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
2266+ var templ_7745c5c3_Var111 string
2267+ templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
19712268 if templ_7745c5c3_Err != nil {
1972- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 366, Col: 55}
2269+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 55}
19732270 }
1974- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
2271+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
19752272 if templ_7745c5c3_Err != nil {
19762273 return templ_7745c5c3_Err
19772274 }
1978- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, " · <span class=\"plus\">+")
2275+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, " · <span class=\"plus\">+")
19792276 if templ_7745c5c3_Err != nil {
19802277 return templ_7745c5c3_Err
19812278 }
1982- var templ_7745c5c3_Var99 string
1983- templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
2279+ var templ_7745c5c3_Var112 string
2280+ templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
19842281 if templ_7745c5c3_Err != nil {
1985- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 366, Col: 95}
2282+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 95}
19862283 }
1987- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
2284+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
19882285 if templ_7745c5c3_Err != nil {
19892286 return templ_7745c5c3_Err
19902287 }
1991- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "</span> <span class=\"minus\">-")
2288+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "</span> <span class=\"minus\">-")
19922289 if templ_7745c5c3_Err != nil {
19932290 return templ_7745c5c3_Err
19942291 }
1995- var templ_7745c5c3_Var100 string
1996- templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
2292+ var templ_7745c5c3_Var113 string
2293+ templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
19972294 if templ_7745c5c3_Err != nil {
1998- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 366, Col: 140}
2295+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 140}
19992296 }
2000- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
2297+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
20012298 if templ_7745c5c3_Err != nil {
20022299 return templ_7745c5c3_Err
20032300 }
2004- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "</span></span>")
2301+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "</span></span>")
20052302 if templ_7745c5c3_Err != nil {
20062303 return templ_7745c5c3_Err
20072304 }
@@ -2017,7 +2314,7 @@ func mrBar(p MRPage) templ.Component {
20172314 if templ_7745c5c3_Err != nil {
20182315 return templ_7745c5c3_Err
20192316 }
2020- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "<span class=\"grow\"></span>")
2317+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "<span class=\"grow\"></span>")
20212318 if templ_7745c5c3_Err != nil {
20222319 return templ_7745c5c3_Err
20232320 }
@@ -2030,7 +2327,7 @@ func mrBar(p MRPage) templ.Component {
20302327 if templ_7745c5c3_Err != nil {
20312328 return templ_7745c5c3_Err
20322329 }
2033- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, " ")
2330+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " ")
20342331 if templ_7745c5c3_Err != nil {
20352332 return templ_7745c5c3_Err
20362333 }
@@ -2039,7 +2336,7 @@ func mrBar(p MRPage) templ.Component {
20392336 return templ_7745c5c3_Err
20402337 }
20412338 }
2042- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</div>")
2339+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</div>")
20432340 if templ_7745c5c3_Err != nil {
20442341 return templ_7745c5c3_Err
20452342 }
@@ -2066,219 +2363,219 @@ func mergeMenu(p MRPage) templ.Component {
20662363 }()
20672364 }
20682365 ctx = templ.InitializeContext(ctx)
2069- templ_7745c5c3_Var101 := templ.GetChildren(ctx)
2070- if templ_7745c5c3_Var101 == nil {
2071- templ_7745c5c3_Var101 = templ.NopComponent
2366+ templ_7745c5c3_Var114 := templ.GetChildren(ctx)
2367+ if templ_7745c5c3_Var114 == nil {
2368+ templ_7745c5c3_Var114 = templ.NopComponent
20722369 }
20732370 ctx = templ.ClearChildren(ctx)
2074- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
2371+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
20752372 if templ_7745c5c3_Err != nil {
20762373 return templ_7745c5c3_Err
20772374 }
20782375 if p.Box.ReadyAction != "" {
2079- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "Draft ")
2376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "Draft ")
20802377 if templ_7745c5c3_Err != nil {
20812378 return templ_7745c5c3_Err
20822379 }
20832380 } else {
2084- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "Merge ")
2381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "Merge ")
20852382 if templ_7745c5c3_Err != nil {
20862383 return templ_7745c5c3_Err
20872384 }
20882385 }
2089- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>")
2386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>")
20902387 if templ_7745c5c3_Err != nil {
20912388 return templ_7745c5c3_Err
20922389 }
20932390 if p.Box.HeadGone {
2094- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "gone: the source branch was deleted")
2391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "gone: the source branch was deleted")
20952392 if templ_7745c5c3_Err != nil {
20962393 return templ_7745c5c3_Err
20972394 }
20982395 } else {
2099- var templ_7745c5c3_Var102 string
2100- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
2396+ var templ_7745c5c3_Var115 string
2397+ templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
21012398 if templ_7745c5c3_Err != nil {
2102- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 399, Col: 19}
2399+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 19}
21032400 }
2104- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
2401+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
21052402 if templ_7745c5c3_Err != nil {
21062403 return templ_7745c5c3_Err
21072404 }
21082405 }
2109- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "</dd><dt>into</dt><dd>")
2406+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</dd><dt>into</dt><dd>")
21102407 if templ_7745c5c3_Err != nil {
21112408 return templ_7745c5c3_Err
21122409 }
2113- var templ_7745c5c3_Var103 string
2114- templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2410+ var templ_7745c5c3_Var116 string
2411+ templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
21152412 if templ_7745c5c3_Err != nil {
2116- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 403, Col: 18}
2413+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 463, Col: 18}
21172414 }
2118- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
2415+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
21192416 if templ_7745c5c3_Err != nil {
21202417 return templ_7745c5c3_Err
21212418 }
2122- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</dd></dl>")
2419+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</dd></dl>")
21232420 if templ_7745c5c3_Err != nil {
21242421 return templ_7745c5c3_Err
21252422 }
21262423 if p.Box.ReadyAction != "" {
2127- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<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=\"")
2424+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "<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=\"")
21282425 if templ_7745c5c3_Err != nil {
21292426 return templ_7745c5c3_Err
21302427 }
2131- var templ_7745c5c3_Var104 templ.SafeURL
2132- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
2428+ var templ_7745c5c3_Var117 templ.SafeURL
2429+ templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
21332430 if templ_7745c5c3_Err != nil {
2134- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 407, Col: 80}
2431+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 467, Col: 80}
21352432 }
2136- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
2433+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
21372434 if templ_7745c5c3_Err != nil {
21382435 return templ_7745c5c3_Err
21392436 }
2140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")
2437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")
21412438 if templ_7745c5c3_Err != nil {
21422439 return templ_7745c5c3_Err
21432440 }
21442441 } else if !p.Box.HeadGone {
2145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<form class=\"d-acts\" method=\"post\" action=\"")
2442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "<form class=\"d-acts\" method=\"post\" action=\"")
21462443 if templ_7745c5c3_Err != nil {
21472444 return templ_7745c5c3_Err
21482445 }
2149- var templ_7745c5c3_Var105 templ.SafeURL
2150- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2446+ var templ_7745c5c3_Var118 templ.SafeURL
2447+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
21512448 if templ_7745c5c3_Err != nil {
2152- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 80}
2449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 471, Col: 80}
21532450 }
2154- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))
2451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
21552452 if templ_7745c5c3_Err != nil {
21562453 return templ_7745c5c3_Err
21572454 }
2158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
21592456 if templ_7745c5c3_Err != nil {
21602457 return templ_7745c5c3_Err
21612458 }
2162- var templ_7745c5c3_Var106 string
2163- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2459+ var templ_7745c5c3_Var119 string
2460+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
21642461 if templ_7745c5c3_Err != nil {
2165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 412, Col: 59}
2462+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 472, Col: 59}
21662463 }
2167- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var106)
2464+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var119)
21682465 if templ_7745c5c3_Err != nil {
21692466 return templ_7745c5c3_Err
21702467 }
2171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
2468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
21722469 if templ_7745c5c3_Err != nil {
21732470 return templ_7745c5c3_Err
21742471 }
21752472 for i, m := range p.Box.Modes {
2176- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "<label><input type=\"radio\" name=\"mode\" value=\"")
2473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<label><input type=\"radio\" name=\"mode\" value=\"")
21772474 if templ_7745c5c3_Err != nil {
21782475 return templ_7745c5c3_Err
21792476 }
2180- var templ_7745c5c3_Var107 string
2181- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2477+ var templ_7745c5c3_Var120 string
2478+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
21822479 if templ_7745c5c3_Err != nil {
2183- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 417, Col: 55}
2480+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 477, Col: 55}
21842481 }
2185- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107)
2482+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var120)
21862483 if templ_7745c5c3_Err != nil {
21872484 return templ_7745c5c3_Err
21882485 }
2189- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "\"")
2486+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "\"")
21902487 if templ_7745c5c3_Err != nil {
21912488 return templ_7745c5c3_Err
21922489 }
21932490 if i == 0 {
2194- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, " checked")
2491+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, " checked")
21952492 if templ_7745c5c3_Err != nil {
21962493 return templ_7745c5c3_Err
21972494 }
21982495 }
2199- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "> ")
2496+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "> ")
22002497 if templ_7745c5c3_Err != nil {
22012498 return templ_7745c5c3_Err
22022499 }
2203- var templ_7745c5c3_Var108 string
2204- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2500+ var templ_7745c5c3_Var121 string
2501+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
22052502 if templ_7745c5c3_Err != nil {
2206- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 17}
2503+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 478, Col: 17}
22072504 }
2208- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
2505+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
22092506 if templ_7745c5c3_Err != nil {
22102507 return templ_7745c5c3_Err
22112508 }
2212- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, " ")
2509+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " ")
22132510 if templ_7745c5c3_Err != nil {
22142511 return templ_7745c5c3_Err
22152512 }
22162513 if i < 9 {
2217- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "<kbd aria-hidden=\"true\">")
2514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<kbd aria-hidden=\"true\">")
22182515 if templ_7745c5c3_Err != nil {
22192516 return templ_7745c5c3_Err
22202517 }
2221- var templ_7745c5c3_Var109 string
2222- templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
2518+ var templ_7745c5c3_Var122 string
2519+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
22232520 if templ_7745c5c3_Err != nil {
2224- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 46}
2521+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 46}
22252522 }
2226- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
2523+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
22272524 if templ_7745c5c3_Err != nil {
22282525 return templ_7745c5c3_Err
22292526 }
2230- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</kbd>")
2527+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "</kbd>")
22312528 if templ_7745c5c3_Err != nil {
22322529 return templ_7745c5c3_Err
22332530 }
22342531 }
2235- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "</label>")
2532+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "</label>")
22362533 if templ_7745c5c3_Err != nil {
22372534 return templ_7745c5c3_Err
22382535 }
22392536 }
2240- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</fieldset>")
2537+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</fieldset>")
22412538 if templ_7745c5c3_Err != nil {
22422539 return templ_7745c5c3_Err
22432540 }
22442541 if p.Box.CanDeleteBranch {
2245- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2542+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
22462543 if templ_7745c5c3_Err != nil {
22472544 return templ_7745c5c3_Err
22482545 }
2249- var templ_7745c5c3_Var110 string
2250- templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2546+ var templ_7745c5c3_Var123 string
2547+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
22512548 if templ_7745c5c3_Err != nil {
2252- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 102}
2549+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 102}
22532550 }
2254- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))
2551+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
22552552 if templ_7745c5c3_Err != nil {
22562553 return templ_7745c5c3_Err
22572554 }
2258- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2555+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
22592556 if templ_7745c5c3_Err != nil {
22602557 return templ_7745c5c3_Err
22612558 }
22622559 }
2263- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default>Merge into ")
2560+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default>Merge into ")
22642561 if templ_7745c5c3_Err != nil {
22652562 return templ_7745c5c3_Err
22662563 }
2267- var templ_7745c5c3_Var111 string
2268- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2564+ var templ_7745c5c3_Var124 string
2565+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
22692566 if templ_7745c5c3_Err != nil {
2270- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 82}
2567+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 82}
22712568 }
2272- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
2569+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
22732570 if templ_7745c5c3_Err != nil {
22742571 return templ_7745c5c3_Err
22752572 }
2276- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</button></div></form>")
2573+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</button></div></form>")
22772574 if templ_7745c5c3_Err != nil {
22782575 return templ_7745c5c3_Err
22792576 }
22802577 }
2281- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</div></details>")
2578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</div></details>")
22822579 if templ_7745c5c3_Err != nil {
22832580 return templ_7745c5c3_Err
22842581 }
@@ -2304,66 +2601,66 @@ func moreMenu(p MRPage) templ.Component {
23042601 }()
23052602 }
23062603 ctx = templ.InitializeContext(ctx)
2307- templ_7745c5c3_Var112 := templ.GetChildren(ctx)
2308- if templ_7745c5c3_Var112 == nil {
2309- templ_7745c5c3_Var112 = templ.NopComponent
2604+ templ_7745c5c3_Var125 := templ.GetChildren(ctx)
2605+ if templ_7745c5c3_Var125 == nil {
2606+ templ_7745c5c3_Var125 = templ.NopComponent
23102607 }
23112608 ctx = templ.ClearChildren(ctx)
2312- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<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\">")
2609+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "<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\">")
23132610 if templ_7745c5c3_Err != nil {
23142611 return templ_7745c5c3_Err
23152612 }
23162613 if p.Box.CanMarkRead {
2317- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "<form method=\"post\" action=\"")
2614+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "<form method=\"post\" action=\"")
23182615 if templ_7745c5c3_Err != nil {
23192616 return templ_7745c5c3_Err
23202617 }
2321- var templ_7745c5c3_Var113 templ.SafeURL
2322- templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
2618+ var templ_7745c5c3_Var126 templ.SafeURL
2619+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
23232620 if templ_7745c5c3_Err != nil {
2324- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 64}
2621+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 504, Col: 64}
23252622 }
2326- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
2623+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
23272624 if templ_7745c5c3_Err != nil {
23282625 return templ_7745c5c3_Err
23292626 }
2330- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2627+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
23312628 if templ_7745c5c3_Err != nil {
23322629 return templ_7745c5c3_Err
23332630 }
2334- var templ_7745c5c3_Var114 string
2335- templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2631+ var templ_7745c5c3_Var127 string
2632+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
23362633 if templ_7745c5c3_Err != nil {
2337- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 445, Col: 59}
2634+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 505, Col: 59}
23382635 }
2339- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var114)
2636+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127)
23402637 if templ_7745c5c3_Err != nil {
23412638 return templ_7745c5c3_Err
23422639 }
2343- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "\"> <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>")
2640+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "\"> <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>")
23442641 if templ_7745c5c3_Err != nil {
23452642 return templ_7745c5c3_Err
23462643 }
23472644 }
2348- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "<form method=\"post\" action=\"")
2645+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "<form method=\"post\" action=\"")
23492646 if templ_7745c5c3_Err != nil {
23502647 return templ_7745c5c3_Err
23512648 }
2352- var templ_7745c5c3_Var115 templ.SafeURL
2353- templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
2649+ var templ_7745c5c3_Var128 templ.SafeURL
2650+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
23542651 if templ_7745c5c3_Err != nil {
2355- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 64}
2652+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 509, Col: 64}
23562653 }
2357- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
2654+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
23582655 if templ_7745c5c3_Err != nil {
23592656 return templ_7745c5c3_Err
23602657 }
2361- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
2658+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
23622659 if templ_7745c5c3_Err != nil {
23632660 return templ_7745c5c3_Err
23642661 }
23652662 if !p.Box.HeadGone && p.Box.ReadyAction == "" {
2366- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
2663+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
23672664 if templ_7745c5c3_Err != nil {
23682665 return templ_7745c5c3_Err
23692666 }
@@ -2371,12 +2668,12 @@ func moreMenu(p MRPage) templ.Component {
23712668 if templ_7745c5c3_Err != nil {
23722669 return templ_7745c5c3_Err
23732670 }
2374- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "</details>")
2671+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "</details>")
23752672 if templ_7745c5c3_Err != nil {
23762673 return templ_7745c5c3_Err
23772674 }
23782675 }
2379- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</div></details>")
2676+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "</div></details>")
23802677 if templ_7745c5c3_Err != nil {
23812678 return templ_7745c5c3_Err
23822679 }
@@ -2400,12 +2697,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
24002697 }()
24012698 }
24022699 ctx = templ.InitializeContext(ctx)
2403- templ_7745c5c3_Var116 := templ.GetChildren(ctx)
2404- if templ_7745c5c3_Var116 == nil {
2405- templ_7745c5c3_Var116 = templ.NopComponent
2700+ templ_7745c5c3_Var129 := templ.GetChildren(ctx)
2701+ if templ_7745c5c3_Var129 == nil {
2702+ templ_7745c5c3_Var129 = templ.NopComponent
24062703 }
24072704 ctx = templ.ClearChildren(ctx)
2408- templ_7745c5c3_Var117 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2705+ templ_7745c5c3_Var130 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
24092706 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
24102707 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
24112708 if !templ_7745c5c3_IsBuffer {
@@ -2417,263 +2714,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
24172714 }()
24182715 }
24192716 ctx = templ.InitializeContext(ctx)
2420- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "<header class=\"show mr-h\"><h1>")
2717+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "<header class=\"show mr-h\"><h1>")
24212718 if templ_7745c5c3_Err != nil {
24222719 return templ_7745c5c3_Err
24232720 }
2424- var templ_7745c5c3_Var118 string
2425- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2721+ var templ_7745c5c3_Var131 string
2722+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
24262723 if templ_7745c5c3_Err != nil {
2427- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 465, Col: 17}
2724+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 17}
24282725 }
2429- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2726+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
24302727 if templ_7745c5c3_Err != nil {
24312728 return templ_7745c5c3_Err
24322729 }
2433- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " → ")
2730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, " → ")
24342731 if templ_7745c5c3_Err != nil {
24352732 return templ_7745c5c3_Err
24362733 }
2437- var templ_7745c5c3_Var119 string
2438- templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2734+ var templ_7745c5c3_Var132 string
2735+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
24392736 if templ_7745c5c3_Err != nil {
2440- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 465, Col: 34}
2737+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 34}
24412738 }
2442- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))
2739+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
24432740 if templ_7745c5c3_Err != nil {
24442741 return templ_7745c5c3_Err
24452742 }
2446- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
2743+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
24472744 if templ_7745c5c3_Err != nil {
24482745 return templ_7745c5c3_Err
24492746 }
2450- var templ_7745c5c3_Var120 templ.SafeURL
2451- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
2747+ var templ_7745c5c3_Var133 templ.SafeURL
2748+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
24522749 if templ_7745c5c3_Err != nil {
2453- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 468, Col: 45}
2750+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 528, Col: 45}
24542751 }
2455- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2752+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
24562753 if templ_7745c5c3_Err != nil {
24572754 return templ_7745c5c3_Err
24582755 }
2459- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "\">")
2756+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "\">")
24602757 if templ_7745c5c3_Err != nil {
24612758 return templ_7745c5c3_Err
24622759 }
2463- var templ_7745c5c3_Var121 string
2464- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2760+ var templ_7745c5c3_Var134 string
2761+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
24652762 if templ_7745c5c3_Err != nil {
2466- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 468, Col: 58}
2763+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 528, Col: 58}
24672764 }
2468- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2765+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
24692766 if templ_7745c5c3_Err != nil {
24702767 return templ_7745c5c3_Err
24712768 }
2472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</a></dd><dt>target</dt><dd><a href=\"")
2769+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "</a></dd><dt>target</dt><dd><a href=\"")
24732770 if templ_7745c5c3_Err != nil {
24742771 return templ_7745c5c3_Err
24752772 }
2476- var templ_7745c5c3_Var122 templ.SafeURL
2477- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
2773+ var templ_7745c5c3_Var135 templ.SafeURL
2774+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
24782775 if templ_7745c5c3_Err != nil {
2479- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 45}
2776+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 530, Col: 45}
24802777 }
2481- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2778+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))
24822779 if templ_7745c5c3_Err != nil {
24832780 return templ_7745c5c3_Err
24842781 }
2485- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "\">")
2782+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "\">")
24862783 if templ_7745c5c3_Err != nil {
24872784 return templ_7745c5c3_Err
24882785 }
2489- var templ_7745c5c3_Var123 string
2490- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2786+ var templ_7745c5c3_Var136 string
2787+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
24912788 if templ_7745c5c3_Err != nil {
2492- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 58}
2789+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 530, Col: 58}
24932790 }
2494- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
2791+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
24952792 if templ_7745c5c3_Err != nil {
24962793 return templ_7745c5c3_Err
24972794 }
2498- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "</a></dd><dt>ahead</dt><dd>")
2795+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</a></dd><dt>ahead</dt><dd>")
24992796 if templ_7745c5c3_Err != nil {
25002797 return templ_7745c5c3_Err
25012798 }
2502- var templ_7745c5c3_Var124 string
2503- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
2799+ var templ_7745c5c3_Var137 string
2800+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
25042801 if templ_7745c5c3_Err != nil {
2505- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 472, Col: 21}
2802+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 21}
25062803 }
2507- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
2804+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
25082805 if templ_7745c5c3_Err != nil {
25092806 return templ_7745c5c3_Err
25102807 }
2511- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</dd></dl></header>")
2808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "</dd></dl></header>")
25122809 if templ_7745c5c3_Err != nil {
25132810 return templ_7745c5c3_Err
25142811 }
25152812 if p.Note != "" {
2516- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "<p class=\"notice\">")
2813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "<p class=\"notice\">")
25172814 if templ_7745c5c3_Err != nil {
25182815 return templ_7745c5c3_Err
25192816 }
2520- var templ_7745c5c3_Var125 string
2521- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
2817+ var templ_7745c5c3_Var138 string
2818+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
25222819 if templ_7745c5c3_Err != nil {
2523- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 476, Col: 29}
2820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 536, Col: 29}
25242821 }
2525- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
2822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
25262823 if templ_7745c5c3_Err != nil {
25272824 return templ_7745c5c3_Err
25282825 }
2529- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</p>")
2826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "</p>")
25302827 if templ_7745c5c3_Err != nil {
25312828 return templ_7745c5c3_Err
25322829 }
25332830 }
2534- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, " ")
2831+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, " ")
25352832 if templ_7745c5c3_Err != nil {
25362833 return templ_7745c5c3_Err
25372834 }
25382835 if p.Existing != nil {
2539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
2836+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
25402837 if templ_7745c5c3_Err != nil {
25412838 return templ_7745c5c3_Err
25422839 }
2543- var templ_7745c5c3_Var126 templ.SafeURL
2544- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
2840+ var templ_7745c5c3_Var139 templ.SafeURL
2841+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
25452842 if templ_7745c5c3_Err != nil {
2546- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 114}
2843+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 539, Col: 114}
25472844 }
2548- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2845+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
25492846 if templ_7745c5c3_Err != nil {
25502847 return templ_7745c5c3_Err
25512848 }
2552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "\">")
2849+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "\">")
25532850 if templ_7745c5c3_Err != nil {
25542851 return templ_7745c5c3_Err
25552852 }
2556- var templ_7745c5c3_Var127 string
2557- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
2853+ var templ_7745c5c3_Var140 string
2854+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
25582855 if templ_7745c5c3_Err != nil {
2559- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 135}
2856+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 539, Col: 135}
25602857 }
2561- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2858+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
25622859 if templ_7745c5c3_Err != nil {
25632860 return templ_7745c5c3_Err
25642861 }
2565- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</a></p>")
2862+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</a></p>")
25662863 if templ_7745c5c3_Err != nil {
25672864 return templ_7745c5c3_Err
25682865 }
25692866 }
2570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " ")
2867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, " ")
25712868 if templ_7745c5c3_Err != nil {
25722869 return templ_7745c5c3_Err
25732870 }
25742871 if p.Create != nil {
2575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<form class=\"cform create\" method=\"post\" action=\"")
2872+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<form class=\"cform create\" method=\"post\" action=\"")
25762873 if templ_7745c5c3_Err != nil {
25772874 return templ_7745c5c3_Err
25782875 }
2579- var templ_7745c5c3_Var128 templ.SafeURL
2580- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
2876+ var templ_7745c5c3_Var141 templ.SafeURL
2877+ templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
25812878 if templ_7745c5c3_Err != nil {
2582- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 83}
2879+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 542, Col: 83}
25832880 }
2584- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2881+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
25852882 if templ_7745c5c3_Err != nil {
25862883 return templ_7745c5c3_Err
25872884 }
2588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "\"><input type=\"hidden\" name=\"source\" value=\"")
2885+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\"><input type=\"hidden\" name=\"source\" value=\"")
25892886 if templ_7745c5c3_Err != nil {
25902887 return templ_7745c5c3_Err
25912888 }
2592- var templ_7745c5c3_Var129 string
2593- templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
2889+ var templ_7745c5c3_Var142 string
2890+ templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
25942891 if templ_7745c5c3_Err != nil {
2595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 483, Col: 62}
2892+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 62}
25962893 }
2597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)
2894+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
25982895 if templ_7745c5c3_Err != nil {
25992896 return templ_7745c5c3_Err
26002897 }
2601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "\"> <input type=\"hidden\" name=\"target\" value=\"")
2898+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\"> <input type=\"hidden\" name=\"target\" value=\"")
26022899 if templ_7745c5c3_Err != nil {
26032900 return templ_7745c5c3_Err
26042901 }
2605- var templ_7745c5c3_Var130 string
2606- templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
2902+ var templ_7745c5c3_Var143 string
2903+ templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
26072904 if templ_7745c5c3_Err != nil {
2608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 62}
2905+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 544, Col: 62}
26092906 }
2610- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
2907+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
26112908 if templ_7745c5c3_Err != nil {
26122909 return templ_7745c5c3_Err
26132910 }
2614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
2911+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
26152912 if templ_7745c5c3_Err != nil {
26162913 return templ_7745c5c3_Err
26172914 }
2618- var templ_7745c5c3_Var131 string
2619- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
2915+ var templ_7745c5c3_Var144 string
2916+ templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
26202917 if templ_7745c5c3_Err != nil {
2621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 97}
2918+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 97}
26222919 }
2623- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)
2920+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
26242921 if templ_7745c5c3_Err != nil {
26252922 return templ_7745c5c3_Err
26262923 }
2627- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "\" 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>")
2924+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\" 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>")
26282925 if templ_7745c5c3_Err != nil {
26292926 return templ_7745c5c3_Err
26302927 }
26312928 }
2632- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, " ")
2929+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, " ")
26332930 if templ_7745c5c3_Err != nil {
26342931 return templ_7745c5c3_Err
26352932 }
26362933 if p.HasDiff {
2637- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
2934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
26382935 if templ_7745c5c3_Err != nil {
26392936 return templ_7745c5c3_Err
26402937 }
2641- var templ_7745c5c3_Var132 string
2642- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
2938+ var templ_7745c5c3_Var145 string
2939+ templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
26432940 if templ_7745c5c3_Err != nil {
2644- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 57}
2941+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 57}
26452942 }
2646- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
2943+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
26472944 if templ_7745c5c3_Err != nil {
26482945 return templ_7745c5c3_Err
26492946 }
2650- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, " · <span class=\"plus\">+")
2947+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, " · <span class=\"plus\">+")
26512948 if templ_7745c5c3_Err != nil {
26522949 return templ_7745c5c3_Err
26532950 }
2654- var templ_7745c5c3_Var133 string
2655- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
2951+ var templ_7745c5c3_Var146 string
2952+ templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
26562953 if templ_7745c5c3_Err != nil {
2657- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 97}
2954+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 97}
26582955 }
2659- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
2956+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
26602957 if templ_7745c5c3_Err != nil {
26612958 return templ_7745c5c3_Err
26622959 }
2663- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</span> <span class=\"minus\">-")
2960+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span> <span class=\"minus\">-")
26642961 if templ_7745c5c3_Err != nil {
26652962 return templ_7745c5c3_Err
26662963 }
2667- var templ_7745c5c3_Var134 string
2668- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
2964+ var templ_7745c5c3_Var147 string
2965+ templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
26692966 if templ_7745c5c3_Err != nil {
2670- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 142}
2967+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 142}
26712968 }
2672- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
2969+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
26732970 if templ_7745c5c3_Err != nil {
26742971 return templ_7745c5c3_Err
26752972 }
2676- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></span> <span class=\"grow\"></span>")
2973+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "</span></span> <span class=\"grow\"></span>")
26772974 if templ_7745c5c3_Err != nil {
26782975 return templ_7745c5c3_Err
26792976 }
@@ -2689,64 +2986,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
26892986 if templ_7745c5c3_Err != nil {
26902987 return templ_7745c5c3_Err
26912988 }
2692- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</div>")
2989+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</div>")
26932990 if templ_7745c5c3_Err != nil {
26942991 return templ_7745c5c3_Err
26952992 }
26962993 if p.Invisible {
2697- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<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>")
2994+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<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>")
26982995 if templ_7745c5c3_Err != nil {
26992996 return templ_7745c5c3_Err
27002997 }
27012998 }
2702- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, " <div id=\"diffs\"")
2999+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, " <div id=\"diffs\"")
27033000 if templ_7745c5c3_Err != nil {
27043001 return templ_7745c5c3_Err
27053002 }
27063003 if p.Both {
2707- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, " data-diff=\"")
3004+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, " data-diff=\"")
27083005 if templ_7745c5c3_Err != nil {
27093006 return templ_7745c5c3_Err
27103007 }
2711- var templ_7745c5c3_Var135 string
2712- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3008+ var templ_7745c5c3_Var148 string
3009+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
27133010 if templ_7745c5c3_Err != nil {
2714- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 23}
3011+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 571, Col: 23}
27153012 }
2716- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
3013+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148)
27173014 if templ_7745c5c3_Err != nil {
27183015 return templ_7745c5c3_Err
27193016 }
2720- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "\"")
3017+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "\"")
27213018 if templ_7745c5c3_Err != nil {
27223019 return templ_7745c5c3_Err
27233020 }
27243021 } else {
2725- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " data-view=\"")
3022+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, " data-view=\"")
27263023 if templ_7745c5c3_Err != nil {
27273024 return templ_7745c5c3_Err
27283025 }
2729- var templ_7745c5c3_Var136 string
2730- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3026+ var templ_7745c5c3_Var149 string
3027+ templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
27313028 if templ_7745c5c3_Err != nil {
2732- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 513, Col: 23}
3029+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 573, Col: 23}
27333030 }
2734- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136)
3031+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149)
27353032 if templ_7745c5c3_Err != nil {
27363033 return templ_7745c5c3_Err
27373034 }
2738- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "\"")
3035+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "\"")
27393036 if templ_7745c5c3_Err != nil {
27403037 return templ_7745c5c3_Err
27413038 }
27423039 }
27433040 if p.Both && !p.Pinned {
2744- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
3041+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
27453042 if templ_7745c5c3_Err != nil {
27463043 return templ_7745c5c3_Err
27473044 }
27483045 }
2749- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, ">")
3046+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, ">")
27503047 if templ_7745c5c3_Err != nil {
27513048 return templ_7745c5c3_Err
27523049 }
@@ -2756,14 +3053,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
27563053 return templ_7745c5c3_Err
27573054 }
27583055 }
2759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "</div>")
3056+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "</div>")
27603057 if templ_7745c5c3_Err != nil {
27613058 return templ_7745c5c3_Err
27623059 }
27633060 }
27643061 return nil
27653062 })
2766- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var117), templ_7745c5c3_Buffer)
3063+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var130), templ_7745c5c3_Buffer)
27673064 if templ_7745c5c3_Err != nil {
27683065 return templ_7745c5c3_Err
27693066 }
@@ -2787,173 +3084,173 @@ func compareRail(p ComparePage) templ.Component {
27873084 }()
27883085 }
27893086 ctx = templ.InitializeContext(ctx)
2790- templ_7745c5c3_Var137 := templ.GetChildren(ctx)
2791- if templ_7745c5c3_Var137 == nil {
2792- templ_7745c5c3_Var137 = templ.NopComponent
3087+ templ_7745c5c3_Var150 := templ.GetChildren(ctx)
3088+ if templ_7745c5c3_Var150 == nil {
3089+ templ_7745c5c3_Var150 = templ.NopComponent
27933090 }
27943091 ctx = templ.ClearChildren(ctx)
2795- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
3092+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
27963093 if templ_7745c5c3_Err != nil {
27973094 return templ_7745c5c3_Err
27983095 }
2799- var templ_7745c5c3_Var138 string
2800- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
3096+ var templ_7745c5c3_Var151 string
3097+ templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
28013098 if templ_7745c5c3_Err != nil {
2802- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 67}
3099+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 67}
28033100 }
2804- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
3101+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
28053102 if templ_7745c5c3_Err != nil {
28063103 return templ_7745c5c3_Err
28073104 }
2808- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</span></h2><ul class=\"pcommits\">")
3105+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</span></h2><ul class=\"pcommits\">")
28093106 if templ_7745c5c3_Err != nil {
28103107 return templ_7745c5c3_Err
28113108 }
28123109 for _, c := range p.Ahead {
2813- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "<li><a href=\"")
3110+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "<li><a href=\"")
28143111 if templ_7745c5c3_Err != nil {
28153112 return templ_7745c5c3_Err
28163113 }
2817- var templ_7745c5c3_Var139 templ.SafeURL
2818- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
3114+ var templ_7745c5c3_Var152 templ.SafeURL
3115+ templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
28193116 if templ_7745c5c3_Err != nil {
2820- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 533, Col: 37}
3117+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 37}
28213118 }
2822- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
3119+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
28233120 if templ_7745c5c3_Err != nil {
28243121 return templ_7745c5c3_Err
28253122 }
2826- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "\">")
3123+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "\">")
28273124 if templ_7745c5c3_Err != nil {
28283125 return templ_7745c5c3_Err
28293126 }
2830- var templ_7745c5c3_Var140 string
2831- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
3127+ var templ_7745c5c3_Var153 string
3128+ templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
28323129 if templ_7745c5c3_Err != nil {
2833- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 533, Col: 49}
3130+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 49}
28343131 }
2835- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
3132+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
28363133 if templ_7745c5c3_Err != nil {
28373134 return templ_7745c5c3_Err
28383135 }
2839- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</a> <span class=\"s\">")
3136+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "</a> <span class=\"s\">")
28403137 if templ_7745c5c3_Err != nil {
28413138 return templ_7745c5c3_Err
28423139 }
2843- var templ_7745c5c3_Var141 string
2844- templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
3140+ var templ_7745c5c3_Var154 string
3141+ templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
28453142 if templ_7745c5c3_Err != nil {
2846- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 534, Col: 32}
3143+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 32}
28473144 }
2848- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
3145+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
28493146 if templ_7745c5c3_Err != nil {
28503147 return templ_7745c5c3_Err
28513148 }
2852- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "</span> <span class=\"mute\">")
3149+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "</span> <span class=\"mute\">")
28533150 if templ_7745c5c3_Err != nil {
28543151 return templ_7745c5c3_Err
28553152 }
2856- var templ_7745c5c3_Var142 string
2857- templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
3153+ var templ_7745c5c3_Var155 string
3154+ templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
28583155 if templ_7745c5c3_Err != nil {
2859- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 536, Col: 16}
3156+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 16}
28603157 }
2861- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142))
3158+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
28623159 if templ_7745c5c3_Err != nil {
28633160 return templ_7745c5c3_Err
28643161 }
2865- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, " ")
3162+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, " ")
28663163 if templ_7745c5c3_Err != nil {
28673164 return templ_7745c5c3_Err
28683165 }
28693166 if c.WhenAt != "" {
2870- var templ_7745c5c3_Var143 string
2871- templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
3167+ var templ_7745c5c3_Var156 string
3168+ templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
28723169 if templ_7745c5c3_Err != nil {
2873- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 538, Col: 15}
3170+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 15}
28743171 }
2875- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143))
3172+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
28763173 if templ_7745c5c3_Err != nil {
28773174 return templ_7745c5c3_Err
28783175 }
2879- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "<time datetime=\"")
3176+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<time datetime=\"")
28803177 if templ_7745c5c3_Err != nil {
28813178 return templ_7745c5c3_Err
28823179 }
2883- var templ_7745c5c3_Var144 string
2884- templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
3180+ var templ_7745c5c3_Var157 string
3181+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
28853182 if templ_7745c5c3_Err != nil {
2886- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 538, Col: 42}
3183+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 42}
28873184 }
2888- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
3185+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var157)
28893186 if templ_7745c5c3_Err != nil {
28903187 return templ_7745c5c3_Err
28913188 }
2892- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "\">")
3189+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "\">")
28933190 if templ_7745c5c3_Err != nil {
28943191 return templ_7745c5c3_Err
28953192 }
2896- var templ_7745c5c3_Var145 string
2897- templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
3193+ var templ_7745c5c3_Var158 string
3194+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
28983195 if templ_7745c5c3_Err != nil {
2899- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 538, Col: 53}
3196+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 53}
29003197 }
2901- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
3198+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
29023199 if templ_7745c5c3_Err != nil {
29033200 return templ_7745c5c3_Err
29043201 }
2905- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</time>")
3202+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "</time>")
29063203 if templ_7745c5c3_Err != nil {
29073204 return templ_7745c5c3_Err
29083205 }
29093206 }
2910- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</span></li>")
3207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, "</span></li>")
29113208 if templ_7745c5c3_Err != nil {
29123209 return templ_7745c5c3_Err
29133210 }
29143211 }
2915- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</ul>")
3212+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "</ul>")
29163213 if templ_7745c5c3_Err != nil {
29173214 return templ_7745c5c3_Err
29183215 }
29193216 if len(p.Ahead) == 0 {
2920- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "<p class=\"dim\">no commit ahead of ")
3217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "<p class=\"dim\">no commit ahead of ")
29213218 if templ_7745c5c3_Err != nil {
29223219 return templ_7745c5c3_Err
29233220 }
2924- var templ_7745c5c3_Var146 string
2925- templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3221+ var templ_7745c5c3_Var159 string
3222+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
29263223 if templ_7745c5c3_Err != nil {
2927- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 545, Col: 47}
3224+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 47}
29283225 }
2929- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
3226+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
29303227 if templ_7745c5c3_Err != nil {
29313228 return templ_7745c5c3_Err
29323229 }
2933- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, ".</p>")
3230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, ".</p>")
29343231 if templ_7745c5c3_Err != nil {
29353232 return templ_7745c5c3_Err
29363233 }
29373234 }
2938- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "</section>")
3235+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</section>")
29393236 if templ_7745c5c3_Err != nil {
29403237 return templ_7745c5c3_Err
29413238 }
29423239 if p.HasDiff {
2943- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
3240+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
29443241 if templ_7745c5c3_Err != nil {
29453242 return templ_7745c5c3_Err
29463243 }
2947- var templ_7745c5c3_Var147 string
2948- templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
3244+ var templ_7745c5c3_Var160 string
3245+ templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
29493246 if templ_7745c5c3_Err != nil {
2950- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 550, Col: 66}
3247+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 610, Col: 66}
29513248 }
2952- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
3249+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
29533250 if templ_7745c5c3_Err != nil {
29543251 return templ_7745c5c3_Err
29553252 }
2956- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "</span></h2>")
3253+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "</span></h2>")
29573254 if templ_7745c5c3_Err != nil {
29583255 return templ_7745c5c3_Err
29593256 }
@@ -2961,12 +3258,12 @@ func compareRail(p ComparePage) templ.Component {
29613258 if templ_7745c5c3_Err != nil {
29623259 return templ_7745c5c3_Err
29633260 }
2964- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "</section>")
3261+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "</section>")
29653262 if templ_7745c5c3_Err != nil {
29663263 return templ_7745c5c3_Err
29673264 }
29683265 }
2969- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
3266+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
29703267 if templ_7745c5c3_Err != nil {
29713268 return templ_7745c5c3_Err
29723269 }
@@ -2974,7 +3271,7 @@ func compareRail(p ComparePage) templ.Component {
29743271 if templ_7745c5c3_Err != nil {
29753272 return templ_7745c5c3_Err
29763273 }
2977- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "</section>")
3274+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "</section>")
29783275 if templ_7745c5c3_Err != nil {
29793276 return templ_7745c5c3_Err
29803277 }
@@ -1215,8 +1215,7 @@ func lineNotes(notes []CommentView, form *CommentForm, suffix string) templ.Comp
1215 }1215 }
1216 1216
1217 // commentView is a note: a ruled block, under its line in the diff or in the1217 // commentView is a note: a ruled block, under its line in the diff or in the
1218-// list of comments. The text is plain: escaped, its line breaks kept, its1218+// list of comments. Body comes only from the safe Markdown renderer.
1219-// http(s) addresses linked.
1220 func commentView(c CommentView, suffix string) templ.Component {1219 func commentView(c CommentView, suffix string) templ.Component {
1221 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {1220 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
1222 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context1221 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -1245,7 +1244,7 @@ func commentView(c CommentView, suffix string) templ.Component {
1245 var templ_7745c5c3_Var59 string1244 var templ_7745c5c3_Var59 string
1246 templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)1245 templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix)
1247 if templ_7745c5c3_Err != nil {1246 if templ_7745c5c3_Err != nil {
1248- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 235, Col: 42}1247+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 42}
1249 }1248 }
1250 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)1249 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
1251 if templ_7745c5c3_Err != nil {1250 if templ_7745c5c3_Err != nil {
@@ -1258,26 +1257,36 @@ func commentView(c CommentView, suffix string) templ.Component {
1258 var templ_7745c5c3_Var60 string1257 var templ_7745c5c3_Var60 string
1259 templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))1258 templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved))
1260 if templ_7745c5c3_Err != nil {1259 if templ_7745c5c3_Err != nil {
1261- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 235, Col: 81}1260+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 234, Col: 81}
1262 }1261 }
1263 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)1262 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
1264 if templ_7745c5c3_Err != nil {1263 if templ_7745c5c3_Err != nil {
1265 return templ_7745c5c3_Err1264 return templ_7745c5c3_Err
1266 }1265 }
1267- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"><p class=\"c-head\"><span class=\"c-who\">")1266+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\"")
1267+ if templ_7745c5c3_Err != nil {
1268+ return templ_7745c5c3_Err
1269+ }
1270+ if c.Reply != nil {
1271+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " data-thread")
1272+ if templ_7745c5c3_Err != nil {
1273+ return templ_7745c5c3_Err
1274+ }
1275+ }
1276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "><p class=\"c-head\"><span class=\"c-who\">")
1268 if templ_7745c5c3_Err != nil {1277 if templ_7745c5c3_Err != nil {
1269 return templ_7745c5c3_Err1278 return templ_7745c5c3_Err
1270 }1279 }
1271 var templ_7745c5c3_Var61 string1280 var templ_7745c5c3_Var61 string
1272 templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)1281 templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
1273 if templ_7745c5c3_Err != nil {1282 if templ_7745c5c3_Err != nil {
1274- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 237, Col: 33}1283+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 236, Col: 33}
1275 }1284 }
1276 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))1285 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
1277 if templ_7745c5c3_Err != nil {1286 if templ_7745c5c3_Err != nil {
1278 return templ_7745c5c3_Err1287 return templ_7745c5c3_Err
1279 }1288 }
1280- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "</span> ")1289+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</span> ")
1281 if templ_7745c5c3_Err != nil {1290 if templ_7745c5c3_Err != nil {
1282 return templ_7745c5c3_Err1291 return templ_7745c5c3_Err
1283 }1292 }
@@ -1285,52 +1294,52 @@ func commentView(c CommentView, suffix string) templ.Component {
1285 var templ_7745c5c3_Var62 string1294 var templ_7745c5c3_Var62 string
1286 templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ")1295 templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1287 if templ_7745c5c3_Err != nil {1296 if templ_7745c5c3_Err != nil {
1288- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 9}1297+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 9}
1289 }1298 }
1290 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))1299 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
1291 if templ_7745c5c3_Err != nil {1300 if templ_7745c5c3_Err != nil {
1292 return templ_7745c5c3_Err1301 return templ_7745c5c3_Err
1293 }1302 }
1294- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "<time class=\"mute\" datetime=\"")1303+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<time class=\"mute\" datetime=\"")
1295 if templ_7745c5c3_Err != nil {1304 if templ_7745c5c3_Err != nil {
1296 return templ_7745c5c3_Err1305 return templ_7745c5c3_Err
1297 }1306 }
1298 var templ_7745c5c3_Var63 string1307 var templ_7745c5c3_Var63 string
1299 templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)1308 templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
1300 if templ_7745c5c3_Err != nil {1309 if templ_7745c5c3_Err != nil {
1301- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 49}1310+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 49}
1302 }1311 }
1303 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)1312 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
1304 if templ_7745c5c3_Err != nil {1313 if templ_7745c5c3_Err != nil {
1305 return templ_7745c5c3_Err1314 return templ_7745c5c3_Err
1306 }1315 }
1307- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\" title=\"")1316+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "\" title=\"")
1308 if templ_7745c5c3_Err != nil {1317 if templ_7745c5c3_Err != nil {
1309 return templ_7745c5c3_Err1318 return templ_7745c5c3_Err
1310 }1319 }
1311 var templ_7745c5c3_Var64 string1320 var templ_7745c5c3_Var64 string
1312 templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)1321 templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs)
1313 if templ_7745c5c3_Err != nil {1322 if templ_7745c5c3_Err != nil {
1314- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 65}1323+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 65}
1315 }1324 }
1316 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)1325 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
1317 if templ_7745c5c3_Err != nil {1326 if templ_7745c5c3_Err != nil {
1318 return templ_7745c5c3_Err1327 return templ_7745c5c3_Err
1319 }1328 }
1320- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\">")1329+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\">")
1321 if templ_7745c5c3_Err != nil {1330 if templ_7745c5c3_Err != nil {
1322 return templ_7745c5c3_Err1331 return templ_7745c5c3_Err
1323 }1332 }
1324 var templ_7745c5c3_Var65 string1333 var templ_7745c5c3_Var65 string
1325 templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)1334 templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
1326 if templ_7745c5c3_Err != nil {1335 if templ_7745c5c3_Err != nil {
1327- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 239, Col: 76}1336+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 238, Col: 76}
1328 }1337 }
1329 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))1338 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
1330 if templ_7745c5c3_Err != nil {1339 if templ_7745c5c3_Err != nil {
1331 return templ_7745c5c3_Err1340 return templ_7745c5c3_Err
1332 }1341 }
1333- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</time> ")1342+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "</time> ")
1334 if templ_7745c5c3_Err != nil {1343 if templ_7745c5c3_Err != nil {
1335 return templ_7745c5c3_Err1344 return templ_7745c5c3_Err
1336 }1345 }
@@ -1339,13 +1348,13 @@ func commentView(c CommentView, suffix string) templ.Component {
1339 var templ_7745c5c3_Var66 string1348 var templ_7745c5c3_Var66 string
1340 templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ")1349 templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1341 if templ_7745c5c3_Err != nil {1350 if templ_7745c5c3_Err != nil {
1342- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 242, Col: 9}1351+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 241, Col: 9}
1343 }1352 }
1344 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))1353 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
1345 if templ_7745c5c3_Err != nil {1354 if templ_7745c5c3_Err != nil {
1346 return templ_7745c5c3_Err1355 return templ_7745c5c3_Err
1347 }1356 }
1348- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "<span class=\"c-state\">resolved</span> ")1357+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<span class=\"c-state\">resolved</span> ")
1349 if templ_7745c5c3_Err != nil {1358 if templ_7745c5c3_Err != nil {
1350 return templ_7745c5c3_Err1359 return templ_7745c5c3_Err
1351 }1360 }
@@ -1354,13 +1363,13 @@ func commentView(c CommentView, suffix string) templ.Component {
1354 var templ_7745c5c3_Var67 string1363 var templ_7745c5c3_Var67 string
1355 templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ")1364 templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1356 if templ_7745c5c3_Err != nil {1365 if templ_7745c5c3_Err != nil {
1357- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 245, Col: 9}1366+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 9}
1358 }1367 }
1359 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))1368 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
1360 if templ_7745c5c3_Err != nil {1369 if templ_7745c5c3_Err != nil {
1361 return templ_7745c5c3_Err1370 return templ_7745c5c3_Err
1362 }1371 }
1363- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")1372+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ")
1364 if templ_7745c5c3_Err != nil {1373 if templ_7745c5c3_Err != nil {
1365 return templ_7745c5c3_Err1374 return templ_7745c5c3_Err
1366 }1375 }
@@ -1369,111 +1378,296 @@ func commentView(c CommentView, suffix string) templ.Component {
1369 var templ_7745c5c3_Var68 string1378 var templ_7745c5c3_Var68 string
1370 templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ")1379 templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
1371 if templ_7745c5c3_Err != nil {1380 if templ_7745c5c3_Err != nil {
1372- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 9}1381+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 9}
1373 }1382 }
1374 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))1383 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
1375 if templ_7745c5c3_Err != nil {1384 if templ_7745c5c3_Err != nil {
1376 return templ_7745c5c3_Err1385 return templ_7745c5c3_Err
1377 }1386 }
1378- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "<span class=\"c-where mute\">")1387+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"c-where mute\">")
1379 if templ_7745c5c3_Err != nil {1388 if templ_7745c5c3_Err != nil {
1380 return templ_7745c5c3_Err1389 return templ_7745c5c3_Err
1381 }1390 }
1382 var templ_7745c5c3_Var69 string1391 var templ_7745c5c3_Var69 string
1383 templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)1392 templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where)
1384 if templ_7745c5c3_Err != nil {1393 if templ_7745c5c3_Err != nil {
1385- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 248, Col: 47}1394+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 47}
1386 }1395 }
1387 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))1396 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
1388 if templ_7745c5c3_Err != nil {1397 if templ_7745c5c3_Err != nil {
1389 return templ_7745c5c3_Err1398 return templ_7745c5c3_Err
1390 }1399 }
1391- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "</span>")1400+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</span>")
1392 if templ_7745c5c3_Err != nil {1401 if templ_7745c5c3_Err != nil {
1393 return templ_7745c5c3_Err1402 return templ_7745c5c3_Err
1394 }1403 }
1395 }1404 }
1396- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</p><p class=\"c-text\">")1405+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</p>")
1397 if templ_7745c5c3_Err != nil {1406 if templ_7745c5c3_Err != nil {
1398 return templ_7745c5c3_Err1407 return templ_7745c5c3_Err
1399 }1408 }
1400- templ_7745c5c3_Err = userText(c.Parts).Render(ctx, templ_7745c5c3_Buffer)1409+ if c.Deleted {
1401- if templ_7745c5c3_Err != nil {1410+ if c.Verdict != "" {
1402- return templ_7745c5c3_Err1411+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "<p class=\"mute\">text deleted</p>")
1412+ if templ_7745c5c3_Err != nil {
1413+ return templ_7745c5c3_Err
1414+ }
1415+ } else {
1416+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "<p class=\"mute\">deleted note</p>")
1417+ if templ_7745c5c3_Err != nil {
1418+ return templ_7745c5c3_Err
1419+ }
1420+ }
1421+ } else {
1422+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<div class=\"c-body prose\">")
1423+ if templ_7745c5c3_Err != nil {
1424+ return templ_7745c5c3_Err
1425+ }
1426+ if suffix == "-s" {
1427+ templ_7745c5c3_Err = templ.Raw(string(c.BodySplit)).Render(ctx, templ_7745c5c3_Buffer)
1428+ if templ_7745c5c3_Err != nil {
1429+ return templ_7745c5c3_Err
1430+ }
1431+ } else {
1432+ templ_7745c5c3_Err = templ.Raw(string(c.Body)).Render(ctx, templ_7745c5c3_Buffer)
1433+ if templ_7745c5c3_Err != nil {
1434+ return templ_7745c5c3_Err
1435+ }
1436+ }
1437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "</div>")
1438+ if templ_7745c5c3_Err != nil {
1439+ return templ_7745c5c3_Err
1440+ }
1403 }1441 }
1404- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "</p>")1442+ if c.Edited {
1405- if templ_7745c5c3_Err != nil {1443+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<span class=\"mute\">edited</span> ")
1406- return templ_7745c5c3_Err1444+ if templ_7745c5c3_Err != nil {
1445+ return templ_7745c5c3_Err
1446+ }
1407 }1447 }
1408- if c.Resolve != nil {1448+ if c.Verdict != "" {
1409- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "<form class=\"c-act\" method=\"post\" action=\"")1449+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "<span class=\"c-state\">")
1410 if templ_7745c5c3_Err != nil {1450 if templ_7745c5c3_Err != nil {
1411 return templ_7745c5c3_Err1451 return templ_7745c5c3_Err
1412 }1452 }
1413- var templ_7745c5c3_Var70 templ.SafeURL1453+ var templ_7745c5c3_Var70 string
1414- templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))1454+ templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(c.Verdict)
1415 if templ_7745c5c3_Err != nil {1455 if templ_7745c5c3_Err != nil {
1416- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 253, Col: 77}1456+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 269, Col: 36}
1417 }1457 }
1418 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))1458 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
1419 if templ_7745c5c3_Err != nil {1459 if templ_7745c5c3_Err != nil {
1420 return templ_7745c5c3_Err1460 return templ_7745c5c3_Err
1421 }1461 }
1422- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\"><input type=\"hidden\" name=\"resolved\" value=\"")1462+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</span> ")
1463+ if templ_7745c5c3_Err != nil {
1464+ return templ_7745c5c3_Err
1465+ }
1466+ }
1467+ if c.Range != "" {
1468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<span class=\"mute\">")
1423 if templ_7745c5c3_Err != nil {1469 if templ_7745c5c3_Err != nil {
1424 return templ_7745c5c3_Err1470 return templ_7745c5c3_Err
1425 }1471 }
1426 var templ_7745c5c3_Var71 string1472 var templ_7745c5c3_Var71 string
1427- templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))1473+ templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(c.Range)
1474+ if templ_7745c5c3_Err != nil {
1475+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 272, Col: 31}
1476+ }
1477+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
1478+ if templ_7745c5c3_Err != nil {
1479+ return templ_7745c5c3_Err
1480+ }
1481+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "</span> ")
1482+ if templ_7745c5c3_Err != nil {
1483+ return templ_7745c5c3_Err
1484+ }
1485+ }
1486+ if c.Quote != "" {
1487+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "<button type=\"button\" class=\"btn\" data-js data-quote=\"")
1488+ if templ_7745c5c3_Err != nil {
1489+ return templ_7745c5c3_Err
1490+ }
1491+ var templ_7745c5c3_Var72 string
1492+ templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Quote)
1493+ if templ_7745c5c3_Err != nil {
1494+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 65}
1495+ }
1496+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
1428 if templ_7745c5c3_Err != nil {1497 if templ_7745c5c3_Err != nil {
1429- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 254, Col: 74}1498+ return templ_7745c5c3_Err
1499+ }
1500+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "\">Quote</button> ")
1501+ if templ_7745c5c3_Err != nil {
1502+ return templ_7745c5c3_Err
1430 }1503 }
1431- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)1504+ }
1505+ if c.Edit != nil {
1506+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "<details><summary>Edit</summary><form method=\"post\" action=\"")
1432 if templ_7745c5c3_Err != nil {1507 if templ_7745c5c3_Err != nil {
1433 return templ_7745c5c3_Err1508 return templ_7745c5c3_Err
1434 }1509 }
1435- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\"> ")1510+ var templ_7745c5c3_Var73 templ.SafeURL
1511+ templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Edit.Action))
1512+ if templ_7745c5c3_Err != nil {
1513+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 92}
1514+ }
1515+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
1516+ if templ_7745c5c3_Err != nil {
1517+ return templ_7745c5c3_Err
1518+ }
1519+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "\"><input type=\"hidden\" name=\"from\" value=\"")
1520+ if templ_7745c5c3_Err != nil {
1521+ return templ_7745c5c3_Err
1522+ }
1523+ var templ_7745c5c3_Var74 string
1524+ templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Edit.From))
1525+ if templ_7745c5c3_Err != nil {
1526+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 153}
1527+ }
1528+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)
1529+ if templ_7745c5c3_Err != nil {
1530+ return templ_7745c5c3_Err
1531+ }
1532+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "\"><textarea name=\"text\" maxlength=\"8000\">")
1533+ if templ_7745c5c3_Err != nil {
1534+ return templ_7745c5c3_Err
1535+ }
1536+ var templ_7745c5c3_Var75 string
1537+ templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Edit.Text)
1538+ if templ_7745c5c3_Err != nil {
1539+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 209}
1540+ }
1541+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
1542+ if templ_7745c5c3_Err != nil {
1543+ return templ_7745c5c3_Err
1544+ }
1545+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "</textarea><button class=\"btn\" type=\"submit\">Save</button></form></details> ")
1546+ if templ_7745c5c3_Err != nil {
1547+ return templ_7745c5c3_Err
1548+ }
1549+ }
1550+ if c.Delete != nil {
1551+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "<details><summary>Delete</summary><form method=\"post\" action=\"")
1552+ if templ_7745c5c3_Err != nil {
1553+ return templ_7745c5c3_Err
1554+ }
1555+ var templ_7745c5c3_Var76 templ.SafeURL
1556+ templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Delete.Action))
1557+ if templ_7745c5c3_Err != nil {
1558+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 281, Col: 96}
1559+ }
1560+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76))
1561+ if templ_7745c5c3_Err != nil {
1562+ return templ_7745c5c3_Err
1563+ }
1564+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "\"><input type=\"hidden\" name=\"from\" value=\"")
1565+ if templ_7745c5c3_Err != nil {
1566+ return templ_7745c5c3_Err
1567+ }
1568+ var templ_7745c5c3_Var77 string
1569+ templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Delete.From))
1570+ if templ_7745c5c3_Err != nil {
1571+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 281, Col: 159}
1572+ }
1573+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
1574+ if templ_7745c5c3_Err != nil {
1575+ return templ_7745c5c3_Err
1576+ }
1577+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\"><p>Delete this note?</p><button class=\"btn\" type=\"submit\">Confirm delete</button></form></details> ")
1578+ if templ_7745c5c3_Err != nil {
1579+ return templ_7745c5c3_Err
1580+ }
1581+ }
1582+ for _, reply := range c.Replies {
1583+ templ_7745c5c3_Err = commentView(reply, suffix).Render(ctx, templ_7745c5c3_Buffer)
1584+ if templ_7745c5c3_Err != nil {
1585+ return templ_7745c5c3_Err
1586+ }
1587+ }
1588+ if c.Reply != nil {
1589+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<details data-reply><summary>Reply</summary>")
1590+ if templ_7745c5c3_Err != nil {
1591+ return templ_7745c5c3_Err
1592+ }
1593+ templ_7745c5c3_Err = commentForm(*c.Reply, suffix).Render(ctx, templ_7745c5c3_Buffer)
1594+ if templ_7745c5c3_Err != nil {
1595+ return templ_7745c5c3_Err
1596+ }
1597+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "</details> ")
1598+ if templ_7745c5c3_Err != nil {
1599+ return templ_7745c5c3_Err
1600+ }
1601+ }
1602+ if c.Resolve != nil {
1603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<form class=\"c-act\" method=\"post\" action=\"")
1604+ if templ_7745c5c3_Err != nil {
1605+ return templ_7745c5c3_Err
1606+ }
1607+ var templ_7745c5c3_Var78 templ.SafeURL
1608+ templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action))
1609+ if templ_7745c5c3_Err != nil {
1610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 292, Col: 77}
1611+ }
1612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78))
1613+ if templ_7745c5c3_Err != nil {
1614+ return templ_7745c5c3_Err
1615+ }
1616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "\"><input type=\"hidden\" name=\"resolved\" value=\"")
1617+ if templ_7745c5c3_Err != nil {
1618+ return templ_7745c5c3_Err
1619+ }
1620+ var templ_7745c5c3_Var79 string
1621+ templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value))
1622+ if templ_7745c5c3_Err != nil {
1623+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 293, Col: 74}
1624+ }
1625+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79)
1626+ if templ_7745c5c3_Err != nil {
1627+ return templ_7745c5c3_Err
1628+ }
1629+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\"> ")
1436 if templ_7745c5c3_Err != nil {1630 if templ_7745c5c3_Err != nil {
1437 return templ_7745c5c3_Err1631 return templ_7745c5c3_Err
1438 }1632 }
1439 if c.Resolve.From > 0 {1633 if c.Resolve.From > 0 {
1440- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<input type=\"hidden\" name=\"from\" value=\"")1634+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "<input type=\"hidden\" name=\"from\" value=\"")
1441 if templ_7745c5c3_Err != nil {1635 if templ_7745c5c3_Err != nil {
1442 return templ_7745c5c3_Err1636 return templ_7745c5c3_Err
1443 }1637 }
1444- var templ_7745c5c3_Var72 string1638+ var templ_7745c5c3_Var80 string
1445- templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))1639+ templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(c.Resolve.From))
1446 if templ_7745c5c3_Err != nil {1640 if templ_7745c5c3_Err != nil {
1447- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 256, Col: 66}1641+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 295, Col: 66}
1448 }1642 }
1449- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)1643+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
1450 if templ_7745c5c3_Err != nil {1644 if templ_7745c5c3_Err != nil {
1451 return templ_7745c5c3_Err1645 return templ_7745c5c3_Err
1452 }1646 }
1453- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\"> ")1647+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "\"> ")
1454 if templ_7745c5c3_Err != nil {1648 if templ_7745c5c3_Err != nil {
1455 return templ_7745c5c3_Err1649 return templ_7745c5c3_Err
1456 }1650 }
1457 }1651 }
1458- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "<button type=\"submit\" class=\"btn\">")1652+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "<button type=\"submit\" class=\"btn\">")
1459 if templ_7745c5c3_Err != nil {1653 if templ_7745c5c3_Err != nil {
1460 return templ_7745c5c3_Err1654 return templ_7745c5c3_Err
1461 }1655 }
1462- var templ_7745c5c3_Var73 string1656+ var templ_7745c5c3_Var81 string
1463- templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)1657+ templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label)
1464 if templ_7745c5c3_Err != nil {1658 if templ_7745c5c3_Err != nil {
1465- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 55}1659+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 55}
1466 }1660 }
1467- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))1661+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81))
1468 if templ_7745c5c3_Err != nil {1662 if templ_7745c5c3_Err != nil {
1469 return templ_7745c5c3_Err1663 return templ_7745c5c3_Err
1470 }1664 }
1471- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</button></form>")1665+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</button></form>")
1472 if templ_7745c5c3_Err != nil {1666 if templ_7745c5c3_Err != nil {
1473 return templ_7745c5c3_Err1667 return templ_7745c5c3_Err
1474 }1668 }
1475 }1669 }
1476- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</div>")1670+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</div>")
1477 if templ_7745c5c3_Err != nil {1671 if templ_7745c5c3_Err != nil {
1478 return templ_7745c5c3_Err1672 return templ_7745c5c3_Err
1479 }1673 }
@@ -1505,165 +1699,203 @@ func commentForm(f CommentForm, suffix string) templ.Component {
1505 }()1699 }()
1506 }1700 }
1507 ctx = templ.InitializeContext(ctx)1701 ctx = templ.InitializeContext(ctx)
1508- templ_7745c5c3_Var74 := templ.GetChildren(ctx)1702+ templ_7745c5c3_Var82 := templ.GetChildren(ctx)
1509- if templ_7745c5c3_Var74 == nil {1703+ if templ_7745c5c3_Var82 == nil {
1510- templ_7745c5c3_Var74 = templ.NopComponent1704+ templ_7745c5c3_Var82 = templ.NopComponent
1511 }1705 }
1512 ctx = templ.ClearChildren(ctx)1706 ctx = templ.ClearChildren(ctx)
1513- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<form class=\"cform\" method=\"post\" action=\"")1707+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<form class=\"cform\" method=\"post\" action=\"")
1514 if templ_7745c5c3_Err != nil {1708 if templ_7745c5c3_Err != nil {
1515 return templ_7745c5c3_Err1709 return templ_7745c5c3_Err
1516 }1710 }
1517- var templ_7745c5c3_Var75 templ.SafeURL1711+ var templ_7745c5c3_Var83 templ.SafeURL
1518- templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))1712+ templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action))
1519 if templ_7745c5c3_Err != nil {1713 if templ_7745c5c3_Err != nil {
1520- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 273, Col: 67}1714+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 312, Col: 67}
1521 }1715 }
1522- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))1716+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))
1523 if templ_7745c5c3_Err != nil {1717 if templ_7745c5c3_Err != nil {
1524 return templ_7745c5c3_Err1718 return templ_7745c5c3_Err
1525 }1719 }
1526- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "\"><label for=\"")1720+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "\"><label for=\"")
1527 if templ_7745c5c3_Err != nil {1721 if templ_7745c5c3_Err != nil {
1528 return templ_7745c5c3_Err1722 return templ_7745c5c3_Err
1529 }1723 }
1530- var templ_7745c5c3_Var76 string1724+ var templ_7745c5c3_Var84 string
1531- templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)1725+ templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1532 if templ_7745c5c3_Err != nil {1726 if templ_7745c5c3_Err != nil {
1533- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 28}1727+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 28}
1534 }1728 }
1535- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76)1729+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84)
1536 if templ_7745c5c3_Err != nil {1730 if templ_7745c5c3_Err != nil {
1537 return templ_7745c5c3_Err1731 return templ_7745c5c3_Err
1538 }1732 }
1539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "\">")1733+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "\">")
1540 if templ_7745c5c3_Err != nil {1734 if templ_7745c5c3_Err != nil {
1541 return templ_7745c5c3_Err1735 return templ_7745c5c3_Err
1542 }1736 }
1543- var templ_7745c5c3_Var77 string1737+ var templ_7745c5c3_Var85 string
1544- templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)1738+ templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title)
1545 if templ_7745c5c3_Err != nil {1739 if templ_7745c5c3_Err != nil {
1546- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 274, Col: 40}1740+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 313, Col: 40}
1547 }1741 }
1548- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77))1742+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
1549 if templ_7745c5c3_Err != nil {1743 if templ_7745c5c3_Err != nil {
1550 return templ_7745c5c3_Err1744 return templ_7745c5c3_Err
1551 }1745 }
1552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</label> <textarea id=\"")1746+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "</label> <textarea id=\"")
1553 if templ_7745c5c3_Err != nil {1747 if templ_7745c5c3_Err != nil {
1554 return templ_7745c5c3_Err1748 return templ_7745c5c3_Err
1555 }1749 }
1556- var templ_7745c5c3_Var78 string1750+ var templ_7745c5c3_Var86 string
1557- templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)1751+ templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix)
1558 if templ_7745c5c3_Err != nil {1752 if templ_7745c5c3_Err != nil {
1559- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 21}1753+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 21}
1560 }1754 }
1561- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78)1755+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var86)
1562 if templ_7745c5c3_Err != nil {1756 if templ_7745c5c3_Err != nil {
1563 return templ_7745c5c3_Err1757 return templ_7745c5c3_Err
1564 }1758 }
1565- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "\" name=\"text\" rows=\"3\" maxlength=\"8000\" required placeholder=\"plain text, 8000 characters at most\"")1759+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\" name=\"text\" rows=\"3\" maxlength=\"8000\" placeholder=\"Markdown, 8000 characters at most\"")
1566 if templ_7745c5c3_Err != nil {1760 if templ_7745c5c3_Err != nil {
1567 return templ_7745c5c3_Err1761 return templ_7745c5c3_Err
1568 }1762 }
1569 if f.Focus {1763 if f.Focus {
1570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, " autofocus")1764+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, " autofocus")
1571 if templ_7745c5c3_Err != nil {1765 if templ_7745c5c3_Err != nil {
1572 return templ_7745c5c3_Err1766 return templ_7745c5c3_Err
1573 }1767 }
1574 }1768 }
1575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "></textarea> ")1769+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "></textarea> ")
1576 if templ_7745c5c3_Err != nil {1770 if templ_7745c5c3_Err != nil {
1577 return templ_7745c5c3_Err1771 return templ_7745c5c3_Err
1578 }1772 }
1579 if f.Path != "" {1773 if f.Path != "" {
1580- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<input type=\"hidden\" name=\"path\" value=\"")1774+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<input type=\"hidden\" name=\"path\" value=\"")
1581 if templ_7745c5c3_Err != nil {1775 if templ_7745c5c3_Err != nil {
1582 return templ_7745c5c3_Err1776 return templ_7745c5c3_Err
1583 }1777 }
1584- var templ_7745c5c3_Var79 string1778+ var templ_7745c5c3_Var87 string
1585- templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)1779+ templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path)
1586 if templ_7745c5c3_Err != nil {1780 if templ_7745c5c3_Err != nil {
1587- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 287, Col: 50}1781+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 325, Col: 50}
1588 }1782 }
1589- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var79)1783+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var87)
1590 if templ_7745c5c3_Err != nil {1784 if templ_7745c5c3_Err != nil {
1591 return templ_7745c5c3_Err1785 return templ_7745c5c3_Err
1592 }1786 }
1593- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "\"> <input type=\"hidden\" name=\"line\" value=\"")1787+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "\"> <input type=\"hidden\" name=\"line\" value=\"")
1594 if templ_7745c5c3_Err != nil {1788 if templ_7745c5c3_Err != nil {
1595 return templ_7745c5c3_Err1789 return templ_7745c5c3_Err
1596 }1790 }
1597- var templ_7745c5c3_Var80 string1791+ var templ_7745c5c3_Var88 string
1598- templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)1792+ templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line)
1599 if templ_7745c5c3_Err != nil {1793 if templ_7745c5c3_Err != nil {
1600- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 288, Col: 50}1794+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 326, Col: 50}
1601 }1795 }
1602- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)1796+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var88)
1603 if templ_7745c5c3_Err != nil {1797 if templ_7745c5c3_Err != nil {
1604 return templ_7745c5c3_Err1798 return templ_7745c5c3_Err
1605 }1799 }
1606- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "\"> <input type=\"hidden\" name=\"side\" value=\"")1800+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "\"> <input type=\"hidden\" name=\"side\" value=\"")
1607 if templ_7745c5c3_Err != nil {1801 if templ_7745c5c3_Err != nil {
1608 return templ_7745c5c3_Err1802 return templ_7745c5c3_Err
1609 }1803 }
1610- var templ_7745c5c3_Var81 string1804+ var templ_7745c5c3_Var89 string
1611- templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)1805+ templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side)
1806+ if templ_7745c5c3_Err != nil {
1807+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 50}
1808+ }
1809+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var89)
1810+ if templ_7745c5c3_Err != nil {
1811+ return templ_7745c5c3_Err
1812+ }
1813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "\"> <label>from line <input type=\"number\" name=\"start_line\" min=\"1\" value=\"")
1814+ if templ_7745c5c3_Err != nil {
1815+ return templ_7745c5c3_Err
1816+ }
1817+ var templ_7745c5c3_Var90 string
1818+ templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.StartLine)
1612 if templ_7745c5c3_Err != nil {1819 if templ_7745c5c3_Err != nil {
1613- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 289, Col: 50}1820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 328, Col: 86}
1614 }1821 }
1615- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var81)1822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var90)
1616 if templ_7745c5c3_Err != nil {1823 if templ_7745c5c3_Err != nil {
1617 return templ_7745c5c3_Err1824 return templ_7745c5c3_Err
1618 }1825 }
1619- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\"> ")1826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "\"></label> ")
1827+ if templ_7745c5c3_Err != nil {
1828+ return templ_7745c5c3_Err
1829+ }
1830+ }
1831+ if f.ReplyTo != "" {
1832+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "<input type=\"hidden\" name=\"reply_to\" value=\"")
1833+ if templ_7745c5c3_Err != nil {
1834+ return templ_7745c5c3_Err
1835+ }
1836+ var templ_7745c5c3_Var91 string
1837+ templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ReplyTo)
1838+ if templ_7745c5c3_Err != nil {
1839+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 57}
1840+ }
1841+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var91)
1842+ if templ_7745c5c3_Err != nil {
1843+ return templ_7745c5c3_Err
1844+ }
1845+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "\"> ")
1846+ if templ_7745c5c3_Err != nil {
1847+ return templ_7745c5c3_Err
1848+ }
1849+ }
1850+ if f.Path == "" && f.ReplyTo == "" {
1851+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "<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>")
1620 if templ_7745c5c3_Err != nil {1852 if templ_7745c5c3_Err != nil {
1621 return templ_7745c5c3_Err1853 return templ_7745c5c3_Err
1622 }1854 }
1623 }1855 }
1624 if f.From > 0 {1856 if f.From > 0 {
1625- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<input type=\"hidden\" name=\"from\" value=\"")1857+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "<input type=\"hidden\" name=\"from\" value=\"")
1626 if templ_7745c5c3_Err != nil {1858 if templ_7745c5c3_Err != nil {
1627 return templ_7745c5c3_Err1859 return templ_7745c5c3_Err
1628 }1860 }
1629- var templ_7745c5c3_Var82 string1861+ var templ_7745c5c3_Var92 string
1630- templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))1862+ templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(itoa(f.From))
1631 if templ_7745c5c3_Err != nil {1863 if templ_7745c5c3_Err != nil {
1632- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 292, Col: 56}1864+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 337, Col: 56}
1633 }1865 }
1634- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)1866+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)
1635 if templ_7745c5c3_Err != nil {1867 if templ_7745c5c3_Err != nil {
1636 return templ_7745c5c3_Err1868 return templ_7745c5c3_Err
1637 }1869 }
1638- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "\">")1870+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "\">")
1639 if templ_7745c5c3_Err != nil {1871 if templ_7745c5c3_Err != nil {
1640 return templ_7745c5c3_Err1872 return templ_7745c5c3_Err
1641 }1873 }
1642 }1874 }
1643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")1875+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "<div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Comment</button> ")
1644 if templ_7745c5c3_Err != nil {1876 if templ_7745c5c3_Err != nil {
1645 return templ_7745c5c3_Err1877 return templ_7745c5c3_Err
1646 }1878 }
1647 if f.Cancel != "" {1879 if f.Cancel != "" {
1648- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "<a class=\"btn\" href=\"")1880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "<a class=\"btn\" href=\"")
1649 if templ_7745c5c3_Err != nil {1881 if templ_7745c5c3_Err != nil {
1650 return templ_7745c5c3_Err1882 return templ_7745c5c3_Err
1651 }1883 }
1652- var templ_7745c5c3_Var83 templ.SafeURL1884+ var templ_7745c5c3_Var93 templ.SafeURL
1653- templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))1885+ templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel))
1654 if templ_7745c5c3_Err != nil {1886 if templ_7745c5c3_Err != nil {
1655- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 297, Col: 49}1887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 342, Col: 49}
1656 }1888 }
1657- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83))1889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))
1658 if templ_7745c5c3_Err != nil {1890 if templ_7745c5c3_Err != nil {
1659 return templ_7745c5c3_Err1891 return templ_7745c5c3_Err
1660 }1892 }
1661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "\">Cancel</a>")1893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "\">Cancel</a>")
1662 if templ_7745c5c3_Err != nil {1894 if templ_7745c5c3_Err != nil {
1663 return templ_7745c5c3_Err1895 return templ_7745c5c3_Err
1664 }1896 }
1665 }1897 }
1666- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "</div></form>")1898+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "</div></form>")
1667 if templ_7745c5c3_Err != nil {1899 if templ_7745c5c3_Err != nil {
1668 return templ_7745c5c3_Err1900 return templ_7745c5c3_Err
1669 }1901 }
@@ -1687,25 +1919,25 @@ func mrRail(p MRPage) templ.Component {
1687 }()1919 }()
1688 }1920 }
1689 ctx = templ.InitializeContext(ctx)1921 ctx = templ.InitializeContext(ctx)
1690- templ_7745c5c3_Var84 := templ.GetChildren(ctx)1922+ templ_7745c5c3_Var94 := templ.GetChildren(ctx)
1691- if templ_7745c5c3_Var84 == nil {1923+ if templ_7745c5c3_Var94 == nil {
1692- templ_7745c5c3_Var84 = templ.NopComponent1924+ templ_7745c5c3_Var94 = templ.NopComponent
1693 }1925 }
1694 ctx = templ.ClearChildren(ctx)1926 ctx = templ.ClearChildren(ctx)
1695- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")1927+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "<section aria-labelledby=\"r-files\" class=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
1696 if templ_7745c5c3_Err != nil {1928 if templ_7745c5c3_Err != nil {
1697 return templ_7745c5c3_Err1929 return templ_7745c5c3_Err
1698 }1930 }
1699- var templ_7745c5c3_Var85 string1931+ var templ_7745c5c3_Var95 string
1700- templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))1932+ templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
1701 if templ_7745c5c3_Err != nil {1933 if templ_7745c5c3_Err != nil {
1702- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 306, Col: 49}1934+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 351, Col: 49}
1703 }1935 }
1704- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))1936+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))
1705 if templ_7745c5c3_Err != nil {1937 if templ_7745c5c3_Err != nil {
1706 return templ_7745c5c3_Err1938 return templ_7745c5c3_Err
1707 }1939 }
1708- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "</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>")1940+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "</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>")
1709 if templ_7745c5c3_Err != nil {1941 if templ_7745c5c3_Err != nil {
1710 return templ_7745c5c3_Err1942 return templ_7745c5c3_Err
1711 }1943 }
@@ -1713,82 +1945,147 @@ func mrRail(p MRPage) templ.Component {
1713 if templ_7745c5c3_Err != nil {1945 if templ_7745c5c3_Err != nil {
1714 return templ_7745c5c3_Err1946 return templ_7745c5c3_Err
1715 }1947 }
1716- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</section>")1948+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "</section>")
1717 if templ_7745c5c3_Err != nil {1949 if templ_7745c5c3_Err != nil {
1718 return templ_7745c5c3_Err1950 return templ_7745c5c3_Err
1719 }1951 }
1952+ if len(p.Verdicts) > 0 {
1953+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "<section><h2>Verdicts</h2><ul>")
1954+ if templ_7745c5c3_Err != nil {
1955+ return templ_7745c5c3_Err
1956+ }
1957+ for _, v := range p.Verdicts {
1958+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<li><a href=\"")
1959+ if templ_7745c5c3_Err != nil {
1960+ return templ_7745c5c3_Err
1961+ }
1962+ var templ_7745c5c3_Var96 templ.SafeURL
1963+ templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(v.Href))
1964+ if templ_7745c5c3_Err != nil {
1965+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 364, Col: 37}
1966+ }
1967+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))
1968+ if templ_7745c5c3_Err != nil {
1969+ return templ_7745c5c3_Err
1970+ }
1971+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\">")
1972+ if templ_7745c5c3_Err != nil {
1973+ return templ_7745c5c3_Err
1974+ }
1975+ var templ_7745c5c3_Var97 string
1976+ templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(v.Author)
1977+ if templ_7745c5c3_Err != nil {
1978+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 364, Col: 50}
1979+ }
1980+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97))
1981+ if templ_7745c5c3_Err != nil {
1982+ return templ_7745c5c3_Err
1983+ }
1984+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, ": ")
1985+ if templ_7745c5c3_Err != nil {
1986+ return templ_7745c5c3_Err
1987+ }
1988+ var templ_7745c5c3_Var98 string
1989+ templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
1990+ if templ_7745c5c3_Err != nil {
1991+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 364, Col: 63}
1992+ }
1993+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))
1994+ if templ_7745c5c3_Err != nil {
1995+ return templ_7745c5c3_Err
1996+ }
1997+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</a> ")
1998+ if templ_7745c5c3_Err != nil {
1999+ return templ_7745c5c3_Err
2000+ }
2001+ if v.Stale {
2002+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<span class=\"mute\">(stale)</span>")
2003+ if templ_7745c5c3_Err != nil {
2004+ return templ_7745c5c3_Err
2005+ }
2006+ }
2007+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "</li>")
2008+ if templ_7745c5c3_Err != nil {
2009+ return templ_7745c5c3_Err
2010+ }
2011+ }
2012+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "</ul></section>")
2013+ if templ_7745c5c3_Err != nil {
2014+ return templ_7745c5c3_Err
2015+ }
2016+ }
1720 templ_7745c5c3_Err = MRChecks(p.Checks).Render(ctx, templ_7745c5c3_Buffer)2017 templ_7745c5c3_Err = MRChecks(p.Checks).Render(ctx, templ_7745c5c3_Buffer)
1721 if templ_7745c5c3_Err != nil {2018 if templ_7745c5c3_Err != nil {
1722 return templ_7745c5c3_Err2019 return templ_7745c5c3_Err
1723 }2020 }
1724- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")2021+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">Commits <span class=\"count\">")
1725 if templ_7745c5c3_Err != nil {2022 if templ_7745c5c3_Err != nil {
1726 return templ_7745c5c3_Err2023 return templ_7745c5c3_Err
1727 }2024 }
1728- var templ_7745c5c3_Var86 string2025+ var templ_7745c5c3_Var99 string
1729- templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))2026+ templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits)))
1730 if templ_7745c5c3_Err != nil {2027 if templ_7745c5c3_Err != nil {
1731- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 71}2028+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 375, Col: 71}
1732 }2029 }
1733- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86))2030+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))
1734 if templ_7745c5c3_Err != nil {2031 if templ_7745c5c3_Err != nil {
1735 return templ_7745c5c3_Err2032 return templ_7745c5c3_Err
1736 }2033 }
1737- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "</span></h2><ul class=\"pcommits\">")2034+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "</span></h2><ul class=\"pcommits\">")
1738 if templ_7745c5c3_Err != nil {2035 if templ_7745c5c3_Err != nil {
1739 return templ_7745c5c3_Err2036 return templ_7745c5c3_Err
1740 }2037 }
1741 for _, c := range p.Commits {2038 for _, c := range p.Commits {
1742- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "<li")2039+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "<li")
1743 if templ_7745c5c3_Err != nil {2040 if templ_7745c5c3_Err != nil {
1744 return templ_7745c5c3_Err2041 return templ_7745c5c3_Err
1745 }2042 }
1746 if c.New {2043 if c.New {
1747- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, " data-new")2044+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, " data-new")
1748 if templ_7745c5c3_Err != nil {2045 if templ_7745c5c3_Err != nil {
1749 return templ_7745c5c3_Err2046 return templ_7745c5c3_Err
1750 }2047 }
1751 }2048 }
1752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "><a href=\"")2049+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "><a href=\"")
1753 if templ_7745c5c3_Err != nil {2050 if templ_7745c5c3_Err != nil {
1754 return templ_7745c5c3_Err2051 return templ_7745c5c3_Err
1755 }2052 }
1756- var templ_7745c5c3_Var87 templ.SafeURL2053+ var templ_7745c5c3_Var100 templ.SafeURL
1757- templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))2054+ templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
1758 if templ_7745c5c3_Err != nil {2055 if templ_7745c5c3_Err != nil {
1759- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 36}2056+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 379, Col: 36}
1760 }2057 }
1761- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87))2058+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))
1762 if templ_7745c5c3_Err != nil {2059 if templ_7745c5c3_Err != nil {
1763 return templ_7745c5c3_Err2060 return templ_7745c5c3_Err
1764 }2061 }
1765- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\">")2062+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\">")
1766 if templ_7745c5c3_Err != nil {2063 if templ_7745c5c3_Err != nil {
1767 return templ_7745c5c3_Err2064 return templ_7745c5c3_Err
1768 }2065 }
1769- var templ_7745c5c3_Var88 string2066+ var templ_7745c5c3_Var101 string
1770- templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)2067+ templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
1771 if templ_7745c5c3_Err != nil {2068 if templ_7745c5c3_Err != nil {
1772- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 319, Col: 48}2069+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 379, Col: 48}
1773 }2070 }
1774- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88))2071+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101))
1775 if templ_7745c5c3_Err != nil {2072 if templ_7745c5c3_Err != nil {
1776 return templ_7745c5c3_Err2073 return templ_7745c5c3_Err
1777 }2074 }
1778- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "</a> <span class=\"s\">")2075+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "</a> <span class=\"s\">")
1779 if templ_7745c5c3_Err != nil {2076 if templ_7745c5c3_Err != nil {
1780 return templ_7745c5c3_Err2077 return templ_7745c5c3_Err
1781 }2078 }
1782- var templ_7745c5c3_Var89 string2079+ var templ_7745c5c3_Var102 string
1783- templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)2080+ templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
1784 if templ_7745c5c3_Err != nil {2081 if templ_7745c5c3_Err != nil {
1785- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 320, Col: 32}2082+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 380, Col: 32}
1786 }2083 }
1787- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89))2084+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))
1788 if templ_7745c5c3_Err != nil {2085 if templ_7745c5c3_Err != nil {
1789 return templ_7745c5c3_Err2086 return templ_7745c5c3_Err
1790 }2087 }
1791- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "</span> ")2088+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</span> ")
1792 if templ_7745c5c3_Err != nil {2089 if templ_7745c5c3_Err != nil {
1793 return templ_7745c5c3_Err2090 return templ_7745c5c3_Err
1794 }2091 }
@@ -1798,138 +2095,138 @@ func mrRail(p MRPage) templ.Component {
1798 return templ_7745c5c3_Err2095 return templ_7745c5c3_Err
1799 }2096 }
1800 }2097 }
1801- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<span class=\"mute\">")2098+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<span class=\"mute\">")
1802 if templ_7745c5c3_Err != nil {2099 if templ_7745c5c3_Err != nil {
1803 return templ_7745c5c3_Err2100 return templ_7745c5c3_Err
1804 }2101 }
1805- var templ_7745c5c3_Var90 string2102+ var templ_7745c5c3_Var103 string
1806- templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)2103+ templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
1807 if templ_7745c5c3_Err != nil {2104 if templ_7745c5c3_Err != nil {
1808- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 325, Col: 16}2105+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 385, Col: 16}
1809 }2106 }
1810- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90))2107+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))
1811 if templ_7745c5c3_Err != nil {2108 if templ_7745c5c3_Err != nil {
1812 return templ_7745c5c3_Err2109 return templ_7745c5c3_Err
1813 }2110 }
1814- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, " ")2111+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, " ")
1815 if templ_7745c5c3_Err != nil {2112 if templ_7745c5c3_Err != nil {
1816 return templ_7745c5c3_Err2113 return templ_7745c5c3_Err
1817 }2114 }
1818 if c.WhenAt != "" {2115 if c.WhenAt != "" {
1819- var templ_7745c5c3_Var91 string2116+ var templ_7745c5c3_Var104 string
1820- templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")2117+ templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
1821 if templ_7745c5c3_Err != nil {2118 if templ_7745c5c3_Err != nil {
1822- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 15}2119+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 15}
1823 }2120 }
1824- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91))2121+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))
1825 if templ_7745c5c3_Err != nil {2122 if templ_7745c5c3_Err != nil {
1826 return templ_7745c5c3_Err2123 return templ_7745c5c3_Err
1827 }2124 }
1828- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "<time datetime=\"")2125+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "<time datetime=\"")
1829 if templ_7745c5c3_Err != nil {2126 if templ_7745c5c3_Err != nil {
1830 return templ_7745c5c3_Err2127 return templ_7745c5c3_Err
1831 }2128 }
1832- var templ_7745c5c3_Var92 string2129+ var templ_7745c5c3_Var105 string
1833- templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)2130+ templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
1834 if templ_7745c5c3_Err != nil {2131 if templ_7745c5c3_Err != nil {
1835- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 42}2132+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 42}
1836 }2133 }
1837- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var92)2134+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var105)
1838 if templ_7745c5c3_Err != nil {2135 if templ_7745c5c3_Err != nil {
1839 return templ_7745c5c3_Err2136 return templ_7745c5c3_Err
1840 }2137 }
1841- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "\">")2138+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\">")
1842 if templ_7745c5c3_Err != nil {2139 if templ_7745c5c3_Err != nil {
1843 return templ_7745c5c3_Err2140 return templ_7745c5c3_Err
1844 }2141 }
1845- var templ_7745c5c3_Var93 string2142+ var templ_7745c5c3_Var106 string
1846- templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)2143+ templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
1847 if templ_7745c5c3_Err != nil {2144 if templ_7745c5c3_Err != nil {
1848- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 53}2145+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 387, Col: 53}
1849 }2146 }
1850- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var93))2147+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106))
1851 if templ_7745c5c3_Err != nil {2148 if templ_7745c5c3_Err != nil {
1852 return templ_7745c5c3_Err2149 return templ_7745c5c3_Err
1853 }2150 }
1854- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</time> ")2151+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</time> ")
1855 if templ_7745c5c3_Err != nil {2152 if templ_7745c5c3_Err != nil {
1856 return templ_7745c5c3_Err2153 return templ_7745c5c3_Err
1857 }2154 }
1858 }2155 }
1859 if c.New {2156 if c.New {
1860- var templ_7745c5c3_Var94 string2157+ var templ_7745c5c3_Var107 string
1861- templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")2158+ templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinStringErrs(" · new")
1862 if templ_7745c5c3_Err != nil {2159 if templ_7745c5c3_Err != nil {
1863- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 330, Col: 18}2160+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 390, Col: 18}
1864 }2161 }
1865- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94))2162+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107))
1866 if templ_7745c5c3_Err != nil {2163 if templ_7745c5c3_Err != nil {
1867 return templ_7745c5c3_Err2164 return templ_7745c5c3_Err
1868 }2165 }
1869 }2166 }
1870- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "</span></li>")2167+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "</span></li>")
1871 if templ_7745c5c3_Err != nil {2168 if templ_7745c5c3_Err != nil {
1872 return templ_7745c5c3_Err2169 return templ_7745c5c3_Err
1873 }2170 }
1874 }2171 }
1875- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "</ul>")2172+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</ul>")
1876 if templ_7745c5c3_Err != nil {2173 if templ_7745c5c3_Err != nil {
1877 return templ_7745c5c3_Err2174 return templ_7745c5c3_Err
1878 }2175 }
1879 if len(p.Commits) == 0 {2176 if len(p.Commits) == 0 {
1880- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "<p class=\"dim\">no commit to merge.</p>")2177+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "<p class=\"dim\">no commit to merge.</p>")
1881 if templ_7745c5c3_Err != nil {2178 if templ_7745c5c3_Err != nil {
1882 return templ_7745c5c3_Err2179 return templ_7745c5c3_Err
1883 }2180 }
1884 }2181 }
1885 if p.CommitsNote != "" {2182 if p.CommitsNote != "" {
1886- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "<p class=\"note\">")2183+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"note\">")
1887 if templ_7745c5c3_Err != nil {2184 if templ_7745c5c3_Err != nil {
1888 return templ_7745c5c3_Err2185 return templ_7745c5c3_Err
1889 }2186 }
1890- var templ_7745c5c3_Var95 string2187+ var templ_7745c5c3_Var108 string
1891- templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)2188+ templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote)
1892 if templ_7745c5c3_Err != nil {2189 if templ_7745c5c3_Err != nil {
1893- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 340, Col: 34}2190+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 34}
1894 }2191 }
1895- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95))2192+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))
1896 if templ_7745c5c3_Err != nil {2193 if templ_7745c5c3_Err != nil {
1897 return templ_7745c5c3_Err2194 return templ_7745c5c3_Err
1898 }2195 }
1899- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</p>")2196+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</p>")
1900 if templ_7745c5c3_Err != nil {2197 if templ_7745c5c3_Err != nil {
1901 return templ_7745c5c3_Err2198 return templ_7745c5c3_Err
1902 }2199 }
1903 }2200 }
1904- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")2201+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">Settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
1905 if templ_7745c5c3_Err != nil {2202 if templ_7745c5c3_Err != nil {
1906 return templ_7745c5c3_Err2203 return templ_7745c5c3_Err
1907 }2204 }
1908- var templ_7745c5c3_Var96 string2205+ var templ_7745c5c3_Var109 string
1909- templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))2206+ templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
1910 if templ_7745c5c3_Err != nil {2207 if templ_7745c5c3_Err != nil {
1911- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 347, Col: 36}2208+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 407, Col: 36}
1912 }2209 }
1913- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96))2210+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))
1914 if templ_7745c5c3_Err != nil {2211 if templ_7745c5c3_Err != nil {
1915 return templ_7745c5c3_Err2212 return templ_7745c5c3_Err
1916 }2213 }
1917- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "</dd><dt>checks</dt><dd>")2214+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "</dd><dt>checks</dt><dd>")
1918 if templ_7745c5c3_Err != nil {2215 if templ_7745c5c3_Err != nil {
1919 return templ_7745c5c3_Err2216 return templ_7745c5c3_Err
1920 }2217 }
1921 if p.ChecksBlock {2218 if p.ChecksBlock {
1922- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "failing ones block the merge")2219+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "failing ones block the merge")
1923 if templ_7745c5c3_Err != nil {2220 if templ_7745c5c3_Err != nil {
1924 return templ_7745c5c3_Err2221 return templ_7745c5c3_Err
1925 }2222 }
1926 } else {2223 } else {
1927- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "do not block the merge")2224+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "do not block the merge")
1928 if templ_7745c5c3_Err != nil {2225 if templ_7745c5c3_Err != nil {
1929 return templ_7745c5c3_Err2226 return templ_7745c5c3_Err
1930 }2227 }
1931 }2228 }
1932- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "</dd></dl></section>")2229+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</dd></dl></section>")
1933 if templ_7745c5c3_Err != nil {2230 if templ_7745c5c3_Err != nil {
1934 return templ_7745c5c3_Err2231 return templ_7745c5c3_Err
1935 }2232 }
@@ -1957,51 +2254,51 @@ func mrBar(p MRPage) templ.Component {
1957 }()2254 }()
1958 }2255 }
1959 ctx = templ.InitializeContext(ctx)2256 ctx = templ.InitializeContext(ctx)
1960- templ_7745c5c3_Var97 := templ.GetChildren(ctx)2257+ templ_7745c5c3_Var110 := templ.GetChildren(ctx)
1961- if templ_7745c5c3_Var97 == nil {2258+ if templ_7745c5c3_Var110 == nil {
1962- templ_7745c5c3_Var97 = templ.NopComponent2259+ templ_7745c5c3_Var110 = templ.NopComponent
1963 }2260 }
1964 ctx = templ.ClearChildren(ctx)2261 ctx = templ.ClearChildren(ctx)
1965- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")2262+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<div class=\"diff-tools mr-bar\" role=\"group\" aria-label=\"Diff view and merge\"><span class=\"dim\">")
1966 if templ_7745c5c3_Err != nil {2263 if templ_7745c5c3_Err != nil {
1967 return templ_7745c5c3_Err2264 return templ_7745c5c3_Err
1968 }2265 }
1969- var templ_7745c5c3_Var98 string2266+ var templ_7745c5c3_Var111 string
1970- templ_7745c5c3_Var98, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))2267+ templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
1971 if templ_7745c5c3_Err != nil {2268 if templ_7745c5c3_Err != nil {
1972- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 366, Col: 55}2269+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 55}
1973 }2270 }
1974- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var98))2271+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))
1975 if templ_7745c5c3_Err != nil {2272 if templ_7745c5c3_Err != nil {
1976 return templ_7745c5c3_Err2273 return templ_7745c5c3_Err
1977 }2274 }
1978- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, " · <span class=\"plus\">+")2275+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, " · <span class=\"plus\">+")
1979 if templ_7745c5c3_Err != nil {2276 if templ_7745c5c3_Err != nil {
1980 return templ_7745c5c3_Err2277 return templ_7745c5c3_Err
1981 }2278 }
1982- var templ_7745c5c3_Var99 string2279+ var templ_7745c5c3_Var112 string
1983- templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))2280+ templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
1984 if templ_7745c5c3_Err != nil {2281 if templ_7745c5c3_Err != nil {
1985- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 366, Col: 95}2282+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 95}
1986 }2283 }
1987- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99))2284+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112))
1988 if templ_7745c5c3_Err != nil {2285 if templ_7745c5c3_Err != nil {
1989 return templ_7745c5c3_Err2286 return templ_7745c5c3_Err
1990 }2287 }
1991- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "</span> <span class=\"minus\">-")2288+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "</span> <span class=\"minus\">-")
1992 if templ_7745c5c3_Err != nil {2289 if templ_7745c5c3_Err != nil {
1993 return templ_7745c5c3_Err2290 return templ_7745c5c3_Err
1994 }2291 }
1995- var templ_7745c5c3_Var100 string2292+ var templ_7745c5c3_Var113 string
1996- templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))2293+ templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
1997 if templ_7745c5c3_Err != nil {2294 if templ_7745c5c3_Err != nil {
1998- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 366, Col: 140}2295+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 140}
1999 }2296 }
2000- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100))2297+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))
2001 if templ_7745c5c3_Err != nil {2298 if templ_7745c5c3_Err != nil {
2002 return templ_7745c5c3_Err2299 return templ_7745c5c3_Err
2003 }2300 }
2004- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "</span></span>")2301+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "</span></span>")
2005 if templ_7745c5c3_Err != nil {2302 if templ_7745c5c3_Err != nil {
2006 return templ_7745c5c3_Err2303 return templ_7745c5c3_Err
2007 }2304 }
@@ -2017,7 +2314,7 @@ func mrBar(p MRPage) templ.Component {
2017 if templ_7745c5c3_Err != nil {2314 if templ_7745c5c3_Err != nil {
2018 return templ_7745c5c3_Err2315 return templ_7745c5c3_Err
2019 }2316 }
2020- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "<span class=\"grow\"></span>")2317+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "<span class=\"grow\"></span>")
2021 if templ_7745c5c3_Err != nil {2318 if templ_7745c5c3_Err != nil {
2022 return templ_7745c5c3_Err2319 return templ_7745c5c3_Err
2023 }2320 }
@@ -2030,7 +2327,7 @@ func mrBar(p MRPage) templ.Component {
2030 if templ_7745c5c3_Err != nil {2327 if templ_7745c5c3_Err != nil {
2031 return templ_7745c5c3_Err2328 return templ_7745c5c3_Err
2032 }2329 }
2033- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, " ")2330+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " ")
2034 if templ_7745c5c3_Err != nil {2331 if templ_7745c5c3_Err != nil {
2035 return templ_7745c5c3_Err2332 return templ_7745c5c3_Err
2036 }2333 }
@@ -2039,7 +2336,7 @@ func mrBar(p MRPage) templ.Component {
2039 return templ_7745c5c3_Err2336 return templ_7745c5c3_Err
2040 }2337 }
2041 }2338 }
2042- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</div>")2339+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</div>")
2043 if templ_7745c5c3_Err != nil {2340 if templ_7745c5c3_Err != nil {
2044 return templ_7745c5c3_Err2341 return templ_7745c5c3_Err
2045 }2342 }
@@ -2066,219 +2363,219 @@ func mergeMenu(p MRPage) templ.Component {
2066 }()2363 }()
2067 }2364 }
2068 ctx = templ.InitializeContext(ctx)2365 ctx = templ.InitializeContext(ctx)
2069- templ_7745c5c3_Var101 := templ.GetChildren(ctx)2366+ templ_7745c5c3_Var114 := templ.GetChildren(ctx)
2070- if templ_7745c5c3_Var101 == nil {2367+ if templ_7745c5c3_Var114 == nil {
2071- templ_7745c5c3_Var101 = templ.NopComponent2368+ templ_7745c5c3_Var114 = templ.NopComponent
2072 }2369 }
2073 ctx = templ.ClearChildren(ctx)2370 ctx = templ.ClearChildren(ctx)
2074- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")2371+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "<details class=\"menu mmenu\" id=\"merge-menu\" data-state=\"open\"><summary class=\"btn primary\" aria-keyshortcuts=\"Shift+M\">")
2075 if templ_7745c5c3_Err != nil {2372 if templ_7745c5c3_Err != nil {
2076 return templ_7745c5c3_Err2373 return templ_7745c5c3_Err
2077 }2374 }
2078 if p.Box.ReadyAction != "" {2375 if p.Box.ReadyAction != "" {
2079- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "Draft ")2376+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "Draft ")
2080 if templ_7745c5c3_Err != nil {2377 if templ_7745c5c3_Err != nil {
2081 return templ_7745c5c3_Err2378 return templ_7745c5c3_Err
2082 }2379 }
2083 } else {2380 } else {
2084- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "Merge ")2381+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "Merge ")
2085 if templ_7745c5c3_Err != nil {2382 if templ_7745c5c3_Err != nil {
2086 return templ_7745c5c3_Err2383 return templ_7745c5c3_Err
2087 }2384 }
2088 }2385 }
2089- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>")2386+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "<kbd aria-hidden=\"true\">M</kbd></summary><div class=\"menu-pop\" role=\"group\" aria-label=\"Merge\"><dl class=\"kv\"><dt>head</dt><dd>")
2090 if templ_7745c5c3_Err != nil {2387 if templ_7745c5c3_Err != nil {
2091 return templ_7745c5c3_Err2388 return templ_7745c5c3_Err
2092 }2389 }
2093 if p.Box.HeadGone {2390 if p.Box.HeadGone {
2094- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "gone: the source branch was deleted")2391+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "gone: the source branch was deleted")
2095 if templ_7745c5c3_Err != nil {2392 if templ_7745c5c3_Err != nil {
2096 return templ_7745c5c3_Err2393 return templ_7745c5c3_Err
2097 }2394 }
2098 } else {2395 } else {
2099- var templ_7745c5c3_Var102 string2396+ var templ_7745c5c3_Var115 string
2100- templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)2397+ templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort)
2101 if templ_7745c5c3_Err != nil {2398 if templ_7745c5c3_Err != nil {
2102- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 399, Col: 19}2399+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 19}
2103 }2400 }
2104- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var102))2401+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))
2105 if templ_7745c5c3_Err != nil {2402 if templ_7745c5c3_Err != nil {
2106 return templ_7745c5c3_Err2403 return templ_7745c5c3_Err
2107 }2404 }
2108 }2405 }
2109- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "</dd><dt>into</dt><dd>")2406+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "</dd><dt>into</dt><dd>")
2110 if templ_7745c5c3_Err != nil {2407 if templ_7745c5c3_Err != nil {
2111 return templ_7745c5c3_Err2408 return templ_7745c5c3_Err
2112 }2409 }
2113- var templ_7745c5c3_Var103 string2410+ var templ_7745c5c3_Var116 string
2114- templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2411+ templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2115 if templ_7745c5c3_Err != nil {2412 if templ_7745c5c3_Err != nil {
2116- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 403, Col: 18}2413+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 463, Col: 18}
2117 }2414 }
2118- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var103))2415+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116))
2119 if templ_7745c5c3_Err != nil {2416 if templ_7745c5c3_Err != nil {
2120 return templ_7745c5c3_Err2417 return templ_7745c5c3_Err
2121 }2418 }
2122- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "</dd></dl>")2419+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</dd></dl>")
2123 if templ_7745c5c3_Err != nil {2420 if templ_7745c5c3_Err != nil {
2124 return templ_7745c5c3_Err2421 return templ_7745c5c3_Err
2125 }2422 }
2126 if p.Box.ReadyAction != "" {2423 if p.Box.ReadyAction != "" {
2127- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<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=\"")2424+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "<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=\"")
2128 if templ_7745c5c3_Err != nil {2425 if templ_7745c5c3_Err != nil {
2129 return templ_7745c5c3_Err2426 return templ_7745c5c3_Err
2130 }2427 }
2131- var templ_7745c5c3_Var104 templ.SafeURL2428+ var templ_7745c5c3_Var117 templ.SafeURL
2132- templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))2429+ templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadyAction))
2133 if templ_7745c5c3_Err != nil {2430 if templ_7745c5c3_Err != nil {
2134- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 407, Col: 80}2431+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 467, Col: 80}
2135 }2432 }
2136- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104))2433+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117))
2137 if templ_7745c5c3_Err != nil {2434 if templ_7745c5c3_Err != nil {
2138 return templ_7745c5c3_Err2435 return templ_7745c5c3_Err
2139 }2436 }
2140- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")2437+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "\"><button type=\"submit\" class=\"btn primary\" data-default title=\"remove the draft prefix from the title\">Ready for review</button></form>")
2141 if templ_7745c5c3_Err != nil {2438 if templ_7745c5c3_Err != nil {
2142 return templ_7745c5c3_Err2439 return templ_7745c5c3_Err
2143 }2440 }
2144 } else if !p.Box.HeadGone {2441 } else if !p.Box.HeadGone {
2145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<form class=\"d-acts\" method=\"post\" action=\"")2442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "<form class=\"d-acts\" method=\"post\" action=\"")
2146 if templ_7745c5c3_Err != nil {2443 if templ_7745c5c3_Err != nil {
2147 return templ_7745c5c3_Err2444 return templ_7745c5c3_Err
2148 }2445 }
2149- var templ_7745c5c3_Var105 templ.SafeURL2446+ var templ_7745c5c3_Var118 templ.SafeURL
2150- templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))2447+ templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2151 if templ_7745c5c3_Err != nil {2448 if templ_7745c5c3_Err != nil {
2152- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 411, Col: 80}2449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 471, Col: 80}
2153 }2450 }
2154- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105))2451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))
2155 if templ_7745c5c3_Err != nil {2452 if templ_7745c5c3_Err != nil {
2156 return templ_7745c5c3_Err2453 return templ_7745c5c3_Err
2157 }2454 }
2158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "\"><input type=\"hidden\" name=\"head_id\" value=\"")2455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2159 if templ_7745c5c3_Err != nil {2456 if templ_7745c5c3_Err != nil {
2160 return templ_7745c5c3_Err2457 return templ_7745c5c3_Err
2161 }2458 }
2162- var templ_7745c5c3_Var106 string2459+ var templ_7745c5c3_Var119 string
2163- templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)2460+ templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2164 if templ_7745c5c3_Err != nil {2461 if templ_7745c5c3_Err != nil {
2165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 412, Col: 59}2462+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 472, Col: 59}
2166 }2463 }
2167- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var106)2464+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var119)
2168 if templ_7745c5c3_Err != nil {2465 if templ_7745c5c3_Err != nil {
2169 return templ_7745c5c3_Err2466 return templ_7745c5c3_Err
2170 }2467 }
2171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")2468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
2172 if templ_7745c5c3_Err != nil {2469 if templ_7745c5c3_Err != nil {
2173 return templ_7745c5c3_Err2470 return templ_7745c5c3_Err
2174 }2471 }
2175 for i, m := range p.Box.Modes {2472 for i, m := range p.Box.Modes {
2176- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "<label><input type=\"radio\" name=\"mode\" value=\"")2473+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<label><input type=\"radio\" name=\"mode\" value=\"")
2177 if templ_7745c5c3_Err != nil {2474 if templ_7745c5c3_Err != nil {
2178 return templ_7745c5c3_Err2475 return templ_7745c5c3_Err
2179 }2476 }
2180- var templ_7745c5c3_Var107 string2477+ var templ_7745c5c3_Var120 string
2181- templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)2478+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2182 if templ_7745c5c3_Err != nil {2479 if templ_7745c5c3_Err != nil {
2183- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 417, Col: 55}2480+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 477, Col: 55}
2184 }2481 }
2185- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107)2482+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var120)
2186 if templ_7745c5c3_Err != nil {2483 if templ_7745c5c3_Err != nil {
2187 return templ_7745c5c3_Err2484 return templ_7745c5c3_Err
2188 }2485 }
2189- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "\"")2486+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "\"")
2190 if templ_7745c5c3_Err != nil {2487 if templ_7745c5c3_Err != nil {
2191 return templ_7745c5c3_Err2488 return templ_7745c5c3_Err
2192 }2489 }
2193 if i == 0 {2490 if i == 0 {
2194- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, " checked")2491+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, " checked")
2195 if templ_7745c5c3_Err != nil {2492 if templ_7745c5c3_Err != nil {
2196 return templ_7745c5c3_Err2493 return templ_7745c5c3_Err
2197 }2494 }
2198 }2495 }
2199- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "> ")2496+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "> ")
2200 if templ_7745c5c3_Err != nil {2497 if templ_7745c5c3_Err != nil {
2201 return templ_7745c5c3_Err2498 return templ_7745c5c3_Err
2202 }2499 }
2203- var templ_7745c5c3_Var108 string2500+ var templ_7745c5c3_Var121 string
2204- templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)2501+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2205 if templ_7745c5c3_Err != nil {2502 if templ_7745c5c3_Err != nil {
2206- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 17}2503+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 478, Col: 17}
2207 }2504 }
2208- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108))2505+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2209 if templ_7745c5c3_Err != nil {2506 if templ_7745c5c3_Err != nil {
2210 return templ_7745c5c3_Err2507 return templ_7745c5c3_Err
2211 }2508 }
2212- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, " ")2509+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " ")
2213 if templ_7745c5c3_Err != nil {2510 if templ_7745c5c3_Err != nil {
2214 return templ_7745c5c3_Err2511 return templ_7745c5c3_Err
2215 }2512 }
2216 if i < 9 {2513 if i < 9 {
2217- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "<kbd aria-hidden=\"true\">")2514+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<kbd aria-hidden=\"true\">")
2218 if templ_7745c5c3_Err != nil {2515 if templ_7745c5c3_Err != nil {
2219 return templ_7745c5c3_Err2516 return templ_7745c5c3_Err
2220 }2517 }
2221- var templ_7745c5c3_Var109 string2518+ var templ_7745c5c3_Var122 string
2222- templ_7745c5c3_Var109, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))2519+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
2223 if templ_7745c5c3_Err != nil {2520 if templ_7745c5c3_Err != nil {
2224- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 46}2521+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 46}
2225 }2522 }
2226- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var109))2523+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2227 if templ_7745c5c3_Err != nil {2524 if templ_7745c5c3_Err != nil {
2228 return templ_7745c5c3_Err2525 return templ_7745c5c3_Err
2229 }2526 }
2230- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</kbd>")2527+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "</kbd>")
2231 if templ_7745c5c3_Err != nil {2528 if templ_7745c5c3_Err != nil {
2232 return templ_7745c5c3_Err2529 return templ_7745c5c3_Err
2233 }2530 }
2234 }2531 }
2235- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "</label>")2532+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "</label>")
2236 if templ_7745c5c3_Err != nil {2533 if templ_7745c5c3_Err != nil {
2237 return templ_7745c5c3_Err2534 return templ_7745c5c3_Err
2238 }2535 }
2239 }2536 }
2240- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</fieldset>")2537+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</fieldset>")
2241 if templ_7745c5c3_Err != nil {2538 if templ_7745c5c3_Err != nil {
2242 return templ_7745c5c3_Err2539 return templ_7745c5c3_Err
2243 }2540 }
2244 if p.Box.CanDeleteBranch {2541 if p.Box.CanDeleteBranch {
2245- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")2542+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2246 if templ_7745c5c3_Err != nil {2543 if templ_7745c5c3_Err != nil {
2247 return templ_7745c5c3_Err2544 return templ_7745c5c3_Err
2248 }2545 }
2249- var templ_7745c5c3_Var110 string2546+ var templ_7745c5c3_Var123 string
2250- templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)2547+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2251 if templ_7745c5c3_Err != nil {2548 if templ_7745c5c3_Err != nil {
2252- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 426, Col: 102}2549+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 102}
2253 }2550 }
2254- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110))2551+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
2255 if templ_7745c5c3_Err != nil {2552 if templ_7745c5c3_Err != nil {
2256 return templ_7745c5c3_Err2553 return templ_7745c5c3_Err
2257 }2554 }
2258- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")2555+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2259 if templ_7745c5c3_Err != nil {2556 if templ_7745c5c3_Err != nil {
2260 return templ_7745c5c3_Err2557 return templ_7745c5c3_Err
2261 }2558 }
2262 }2559 }
2263- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default>Merge into ")2560+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default>Merge into ")
2264 if templ_7745c5c3_Err != nil {2561 if templ_7745c5c3_Err != nil {
2265 return templ_7745c5c3_Err2562 return templ_7745c5c3_Err
2266 }2563 }
2267- var templ_7745c5c3_Var111 string2564+ var templ_7745c5c3_Var124 string
2268- templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2565+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2269 if templ_7745c5c3_Err != nil {2566 if templ_7745c5c3_Err != nil {
2270- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 429, Col: 82}2567+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 82}
2271 }2568 }
2272- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111))2569+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
2273 if templ_7745c5c3_Err != nil {2570 if templ_7745c5c3_Err != nil {
2274 return templ_7745c5c3_Err2571 return templ_7745c5c3_Err
2275 }2572 }
2276- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</button></div></form>")2573+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</button></div></form>")
2277 if templ_7745c5c3_Err != nil {2574 if templ_7745c5c3_Err != nil {
2278 return templ_7745c5c3_Err2575 return templ_7745c5c3_Err
2279 }2576 }
2280 }2577 }
2281- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "</div></details>")2578+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</div></details>")
2282 if templ_7745c5c3_Err != nil {2579 if templ_7745c5c3_Err != nil {
2283 return templ_7745c5c3_Err2580 return templ_7745c5c3_Err
2284 }2581 }
@@ -2304,66 +2601,66 @@ func moreMenu(p MRPage) templ.Component {
2304 }()2601 }()
2305 }2602 }
2306 ctx = templ.InitializeContext(ctx)2603 ctx = templ.InitializeContext(ctx)
2307- templ_7745c5c3_Var112 := templ.GetChildren(ctx)2604+ templ_7745c5c3_Var125 := templ.GetChildren(ctx)
2308- if templ_7745c5c3_Var112 == nil {2605+ if templ_7745c5c3_Var125 == nil {
2309- templ_7745c5c3_Var112 = templ.NopComponent2606+ templ_7745c5c3_Var125 = templ.NopComponent
2310 }2607 }
2311 ctx = templ.ClearChildren(ctx)2608 ctx = templ.ClearChildren(ctx)
2312- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<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\">")2609+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "<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\">")
2313 if templ_7745c5c3_Err != nil {2610 if templ_7745c5c3_Err != nil {
2314 return templ_7745c5c3_Err2611 return templ_7745c5c3_Err
2315 }2612 }
2316 if p.Box.CanMarkRead {2613 if p.Box.CanMarkRead {
2317- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "<form method=\"post\" action=\"")2614+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "<form method=\"post\" action=\"")
2318 if templ_7745c5c3_Err != nil {2615 if templ_7745c5c3_Err != nil {
2319 return templ_7745c5c3_Err2616 return templ_7745c5c3_Err
2320 }2617 }
2321- var templ_7745c5c3_Var113 templ.SafeURL2618+ var templ_7745c5c3_Var126 templ.SafeURL
2322- templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))2619+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
2323 if templ_7745c5c3_Err != nil {2620 if templ_7745c5c3_Err != nil {
2324- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 64}2621+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 504, Col: 64}
2325 }2622 }
2326- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113))2623+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2327 if templ_7745c5c3_Err != nil {2624 if templ_7745c5c3_Err != nil {
2328 return templ_7745c5c3_Err2625 return templ_7745c5c3_Err
2329 }2626 }
2330- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "\"><input type=\"hidden\" name=\"head_id\" value=\"")2627+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2331 if templ_7745c5c3_Err != nil {2628 if templ_7745c5c3_Err != nil {
2332 return templ_7745c5c3_Err2629 return templ_7745c5c3_Err
2333 }2630 }
2334- var templ_7745c5c3_Var114 string2631+ var templ_7745c5c3_Var127 string
2335- templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)2632+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2336 if templ_7745c5c3_Err != nil {2633 if templ_7745c5c3_Err != nil {
2337- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 445, Col: 59}2634+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 505, Col: 59}
2338 }2635 }
2339- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var114)2636+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127)
2340 if templ_7745c5c3_Err != nil {2637 if templ_7745c5c3_Err != nil {
2341 return templ_7745c5c3_Err2638 return templ_7745c5c3_Err
2342 }2639 }
2343- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "\"> <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>")2640+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "\"> <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>")
2344 if templ_7745c5c3_Err != nil {2641 if templ_7745c5c3_Err != nil {
2345 return templ_7745c5c3_Err2642 return templ_7745c5c3_Err
2346 }2643 }
2347 }2644 }
2348- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "<form method=\"post\" action=\"")2645+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "<form method=\"post\" action=\"")
2349 if templ_7745c5c3_Err != nil {2646 if templ_7745c5c3_Err != nil {
2350 return templ_7745c5c3_Err2647 return templ_7745c5c3_Err
2351 }2648 }
2352- var templ_7745c5c3_Var115 templ.SafeURL2649+ var templ_7745c5c3_Var128 templ.SafeURL
2353- templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))2650+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
2354 if templ_7745c5c3_Err != nil {2651 if templ_7745c5c3_Err != nil {
2355- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 449, Col: 64}2652+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 509, Col: 64}
2356 }2653 }
2357- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115))2654+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2358 if templ_7745c5c3_Err != nil {2655 if templ_7745c5c3_Err != nil {
2359 return templ_7745c5c3_Err2656 return templ_7745c5c3_Err
2360 }2657 }
2361- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")2658+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
2362 if templ_7745c5c3_Err != nil {2659 if templ_7745c5c3_Err != nil {
2363 return templ_7745c5c3_Err2660 return templ_7745c5c3_Err
2364 }2661 }
2365 if !p.Box.HeadGone && p.Box.ReadyAction == "" {2662 if !p.Box.HeadGone && p.Box.ReadyAction == "" {
2366- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")2663+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
2367 if templ_7745c5c3_Err != nil {2664 if templ_7745c5c3_Err != nil {
2368 return templ_7745c5c3_Err2665 return templ_7745c5c3_Err
2369 }2666 }
@@ -2371,12 +2668,12 @@ func moreMenu(p MRPage) templ.Component {
2371 if templ_7745c5c3_Err != nil {2668 if templ_7745c5c3_Err != nil {
2372 return templ_7745c5c3_Err2669 return templ_7745c5c3_Err
2373 }2670 }
2374- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "</details>")2671+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "</details>")
2375 if templ_7745c5c3_Err != nil {2672 if templ_7745c5c3_Err != nil {
2376 return templ_7745c5c3_Err2673 return templ_7745c5c3_Err
2377 }2674 }
2378 }2675 }
2379- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</div></details>")2676+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "</div></details>")
2380 if templ_7745c5c3_Err != nil {2677 if templ_7745c5c3_Err != nil {
2381 return templ_7745c5c3_Err2678 return templ_7745c5c3_Err
2382 }2679 }
@@ -2400,12 +2697,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
2400 }()2697 }()
2401 }2698 }
2402 ctx = templ.InitializeContext(ctx)2699 ctx = templ.InitializeContext(ctx)
2403- templ_7745c5c3_Var116 := templ.GetChildren(ctx)2700+ templ_7745c5c3_Var129 := templ.GetChildren(ctx)
2404- if templ_7745c5c3_Var116 == nil {2701+ if templ_7745c5c3_Var129 == nil {
2405- templ_7745c5c3_Var116 = templ.NopComponent2702+ templ_7745c5c3_Var129 = templ.NopComponent
2406 }2703 }
2407 ctx = templ.ClearChildren(ctx)2704 ctx = templ.ClearChildren(ctx)
2408- templ_7745c5c3_Var117 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {2705+ templ_7745c5c3_Var130 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
2409 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context2706 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
2410 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)2707 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
2411 if !templ_7745c5c3_IsBuffer {2708 if !templ_7745c5c3_IsBuffer {
@@ -2417,263 +2714,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
2417 }()2714 }()
2418 }2715 }
2419 ctx = templ.InitializeContext(ctx)2716 ctx = templ.InitializeContext(ctx)
2420- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "<header class=\"show mr-h\"><h1>")2717+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, "<header class=\"show mr-h\"><h1>")
2421 if templ_7745c5c3_Err != nil {2718 if templ_7745c5c3_Err != nil {
2422 return templ_7745c5c3_Err2719 return templ_7745c5c3_Err
2423 }2720 }
2424- var templ_7745c5c3_Var118 string2721+ var templ_7745c5c3_Var131 string
2425- templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)2722+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2426 if templ_7745c5c3_Err != nil {2723 if templ_7745c5c3_Err != nil {
2427- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 465, Col: 17}2724+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 17}
2428 }2725 }
2429- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118))2726+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131))
2430 if templ_7745c5c3_Err != nil {2727 if templ_7745c5c3_Err != nil {
2431 return templ_7745c5c3_Err2728 return templ_7745c5c3_Err
2432 }2729 }
2433- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " → ")2730+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, " → ")
2434 if templ_7745c5c3_Err != nil {2731 if templ_7745c5c3_Err != nil {
2435 return templ_7745c5c3_Err2732 return templ_7745c5c3_Err
2436 }2733 }
2437- var templ_7745c5c3_Var119 string2734+ var templ_7745c5c3_Var132 string
2438- templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2735+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2439 if templ_7745c5c3_Err != nil {2736 if templ_7745c5c3_Err != nil {
2440- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 465, Col: 34}2737+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 34}
2441 }2738 }
2442- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119))2739+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
2443 if templ_7745c5c3_Err != nil {2740 if templ_7745c5c3_Err != nil {
2444 return templ_7745c5c3_Err2741 return templ_7745c5c3_Err
2445 }2742 }
2446- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")2743+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
2447 if templ_7745c5c3_Err != nil {2744 if templ_7745c5c3_Err != nil {
2448 return templ_7745c5c3_Err2745 return templ_7745c5c3_Err
2449 }2746 }
2450- var templ_7745c5c3_Var120 templ.SafeURL2747+ var templ_7745c5c3_Var133 templ.SafeURL
2451- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))2748+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
2452 if templ_7745c5c3_Err != nil {2749 if templ_7745c5c3_Err != nil {
2453- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 468, Col: 45}2750+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 528, Col: 45}
2454 }2751 }
2455- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))2752+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
2456 if templ_7745c5c3_Err != nil {2753 if templ_7745c5c3_Err != nil {
2457 return templ_7745c5c3_Err2754 return templ_7745c5c3_Err
2458 }2755 }
2459- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, "\">")2756+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "\">")
2460 if templ_7745c5c3_Err != nil {2757 if templ_7745c5c3_Err != nil {
2461 return templ_7745c5c3_Err2758 return templ_7745c5c3_Err
2462 }2759 }
2463- var templ_7745c5c3_Var121 string2760+ var templ_7745c5c3_Var134 string
2464- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)2761+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2465 if templ_7745c5c3_Err != nil {2762 if templ_7745c5c3_Err != nil {
2466- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 468, Col: 58}2763+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 528, Col: 58}
2467 }2764 }
2468- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))2765+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
2469 if templ_7745c5c3_Err != nil {2766 if templ_7745c5c3_Err != nil {
2470 return templ_7745c5c3_Err2767 return templ_7745c5c3_Err
2471 }2768 }
2472- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "</a></dd><dt>target</dt><dd><a href=\"")2769+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "</a></dd><dt>target</dt><dd><a href=\"")
2473 if templ_7745c5c3_Err != nil {2770 if templ_7745c5c3_Err != nil {
2474 return templ_7745c5c3_Err2771 return templ_7745c5c3_Err
2475 }2772 }
2476- var templ_7745c5c3_Var122 templ.SafeURL2773+ var templ_7745c5c3_Var135 templ.SafeURL
2477- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))2774+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
2478 if templ_7745c5c3_Err != nil {2775 if templ_7745c5c3_Err != nil {
2479- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 45}2776+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 530, Col: 45}
2480 }2777 }
2481- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))2778+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135))
2482 if templ_7745c5c3_Err != nil {2779 if templ_7745c5c3_Err != nil {
2483 return templ_7745c5c3_Err2780 return templ_7745c5c3_Err
2484 }2781 }
2485- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "\">")2782+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "\">")
2486 if templ_7745c5c3_Err != nil {2783 if templ_7745c5c3_Err != nil {
2487 return templ_7745c5c3_Err2784 return templ_7745c5c3_Err
2488 }2785 }
2489- var templ_7745c5c3_Var123 string2786+ var templ_7745c5c3_Var136 string
2490- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2787+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2491 if templ_7745c5c3_Err != nil {2788 if templ_7745c5c3_Err != nil {
2492- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 470, Col: 58}2789+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 530, Col: 58}
2493 }2790 }
2494- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))2791+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
2495 if templ_7745c5c3_Err != nil {2792 if templ_7745c5c3_Err != nil {
2496 return templ_7745c5c3_Err2793 return templ_7745c5c3_Err
2497 }2794 }
2498- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, "</a></dd><dt>ahead</dt><dd>")2795+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</a></dd><dt>ahead</dt><dd>")
2499 if templ_7745c5c3_Err != nil {2796 if templ_7745c5c3_Err != nil {
2500 return templ_7745c5c3_Err2797 return templ_7745c5c3_Err
2501 }2798 }
2502- var templ_7745c5c3_Var124 string2799+ var templ_7745c5c3_Var137 string
2503- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)2800+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
2504 if templ_7745c5c3_Err != nil {2801 if templ_7745c5c3_Err != nil {
2505- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 472, Col: 21}2802+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 532, Col: 21}
2506 }2803 }
2507- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))2804+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
2508 if templ_7745c5c3_Err != nil {2805 if templ_7745c5c3_Err != nil {
2509 return templ_7745c5c3_Err2806 return templ_7745c5c3_Err
2510 }2807 }
2511- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</dd></dl></header>")2808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "</dd></dl></header>")
2512 if templ_7745c5c3_Err != nil {2809 if templ_7745c5c3_Err != nil {
2513 return templ_7745c5c3_Err2810 return templ_7745c5c3_Err
2514 }2811 }
2515 if p.Note != "" {2812 if p.Note != "" {
2516- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "<p class=\"notice\">")2813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, "<p class=\"notice\">")
2517 if templ_7745c5c3_Err != nil {2814 if templ_7745c5c3_Err != nil {
2518 return templ_7745c5c3_Err2815 return templ_7745c5c3_Err
2519 }2816 }
2520- var templ_7745c5c3_Var125 string2817+ var templ_7745c5c3_Var138 string
2521- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)2818+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
2522 if templ_7745c5c3_Err != nil {2819 if templ_7745c5c3_Err != nil {
2523- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 476, Col: 29}2820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 536, Col: 29}
2524 }2821 }
2525- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))2822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
2526 if templ_7745c5c3_Err != nil {2823 if templ_7745c5c3_Err != nil {
2527 return templ_7745c5c3_Err2824 return templ_7745c5c3_Err
2528 }2825 }
2529- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</p>")2826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "</p>")
2530 if templ_7745c5c3_Err != nil {2827 if templ_7745c5c3_Err != nil {
2531 return templ_7745c5c3_Err2828 return templ_7745c5c3_Err
2532 }2829 }
2533 }2830 }
2534- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, " ")2831+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, " ")
2535 if templ_7745c5c3_Err != nil {2832 if templ_7745c5c3_Err != nil {
2536 return templ_7745c5c3_Err2833 return templ_7745c5c3_Err
2537 }2834 }
2538 if p.Existing != nil {2835 if p.Existing != nil {
2539- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")2836+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
2540 if templ_7745c5c3_Err != nil {2837 if templ_7745c5c3_Err != nil {
2541 return templ_7745c5c3_Err2838 return templ_7745c5c3_Err
2542 }2839 }
2543- var templ_7745c5c3_Var126 templ.SafeURL2840+ var templ_7745c5c3_Var139 templ.SafeURL
2544- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))2841+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
2545 if templ_7745c5c3_Err != nil {2842 if templ_7745c5c3_Err != nil {
2546- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 114}2843+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 539, Col: 114}
2547 }2844 }
2548- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))2845+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
2549 if templ_7745c5c3_Err != nil {2846 if templ_7745c5c3_Err != nil {
2550 return templ_7745c5c3_Err2847 return templ_7745c5c3_Err
2551 }2848 }
2552- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "\">")2849+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "\">")
2553 if templ_7745c5c3_Err != nil {2850 if templ_7745c5c3_Err != nil {
2554 return templ_7745c5c3_Err2851 return templ_7745c5c3_Err
2555 }2852 }
2556- var templ_7745c5c3_Var127 string2853+ var templ_7745c5c3_Var140 string
2557- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)2854+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
2558 if templ_7745c5c3_Err != nil {2855 if templ_7745c5c3_Err != nil {
2559- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 479, Col: 135}2856+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 539, Col: 135}
2560 }2857 }
2561- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))2858+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2562 if templ_7745c5c3_Err != nil {2859 if templ_7745c5c3_Err != nil {
2563 return templ_7745c5c3_Err2860 return templ_7745c5c3_Err
2564 }2861 }
2565- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</a></p>")2862+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</a></p>")
2566 if templ_7745c5c3_Err != nil {2863 if templ_7745c5c3_Err != nil {
2567 return templ_7745c5c3_Err2864 return templ_7745c5c3_Err
2568 }2865 }
2569 }2866 }
2570- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " ")2867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, " ")
2571 if templ_7745c5c3_Err != nil {2868 if templ_7745c5c3_Err != nil {
2572 return templ_7745c5c3_Err2869 return templ_7745c5c3_Err
2573 }2870 }
2574 if p.Create != nil {2871 if p.Create != nil {
2575- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<form class=\"cform create\" method=\"post\" action=\"")2872+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<form class=\"cform create\" method=\"post\" action=\"")
2576 if templ_7745c5c3_Err != nil {2873 if templ_7745c5c3_Err != nil {
2577 return templ_7745c5c3_Err2874 return templ_7745c5c3_Err
2578 }2875 }
2579- var templ_7745c5c3_Var128 templ.SafeURL2876+ var templ_7745c5c3_Var141 templ.SafeURL
2580- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))2877+ templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
2581 if templ_7745c5c3_Err != nil {2878 if templ_7745c5c3_Err != nil {
2582- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 83}2879+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 542, Col: 83}
2583 }2880 }
2584- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))2881+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))
2585 if templ_7745c5c3_Err != nil {2882 if templ_7745c5c3_Err != nil {
2586 return templ_7745c5c3_Err2883 return templ_7745c5c3_Err
2587 }2884 }
2588- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "\"><input type=\"hidden\" name=\"source\" value=\"")2885+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\"><input type=\"hidden\" name=\"source\" value=\"")
2589 if templ_7745c5c3_Err != nil {2886 if templ_7745c5c3_Err != nil {
2590 return templ_7745c5c3_Err2887 return templ_7745c5c3_Err
2591 }2888 }
2592- var templ_7745c5c3_Var129 string2889+ var templ_7745c5c3_Var142 string
2593- templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)2890+ templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
2594 if templ_7745c5c3_Err != nil {2891 if templ_7745c5c3_Err != nil {
2595- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 483, Col: 62}2892+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 62}
2596 }2893 }
2597- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)2894+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
2598 if templ_7745c5c3_Err != nil {2895 if templ_7745c5c3_Err != nil {
2599 return templ_7745c5c3_Err2896 return templ_7745c5c3_Err
2600 }2897 }
2601- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "\"> <input type=\"hidden\" name=\"target\" value=\"")2898+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\"> <input type=\"hidden\" name=\"target\" value=\"")
2602 if templ_7745c5c3_Err != nil {2899 if templ_7745c5c3_Err != nil {
2603 return templ_7745c5c3_Err2900 return templ_7745c5c3_Err
2604 }2901 }
2605- var templ_7745c5c3_Var130 string2902+ var templ_7745c5c3_Var143 string
2606- templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)2903+ templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
2607 if templ_7745c5c3_Err != nil {2904 if templ_7745c5c3_Err != nil {
2608- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 62}2905+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 544, Col: 62}
2609 }2906 }
2610- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)2907+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
2611 if templ_7745c5c3_Err != nil {2908 if templ_7745c5c3_Err != nil {
2612 return templ_7745c5c3_Err2909 return templ_7745c5c3_Err
2613 }2910 }
2614- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")2911+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
2615 if templ_7745c5c3_Err != nil {2912 if templ_7745c5c3_Err != nil {
2616 return templ_7745c5c3_Err2913 return templ_7745c5c3_Err
2617 }2914 }
2618- var templ_7745c5c3_Var131 string2915+ var templ_7745c5c3_Var144 string
2619- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)2916+ templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
2620 if templ_7745c5c3_Err != nil {2917 if templ_7745c5c3_Err != nil {
2621- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 97}2918+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 97}
2622 }2919 }
2623- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)2920+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
2624 if templ_7745c5c3_Err != nil {2921 if templ_7745c5c3_Err != nil {
2625 return templ_7745c5c3_Err2922 return templ_7745c5c3_Err
2626 }2923 }
2627- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "\" 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>")2924+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\" 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>")
2628 if templ_7745c5c3_Err != nil {2925 if templ_7745c5c3_Err != nil {
2629 return templ_7745c5c3_Err2926 return templ_7745c5c3_Err
2630 }2927 }
2631 }2928 }
2632- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, " ")2929+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, " ")
2633 if templ_7745c5c3_Err != nil {2930 if templ_7745c5c3_Err != nil {
2634 return templ_7745c5c3_Err2931 return templ_7745c5c3_Err
2635 }2932 }
2636 if p.HasDiff {2933 if p.HasDiff {
2637- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")2934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
2638 if templ_7745c5c3_Err != nil {2935 if templ_7745c5c3_Err != nil {
2639 return templ_7745c5c3_Err2936 return templ_7745c5c3_Err
2640 }2937 }
2641- var templ_7745c5c3_Var132 string2938+ var templ_7745c5c3_Var145 string
2642- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))2939+ templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
2643 if templ_7745c5c3_Err != nil {2940 if templ_7745c5c3_Err != nil {
2644- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 57}2941+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 57}
2645 }2942 }
2646- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))2943+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
2647 if templ_7745c5c3_Err != nil {2944 if templ_7745c5c3_Err != nil {
2648 return templ_7745c5c3_Err2945 return templ_7745c5c3_Err
2649 }2946 }
2650- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, " · <span class=\"plus\">+")2947+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, " · <span class=\"plus\">+")
2651 if templ_7745c5c3_Err != nil {2948 if templ_7745c5c3_Err != nil {
2652 return templ_7745c5c3_Err2949 return templ_7745c5c3_Err
2653 }2950 }
2654- var templ_7745c5c3_Var133 string2951+ var templ_7745c5c3_Var146 string
2655- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))2952+ templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
2656 if templ_7745c5c3_Err != nil {2953 if templ_7745c5c3_Err != nil {
2657- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 97}2954+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 97}
2658 }2955 }
2659- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))2956+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
2660 if templ_7745c5c3_Err != nil {2957 if templ_7745c5c3_Err != nil {
2661 return templ_7745c5c3_Err2958 return templ_7745c5c3_Err
2662 }2959 }
2663- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "</span> <span class=\"minus\">-")2960+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span> <span class=\"minus\">-")
2664 if templ_7745c5c3_Err != nil {2961 if templ_7745c5c3_Err != nil {
2665 return templ_7745c5c3_Err2962 return templ_7745c5c3_Err
2666 }2963 }
2667- var templ_7745c5c3_Var134 string2964+ var templ_7745c5c3_Var147 string
2668- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))2965+ templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
2669 if templ_7745c5c3_Err != nil {2966 if templ_7745c5c3_Err != nil {
2670- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 142}2967+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 556, Col: 142}
2671 }2968 }
2672- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))2969+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))
2673 if templ_7745c5c3_Err != nil {2970 if templ_7745c5c3_Err != nil {
2674 return templ_7745c5c3_Err2971 return templ_7745c5c3_Err
2675 }2972 }
2676- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></span> <span class=\"grow\"></span>")2973+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "</span></span> <span class=\"grow\"></span>")
2677 if templ_7745c5c3_Err != nil {2974 if templ_7745c5c3_Err != nil {
2678 return templ_7745c5c3_Err2975 return templ_7745c5c3_Err
2679 }2976 }
@@ -2689,64 +2986,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
2689 if templ_7745c5c3_Err != nil {2986 if templ_7745c5c3_Err != nil {
2690 return templ_7745c5c3_Err2987 return templ_7745c5c3_Err
2691 }2988 }
2692- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</div>")2989+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</div>")
2693 if templ_7745c5c3_Err != nil {2990 if templ_7745c5c3_Err != nil {
2694 return templ_7745c5c3_Err2991 return templ_7745c5c3_Err
2695 }2992 }
2696 if p.Invisible {2993 if p.Invisible {
2697- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<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>")2994+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<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>")
2698 if templ_7745c5c3_Err != nil {2995 if templ_7745c5c3_Err != nil {
2699 return templ_7745c5c3_Err2996 return templ_7745c5c3_Err
2700 }2997 }
2701 }2998 }
2702- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, " <div id=\"diffs\"")2999+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, " <div id=\"diffs\"")
2703 if templ_7745c5c3_Err != nil {3000 if templ_7745c5c3_Err != nil {
2704 return templ_7745c5c3_Err3001 return templ_7745c5c3_Err
2705 }3002 }
2706 if p.Both {3003 if p.Both {
2707- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, " data-diff=\"")3004+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, " data-diff=\"")
2708 if templ_7745c5c3_Err != nil {3005 if templ_7745c5c3_Err != nil {
2709 return templ_7745c5c3_Err3006 return templ_7745c5c3_Err
2710 }3007 }
2711- var templ_7745c5c3_Var135 string3008+ var templ_7745c5c3_Var148 string
2712- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)3009+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
2713 if templ_7745c5c3_Err != nil {3010 if templ_7745c5c3_Err != nil {
2714- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 23}3011+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 571, Col: 23}
2715 }3012 }
2716- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)3013+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var148)
2717 if templ_7745c5c3_Err != nil {3014 if templ_7745c5c3_Err != nil {
2718 return templ_7745c5c3_Err3015 return templ_7745c5c3_Err
2719 }3016 }
2720- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "\"")3017+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "\"")
2721 if templ_7745c5c3_Err != nil {3018 if templ_7745c5c3_Err != nil {
2722 return templ_7745c5c3_Err3019 return templ_7745c5c3_Err
2723 }3020 }
2724 } else {3021 } else {
2725- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " data-view=\"")3022+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, " data-view=\"")
2726 if templ_7745c5c3_Err != nil {3023 if templ_7745c5c3_Err != nil {
2727 return templ_7745c5c3_Err3024 return templ_7745c5c3_Err
2728 }3025 }
2729- var templ_7745c5c3_Var136 string3026+ var templ_7745c5c3_Var149 string
2730- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)3027+ templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
2731 if templ_7745c5c3_Err != nil {3028 if templ_7745c5c3_Err != nil {
2732- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 513, Col: 23}3029+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 573, Col: 23}
2733 }3030 }
2734- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136)3031+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var149)
2735 if templ_7745c5c3_Err != nil {3032 if templ_7745c5c3_Err != nil {
2736 return templ_7745c5c3_Err3033 return templ_7745c5c3_Err
2737 }3034 }
2738- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "\"")3035+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "\"")
2739 if templ_7745c5c3_Err != nil {3036 if templ_7745c5c3_Err != nil {
2740 return templ_7745c5c3_Err3037 return templ_7745c5c3_Err
2741 }3038 }
2742 }3039 }
2743 if p.Both && !p.Pinned {3040 if p.Both && !p.Pinned {
2744- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")3041+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
2745 if templ_7745c5c3_Err != nil {3042 if templ_7745c5c3_Err != nil {
2746 return templ_7745c5c3_Err3043 return templ_7745c5c3_Err
2747 }3044 }
2748 }3045 }
2749- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, ">")3046+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, ">")
2750 if templ_7745c5c3_Err != nil {3047 if templ_7745c5c3_Err != nil {
2751 return templ_7745c5c3_Err3048 return templ_7745c5c3_Err
2752 }3049 }
@@ -2756,14 +3053,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
2756 return templ_7745c5c3_Err3053 return templ_7745c5c3_Err
2757 }3054 }
2758 }3055 }
2759- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "</div>")3056+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "</div>")
2760 if templ_7745c5c3_Err != nil {3057 if templ_7745c5c3_Err != nil {
2761 return templ_7745c5c3_Err3058 return templ_7745c5c3_Err
2762 }3059 }
2763 }3060 }
2764 return nil3061 return nil
2765 })3062 })
2766- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var117), templ_7745c5c3_Buffer)3063+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var130), templ_7745c5c3_Buffer)
2767 if templ_7745c5c3_Err != nil {3064 if templ_7745c5c3_Err != nil {
2768 return templ_7745c5c3_Err3065 return templ_7745c5c3_Err
2769 }3066 }
@@ -2787,173 +3084,173 @@ func compareRail(p ComparePage) templ.Component {
2787 }()3084 }()
2788 }3085 }
2789 ctx = templ.InitializeContext(ctx)3086 ctx = templ.InitializeContext(ctx)
2790- templ_7745c5c3_Var137 := templ.GetChildren(ctx)3087+ templ_7745c5c3_Var150 := templ.GetChildren(ctx)
2791- if templ_7745c5c3_Var137 == nil {3088+ if templ_7745c5c3_Var150 == nil {
2792- templ_7745c5c3_Var137 = templ.NopComponent3089+ templ_7745c5c3_Var150 = templ.NopComponent
2793 }3090 }
2794 ctx = templ.ClearChildren(ctx)3091 ctx = templ.ClearChildren(ctx)
2795- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")3092+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
2796 if templ_7745c5c3_Err != nil {3093 if templ_7745c5c3_Err != nil {
2797 return templ_7745c5c3_Err3094 return templ_7745c5c3_Err
2798 }3095 }
2799- var templ_7745c5c3_Var138 string3096+ var templ_7745c5c3_Var151 string
2800- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))3097+ templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
2801 if templ_7745c5c3_Err != nil {3098 if templ_7745c5c3_Err != nil {
2802- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 67}3099+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 67}
2803 }3100 }
2804- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))3101+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
2805 if templ_7745c5c3_Err != nil {3102 if templ_7745c5c3_Err != nil {
2806 return templ_7745c5c3_Err3103 return templ_7745c5c3_Err
2807 }3104 }
2808- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</span></h2><ul class=\"pcommits\">")3105+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</span></h2><ul class=\"pcommits\">")
2809 if templ_7745c5c3_Err != nil {3106 if templ_7745c5c3_Err != nil {
2810 return templ_7745c5c3_Err3107 return templ_7745c5c3_Err
2811 }3108 }
2812 for _, c := range p.Ahead {3109 for _, c := range p.Ahead {
2813- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "<li><a href=\"")3110+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "<li><a href=\"")
2814 if templ_7745c5c3_Err != nil {3111 if templ_7745c5c3_Err != nil {
2815 return templ_7745c5c3_Err3112 return templ_7745c5c3_Err
2816 }3113 }
2817- var templ_7745c5c3_Var139 templ.SafeURL3114+ var templ_7745c5c3_Var152 templ.SafeURL
2818- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))3115+ templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
2819 if templ_7745c5c3_Err != nil {3116 if templ_7745c5c3_Err != nil {
2820- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 533, Col: 37}3117+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 37}
2821 }3118 }
2822- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))3119+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
2823 if templ_7745c5c3_Err != nil {3120 if templ_7745c5c3_Err != nil {
2824 return templ_7745c5c3_Err3121 return templ_7745c5c3_Err
2825 }3122 }
2826- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "\">")3123+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "\">")
2827 if templ_7745c5c3_Err != nil {3124 if templ_7745c5c3_Err != nil {
2828 return templ_7745c5c3_Err3125 return templ_7745c5c3_Err
2829 }3126 }
2830- var templ_7745c5c3_Var140 string3127+ var templ_7745c5c3_Var153 string
2831- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)3128+ templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
2832 if templ_7745c5c3_Err != nil {3129 if templ_7745c5c3_Err != nil {
2833- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 533, Col: 49}3130+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 49}
2834 }3131 }
2835- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))3132+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
2836 if templ_7745c5c3_Err != nil {3133 if templ_7745c5c3_Err != nil {
2837 return templ_7745c5c3_Err3134 return templ_7745c5c3_Err
2838 }3135 }
2839- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, "</a> <span class=\"s\">")3136+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "</a> <span class=\"s\">")
2840 if templ_7745c5c3_Err != nil {3137 if templ_7745c5c3_Err != nil {
2841 return templ_7745c5c3_Err3138 return templ_7745c5c3_Err
2842 }3139 }
2843- var templ_7745c5c3_Var141 string3140+ var templ_7745c5c3_Var154 string
2844- templ_7745c5c3_Var141, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)3141+ templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
2845 if templ_7745c5c3_Err != nil {3142 if templ_7745c5c3_Err != nil {
2846- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 534, Col: 32}3143+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 32}
2847 }3144 }
2848- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var141))3145+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
2849 if templ_7745c5c3_Err != nil {3146 if templ_7745c5c3_Err != nil {
2850 return templ_7745c5c3_Err3147 return templ_7745c5c3_Err
2851 }3148 }
2852- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "</span> <span class=\"mute\">")3149+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "</span> <span class=\"mute\">")
2853 if templ_7745c5c3_Err != nil {3150 if templ_7745c5c3_Err != nil {
2854 return templ_7745c5c3_Err3151 return templ_7745c5c3_Err
2855 }3152 }
2856- var templ_7745c5c3_Var142 string3153+ var templ_7745c5c3_Var155 string
2857- templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)3154+ templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
2858 if templ_7745c5c3_Err != nil {3155 if templ_7745c5c3_Err != nil {
2859- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 536, Col: 16}3156+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 16}
2860 }3157 }
2861- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var142))3158+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
2862 if templ_7745c5c3_Err != nil {3159 if templ_7745c5c3_Err != nil {
2863 return templ_7745c5c3_Err3160 return templ_7745c5c3_Err
2864 }3161 }
2865- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, " ")3162+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, " ")
2866 if templ_7745c5c3_Err != nil {3163 if templ_7745c5c3_Err != nil {
2867 return templ_7745c5c3_Err3164 return templ_7745c5c3_Err
2868 }3165 }
2869 if c.WhenAt != "" {3166 if c.WhenAt != "" {
2870- var templ_7745c5c3_Var143 string3167+ var templ_7745c5c3_Var156 string
2871- templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")3168+ templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
2872 if templ_7745c5c3_Err != nil {3169 if templ_7745c5c3_Err != nil {
2873- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 538, Col: 15}3170+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 15}
2874 }3171 }
2875- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var143))3172+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
2876 if templ_7745c5c3_Err != nil {3173 if templ_7745c5c3_Err != nil {
2877 return templ_7745c5c3_Err3174 return templ_7745c5c3_Err
2878 }3175 }
2879- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "<time datetime=\"")3176+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<time datetime=\"")
2880 if templ_7745c5c3_Err != nil {3177 if templ_7745c5c3_Err != nil {
2881 return templ_7745c5c3_Err3178 return templ_7745c5c3_Err
2882 }3179 }
2883- var templ_7745c5c3_Var144 string3180+ var templ_7745c5c3_Var157 string
2884- templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)3181+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
2885 if templ_7745c5c3_Err != nil {3182 if templ_7745c5c3_Err != nil {
2886- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 538, Col: 42}3183+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 42}
2887 }3184 }
2888- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)3185+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var157)
2889 if templ_7745c5c3_Err != nil {3186 if templ_7745c5c3_Err != nil {
2890 return templ_7745c5c3_Err3187 return templ_7745c5c3_Err
2891 }3188 }
2892- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "\">")3189+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "\">")
2893 if templ_7745c5c3_Err != nil {3190 if templ_7745c5c3_Err != nil {
2894 return templ_7745c5c3_Err3191 return templ_7745c5c3_Err
2895 }3192 }
2896- var templ_7745c5c3_Var145 string3193+ var templ_7745c5c3_Var158 string
2897- templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)3194+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
2898 if templ_7745c5c3_Err != nil {3195 if templ_7745c5c3_Err != nil {
2899- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 538, Col: 53}3196+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 598, Col: 53}
2900 }3197 }
2901- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))3198+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
2902 if templ_7745c5c3_Err != nil {3199 if templ_7745c5c3_Err != nil {
2903 return templ_7745c5c3_Err3200 return templ_7745c5c3_Err
2904 }3201 }
2905- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</time>")3202+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "</time>")
2906 if templ_7745c5c3_Err != nil {3203 if templ_7745c5c3_Err != nil {
2907 return templ_7745c5c3_Err3204 return templ_7745c5c3_Err
2908 }3205 }
2909 }3206 }
2910- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</span></li>")3207+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, "</span></li>")
2911 if templ_7745c5c3_Err != nil {3208 if templ_7745c5c3_Err != nil {
2912 return templ_7745c5c3_Err3209 return templ_7745c5c3_Err
2913 }3210 }
2914 }3211 }
2915- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</ul>")3212+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "</ul>")
2916 if templ_7745c5c3_Err != nil {3213 if templ_7745c5c3_Err != nil {
2917 return templ_7745c5c3_Err3214 return templ_7745c5c3_Err
2918 }3215 }
2919 if len(p.Ahead) == 0 {3216 if len(p.Ahead) == 0 {
2920- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "<p class=\"dim\">no commit ahead of ")3217+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "<p class=\"dim\">no commit ahead of ")
2921 if templ_7745c5c3_Err != nil {3218 if templ_7745c5c3_Err != nil {
2922 return templ_7745c5c3_Err3219 return templ_7745c5c3_Err
2923 }3220 }
2924- var templ_7745c5c3_Var146 string3221+ var templ_7745c5c3_Var159 string
2925- templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)3222+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2926 if templ_7745c5c3_Err != nil {3223 if templ_7745c5c3_Err != nil {
2927- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 545, Col: 47}3224+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 47}
2928 }3225 }
2929- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))3226+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
2930 if templ_7745c5c3_Err != nil {3227 if templ_7745c5c3_Err != nil {
2931 return templ_7745c5c3_Err3228 return templ_7745c5c3_Err
2932 }3229 }
2933- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, ".</p>")3230+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, ".</p>")
2934 if templ_7745c5c3_Err != nil {3231 if templ_7745c5c3_Err != nil {
2935 return templ_7745c5c3_Err3232 return templ_7745c5c3_Err
2936 }3233 }
2937 }3234 }
2938- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "</section>")3235+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</section>")
2939 if templ_7745c5c3_Err != nil {3236 if templ_7745c5c3_Err != nil {
2940 return templ_7745c5c3_Err3237 return templ_7745c5c3_Err
2941 }3238 }
2942 if p.HasDiff {3239 if p.HasDiff {
2943- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")3240+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
2944 if templ_7745c5c3_Err != nil {3241 if templ_7745c5c3_Err != nil {
2945 return templ_7745c5c3_Err3242 return templ_7745c5c3_Err
2946 }3243 }
2947- var templ_7745c5c3_Var147 string3244+ var templ_7745c5c3_Var160 string
2948- templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))3245+ templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
2949 if templ_7745c5c3_Err != nil {3246 if templ_7745c5c3_Err != nil {
2950- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 550, Col: 66}3247+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 610, Col: 66}
2951 }3248 }
2952- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var147))3249+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
2953 if templ_7745c5c3_Err != nil {3250 if templ_7745c5c3_Err != nil {
2954 return templ_7745c5c3_Err3251 return templ_7745c5c3_Err
2955 }3252 }
2956- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "</span></h2>")3253+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "</span></h2>")
2957 if templ_7745c5c3_Err != nil {3254 if templ_7745c5c3_Err != nil {
2958 return templ_7745c5c3_Err3255 return templ_7745c5c3_Err
2959 }3256 }
@@ -2961,12 +3258,12 @@ func compareRail(p ComparePage) templ.Component {
2961 if templ_7745c5c3_Err != nil {3258 if templ_7745c5c3_Err != nil {
2962 return templ_7745c5c3_Err3259 return templ_7745c5c3_Err
2963 }3260 }
2964- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "</section>")3261+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "</section>")
2965 if templ_7745c5c3_Err != nil {3262 if templ_7745c5c3_Err != nil {
2966 return templ_7745c5c3_Err3263 return templ_7745c5c3_Err
2967 }3264 }
2968 }3265 }
2969- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")3266+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
2970 if templ_7745c5c3_Err != nil {3267 if templ_7745c5c3_Err != nil {
2971 return templ_7745c5c3_Err3268 return templ_7745c5c3_Err
2972 }3269 }
@@ -2974,7 +3271,7 @@ func compareRail(p ComparePage) templ.Component {
2974 if templ_7745c5c3_Err != nil {3271 if templ_7745c5c3_Err != nil {
2975 return templ_7745c5c3_Err3272 return templ_7745c5c3_Err
2976 }3273 }
2977- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "</section>")3274+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "</section>")
2978 if templ_7745c5c3_Err != nil {3275 if templ_7745c5c3_Err != nil {
2979 return templ_7745c5c3_Err3276 return templ_7745c5c3_Err
2980 }3277 }
M internal/adapters/web/static/css/55-diff.css +4 -0
M internal/adapters/web/static/css/60-mr.css +8 -3
M internal/adapters/web/static/js/mr.js +51 -2
M internal/adapters/web/web.go +2 -0
M internal/app/cli_mr.go +226 -25
M internal/app/cli_mr_e2e_test.go +72 -0
M internal/app/cli_mr_test.go +89 -0
M internal/app/cli_token.go +1 -1
M internal/app/cli_watch.go +21 -8
M internal/app/cli_watch_test.go +24 -0
M internal/app/web_mr_test.go +6 -3
M internal/domain/event.go +39 -0
M internal/domain/mergerequest.go +80 -0
M internal/domain/mergerequest_test.go +48 -0
M internal/domain/principal.go +15 -0
M internal/domain/token.go +4 -2
M internal/domain/token_test.go +1 -1
M internal/ports/dto.go +8 -0
M internal/ports/mocks/MergeRequestService.go +174 -0
M internal/ports/services.go +2 -0
M internal/usecase/access.go +0 -15
M internal/usecase/ci_dispatch.go +1 -1
M internal/usecase/ci_pull.go +1 -1
M internal/usecase/ci_runs.go +1 -1
M internal/usecase/ci_tips.go +1 -1
M internal/usecase/ci_trigger.go +4 -4
M internal/usecase/events.go +12 -0
M internal/usecase/events_test.go +32 -0
M internal/usecase/export_test.go +0 -3
M internal/usecase/mergerequests.go +137 -9
M internal/usecase/mergerequests_integration_test.go +107 -1
M internal/usecase/mergerequests_test.go +116 -0
M internal/usecase/pushes.go +1 -1
M internal/usecase/sessions_test.go +0 -9
M proto/ermite/storage/merge_request.proto +12 -0
M skills/ermite/CLI.md +11 -6
M skills/ermite/SKILL.md +13 -4

Comments0 open / 0

no comments yet.

Keyboard shortcuts

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

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

Branches 2

Tags 42

↑↓ moveenter openesc close