detached 6135dd5

#19 feat(mr): merge request auto-merge

state
merged · 6135dd5 · fast-forward
branch
feat/auto-merge → main
opened
bnema ·
updated

Merge request auto-merge, step 1 of ADR 0033.

  • Repository setting allow_auto_merge (default off): ermite repo edit --allow-auto-merge.
  • Enable with ermite mr merge --auto --head <sha> --mode <mode> or Merge when ready on the web; cancel with --cancel-auto.
  • Merges once CI has settled on the recorded head, every run is complete, at least one check passed and none fails (whatever failing_checks_block says), as the principal who enabled it. That principal is re-checked at every evaluation and under the merge lock.
  • Failing or conflicting heads wait. A new head, close, setting off, mode no longer allowed or a principal that can no longer merge cancels it.
  • Events mr.auto_merge_enabled, mr.auto_merge_cancelled, merged.auto; feature merge_requests.auto_merge.
  • Settings changes that affect merges take the merge lock (MergeGate). CI keeps a pushed head unsettled until its pull events are discovered.

Requires ermite-api v0.14.0.

Checks run locally: build, vet, hexcheck -fail-on warn, mocks/templ drift, race tests (all packages). The two golden failures (TestWebFilesGolden/blob-markdown, TestWebGolden/summary) and three errcheck lint findings also fail on main.

CommitSubjectAuthorDateSignatureStatus
6135dd5test(mr): close linked issues when a merge request auto-mergesbriceGPG Verified
4b10ba7merge: main with issue and merge request links into feat/auto-mergebriceGPG Verified
b2cf6d4build(deps): require ermite-api v0.14.0 for merge request auto-mergebriceGPG Verified
be277d4test(mr): cover failing checks with failing_checks_block off, the pending auto-merge index, and stop relying on event orderbriceGPG Verified
84db480perf(mr): skip auto-merge scans of repositories without a pending auto-merge, read checks once under the merge lock and keep the manual pending-checks errorbriceGPG Verified
b6e9374fix(web): give merge when ready its own mode choice and drop branch deletion from itbriceGPG Verified
355f5f4fix(mr): cancel auto-merge when the enabler can no longer merge, even while checks are pendingbriceGPG Verified
cb71bdddocs(mr): describe when auto-merge waits and when it is clearedbriceGPG Verified
22ff0d6fix(ci): keep a pushed head unsettled until its pull events are discoveredbriceGPG Verified
6609752fix(mr): resolve the auto-merge enabler under the merge lockbriceGPG Verified
8539d6ffix(repo): serialize merge settings updates with mergesbriceGPG Verified
63e55ecfeat(mr): require the head the caller saw to enable auto-mergebriceGPG Verified
b5db7e3docs(mr): document merge request auto-mergebriceGPG Verified
718df65merge: web auto-merge controls into feat/auto-mergebriceGPG Verified
b271fd8feat(cli): add mr merge --auto/--cancel-auto and repo edit --allow-auto-mergebriceGPG Verified
d1f49e5feat(rpc): serve merge request auto-merge and its eventsbriceGPG Verified
5dfb71bfeat(web): add merge when ready and auto-merge state to the merge boxbriceGPG Verified
31adb55style(web): gofmt mr_test.gobriceGPG Verified
ba47d48feat(app): wire the auto-merge evaluatorbriceGPG Verified
1314a0dfeat(mr): enable, cancel and evaluate merge request auto-mergebriceGPG Verified
9795e63feat(ports): add auto-merge service methods, CIActivity and AutoMergeTrigger portsbriceGPG Verified
4bb6db7feat(mr): store auto-merge state and allow_auto_merge settingbriceGPG Verified

Checks 8

70 files · +3994 -492
7/8
M docs/cli.md +3 -2
@@ -48,7 +48,7 @@ It does not run:
4848
4949 ```sh
5050 ermite repo list | show <name> | create <name> [--description …] [--visibility public]
51-ermite repo edit <name> [--description …] [--visibility …] [--default-branch …] [--merge-modes …] [--failing-checks-block=false] [--issues off|owner|public]
51+ermite repo edit <name> [--description …] [--visibility …] [--default-branch …] [--merge-modes …] [--failing-checks-block=false] [--allow-auto-merge] [--issues off|owner|public]
5252 ermite repo delete <name> [--yes]
5353
5454 ermite token create <label> [--preset cli|agent|ci|runner] [--scope …] [--repo …] [--branch-prefix …] [--expires 30d]
@@ -57,7 +57,7 @@ ermite token list | revoke <id>
5757 ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff] # show prints the mergeability: up to date, behind, conflict
5858 ermite mr comment <repo> <n> -m … [--path … --line N|A-B --side old|new] [--reply <id>] [--approve|--request-changes]
5959 ermite mr resolve <repo> <n> <comment> | edit-comment <repo> <n> <comment> -m … | delete-comment <repo> <n> <comment>
60-ermite mr merge <repo> <n> --head <id> [--mode fast-forward|squash|merge] [--delete-branch] | close <repo> <n>
60+ermite mr merge <repo> <n> --head <id> [--mode fast-forward|squash|merge] [--delete-branch] | merge <repo> <n> --head <id> --auto [--mode …] | merge <repo> <n> --cancel-auto | close <repo> <n>
6161
6262 ermite issue create <repo> --title … [--body … | --body-file …] | list <repo> [--state open|closed|all]
6363 ermite issue show <repo> <n> | comment <repo> <n> -m … | close <repo> <n> | reopen <repo> <n>
@@ -100,6 +100,7 @@ Mirrors push repositories to a remote over SSH, never forcing. See [Push mirror]
100100 - `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list`, `mirror key|config|status` and `version`.
101101 - `watch --json` prints one object per line. `merge_request_mergeability` events carry `mergeability` (`state`, `behind`, `conflict_paths`…) and print as `repo!n mergeability: behind by 2`.
102102 - `mr merge --mode squash|merge` creates a commit signed by the server's own key (generated on first start); the output names its fingerprint, `ermite signing-key show` prints the key. Every mode is allowed by default; `repo edit --merge-modes fast-forward,squash` disables `merge`.
103+- `mr merge --auto` makes the server merge once CI passed on the head (`repo edit --allow-auto-merge` first); `--cancel-auto` cancels it; both are exclusive with each other and `--auto` with `--delete-branch`. `watch --json` adds `merge_request_auto_merge_enabled`, `merge_request_auto_merge_cancelled` (with `reason`), and `auto` (true) on `merge_request_merged` after an auto-merge. See [Merge requests](merge-requests.md#auto-merge).
103104 - Errors are one line on stderr (`ermite: …`) with exit status 1.
104105 - Shell completion completes repository names from the server.
105106
@@ -48,7 +48,7 @@ It does not run:
48 48
49 ```sh49 ```sh
50 ermite repo list | show <name> | create <name> [--description …] [--visibility public]50 ermite repo list | show <name> | create <name> [--description …] [--visibility public]
51-ermite repo edit <name> [--description …] [--visibility …] [--default-branch …] [--merge-modes …] [--failing-checks-block=false] [--issues off|owner|public]51+ermite repo edit <name> [--description …] [--visibility …] [--default-branch …] [--merge-modes …] [--failing-checks-block=false] [--allow-auto-merge] [--issues off|owner|public]
52 ermite repo delete <name> [--yes]52 ermite repo delete <name> [--yes]
53 53
54 ermite token create <label> [--preset cli|agent|ci|runner] [--scope …] [--repo …] [--branch-prefix …] [--expires 30d]54 ermite token create <label> [--preset cli|agent|ci|runner] [--scope …] [--repo …] [--branch-prefix …] [--expires 30d]
@@ -57,7 +57,7 @@ ermite token list | revoke <id>
57 ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff] # show prints the mergeability: up to date, behind, conflict57 ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff] # show prints the mergeability: up to date, behind, conflict
58 ermite mr comment <repo> <n> -m … [--path … --line N|A-B --side old|new] [--reply <id>] [--approve|--request-changes]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 resolve <repo> <n> <comment> | edit-comment <repo> <n> <comment> -m … | delete-comment <repo> <n> <comment>
60-ermite mr merge <repo> <n> --head <id> [--mode fast-forward|squash|merge] [--delete-branch] | close <repo> <n>60+ermite mr merge <repo> <n> --head <id> [--mode fast-forward|squash|merge] [--delete-branch] | merge <repo> <n> --head <id> --auto [--mode …] | merge <repo> <n> --cancel-auto | close <repo> <n>
61 61
62 ermite issue create <repo> --title … [--body … | --body-file …] | list <repo> [--state open|closed|all]62 ermite issue create <repo> --title … [--body … | --body-file …] | list <repo> [--state open|closed|all]
63 ermite issue show <repo> <n> | comment <repo> <n> -m … | close <repo> <n> | reopen <repo> <n>63 ermite issue show <repo> <n> | comment <repo> <n> -m … | close <repo> <n> | reopen <repo> <n>
@@ -100,6 +100,7 @@ Mirrors push repositories to a remote over SSH, never forcing. See [Push mirror]
100 - `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list`, `mirror key|config|status` and `version`.100 - `--json` prints stable field names (listed in each `--help`) on `repo list|show|create|edit`, `issue create|list|show|comment|close|reopen`, `token create|list`, `run list|view|cancel|rerun`, `runner list`, `mirror key|config|status` and `version`.
101 - `watch --json` prints one object per line. `merge_request_mergeability` events carry `mergeability` (`state`, `behind`, `conflict_paths`…) and print as `repo!n mergeability: behind by 2`.101 - `watch --json` prints one object per line. `merge_request_mergeability` events carry `mergeability` (`state`, `behind`, `conflict_paths`…) and print as `repo!n mergeability: behind by 2`.
102 - `mr merge --mode squash|merge` creates a commit signed by the server's own key (generated on first start); the output names its fingerprint, `ermite signing-key show` prints the key. Every mode is allowed by default; `repo edit --merge-modes fast-forward,squash` disables `merge`.102 - `mr merge --mode squash|merge` creates a commit signed by the server's own key (generated on first start); the output names its fingerprint, `ermite signing-key show` prints the key. Every mode is allowed by default; `repo edit --merge-modes fast-forward,squash` disables `merge`.
103+- `mr merge --auto` makes the server merge once CI passed on the head (`repo edit --allow-auto-merge` first); `--cancel-auto` cancels it; both are exclusive with each other and `--auto` with `--delete-branch`. `watch --json` adds `merge_request_auto_merge_enabled`, `merge_request_auto_merge_cancelled` (with `reason`), and `auto` (true) on `merge_request_merged` after an auto-merge. See [Merge requests](merge-requests.md#auto-merge).
103 - Errors are one line on stderr (`ermite: …`) with exit status 1.104 - Errors are one line on stderr (`ermite: …`) with exit status 1.
104 - Shell completion completes repository names from the server.105 - Shell completion completes repository names from the server.
105 106
M docs/merge-requests.md +14 -0
@@ -41,6 +41,20 @@ git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=allowed_signers verify-commi
4141
4242 The merge box of the web page and the output of `mr merge --mode squash|merge` repeat this with the fingerprint.
4343
44+## Auto-merge
45+
46+A merge request can merge by itself once its checks passed.
47+
48+- The repository setting `allow_auto_merge` is off by default: `ermite repo edit <repo> --allow-auto-merge` (`=false` to turn it off).
49+- Enable it with `ermite mr merge <repo> <n> --head <commit> --auto --mode squash` (`--head` is required, as for a plain merge; a head that moved is refused with "branch moved"), or with **Merge when ready** in the web merge box. Its separate mode choice offers every repository-allowed mode, even while the head conflicts or is behind. Auto-merge keeps the source branch; the delete-source checkbox applies only to an immediate merge. It needs `mr:merge` and a server with CI. Re-enabling replaces the previous mode and enabler.
50+- The server merges when CI has settled on the head with every run complete and at least one check passed. A failing or unfinished check always blocks, whatever `failing_checks_block` says. The mode must still be allowed.
51+- The merge is made as the person who enabled it. Their token is checked at every evaluation, even while checks are pending, and again at merge time under the merge lock: it must still be valid and allow `mr:merge` on the repository and the target branch. A token that no longer qualifies cancels the auto-merge.
52+- A pending auto-merge shows in `mr show` (`auto_merge`), in the web merge box and as `merge_request_auto_merge_enabled` in `ermite watch`. A merge request merged this way has `auto_merged` true.
53+- A failing, unfinished or conflicting head waits with auto-merge still on. A re-run or a target fix can make it pass; pushing a new source head cancels the recorded intent (the reason names the new head).
54+- A manual merge supersedes auto-merge and publishes `merge_request_merged` with `auto=false`, without a cancelled event. A manual close clears it and publishes `merge_request_closed` followed by `merge_request_auto_merge_cancelled` with reason `closed`.
55+- Cancellation reasons are `cancelled by <author>`, `closed`, `auto-merge was turned off in the repository settings`, `the <mode> merge mode is no longer allowed`, `the source branch was deleted`, `the branch moved to <short-head>`, and `the principal that enabled auto-merge can no longer merge <target>`. If a change wins the merge lock after the evaluator's first read, the merge-time reason is instead `auto-merge is off in <repo>`, `the <mode> merge mode is not allowed in <repo>`, `the source branch "<source>" no longer exists`, or `branch moved, read again: "<source>" is at <head>, not <recorded-head>`. Other permanent merge-time preconditions report `!<n> is <state>`, `!<n> is a draft: mark it ready for review first`, `the target branch "<target>" no longer exists`, `nothing to merge, "<target>" already is at <head>`, or `nothing to merge, "<target>" already contains <head>`.
56+- Cancel it with `ermite mr merge <repo> <n> --cancel-auto` or **Cancel auto-merge** in the web merge box.
57+
4458 ## Mergeability
4559
4660 The server checks in the background whether each open request merges with its target, and again whenever the source or the target moves. `mr show` prints it; the web merge box shows it as a live badge; `ermite watch` reports each change.
@@ -41,6 +41,20 @@ git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=allowed_signers verify-commi
41 41
42 The merge box of the web page and the output of `mr merge --mode squash|merge` repeat this with the fingerprint.42 The merge box of the web page and the output of `mr merge --mode squash|merge` repeat this with the fingerprint.
43 43
44+## Auto-merge
45+
46+A merge request can merge by itself once its checks passed.
47+
48+- The repository setting `allow_auto_merge` is off by default: `ermite repo edit <repo> --allow-auto-merge` (`=false` to turn it off).
49+- Enable it with `ermite mr merge <repo> <n> --head <commit> --auto --mode squash` (`--head` is required, as for a plain merge; a head that moved is refused with "branch moved"), or with **Merge when ready** in the web merge box. Its separate mode choice offers every repository-allowed mode, even while the head conflicts or is behind. Auto-merge keeps the source branch; the delete-source checkbox applies only to an immediate merge. It needs `mr:merge` and a server with CI. Re-enabling replaces the previous mode and enabler.
50+- The server merges when CI has settled on the head with every run complete and at least one check passed. A failing or unfinished check always blocks, whatever `failing_checks_block` says. The mode must still be allowed.
51+- The merge is made as the person who enabled it. Their token is checked at every evaluation, even while checks are pending, and again at merge time under the merge lock: it must still be valid and allow `mr:merge` on the repository and the target branch. A token that no longer qualifies cancels the auto-merge.
52+- A pending auto-merge shows in `mr show` (`auto_merge`), in the web merge box and as `merge_request_auto_merge_enabled` in `ermite watch`. A merge request merged this way has `auto_merged` true.
53+- A failing, unfinished or conflicting head waits with auto-merge still on. A re-run or a target fix can make it pass; pushing a new source head cancels the recorded intent (the reason names the new head).
54+- A manual merge supersedes auto-merge and publishes `merge_request_merged` with `auto=false`, without a cancelled event. A manual close clears it and publishes `merge_request_closed` followed by `merge_request_auto_merge_cancelled` with reason `closed`.
55+- Cancellation reasons are `cancelled by <author>`, `closed`, `auto-merge was turned off in the repository settings`, `the <mode> merge mode is no longer allowed`, `the source branch was deleted`, `the branch moved to <short-head>`, and `the principal that enabled auto-merge can no longer merge <target>`. If a change wins the merge lock after the evaluator's first read, the merge-time reason is instead `auto-merge is off in <repo>`, `the <mode> merge mode is not allowed in <repo>`, `the source branch "<source>" no longer exists`, or `branch moved, read again: "<source>" is at <head>, not <recorded-head>`. Other permanent merge-time preconditions report `!<n> is <state>`, `!<n> is a draft: mark it ready for review first`, `the target branch "<target>" no longer exists`, `nothing to merge, "<target>" already is at <head>`, or `nothing to merge, "<target>" already contains <head>`.
56+- Cancel it with `ermite mr merge <repo> <n> --cancel-auto` or **Cancel auto-merge** in the web merge box.
57+
44 ## Mergeability58 ## Mergeability
45 59
46 The server checks in the background whether each open request merges with its target, and again whenever the source or the target moves. `mr show` prints it; the web merge box shows it as a live badge; `ermite watch` reports each change.60 The server checks in the background whether each open request merges with its target, and again whenever the source or the target moves. `mr show` prints it; the web merge box shows it as a live badge; `ermite watch` reports each change.
M gen/ermite/storage/merge_request.pb.go +163 -35 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.13.0
16+ bnema.dev/ermite-api v0.14.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.13.016+ bnema.dev/ermite-api v0.14.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/client_test.go +3 -3
@@ -139,16 +139,16 @@ func TestDescribeBoundsLongMessages(t *testing.T) {
139139 func TestMissingFeatures(t *testing.T) {
140140 assert.Empty(t, client.MissingFeatures(client.UsedFeatures()))
141141 assert.Equal(t,
142- []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures},
142+ []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge},
143143 client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, client.FeaturePasskeys, "other"}))
144144 assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{
145145 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSession,
146146 client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows,
147- client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures,
147+ client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures, client.FeatureAutoMerge,
148148 }))
149149 assert.Equal(t, []string{client.FeaturePasskeys}, client.MissingFeatures([]string{
150150 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureSession,
151- client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures,
151+ client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge,
152152 }))
153153 assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil))
154154 }
@@ -139,16 +139,16 @@ func TestDescribeBoundsLongMessages(t *testing.T) {
139 func TestMissingFeatures(t *testing.T) {139 func TestMissingFeatures(t *testing.T) {
140 assert.Empty(t, client.MissingFeatures(client.UsedFeatures()))140 assert.Empty(t, client.MissingFeatures(client.UsedFeatures()))
141 assert.Equal(t,141 assert.Equal(t,
142- []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures},142+ []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge},
143 client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, client.FeaturePasskeys, "other"}))143 client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, client.FeaturePasskeys, "other"}))
144 assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{144 assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{
145 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSession,145 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSession,
146 client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows,146 client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows,
147- client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures,147+ client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures, client.FeatureAutoMerge,
148 }))148 }))
149 assert.Equal(t, []string{client.FeaturePasskeys}, client.MissingFeatures([]string{149 assert.Equal(t, []string{client.FeaturePasskeys}, client.MissingFeatures([]string{
150 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureSession,150 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureSession,
151- client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures,151+ client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge,
152 }))152 }))
153 assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil))153 assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil))
154 }154 }
M internal/adapters/client/features.go +5 -1
@@ -57,11 +57,15 @@ const (
5757 // FeatureMergeability is MergeRequest.mergeability and the
5858 // merge_request_mergeability event.
5959 FeatureMergeability = "merge_requests.mergeability"
60+ // FeatureAutoMerge is MergeRequestService.EnableAutoMerge and
61+ // CancelAutoMerge, RepoSettings.allow_auto_merge and the auto-merge events
62+ // (`ermite mr merge --auto`).
63+ FeatureAutoMerge = "merge_requests.auto_merge"
6064 )
6165
6266 // UsedFeatures lists the features the CLI uses, in the order they are reported.
6367 func UsedFeatures() []string {
64- return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures}
68+ return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures, FeatureAutoMerge}
6569 }
6670
6771 // MissingFeatures returns the features the CLI uses that have is lacking.
@@ -57,11 +57,15 @@ const (
57 // FeatureMergeability is MergeRequest.mergeability and the57 // FeatureMergeability is MergeRequest.mergeability and the
58 // merge_request_mergeability event.58 // merge_request_mergeability event.
59 FeatureMergeability = "merge_requests.mergeability"59 FeatureMergeability = "merge_requests.mergeability"
60+ // FeatureAutoMerge is MergeRequestService.EnableAutoMerge and
61+ // CancelAutoMerge, RepoSettings.allow_auto_merge and the auto-merge events
62+ // (`ermite mr merge --auto`).
63+ FeatureAutoMerge = "merge_requests.auto_merge"
60 )64 )
61 65
62 // UsedFeatures lists the features the CLI uses, in the order they are reported.66 // UsedFeatures lists the features the CLI uses, in the order they are reported.
63 func UsedFeatures() []string {67 func UsedFeatures() []string {
64- return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures}68+ return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures, FeatureAutoMerge}
65 }69 }
66 70
67 // MissingFeatures returns the features the CLI uses that have is lacking.71 // MissingFeatures returns the features the CLI uses that have is lacking.
M internal/adapters/kvstore/mergerequests.go +32 -2
@@ -728,6 +728,18 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
728728 if err != nil {
729729 return nil, err
730730 }
731+ var autoMerge *storagepb.AutoMerge
732+ if am := mr.AutoMerge; am != nil {
733+ amMode, err := encodeMode(am.Mode)
734+ if err != nil || amMode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
735+ return nil, fmt.Errorf("%w: merge mode %q in the auto-merge", domain.ErrInvalid, am.Mode)
736+ }
737+ autoMerge = &storagepb.AutoMerge{
738+ Mode: amMode, HeadId: am.HeadID.String(),
739+ EnablerKind: string(am.EnabledBy.Kind), EnablerTokenId: am.EnabledBy.TokenID,
740+ EnablerLabel: am.EnablerLabel, EnabledAt: encodeTime(am.EnabledAt),
741+ }
742+ }
731743 return mustMarshal(&storagepb.MergeRequest{
732744 Number: mr.Number,
733745 Title: mr.Title.String(),
@@ -743,6 +755,8 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
743755 MergedCommitId: mr.MergedCommitID.String(),
744756 MergeMode: mode,
745757 CommentSeq: mr.CommentSeq,
758+ AutoMerge: autoMerge,
759+ AutoMerged: mr.AutoMerged,
746760 }), nil
747761 }
748762
@@ -773,8 +787,24 @@ func decodeMergeRequest(repo domain.RepoName, item kv.Item) (domain.MergeRequest
773787 CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()),
774788 MergeMode: decodeMode(pb.GetMergeMode()),
775789 CommentSeq: pb.GetCommentSeq(),
790+ AutoMerged: pb.GetAutoMerged(),
776791 Version: item.Version,
777792 }
793+ if am := pb.GetAutoMerge(); am != nil {
794+ head, err := domain.ParseObjectID(am.GetHeadId())
795+ if err != nil {
796+ return fail(err)
797+ }
798+ mode := decodeMode(am.GetMode())
799+ if mode == "" {
800+ return fail(fmt.Errorf("unknown auto-merge mode %d", am.GetMode()))
801+ }
802+ mr.AutoMerge = &domain.AutoMerge{
803+ Mode: mode, HeadID: head,
804+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherKind(am.GetEnablerKind()), TokenID: am.GetEnablerTokenId()},
805+ EnablerLabel: am.GetEnablerLabel(), EnabledAt: decodeTime(am.GetEnabledAt()),
806+ }
807+ }
778808 for _, f := range []struct {
779809 dst *domain.ObjectID
780810 src string
@@ -909,7 +939,7 @@ func decodeSide(s storagepb.DiffSide) domain.DiffSide {
909939 }
910940
911941 func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) {
912- pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true}
942+ pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge}
913943 for _, m := range s.AllowedMergeModes {
914944 mode, err := encodeMode(m)
915945 if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
@@ -949,5 +979,5 @@ func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings {
949979 if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) {
950980 normalized = domain.MergeModes()
951981 }
952- return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues())}
982+ return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()}
953983 }
@@ -728,6 +728,18 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
728 if err != nil {728 if err != nil {
729 return nil, err729 return nil, err
730 }730 }
731+ var autoMerge *storagepb.AutoMerge
732+ if am := mr.AutoMerge; am != nil {
733+ amMode, err := encodeMode(am.Mode)
734+ if err != nil || amMode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
735+ return nil, fmt.Errorf("%w: merge mode %q in the auto-merge", domain.ErrInvalid, am.Mode)
736+ }
737+ autoMerge = &storagepb.AutoMerge{
738+ Mode: amMode, HeadId: am.HeadID.String(),
739+ EnablerKind: string(am.EnabledBy.Kind), EnablerTokenId: am.EnabledBy.TokenID,
740+ EnablerLabel: am.EnablerLabel, EnabledAt: encodeTime(am.EnabledAt),
741+ }
742+ }
731 return mustMarshal(&storagepb.MergeRequest{743 return mustMarshal(&storagepb.MergeRequest{
732 Number: mr.Number,744 Number: mr.Number,
733 Title: mr.Title.String(),745 Title: mr.Title.String(),
@@ -743,6 +755,8 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
743 MergedCommitId: mr.MergedCommitID.String(),755 MergedCommitId: mr.MergedCommitID.String(),
744 MergeMode: mode,756 MergeMode: mode,
745 CommentSeq: mr.CommentSeq,757 CommentSeq: mr.CommentSeq,
758+ AutoMerge: autoMerge,
759+ AutoMerged: mr.AutoMerged,
746 }), nil760 }), nil
747 }761 }
748 762
@@ -773,8 +787,24 @@ func decodeMergeRequest(repo domain.RepoName, item kv.Item) (domain.MergeRequest
773 CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()),787 CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()),
774 MergeMode: decodeMode(pb.GetMergeMode()),788 MergeMode: decodeMode(pb.GetMergeMode()),
775 CommentSeq: pb.GetCommentSeq(),789 CommentSeq: pb.GetCommentSeq(),
790+ AutoMerged: pb.GetAutoMerged(),
776 Version: item.Version,791 Version: item.Version,
777 }792 }
793+ if am := pb.GetAutoMerge(); am != nil {
794+ head, err := domain.ParseObjectID(am.GetHeadId())
795+ if err != nil {
796+ return fail(err)
797+ }
798+ mode := decodeMode(am.GetMode())
799+ if mode == "" {
800+ return fail(fmt.Errorf("unknown auto-merge mode %d", am.GetMode()))
801+ }
802+ mr.AutoMerge = &domain.AutoMerge{
803+ Mode: mode, HeadID: head,
804+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherKind(am.GetEnablerKind()), TokenID: am.GetEnablerTokenId()},
805+ EnablerLabel: am.GetEnablerLabel(), EnabledAt: decodeTime(am.GetEnabledAt()),
806+ }
807+ }
778 for _, f := range []struct {808 for _, f := range []struct {
779 dst *domain.ObjectID809 dst *domain.ObjectID
780 src string810 src string
@@ -909,7 +939,7 @@ func decodeSide(s storagepb.DiffSide) domain.DiffSide {
909 }939 }
910 940
911 func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) {941 func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) {
912- pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true}942+ pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge}
913 for _, m := range s.AllowedMergeModes {943 for _, m := range s.AllowedMergeModes {
914 mode, err := encodeMode(m)944 mode, err := encodeMode(m)
915 if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {945 if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
@@ -949,5 +979,5 @@ func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings {
949 if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) {979 if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) {
950 normalized = domain.MergeModes()980 normalized = domain.MergeModes()
951 }981 }
952- return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues())}982+ return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()}
953 }983 }
M internal/adapters/kvstore/mergerequests_test.go +47 -0
@@ -94,6 +94,35 @@ func TestMergeRequestRoundTrip(t *testing.T) {
9494 assert.ErrorIs(t, err, domain.ErrNotFound)
9595 }
9696
97+func TestMergeRequestAutoMergeRoundTrip(t *testing.T) {
98+ ctx := context.Background()
99+ _, mrs, n := repoWithMRs(t)
100+
101+ set := newMR(t, n, "Auto")
102+ set.AutoMerge = &domain.AutoMerge{
103+ Mode: domain.MergeModeSquash, HeadID: oid('d'),
104+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherToken, TokenID: "tok1"},
105+ EnablerLabel: "ci-bot", EnabledAt: created,
106+ }
107+ created1, err := mrs.Create(ctx, set)
108+ require.NoError(t, err)
109+ got, err := mrs.Get(ctx, n, created1.Number)
110+ require.NoError(t, err)
111+ require.NotNil(t, got.AutoMerge)
112+ assert.Equal(t, *set.AutoMerge, *got.AutoMerge)
113+ assert.False(t, got.AutoMerged)
114+
115+ unset := newMR(t, n, "Merged by auto")
116+ unset.State = domain.MRStateMerged
117+ unset.AutoMerged = true
118+ created2, err := mrs.Create(ctx, unset)
119+ require.NoError(t, err)
120+ got, err = mrs.Get(ctx, n, created2.Number)
121+ require.NoError(t, err)
122+ assert.Nil(t, got.AutoMerge)
123+ assert.True(t, got.AutoMerged)
124+}
125+
97126 func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) {
98127 ctx := context.Background()
99128 s, mrs, n := repoWithMRs(t)
@@ -453,6 +482,24 @@ func TestRepoSettings(t *testing.T) {
453482 assert.ErrorIs(t, err, domain.ErrInvalid)
454483 }
455484
485+func TestRepoSettingsKeepAllowAutoMerge(t *testing.T) {
486+ ctx := context.Background()
487+ s := openStore(t, t.TempDir())
488+ n := name(t, "proj")
489+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate}))
490+ got, err := s.Get(ctx, n)
491+ require.NoError(t, err)
492+ assert.False(t, got.Settings.AllowAutoMerge)
493+
494+ settings := domain.DefaultRepoSettings()
495+ settings.AllowAutoMerge = true
496+ _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = settings; return true, nil })
497+ require.NoError(t, err)
498+ got, err = s.Get(ctx, n)
499+ require.NoError(t, err)
500+ assert.Equal(t, settings, got.Settings)
501+}
502+
456503 // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before
457504 // merge requests existed has no settings field.
458505 func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) {
@@ -94,6 +94,35 @@ func TestMergeRequestRoundTrip(t *testing.T) {
94 assert.ErrorIs(t, err, domain.ErrNotFound)94 assert.ErrorIs(t, err, domain.ErrNotFound)
95 }95 }
96 96
97+func TestMergeRequestAutoMergeRoundTrip(t *testing.T) {
98+ ctx := context.Background()
99+ _, mrs, n := repoWithMRs(t)
100+
101+ set := newMR(t, n, "Auto")
102+ set.AutoMerge = &domain.AutoMerge{
103+ Mode: domain.MergeModeSquash, HeadID: oid('d'),
104+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherToken, TokenID: "tok1"},
105+ EnablerLabel: "ci-bot", EnabledAt: created,
106+ }
107+ created1, err := mrs.Create(ctx, set)
108+ require.NoError(t, err)
109+ got, err := mrs.Get(ctx, n, created1.Number)
110+ require.NoError(t, err)
111+ require.NotNil(t, got.AutoMerge)
112+ assert.Equal(t, *set.AutoMerge, *got.AutoMerge)
113+ assert.False(t, got.AutoMerged)
114+
115+ unset := newMR(t, n, "Merged by auto")
116+ unset.State = domain.MRStateMerged
117+ unset.AutoMerged = true
118+ created2, err := mrs.Create(ctx, unset)
119+ require.NoError(t, err)
120+ got, err = mrs.Get(ctx, n, created2.Number)
121+ require.NoError(t, err)
122+ assert.Nil(t, got.AutoMerge)
123+ assert.True(t, got.AutoMerged)
124+}
125+
97 func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) {126 func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) {
98 ctx := context.Background()127 ctx := context.Background()
99 s, mrs, n := repoWithMRs(t)128 s, mrs, n := repoWithMRs(t)
@@ -453,6 +482,24 @@ func TestRepoSettings(t *testing.T) {
453 assert.ErrorIs(t, err, domain.ErrInvalid)482 assert.ErrorIs(t, err, domain.ErrInvalid)
454 }483 }
455 484
485+func TestRepoSettingsKeepAllowAutoMerge(t *testing.T) {
486+ ctx := context.Background()
487+ s := openStore(t, t.TempDir())
488+ n := name(t, "proj")
489+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate}))
490+ got, err := s.Get(ctx, n)
491+ require.NoError(t, err)
492+ assert.False(t, got.Settings.AllowAutoMerge)
493+
494+ settings := domain.DefaultRepoSettings()
495+ settings.AllowAutoMerge = true
496+ _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = settings; return true, nil })
497+ require.NoError(t, err)
498+ got, err = s.Get(ctx, n)
499+ require.NoError(t, err)
500+ assert.Equal(t, settings, got.Settings)
501+}
502+
456 // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before503 // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before
457 // merge requests existed has no settings field.504 // merge requests existed has no settings field.
458 func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) {505 func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) {
M internal/adapters/rpc/api.go +1 -1
@@ -35,7 +35,7 @@ const RunnerMaxRequestBytes = MaxRequestBytes + 64<<10
3535 // always implements, as GetVersion lists them. FeaturePasskeys comes on top
3636 // when Config.Passkeys is set, and FeatureCISecrets when the settings services
3737 // are, and FeatureCommitSignatures when Config.TrustedKeys is set.
38-var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows"}
38+var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge"}
3939
4040 // FeaturePasskeys is listed by GetVersion when passkeys are available.
4141 // FeatureCIArtifacts (artifact.go) when the artifact services are served.
@@ -35,7 +35,7 @@ const RunnerMaxRequestBytes = MaxRequestBytes + 64<<10
35 // always implements, as GetVersion lists them. FeaturePasskeys comes on top35 // always implements, as GetVersion lists them. FeaturePasskeys comes on top
36 // when Config.Passkeys is set, and FeatureCISecrets when the settings services36 // when Config.Passkeys is set, and FeatureCISecrets when the settings services
37 // are, and FeatureCommitSignatures when Config.TrustedKeys is set.37 // are, and FeatureCommitSignatures when Config.TrustedKeys is set.
38-var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows"}38+var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge"}
39 39
40 // FeaturePasskeys is listed by GetVersion when passkeys are available.40 // FeaturePasskeys is listed by GetVersion when passkeys are available.
41 // FeatureCIArtifacts (artifact.go) when the artifact services are served.41 // FeatureCIArtifacts (artifact.go) when the artifact services are served.
M internal/adapters/rpc/convert.go +8 -2
@@ -72,7 +72,7 @@ func repoToPB(v ports.RepoView) *pb.Repo {
7272 }
7373
7474 func settingsToPB(s domain.RepoSettings) *pb.RepoSettings {
75- out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, Issues: issuesModeToPB(s.Issues)}
75+ out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)}
7676 for _, m := range s.AllowedMergeModes {
7777 out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m))
7878 }
@@ -483,7 +483,13 @@ func watchEventToPB(we ports.WatchEvent) *pb.Event {
483483 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
484484 case domain.MergeRequestMerged:
485485 out.Time = timestamp(ev.At)
486- out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
486+ out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest), Auto: ev.Auto}}
487+ case domain.MergeRequestAutoMergeEnabled:
488+ out.Time = timestamp(ev.At)
489+ out.Kind = &pb.Event_MergeRequestAutoMergeEnabled{MergeRequestAutoMergeEnabled: &pb.MergeRequestAutoMergeEnabled{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
490+ case domain.MergeRequestAutoMergeCancelled:
491+ out.Time = timestamp(ev.At)
492+ out.Kind = &pb.Event_MergeRequestAutoMergeCancelled{MergeRequestAutoMergeCancelled: &pb.MergeRequestAutoMergeCancelled{MergeRequest: mergeRequestToPB(ev.MergeRequest), Reason: ev.Reason}}
487493 case domain.MergeRequestClosed:
488494 out.Time = timestamp(ev.At)
489495 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
@@ -72,7 +72,7 @@ func repoToPB(v ports.RepoView) *pb.Repo {
72 }72 }
73 73
74 func settingsToPB(s domain.RepoSettings) *pb.RepoSettings {74 func settingsToPB(s domain.RepoSettings) *pb.RepoSettings {
75- out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, Issues: issuesModeToPB(s.Issues)}75+ out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)}
76 for _, m := range s.AllowedMergeModes {76 for _, m := range s.AllowedMergeModes {
77 out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m))77 out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m))
78 }78 }
@@ -483,7 +483,13 @@ func watchEventToPB(we ports.WatchEvent) *pb.Event {
483 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}483 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
484 case domain.MergeRequestMerged:484 case domain.MergeRequestMerged:
485 out.Time = timestamp(ev.At)485 out.Time = timestamp(ev.At)
486- out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}486+ out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest), Auto: ev.Auto}}
487+ case domain.MergeRequestAutoMergeEnabled:
488+ out.Time = timestamp(ev.At)
489+ out.Kind = &pb.Event_MergeRequestAutoMergeEnabled{MergeRequestAutoMergeEnabled: &pb.MergeRequestAutoMergeEnabled{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
490+ case domain.MergeRequestAutoMergeCancelled:
491+ out.Time = timestamp(ev.At)
492+ out.Kind = &pb.Event_MergeRequestAutoMergeCancelled{MergeRequestAutoMergeCancelled: &pb.MergeRequestAutoMergeCancelled{MergeRequest: mergeRequestToPB(ev.MergeRequest), Reason: ev.Reason}}
487 case domain.MergeRequestClosed:493 case domain.MergeRequestClosed:
488 out.Time = timestamp(ev.At)494 out.Time = timestamp(ev.At)
489 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}495 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
M internal/adapters/rpc/mergerequest.go +39 -0
@@ -19,6 +19,38 @@ type mergeRequestService struct {
1919 mrs ports.MergeRequestService
2020 }
2121
22+func (s *mergeRequestService) EnableAutoMerge(ctx context.Context, req *connect.Request[pb.EnableAutoMergeRequest]) (*connect.Response[pb.EnableAutoMergeResponse], error) {
23+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
24+ if err != nil {
25+ return nil, connectError(ctx, err)
26+ }
27+ mode, err := mergeModeFromPB(req.Msg.GetMode())
28+ if err != nil {
29+ return nil, connectError(ctx, err)
30+ }
31+ head, err := headID(req.Msg.GetHeadId())
32+ if err != nil {
33+ return nil, connectError(ctx, err)
34+ }
35+ mr, err := s.mrs.EnableAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.EnableAutoMergeParams{Mode: mode, HeadID: head})
36+ if err != nil {
37+ return nil, connectError(ctx, err)
38+ }
39+ return connect.NewResponse(&pb.EnableAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
40+}
41+
42+func (s *mergeRequestService) CancelAutoMerge(ctx context.Context, req *connect.Request[pb.CancelAutoMergeRequest]) (*connect.Response[pb.CancelAutoMergeResponse], error) {
43+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
44+ if err != nil {
45+ return nil, connectError(ctx, err)
46+ }
47+ mr, err := s.mrs.CancelAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber())
48+ if err != nil {
49+ return nil, connectError(ctx, err)
50+ }
51+ return connect.NewResponse(&pb.CancelAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
52+}
53+
2254 func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {
2355 name, err := domain.ParseRepoName(req.Msg.GetRepo())
2456 if err != nil {
@@ -133,6 +165,13 @@ func mergeRequestToPB(mr domain.MergeRequest) *pb.MergeRequest {
133165 HeadId: mr.HeadID.String(),
134166 MergedCommitId: mr.MergedCommitID.String(),
135167 MergeMode: mergeModeToPB(mr.MergeMode),
168+ AutoMerged: mr.AutoMerged,
169+ }
170+ if am := mr.AutoMerge; am != nil {
171+ out.AutoMerge = &pb.AutoMerge{
172+ Mode: mergeModeToPB(am.Mode), HeadId: am.HeadID.String(),
173+ EnabledBy: am.EnablerLabel, EnabledAt: timestamp(am.EnabledAt),
174+ }
136175 }
137176 if mr.IsOpen() {
138177 out.Mergeability = &pb.Mergeability{State: pb.MergeabilityState_MERGEABILITY_STATE_CHECKING}
@@ -19,6 +19,38 @@ type mergeRequestService struct {
19 mrs ports.MergeRequestService19 mrs ports.MergeRequestService
20 }20 }
21 21
22+func (s *mergeRequestService) EnableAutoMerge(ctx context.Context, req *connect.Request[pb.EnableAutoMergeRequest]) (*connect.Response[pb.EnableAutoMergeResponse], error) {
23+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
24+ if err != nil {
25+ return nil, connectError(ctx, err)
26+ }
27+ mode, err := mergeModeFromPB(req.Msg.GetMode())
28+ if err != nil {
29+ return nil, connectError(ctx, err)
30+ }
31+ head, err := headID(req.Msg.GetHeadId())
32+ if err != nil {
33+ return nil, connectError(ctx, err)
34+ }
35+ mr, err := s.mrs.EnableAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.EnableAutoMergeParams{Mode: mode, HeadID: head})
36+ if err != nil {
37+ return nil, connectError(ctx, err)
38+ }
39+ return connect.NewResponse(&pb.EnableAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
40+}
41+
42+func (s *mergeRequestService) CancelAutoMerge(ctx context.Context, req *connect.Request[pb.CancelAutoMergeRequest]) (*connect.Response[pb.CancelAutoMergeResponse], error) {
43+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
44+ if err != nil {
45+ return nil, connectError(ctx, err)
46+ }
47+ mr, err := s.mrs.CancelAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber())
48+ if err != nil {
49+ return nil, connectError(ctx, err)
50+ }
51+ return connect.NewResponse(&pb.CancelAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
52+}
53+
22 func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {54 func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {
23 name, err := domain.ParseRepoName(req.Msg.GetRepo())55 name, err := domain.ParseRepoName(req.Msg.GetRepo())
24 if err != nil {56 if err != nil {
@@ -133,6 +165,13 @@ func mergeRequestToPB(mr domain.MergeRequest) *pb.MergeRequest {
133 HeadId: mr.HeadID.String(),165 HeadId: mr.HeadID.String(),
134 MergedCommitId: mr.MergedCommitID.String(),166 MergedCommitId: mr.MergedCommitID.String(),
135 MergeMode: mergeModeToPB(mr.MergeMode),167 MergeMode: mergeModeToPB(mr.MergeMode),
168+ AutoMerged: mr.AutoMerged,
169+ }
170+ if am := mr.AutoMerge; am != nil {
171+ out.AutoMerge = &pb.AutoMerge{
172+ Mode: mergeModeToPB(am.Mode), HeadId: am.HeadID.String(),
173+ EnabledBy: am.EnablerLabel, EnabledAt: timestamp(am.EnabledAt),
174+ }
136 }175 }
137 if mr.IsOpen() {176 if mr.IsOpen() {
138 out.Mergeability = &pb.Mergeability{State: pb.MergeabilityState_MERGEABILITY_STATE_CHECKING}177 out.Mergeability = &pb.Mergeability{State: pb.MergeabilityState_MERGEABILITY_STATE_CHECKING}
M internal/adapters/rpc/mr_api_test.go +80 -0
@@ -3,6 +3,7 @@
33 package rpc
44
55 import (
6+ "strings"
67 "testing"
78 "time"
89
@@ -122,3 +123,82 @@ func TestNoteVerdictsDetailAndScope(t *testing.T) {
122123 _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))
123124 requireCode(t, connect.CodePermissionDenied, err)
124125 }
126+
127+func TestAutoMergeRPCCodesAndFields(t *testing.T) {
128+ now := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
129+ head, err := domain.ParseObjectID(strings.Repeat("a", 40))
130+ require.NoError(t, err)
131+ armed := domain.MergeRequest{
132+ Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen, Source: "feat", Target: "main", HeadID: head,
133+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeSquash, HeadID: head, EnablerLabel: "owner", EnabledAt: now},
134+ }
135+ idle := armed
136+ idle.AutoMerge = nil
137+
138+ pbArmed := mergeRequestToPB(armed)
139+ require.NotNil(t, pbArmed.GetAutoMerge())
140+ assert.Equal(t, pb.MergeMode_MERGE_MODE_SQUASH, pbArmed.GetAutoMerge().GetMode())
141+ assert.Equal(t, head.String(), pbArmed.GetAutoMerge().GetHeadId())
142+ assert.Equal(t, "owner", pbArmed.GetAutoMerge().GetEnabledBy())
143+ assert.Equal(t, now, pbArmed.GetAutoMerge().GetEnabledAt().AsTime())
144+ assert.False(t, pbArmed.GetAutoMerged())
145+ assert.Nil(t, mergeRequestToPB(idle).GetAutoMerge())
146+ done := idle
147+ done.State, done.AutoMerged = domain.MRStateMerged, true
148+ assert.True(t, mergeRequestToPB(done).GetAutoMerged())
149+
150+ // Success: the mode and the request reach the service, the fields come back.
151+ m := mocks.NewMergeRequestService(t)
152+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.EnableAutoMergeParams{Mode: domain.MergeModeSquash, HeadID: mustObjectID(t, strings.Repeat("a", domain.ObjectIDLen))}).Return(armed, nil).Once()
153+ res, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode_MERGE_MODE_SQUASH, HeadId: strings.Repeat("a", domain.ObjectIDLen)}))
154+ require.NoError(t, err)
155+ assert.Equal(t, pbArmed, res.Msg.GetMergeRequest())
156+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(idle, nil).Once()
157+ cres, err := (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
158+ require.NoError(t, err)
159+ assert.Nil(t, cres.Msg.GetMergeRequest().GetAutoMerge())
160+
161+ // Bad input never reaches the service.
162+ _, err = (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode(99), HeadId: strings.Repeat("a", domain.ObjectIDLen)}))
163+ requireCode(t, connect.CodeInvalidArgument, err)
164+ _, err = (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, HeadId: "abc"}))
165+ requireCode(t, connect.CodeInvalidArgument, err)
166+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "", Number: 1}))
167+ requireCode(t, connect.CodeInvalidArgument, err)
168+
169+ // Domain errors map like the other merge request calls.
170+ for _, tc := range []struct {
171+ err error
172+ code connect.Code
173+ }{
174+ {domain.ErrPreconditionFailed, connect.CodeFailedPrecondition},
175+ {domain.ErrForbidden, connect.CodePermissionDenied},
176+ {domain.ErrNotFound, connect.CodeNotFound},
177+ } {
178+ m := mocks.NewMergeRequestService(t)
179+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.Anything).Return(domain.MergeRequest{}, tc.err).Once()
180+ _, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, HeadId: strings.Repeat("a", domain.ObjectIDLen)}))
181+ requireCode(t, tc.code, err)
182+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(domain.MergeRequest{}, tc.err).Once()
183+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
184+ requireCode(t, tc.code, err)
185+ }
186+}
187+
188+func TestAutoMergeWatchEventsAndSettingsToPB(t *testing.T) {
189+ armed := domain.MergeRequest{Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen,
190+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeMerge, EnablerLabel: "owner"}}
191+ got := watchEventToPB(ports.WatchEvent{ID: "1", Event: domain.MergeRequestAutoMergeEnabled{MergeRequest: armed}})
192+ require.NotNil(t, got.GetMergeRequestAutoMergeEnabled())
193+ assert.Equal(t, pb.MergeMode_MERGE_MODE_MERGE_COMMIT, got.GetMergeRequestAutoMergeEnabled().GetMergeRequest().GetAutoMerge().GetMode())
194+ got = watchEventToPB(ports.WatchEvent{ID: "2", Event: domain.MergeRequestAutoMergeCancelled{MergeRequest: armed, Reason: "the branch moved"}})
195+ require.NotNil(t, got.GetMergeRequestAutoMergeCancelled())
196+ assert.Equal(t, "the branch moved", got.GetMergeRequestAutoMergeCancelled().GetReason())
197+ got = watchEventToPB(ports.WatchEvent{ID: "3", Event: domain.MergeRequestMerged{MergeRequest: armed, Auto: true}})
198+ assert.True(t, got.GetMergeRequestMerged().GetAuto())
199+ got = watchEventToPB(ports.WatchEvent{ID: "4", Event: domain.MergeRequestMerged{MergeRequest: armed}})
200+ assert.False(t, got.GetMergeRequestMerged().GetAuto())
201+
202+ assert.True(t, settingsToPB(domain.RepoSettings{AllowAutoMerge: true}).GetAllowAutoMerge())
203+ assert.False(t, settingsToPB(domain.RepoSettings{}).GetAllowAutoMerge())
204+}
@@ -3,6 +3,7 @@
3 package rpc3 package rpc
4 4
5 import (5 import (
6+ "strings"
6 "testing"7 "testing"
7 "time"8 "time"
8 9
@@ -122,3 +123,82 @@ func TestNoteVerdictsDetailAndScope(t *testing.T) {
122 _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))123 _, 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 requireCode(t, connect.CodePermissionDenied, err)
124 }125 }
126+
127+func TestAutoMergeRPCCodesAndFields(t *testing.T) {
128+ now := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
129+ head, err := domain.ParseObjectID(strings.Repeat("a", 40))
130+ require.NoError(t, err)
131+ armed := domain.MergeRequest{
132+ Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen, Source: "feat", Target: "main", HeadID: head,
133+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeSquash, HeadID: head, EnablerLabel: "owner", EnabledAt: now},
134+ }
135+ idle := armed
136+ idle.AutoMerge = nil
137+
138+ pbArmed := mergeRequestToPB(armed)
139+ require.NotNil(t, pbArmed.GetAutoMerge())
140+ assert.Equal(t, pb.MergeMode_MERGE_MODE_SQUASH, pbArmed.GetAutoMerge().GetMode())
141+ assert.Equal(t, head.String(), pbArmed.GetAutoMerge().GetHeadId())
142+ assert.Equal(t, "owner", pbArmed.GetAutoMerge().GetEnabledBy())
143+ assert.Equal(t, now, pbArmed.GetAutoMerge().GetEnabledAt().AsTime())
144+ assert.False(t, pbArmed.GetAutoMerged())
145+ assert.Nil(t, mergeRequestToPB(idle).GetAutoMerge())
146+ done := idle
147+ done.State, done.AutoMerged = domain.MRStateMerged, true
148+ assert.True(t, mergeRequestToPB(done).GetAutoMerged())
149+
150+ // Success: the mode and the request reach the service, the fields come back.
151+ m := mocks.NewMergeRequestService(t)
152+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.EnableAutoMergeParams{Mode: domain.MergeModeSquash, HeadID: mustObjectID(t, strings.Repeat("a", domain.ObjectIDLen))}).Return(armed, nil).Once()
153+ res, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode_MERGE_MODE_SQUASH, HeadId: strings.Repeat("a", domain.ObjectIDLen)}))
154+ require.NoError(t, err)
155+ assert.Equal(t, pbArmed, res.Msg.GetMergeRequest())
156+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(idle, nil).Once()
157+ cres, err := (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
158+ require.NoError(t, err)
159+ assert.Nil(t, cres.Msg.GetMergeRequest().GetAutoMerge())
160+
161+ // Bad input never reaches the service.
162+ _, err = (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode(99), HeadId: strings.Repeat("a", domain.ObjectIDLen)}))
163+ requireCode(t, connect.CodeInvalidArgument, err)
164+ _, err = (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, HeadId: "abc"}))
165+ requireCode(t, connect.CodeInvalidArgument, err)
166+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "", Number: 1}))
167+ requireCode(t, connect.CodeInvalidArgument, err)
168+
169+ // Domain errors map like the other merge request calls.
170+ for _, tc := range []struct {
171+ err error
172+ code connect.Code
173+ }{
174+ {domain.ErrPreconditionFailed, connect.CodeFailedPrecondition},
175+ {domain.ErrForbidden, connect.CodePermissionDenied},
176+ {domain.ErrNotFound, connect.CodeNotFound},
177+ } {
178+ m := mocks.NewMergeRequestService(t)
179+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.Anything).Return(domain.MergeRequest{}, tc.err).Once()
180+ _, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, HeadId: strings.Repeat("a", domain.ObjectIDLen)}))
181+ requireCode(t, tc.code, err)
182+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(domain.MergeRequest{}, tc.err).Once()
183+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
184+ requireCode(t, tc.code, err)
185+ }
186+}
187+
188+func TestAutoMergeWatchEventsAndSettingsToPB(t *testing.T) {
189+ armed := domain.MergeRequest{Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen,
190+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeMerge, EnablerLabel: "owner"}}
191+ got := watchEventToPB(ports.WatchEvent{ID: "1", Event: domain.MergeRequestAutoMergeEnabled{MergeRequest: armed}})
192+ require.NotNil(t, got.GetMergeRequestAutoMergeEnabled())
193+ assert.Equal(t, pb.MergeMode_MERGE_MODE_MERGE_COMMIT, got.GetMergeRequestAutoMergeEnabled().GetMergeRequest().GetAutoMerge().GetMode())
194+ got = watchEventToPB(ports.WatchEvent{ID: "2", Event: domain.MergeRequestAutoMergeCancelled{MergeRequest: armed, Reason: "the branch moved"}})
195+ require.NotNil(t, got.GetMergeRequestAutoMergeCancelled())
196+ assert.Equal(t, "the branch moved", got.GetMergeRequestAutoMergeCancelled().GetReason())
197+ got = watchEventToPB(ports.WatchEvent{ID: "3", Event: domain.MergeRequestMerged{MergeRequest: armed, Auto: true}})
198+ assert.True(t, got.GetMergeRequestMerged().GetAuto())
199+ got = watchEventToPB(ports.WatchEvent{ID: "4", Event: domain.MergeRequestMerged{MergeRequest: armed}})
200+ assert.False(t, got.GetMergeRequestMerged().GetAuto())
201+
202+ assert.True(t, settingsToPB(domain.RepoSettings{AllowAutoMerge: true}).GetAllowAutoMerge())
203+ assert.False(t, settingsToPB(domain.RepoSettings{}).GetAllowAutoMerge())
204+}
M internal/adapters/rpc/repo.go +9 -5
@@ -80,8 +80,8 @@ func (s *repoService) UpdateRepo(ctx context.Context, req *connect.Request[pb.Up
8080
8181 // updateParams maps the field mask of an update. The mask must name at least
8282 // one of "description", "topics", "visibility", "default_branch", "settings",
83-// "settings.allowed_merge_modes" and "settings.failing_checks_block", and
84-// nothing else; the optional issues field stands outside the mask, and alone
83+// "settings.allowed_merge_modes", "settings.failing_checks_block" and
84+// "settings.allow_auto_merge", and nothing else; the optional issues field stands outside the mask, and alone
8585 // it is a valid update.
8686 func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
8787 var params ports.UpdateRepoParams
@@ -113,17 +113,21 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
113113 params.Visibility = &vis
114114 case "default_branch":
115115 params.DefaultBranch = new(msg.GetDefaultBranch())
116- case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block":
117- if path != "settings.failing_checks_block" {
116+ case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block", "settings.allow_auto_merge":
117+ all := path == "settings"
118+ if all || path == "settings.allowed_merge_modes" {
118119 modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes())
119120 if err != nil {
120121 return params, err
121122 }
122123 params.AllowedMergeModes = &modes
123124 }
124- if path != "settings.allowed_merge_modes" {
125+ if all || path == "settings.failing_checks_block" {
125126 params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock())
126127 }
128+ if all || path == "settings.allow_auto_merge" {
129+ params.AllowAutoMerge = new(msg.GetSettings().GetAllowAutoMerge())
130+ }
127131 default:
128132 return params, fmt.Errorf("%w: unknown path %q in update_mask", domain.ErrInvalid, path)
129133 }
@@ -80,8 +80,8 @@ func (s *repoService) UpdateRepo(ctx context.Context, req *connect.Request[pb.Up
80 80
81 // updateParams maps the field mask of an update. The mask must name at least81 // updateParams maps the field mask of an update. The mask must name at least
82 // one of "description", "topics", "visibility", "default_branch", "settings",82 // one of "description", "topics", "visibility", "default_branch", "settings",
83-// "settings.allowed_merge_modes" and "settings.failing_checks_block", and83+// "settings.allowed_merge_modes", "settings.failing_checks_block" and
84-// nothing else; the optional issues field stands outside the mask, and alone84+// "settings.allow_auto_merge", and nothing else; the optional issues field stands outside the mask, and alone
85 // it is a valid update.85 // it is a valid update.
86 func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {86 func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
87 var params ports.UpdateRepoParams87 var params ports.UpdateRepoParams
@@ -113,17 +113,21 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
113 params.Visibility = &vis113 params.Visibility = &vis
114 case "default_branch":114 case "default_branch":
115 params.DefaultBranch = new(msg.GetDefaultBranch())115 params.DefaultBranch = new(msg.GetDefaultBranch())
116- case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block":116+ case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block", "settings.allow_auto_merge":
117- if path != "settings.failing_checks_block" {117+ all := path == "settings"
118+ if all || path == "settings.allowed_merge_modes" {
118 modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes())119 modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes())
119 if err != nil {120 if err != nil {
120 return params, err121 return params, err
121 }122 }
122 params.AllowedMergeModes = &modes123 params.AllowedMergeModes = &modes
123 }124 }
124- if path != "settings.allowed_merge_modes" {125+ if all || path == "settings.failing_checks_block" {
125 params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock())126 params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock())
126 }127 }
128+ if all || path == "settings.allow_auto_merge" {
129+ params.AllowAutoMerge = new(msg.GetSettings().GetAllowAutoMerge())
130+ }
127 default:131 default:
128 return params, fmt.Errorf("%w: unknown path %q in update_mask", domain.ErrInvalid, path)132 return params, fmt.Errorf("%w: unknown path %q in update_mask", domain.ErrInvalid, path)
129 }133 }
M internal/adapters/rpc/repo_test.go +25 -0
@@ -280,3 +280,28 @@ func TestRepoToPBTopics(t *testing.T) {
280280 assert.Equal(t, []string{"cli", "go"}, got.GetTopics())
281281 assert.Empty(t, repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustRepoName(t, "demo")}}).GetTopics())
282282 }
283+
284+func TestUpdateParamsAllowAutoMerge(t *testing.T) {
285+ t.Parallel()
286+ mask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} }
287+ settings := &pb.RepoSettings{AllowAutoMerge: true, FailingChecksBlock: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}}
288+
289+ params, err := updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.allow_auto_merge"), Settings: settings})
290+ require.NoError(t, err)
291+ require.NotNil(t, params.AllowAutoMerge)
292+ assert.True(t, *params.AllowAutoMerge)
293+ assert.Nil(t, params.FailingChecksBlock, "only the masked setting is set")
294+ assert.Nil(t, params.AllowedMergeModes)
295+
296+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings"), Settings: settings})
297+ require.NoError(t, err)
298+ require.NotNil(t, params.AllowAutoMerge, "settings covers allow_auto_merge")
299+ assert.True(t, *params.AllowAutoMerge)
300+ require.NotNil(t, params.FailingChecksBlock)
301+ require.NotNil(t, params.AllowedMergeModes)
302+
303+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.failing_checks_block"), Settings: settings})
304+ require.NoError(t, err)
305+ assert.Nil(t, params.AllowAutoMerge)
306+ require.NotNil(t, params.FailingChecksBlock)
307+}
@@ -280,3 +280,28 @@ func TestRepoToPBTopics(t *testing.T) {
280 assert.Equal(t, []string{"cli", "go"}, got.GetTopics())280 assert.Equal(t, []string{"cli", "go"}, got.GetTopics())
281 assert.Empty(t, repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustRepoName(t, "demo")}}).GetTopics())281 assert.Empty(t, repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustRepoName(t, "demo")}}).GetTopics())
282 }282 }
283+
284+func TestUpdateParamsAllowAutoMerge(t *testing.T) {
285+ t.Parallel()
286+ mask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} }
287+ settings := &pb.RepoSettings{AllowAutoMerge: true, FailingChecksBlock: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}}
288+
289+ params, err := updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.allow_auto_merge"), Settings: settings})
290+ require.NoError(t, err)
291+ require.NotNil(t, params.AllowAutoMerge)
292+ assert.True(t, *params.AllowAutoMerge)
293+ assert.Nil(t, params.FailingChecksBlock, "only the masked setting is set")
294+ assert.Nil(t, params.AllowedMergeModes)
295+
296+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings"), Settings: settings})
297+ require.NoError(t, err)
298+ require.NotNil(t, params.AllowAutoMerge, "settings covers allow_auto_merge")
299+ assert.True(t, *params.AllowAutoMerge)
300+ require.NotNil(t, params.FailingChecksBlock)
301+ require.NotNil(t, params.AllowedMergeModes)
302+
303+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.failing_checks_block"), Settings: settings})
304+ require.NoError(t, err)
305+ assert.Nil(t, params.AllowAutoMerge)
306+ require.NotNil(t, params.FailingChecksBlock)
307+}
M internal/adapters/web/fragments.go +5 -1
@@ -61,7 +61,7 @@ func liveEvent(we ports.WatchEvent) bool {
6161 }
6262 switch we.Event.(type) {
6363 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
64- domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
64+ domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestAutoMergeEnabled, domain.MergeRequestAutoMergeCancelled, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
6565 domain.RunUpdated, domain.JobUpdated,
6666 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved, domain.IssueReferenced:
6767 return true
@@ -189,6 +189,10 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
189189 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
190190 case domain.MergeRequestReady:
191191 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")
192+ case domain.MergeRequestAutoMergeEnabled:
193+ return l.mrNotice(ctx, ev.MergeRequest.Number, "auto-merge was enabled — reload")
194+ case domain.MergeRequestAutoMergeCancelled:
195+ return l.mrNotice(ctx, ev.MergeRequest.Number, "auto-merge was cancelled — reload")
192196 case domain.MergeRequestMergeability:
193197 if l.view != "mr" || strconv.FormatInt(ev.Number, 10) != l.n {
194198 return nil, nil
@@ -61,7 +61,7 @@ func liveEvent(we ports.WatchEvent) bool {
61 }61 }
62 switch we.Event.(type) {62 switch we.Event.(type) {
63 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,63 case domain.RepoCreated, domain.RepoUpdated, domain.RepoDeleted, domain.PushEvent,
64- domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,64+ domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.MergeRequestAutoMergeEnabled, domain.MergeRequestAutoMergeCancelled, domain.MergeRequestMergeability, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
65 domain.RunUpdated, domain.JobUpdated,65 domain.RunUpdated, domain.JobUpdated,
66 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved, domain.IssueReferenced:66 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved, domain.IssueReferenced:
67 return true67 return true
@@ -189,6 +189,10 @@ func (l *liveStream) repoPage(ctx context.Context, we ports.WatchEvent) ([]fragm
189 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")189 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request was closed — reload")
190 case domain.MergeRequestReady:190 case domain.MergeRequestReady:
191 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")191 return l.mrNotice(ctx, ev.MergeRequest.Number, "this merge request is ready for review — reload")
192+ case domain.MergeRequestAutoMergeEnabled:
193+ return l.mrNotice(ctx, ev.MergeRequest.Number, "auto-merge was enabled — reload")
194+ case domain.MergeRequestAutoMergeCancelled:
195+ return l.mrNotice(ctx, ev.MergeRequest.Number, "auto-merge was cancelled — reload")
192 case domain.MergeRequestMergeability:196 case domain.MergeRequestMergeability:
193 if l.view != "mr" || strconv.FormatInt(ev.Number, 10) != l.n {197 if l.view != "mr" || strconv.FormatInt(ev.Number, 10) != l.n {
194 return nil, nil198 return nil, nil
M internal/adapters/web/mr.go +26 -0
@@ -11,6 +11,7 @@ import (
1111 "net/url"
1212 "strconv"
1313 "strings"
14+ "time"
1415
1516 "github.com/a-h/templ"
1617
@@ -202,6 +203,18 @@ type MergeBox struct {
202203 Fingerprint string
203204 // Error is why the last merge was refused, shown in the box.
204205 Error string
206+ // AutoAction and CancelAutoAction post to the auto-merge routes.
207+ AutoAction, CancelAutoAction string
208+ // CanAutoMerge offers "Merge when ready": the repository allows
209+ // auto-merge and the server has CI to wait for.
210+ CanAutoMerge bool
211+ // AutoMerge is the pending auto-merge, nil when off.
212+ AutoMerge *AutoMergeView
213+}
214+
215+// AutoMergeView is a pending auto-merge, ready to show.
216+type AutoMergeView struct {
217+ Mode, EnabledBy, When, WhenAt, HeadShort string
205218 }
206219
207220 // ModeOption is a merge mode the repository allows. Disabled says that the
@@ -666,9 +679,20 @@ func (s *Server) mrBox(ctx context.Context, d *repoData, res ports.MergeRequestD
666679 if m.IsDraft() {
667680 box.ReadyAction = mrHref(d.name(), m.Number, "ready")
668681 }
682+ box.AutoMerge = autoMergeView(s.cfg.Now(), m.AutoMerge)
669683 return box
670684 }
671685
686+// autoMergeView is the pending auto-merge ready to show, nil when off.
687+func autoMergeView(now time.Time, am *domain.AutoMerge) *AutoMergeView {
688+ if am == nil {
689+ return nil
690+ }
691+ v := &AutoMergeView{Mode: am.Mode.String(), EnabledBy: am.EnablerLabel, HeadShort: short(am.HeadID.String())}
692+ v.When, v.WhenAt = whenOf(now, am.EnabledAt)
693+ return v
694+}
695+
672696 // mrFacts formats the description, metadata, branch links and checks.
673697 func (s *Server) mrFacts(ctx context.Context, r *http.Request, d *repoData, p *MRPage, res ports.MergeRequestDetail) error {
674698 m := res.MergeRequest
@@ -790,6 +814,8 @@ func (s *Server) mergeBox(d *repoData, n int64, source, head string, canMarkRead
790814 CanMarkRead: canMarkRead,
791815 CanDeleteBranch: source != d.repo.DefaultBranch,
792816 Mergeability: mg,
817+ AutoAction: mrHref(d.name(), n, "auto-merge"), CancelAutoAction: mrHref(d.name(), n, "auto-merge/cancel"),
818+ CanAutoMerge: d.repo.Settings.AllowAutoMerge && s.cfg.Runs != nil,
793819 }
794820 var lines []string
795821 cmd := ""
@@ -11,6 +11,7 @@ import (
11 "net/url"11 "net/url"
12 "strconv"12 "strconv"
13 "strings"13 "strings"
14+ "time"
14 15
15 "github.com/a-h/templ"16 "github.com/a-h/templ"
16 17
@@ -202,6 +203,18 @@ type MergeBox struct {
202 Fingerprint string203 Fingerprint string
203 // Error is why the last merge was refused, shown in the box.204 // Error is why the last merge was refused, shown in the box.
204 Error string205 Error string
206+ // AutoAction and CancelAutoAction post to the auto-merge routes.
207+ AutoAction, CancelAutoAction string
208+ // CanAutoMerge offers "Merge when ready": the repository allows
209+ // auto-merge and the server has CI to wait for.
210+ CanAutoMerge bool
211+ // AutoMerge is the pending auto-merge, nil when off.
212+ AutoMerge *AutoMergeView
213+}
214+
215+// AutoMergeView is a pending auto-merge, ready to show.
216+type AutoMergeView struct {
217+ Mode, EnabledBy, When, WhenAt, HeadShort string
205 }218 }
206 219
207 // ModeOption is a merge mode the repository allows. Disabled says that the220 // ModeOption is a merge mode the repository allows. Disabled says that the
@@ -666,9 +679,20 @@ func (s *Server) mrBox(ctx context.Context, d *repoData, res ports.MergeRequestD
666 if m.IsDraft() {679 if m.IsDraft() {
667 box.ReadyAction = mrHref(d.name(), m.Number, "ready")680 box.ReadyAction = mrHref(d.name(), m.Number, "ready")
668 }681 }
682+ box.AutoMerge = autoMergeView(s.cfg.Now(), m.AutoMerge)
669 return box683 return box
670 }684 }
671 685
686+// autoMergeView is the pending auto-merge ready to show, nil when off.
687+func autoMergeView(now time.Time, am *domain.AutoMerge) *AutoMergeView {
688+ if am == nil {
689+ return nil
690+ }
691+ v := &AutoMergeView{Mode: am.Mode.String(), EnabledBy: am.EnablerLabel, HeadShort: short(am.HeadID.String())}
692+ v.When, v.WhenAt = whenOf(now, am.EnabledAt)
693+ return v
694+}
695+
672 // mrFacts formats the description, metadata, branch links and checks.696 // mrFacts formats the description, metadata, branch links and checks.
673 func (s *Server) mrFacts(ctx context.Context, r *http.Request, d *repoData, p *MRPage, res ports.MergeRequestDetail) error {697 func (s *Server) mrFacts(ctx context.Context, r *http.Request, d *repoData, p *MRPage, res ports.MergeRequestDetail) error {
674 m := res.MergeRequest698 m := res.MergeRequest
@@ -790,6 +814,8 @@ func (s *Server) mergeBox(d *repoData, n int64, source, head string, canMarkRead
790 CanMarkRead: canMarkRead,814 CanMarkRead: canMarkRead,
791 CanDeleteBranch: source != d.repo.DefaultBranch,815 CanDeleteBranch: source != d.repo.DefaultBranch,
792 Mergeability: mg,816 Mergeability: mg,
817+ AutoAction: mrHref(d.name(), n, "auto-merge"), CancelAutoAction: mrHref(d.name(), n, "auto-merge/cancel"),
818+ CanAutoMerge: d.repo.Settings.AllowAutoMerge && s.cfg.Runs != nil,
793 }819 }
794 var lines []string820 var lines []string
795 cmd := ""821 cmd := ""
M internal/adapters/web/mr.templ +28 -0
@@ -508,6 +508,20 @@ templ mergeMenu(p MRPage) {
508508 </form>
509509 } else if !p.Box.HeadGone {
510510 @mrMergeabilitySlot(p.Box.Mergeability)
511+ if p.Box.AutoMerge != nil {
512+ <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.CancelAutoAction) }>
513+ <p class="dim" role="status">
514+ Auto-merge: { p.Box.AutoMerge.Mode } when ready, enabled by { p.Box.AutoMerge.EnabledBy }
515+ if p.Box.AutoMerge.When != "" {
516+ { " " }<time datetime={ p.Box.AutoMerge.WhenAt }>{ p.Box.AutoMerge.When }</time>
517+ }
518+ { " " }(head { p.Box.AutoMerge.HeadShort })
519+ </p>
520+ <div class="d-btns">
521+ <button type="submit" class="btn">Cancel auto-merge</button>
522+ </div>
523+ </form>
524+ }
511525 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>
512526 <input type="hidden" name="head_id" value={ p.Box.Head }/>
513527 <fieldset class="d-modes">
@@ -546,6 +560,20 @@ templ mergeMenu(p MRPage) {
546560 >Merge into { p.Target }</button>
547561 </div>
548562 </form>
563+ if p.Box.CanAutoMerge {
564+ <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.AutoAction) }>
565+ <input type="hidden" name="head_id" value={ p.Box.Head }/>
566+ <fieldset class="d-modes">
567+ <legend>Auto-merge mode</legend>
568+ for i, m := range p.Box.Modes {
569+ <label><input type="radio" name="mode" value={ m.Value } checked?={ i == 0 }/> { m.Label }</label>
570+ }
571+ </fieldset>
572+ <div class="d-btns">
573+ <button type="submit" class="btn" disabled?={ len(p.Box.Modes) == 0 } title="merge once the checks pass">Merge when ready</button>
574+ </div>
575+ </form>
576+ }
549577 }
550578 </div>
551579 </details>
@@ -508,6 +508,20 @@ templ mergeMenu(p MRPage) {
508 </form>508 </form>
509 } else if !p.Box.HeadGone {509 } else if !p.Box.HeadGone {
510 @mrMergeabilitySlot(p.Box.Mergeability)510 @mrMergeabilitySlot(p.Box.Mergeability)
511+ if p.Box.AutoMerge != nil {
512+ <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.CancelAutoAction) }>
513+ <p class="dim" role="status">
514+ Auto-merge: { p.Box.AutoMerge.Mode } when ready, enabled by { p.Box.AutoMerge.EnabledBy }
515+ if p.Box.AutoMerge.When != "" {
516+ { " " }<time datetime={ p.Box.AutoMerge.WhenAt }>{ p.Box.AutoMerge.When }</time>
517+ }
518+ { " " }(head { p.Box.AutoMerge.HeadShort })
519+ </p>
520+ <div class="d-btns">
521+ <button type="submit" class="btn">Cancel auto-merge</button>
522+ </div>
523+ </form>
524+ }
511 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>525 <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }>
512 <input type="hidden" name="head_id" value={ p.Box.Head }/>526 <input type="hidden" name="head_id" value={ p.Box.Head }/>
513 <fieldset class="d-modes">527 <fieldset class="d-modes">
@@ -546,6 +560,20 @@ templ mergeMenu(p MRPage) {
546 >Merge into { p.Target }</button>560 >Merge into { p.Target }</button>
547 </div>561 </div>
548 </form>562 </form>
563+ if p.Box.CanAutoMerge {
564+ <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.AutoAction) }>
565+ <input type="hidden" name="head_id" value={ p.Box.Head }/>
566+ <fieldset class="d-modes">
567+ <legend>Auto-merge mode</legend>
568+ for i, m := range p.Box.Modes {
569+ <label><input type="radio" name="mode" value={ m.Value } checked?={ i == 0 }/> { m.Label }</label>
570+ }
571+ </fieldset>
572+ <div class="d-btns">
573+ <button type="submit" class="btn" disabled?={ len(p.Box.Modes) == 0 } title="merge once the checks pass">Merge when ready</button>
574+ </div>
575+ </form>
576+ }
549 }577 }
550 </div>578 </div>
551 </details>579 </details>
M internal/adapters/web/mr_post.go +62 -0
@@ -391,6 +391,68 @@ func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string,
391391 return back, back, nil
392392 }
393393
394+// mrAutoMerge is POST /<repo>/merge-requests/<n>/auto-merge: the form of the
395+// merge (mode, head_id) asks to merge once the checks pass. The server records
396+// refuses to enable when the source branch is no longer at head_id.
397+func (s *Server) mrAutoMerge(ctx context.Context, r *http.Request) (string, string, error) {
398+ repo := r.PathValue("repo")
399+ n, err := number(r)
400+ if err != nil {
401+ return "", "", err
402+ }
403+ back := mrHref(repo, n)
404+ var mode domain.MergeMode
405+ switch r.PostFormValue("mode") {
406+ case "fast-forward":
407+ mode = domain.MergeModeFastForward
408+ case "squash":
409+ mode = domain.MergeModeSquash
410+ case "merge":
411+ mode = domain.MergeModeMerge
412+ default:
413+ err := formError("mode must be fast-forward, squash or merge")
414+ return "", back, &rerender{status: http.StatusBadRequest, msg: err.Error(), err: err}
415+ }
416+ name, err := domain.ParseRepoName(repo)
417+ if err != nil {
418+ return "", back, err
419+ }
420+ head, err := headID(r.PostFormValue("head_id"))
421+ if err != nil {
422+ return "", back, err
423+ }
424+ _, err = s.cfg.MergeRequests.EnableAutoMerge(ctx, principal(ctx), name, n, ports.EnableAutoMergeParams{Mode: mode, HeadID: head})
425+ switch {
426+ case errors.Is(err, domain.ErrPreconditionFailed), errors.Is(err, domain.ErrConflict):
427+ return "", back, &rerender{status: http.StatusConflict, msg: rule(err.Error(), "the merge request is not in a state that allows this."), err: err}
428+ case err != nil:
429+ return "", back, err
430+ }
431+ return back, back, nil
432+}
433+
434+// mrCancelAutoMerge is POST /<repo>/merge-requests/<n>/auto-merge/cancel.
435+func (s *Server) mrCancelAutoMerge(ctx context.Context, r *http.Request) (string, string, error) {
436+ repo := r.PathValue("repo")
437+ n, err := number(r)
438+ if err != nil {
439+ return "", "", err
440+ }
441+ back := mrHref(repo, n)
442+ name, err := domain.ParseRepoName(repo)
443+ if err != nil {
444+ return "", back, err
445+ }
446+ _, err = s.cfg.MergeRequests.CancelAutoMerge(ctx, principal(ctx), name, n)
447+ switch {
448+ case errors.Is(err, domain.ErrPreconditionFailed), errors.Is(err, domain.ErrConflict):
449+ return "", back, &rerender{status: http.StatusConflict, msg: rule(err.Error(), "the merge request is not in a state that allows this."), err: err}
450+ case err != nil:
451+ return "", back, err
452+ }
453+ return back, back, nil
454+}
455+
394456 // mrRead is POST /<repo>/merge-requests/<n>/read: the owner marks the head
395457 // they were shown as read. Reading a page never does it (see the top of mr.go).
396458 func (s *Server) mrRead(ctx context.Context, r *http.Request) (string, string, error) {
@@ -391,6 +391,68 @@ func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string,
391 return back, back, nil391 return back, back, nil
392 }392 }
393 393
394+// mrAutoMerge is POST /<repo>/merge-requests/<n>/auto-merge: the form of the
395+// merge (mode, head_id) asks to merge once the checks pass. The server records
396+// refuses to enable when the source branch is no longer at head_id.
397+func (s *Server) mrAutoMerge(ctx context.Context, r *http.Request) (string, string, error) {
398+ repo := r.PathValue("repo")
399+ n, err := number(r)
400+ if err != nil {
401+ return "", "", err
402+ }
403+ back := mrHref(repo, n)
404+ var mode domain.MergeMode
405+ switch r.PostFormValue("mode") {
406+ case "fast-forward":
407+ mode = domain.MergeModeFastForward
408+ case "squash":
409+ mode = domain.MergeModeSquash
410+ case "merge":
411+ mode = domain.MergeModeMerge
412+ default:
413+ err := formError("mode must be fast-forward, squash or merge")
414+ return "", back, &rerender{status: http.StatusBadRequest, msg: err.Error(), err: err}
415+ }
416+ name, err := domain.ParseRepoName(repo)
417+ if err != nil {
418+ return "", back, err
419+ }
420+ head, err := headID(r.PostFormValue("head_id"))
421+ if err != nil {
422+ return "", back, err
423+ }
424+ _, err = s.cfg.MergeRequests.EnableAutoMerge(ctx, principal(ctx), name, n, ports.EnableAutoMergeParams{Mode: mode, HeadID: head})
425+ switch {
426+ case errors.Is(err, domain.ErrPreconditionFailed), errors.Is(err, domain.ErrConflict):
427+ return "", back, &rerender{status: http.StatusConflict, msg: rule(err.Error(), "the merge request is not in a state that allows this."), err: err}
428+ case err != nil:
429+ return "", back, err
430+ }
431+ return back, back, nil
432+}
433+
434+// mrCancelAutoMerge is POST /<repo>/merge-requests/<n>/auto-merge/cancel.
435+func (s *Server) mrCancelAutoMerge(ctx context.Context, r *http.Request) (string, string, error) {
436+ repo := r.PathValue("repo")
437+ n, err := number(r)
438+ if err != nil {
439+ return "", "", err
440+ }
441+ back := mrHref(repo, n)
442+ name, err := domain.ParseRepoName(repo)
443+ if err != nil {
444+ return "", back, err
445+ }
446+ _, err = s.cfg.MergeRequests.CancelAutoMerge(ctx, principal(ctx), name, n)
447+ switch {
448+ case errors.Is(err, domain.ErrPreconditionFailed), errors.Is(err, domain.ErrConflict):
449+ return "", back, &rerender{status: http.StatusConflict, msg: rule(err.Error(), "the merge request is not in a state that allows this."), err: err}
450+ case err != nil:
451+ return "", back, err
452+ }
453+ return back, back, nil
454+}
455+
394 // mrRead is POST /<repo>/merge-requests/<n>/read: the owner marks the head456 // mrRead is POST /<repo>/merge-requests/<n>/read: the owner marks the head
395 // they were shown as read. Reading a page never does it (see the top of mr.go).457 // they were shown as read. Reading a page never does it (see the top of mr.go).
396 func (s *Server) mrRead(ctx context.Context, r *http.Request) (string, string, error) {458 func (s *Server) mrRead(ctx context.Context, r *http.Request) (string, string, error) {
M internal/adapters/web/mr_templ.go +554 -350
@@ -2571,185 +2571,389 @@ func mergeMenu(p MRPage) templ.Component {
25712571 if templ_7745c5c3_Err != nil {
25722572 return templ_7745c5c3_Err
25732573 }
2574- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, " <form class=\"d-acts\" method=\"post\" action=\"")
2574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, " ")
25752575 if templ_7745c5c3_Err != nil {
25762576 return templ_7745c5c3_Err
25772577 }
2578- var templ_7745c5c3_Var120 templ.SafeURL
2579- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2578+ if p.Box.AutoMerge != nil {
2579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "<form class=\"d-acts\" method=\"post\" action=\"")
2580+ if templ_7745c5c3_Err != nil {
2581+ return templ_7745c5c3_Err
2582+ }
2583+ var templ_7745c5c3_Var120 templ.SafeURL
2584+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CancelAutoAction))
2585+ if templ_7745c5c3_Err != nil {
2586+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 86}
2587+ }
2588+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2589+ if templ_7745c5c3_Err != nil {
2590+ return templ_7745c5c3_Err
2591+ }
2592+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "\"><p class=\"dim\" role=\"status\">Auto-merge: ")
2593+ if templ_7745c5c3_Err != nil {
2594+ return templ_7745c5c3_Err
2595+ }
2596+ var templ_7745c5c3_Var121 string
2597+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.Mode)
2598+ if templ_7745c5c3_Err != nil {
2599+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 514, Col: 41}
2600+ }
2601+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2602+ if templ_7745c5c3_Err != nil {
2603+ return templ_7745c5c3_Err
2604+ }
2605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, " when ready, enabled by ")
2606+ if templ_7745c5c3_Err != nil {
2607+ return templ_7745c5c3_Err
2608+ }
2609+ var templ_7745c5c3_Var122 string
2610+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.EnabledBy)
2611+ if templ_7745c5c3_Err != nil {
2612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 514, Col: 94}
2613+ }
2614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2615+ if templ_7745c5c3_Err != nil {
2616+ return templ_7745c5c3_Err
2617+ }
2618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, " ")
2619+ if templ_7745c5c3_Err != nil {
2620+ return templ_7745c5c3_Err
2621+ }
2622+ if p.Box.AutoMerge.When != "" {
2623+ var templ_7745c5c3_Var123 string
2624+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2625+ if templ_7745c5c3_Err != nil {
2626+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 13}
2627+ }
2628+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
2629+ if templ_7745c5c3_Err != nil {
2630+ return templ_7745c5c3_Err
2631+ }
2632+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<time datetime=\"")
2633+ if templ_7745c5c3_Err != nil {
2634+ return templ_7745c5c3_Err
2635+ }
2636+ var templ_7745c5c3_Var124 string
2637+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.AutoMerge.WhenAt)
2638+ if templ_7745c5c3_Err != nil {
2639+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 54}
2640+ }
2641+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
2642+ if templ_7745c5c3_Err != nil {
2643+ return templ_7745c5c3_Err
2644+ }
2645+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "\">")
2646+ if templ_7745c5c3_Err != nil {
2647+ return templ_7745c5c3_Err
2648+ }
2649+ var templ_7745c5c3_Var125 string
2650+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.When)
2651+ if templ_7745c5c3_Err != nil {
2652+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 79}
2653+ }
2654+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
2655+ if templ_7745c5c3_Err != nil {
2656+ return templ_7745c5c3_Err
2657+ }
2658+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "</time> ")
2659+ if templ_7745c5c3_Err != nil {
2660+ return templ_7745c5c3_Err
2661+ }
2662+ }
2663+ var templ_7745c5c3_Var126 string
2664+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2665+ if templ_7745c5c3_Err != nil {
2666+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 518, Col: 12}
2667+ }
2668+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2669+ if templ_7745c5c3_Err != nil {
2670+ return templ_7745c5c3_Err
2671+ }
2672+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "(head ")
2673+ if templ_7745c5c3_Err != nil {
2674+ return templ_7745c5c3_Err
2675+ }
2676+ var templ_7745c5c3_Var127 string
2677+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.HeadShort)
2678+ if templ_7745c5c3_Err != nil {
2679+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 518, Col: 47}
2680+ }
2681+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2682+ if templ_7745c5c3_Err != nil {
2683+ return templ_7745c5c3_Err
2684+ }
2685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, ")</p><div class=\"d-btns\"><button type=\"submit\" class=\"btn\">Cancel auto-merge</button></div></form>")
2686+ if templ_7745c5c3_Err != nil {
2687+ return templ_7745c5c3_Err
2688+ }
2689+ }
2690+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, " <form class=\"d-acts\" method=\"post\" action=\"")
2691+ if templ_7745c5c3_Err != nil {
2692+ return templ_7745c5c3_Err
2693+ }
2694+ var templ_7745c5c3_Var128 templ.SafeURL
2695+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
25802696 if templ_7745c5c3_Err != nil {
2581- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 80}
2697+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 80}
25822698 }
2583- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2699+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
25842700 if templ_7745c5c3_Err != nil {
25852701 return templ_7745c5c3_Err
25862702 }
2587- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
25882704 if templ_7745c5c3_Err != nil {
25892705 return templ_7745c5c3_Err
25902706 }
2591- var templ_7745c5c3_Var121 string
2592- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2707+ var templ_7745c5c3_Var129 string
2708+ templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
25932709 if templ_7745c5c3_Err != nil {
2594- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 59}
2710+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 526, Col: 59}
25952711 }
2596- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var121)
2712+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)
25972713 if templ_7745c5c3_Err != nil {
25982714 return templ_7745c5c3_Err
25992715 }
2600- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
2716+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
26012717 if templ_7745c5c3_Err != nil {
26022718 return templ_7745c5c3_Err
26032719 }
26042720 for i, m := range p.Box.Modes {
2605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "<label><input type=\"radio\" name=\"mode\" value=\"")
2721+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "<label><input type=\"radio\" name=\"mode\" value=\"")
26062722 if templ_7745c5c3_Err != nil {
26072723 return templ_7745c5c3_Err
26082724 }
2609- var templ_7745c5c3_Var122 string
2610- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2725+ var templ_7745c5c3_Var130 string
2726+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
26112727 if templ_7745c5c3_Err != nil {
2612- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 520, Col: 24}
2728+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 534, Col: 24}
26132729 }
2614- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122)
2730+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
26152731 if templ_7745c5c3_Err != nil {
26162732 return templ_7745c5c3_Err
26172733 }
2618- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "\"")
2734+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "\"")
26192735 if templ_7745c5c3_Err != nil {
26202736 return templ_7745c5c3_Err
26212737 }
26222738 if i == p.Box.Checked() {
2623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, " checked")
2739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, " checked")
26242740 if templ_7745c5c3_Err != nil {
26252741 return templ_7745c5c3_Err
26262742 }
26272743 }
26282744 if m.Disabled {
2629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " disabled")
2745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, " disabled")
26302746 if templ_7745c5c3_Err != nil {
26312747 return templ_7745c5c3_Err
26322748 }
26332749 }
2634- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, " title=\"")
2750+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, " title=\"")
26352751 if templ_7745c5c3_Err != nil {
26362752 return templ_7745c5c3_Err
26372753 }
2638- var templ_7745c5c3_Var123 string
2639- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)
2754+ var templ_7745c5c3_Var131 string
2755+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)
26402756 if templ_7745c5c3_Err != nil {
2641- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 523, Col: 25}
2757+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 537, Col: 25}
26422758 }
2643- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)
2759+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)
26442760 if templ_7745c5c3_Err != nil {
26452761 return templ_7745c5c3_Err
26462762 }
2647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "\" :disabled=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffOk !== true : q('[data-mergeability]').data.mergeOk !== true\" :title=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffReason : q('[data-mergeability]').data.mergeReason\"> ")
2763+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "\" :disabled=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffOk !== true : q('[data-mergeability]').data.mergeOk !== true\" :title=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffReason : q('[data-mergeability]').data.mergeReason\"> ")
26482764 if templ_7745c5c3_Err != nil {
26492765 return templ_7745c5c3_Err
26502766 }
2651- var templ_7745c5c3_Var124 string
2652- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2767+ var templ_7745c5c3_Var132 string
2768+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
26532769 if templ_7745c5c3_Err != nil {
2654- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 527, Col: 17}
2770+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 541, Col: 17}
26552771 }
2656- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))
2772+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
26572773 if templ_7745c5c3_Err != nil {
26582774 return templ_7745c5c3_Err
26592775 }
2660- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, " ")
2776+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, " ")
26612777 if templ_7745c5c3_Err != nil {
26622778 return templ_7745c5c3_Err
26632779 }
26642780 if i < 9 {
2665- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "<kbd aria-hidden=\"true\">")
2781+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "<kbd aria-hidden=\"true\">")
26662782 if templ_7745c5c3_Err != nil {
26672783 return templ_7745c5c3_Err
26682784 }
2669- var templ_7745c5c3_Var125 string
2670- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
2785+ var templ_7745c5c3_Var133 string
2786+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
26712787 if templ_7745c5c3_Err != nil {
2672- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 46}
2788+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 46}
26732789 }
2674- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
2790+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
26752791 if templ_7745c5c3_Err != nil {
26762792 return templ_7745c5c3_Err
26772793 }
2678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</kbd>")
2794+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</kbd>")
26792795 if templ_7745c5c3_Err != nil {
26802796 return templ_7745c5c3_Err
26812797 }
26822798 }
2683- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</label>")
2799+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</label>")
26842800 if templ_7745c5c3_Err != nil {
26852801 return templ_7745c5c3_Err
26862802 }
26872803 }
2688- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "</fieldset>")
2804+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "</fieldset>")
26892805 if templ_7745c5c3_Err != nil {
26902806 return templ_7745c5c3_Err
26912807 }
26922808 if p.Box.CanDeleteBranch {
2693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2809+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
26942810 if templ_7745c5c3_Err != nil {
26952811 return templ_7745c5c3_Err
26962812 }
2697- var templ_7745c5c3_Var126 string
2698- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2813+ var templ_7745c5c3_Var134 string
2814+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
26992815 if templ_7745c5c3_Err != nil {
2700- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 535, Col: 102}
2816+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 549, Col: 102}
27012817 }
2702- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2818+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
27032819 if templ_7745c5c3_Err != nil {
27042820 return templ_7745c5c3_Err
27052821 }
2706- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2822+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
27072823 if templ_7745c5c3_Err != nil {
27082824 return templ_7745c5c3_Err
27092825 }
27102826 }
2711- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")
2827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")
27122828 if templ_7745c5c3_Err != nil {
27132829 return templ_7745c5c3_Err
27142830 }
27152831 if !p.Box.CanMerge() {
2716- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, " disabled")
2832+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, " disabled")
27172833 if templ_7745c5c3_Err != nil {
27182834 return templ_7745c5c3_Err
27192835 }
27202836 }
2721- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, " title=\"")
2837+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, " title=\"")
27222838 if templ_7745c5c3_Err != nil {
27232839 return templ_7745c5c3_Err
27242840 }
2725- var templ_7745c5c3_Var127 string
2726- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
2841+ var templ_7745c5c3_Var135 string
2842+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
27272843 if templ_7745c5c3_Err != nil {
2728- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 33}
2844+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 557, Col: 33}
27292845 }
2730- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127)
2846+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
27312847 if templ_7745c5c3_Err != nil {
27322848 return templ_7745c5c3_Err
27332849 }
2734- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "\" :disabled=\"!this.form.querySelector('input[name=mode]:checked:not(:disabled)')\" :title=\"(this.form.querySelector('input[name=mode]:checked') || {}).title || ''\">Merge into ")
2850+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "\" :disabled=\"!this.form.querySelector('input[name=mode]:checked:not(:disabled)')\" :title=\"(this.form.querySelector('input[name=mode]:checked') || {}).title || ''\">Merge into ")
27352851 if templ_7745c5c3_Err != nil {
27362852 return templ_7745c5c3_Err
27372853 }
2738- var templ_7745c5c3_Var128 string
2739- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2854+ var templ_7745c5c3_Var136 string
2855+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
27402856 if templ_7745c5c3_Err != nil {
2741- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 28}
2857+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 28}
27422858 }
2743- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2859+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
27442860 if templ_7745c5c3_Err != nil {
27452861 return templ_7745c5c3_Err
27462862 }
2747- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</button></div></form>")
2863+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "</button></div></form>")
27482864 if templ_7745c5c3_Err != nil {
27492865 return templ_7745c5c3_Err
27502866 }
2867+ if p.Box.CanAutoMerge {
2868+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "<form class=\"d-acts\" method=\"post\" action=\"")
2869+ if templ_7745c5c3_Err != nil {
2870+ return templ_7745c5c3_Err
2871+ }
2872+ var templ_7745c5c3_Var137 templ.SafeURL
2873+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.AutoAction))
2874+ if templ_7745c5c3_Err != nil {
2875+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 80}
2876+ }
2877+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
2878+ if templ_7745c5c3_Err != nil {
2879+ return templ_7745c5c3_Err
2880+ }
2881+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2882+ if templ_7745c5c3_Err != nil {
2883+ return templ_7745c5c3_Err
2884+ }
2885+ var templ_7745c5c3_Var138 string
2886+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2887+ if templ_7745c5c3_Err != nil {
2888+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 60}
2889+ }
2890+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2891+ if templ_7745c5c3_Err != nil {
2892+ return templ_7745c5c3_Err
2893+ }
2894+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "\"><fieldset class=\"d-modes\"><legend>Auto-merge mode</legend> ")
2895+ if templ_7745c5c3_Err != nil {
2896+ return templ_7745c5c3_Err
2897+ }
2898+ for i, m := range p.Box.Modes {
2899+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "<label><input type=\"radio\" name=\"mode\" value=\"")
2900+ if templ_7745c5c3_Err != nil {
2901+ return templ_7745c5c3_Err
2902+ }
2903+ var templ_7745c5c3_Var139 string
2904+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2905+ if templ_7745c5c3_Err != nil {
2906+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 62}
2907+ }
2908+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var139)
2909+ if templ_7745c5c3_Err != nil {
2910+ return templ_7745c5c3_Err
2911+ }
2912+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "\"")
2913+ if templ_7745c5c3_Err != nil {
2914+ return templ_7745c5c3_Err
2915+ }
2916+ if i == 0 {
2917+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, " checked")
2918+ if templ_7745c5c3_Err != nil {
2919+ return templ_7745c5c3_Err
2920+ }
2921+ }
2922+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "> ")
2923+ if templ_7745c5c3_Err != nil {
2924+ return templ_7745c5c3_Err
2925+ }
2926+ var templ_7745c5c3_Var140 string
2927+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2928+ if templ_7745c5c3_Err != nil {
2929+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 96}
2930+ }
2931+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2932+ if templ_7745c5c3_Err != nil {
2933+ return templ_7745c5c3_Err
2934+ }
2935+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</label>")
2936+ if templ_7745c5c3_Err != nil {
2937+ return templ_7745c5c3_Err
2938+ }
2939+ }
2940+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</fieldset><div class=\"d-btns\"><button type=\"submit\" class=\"btn\"")
2941+ if templ_7745c5c3_Err != nil {
2942+ return templ_7745c5c3_Err
2943+ }
2944+ if len(p.Box.Modes) == 0 {
2945+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, " disabled")
2946+ if templ_7745c5c3_Err != nil {
2947+ return templ_7745c5c3_Err
2948+ }
2949+ }
2950+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, " title=\"merge once the checks pass\">Merge when ready</button></div></form>")
2951+ if templ_7745c5c3_Err != nil {
2952+ return templ_7745c5c3_Err
2953+ }
2954+ }
27512955 }
2752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</div></details>")
2956+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</div></details>")
27532957 if templ_7745c5c3_Err != nil {
27542958 return templ_7745c5c3_Err
27552959 }
@@ -2775,182 +2979,182 @@ func MRMergeability(v MergeabilityView) templ.Component {
27752979 }()
27762980 }
27772981 ctx = templ.InitializeContext(ctx)
2778- templ_7745c5c3_Var129 := templ.GetChildren(ctx)
2779- if templ_7745c5c3_Var129 == nil {
2780- templ_7745c5c3_Var129 = templ.NopComponent
2982+ templ_7745c5c3_Var141 := templ.GetChildren(ctx)
2983+ if templ_7745c5c3_Var141 == nil {
2984+ templ_7745c5c3_Var141 = templ.NopComponent
27812985 }
27822986 ctx = templ.ClearChildren(ctx)
2783- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "<span data-mergeability data-state=\"")
2987+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "<span data-mergeability data-state=\"")
27842988 if templ_7745c5c3_Err != nil {
27852989 return templ_7745c5c3_Err
27862990 }
2787- var templ_7745c5c3_Var130 string
2788- templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2991+ var templ_7745c5c3_Var142 string
2992+ templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
27892993 if templ_7745c5c3_Err != nil {
2790- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 559, Col: 22}
2994+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 587, Col: 22}
27912995 }
2792- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
2996+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
27932997 if templ_7745c5c3_Err != nil {
27942998 return templ_7745c5c3_Err
27952999 }
2796- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "\" data-ff-ok=\"")
3000+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "\" data-ff-ok=\"")
27973001 if templ_7745c5c3_Err != nil {
27983002 return templ_7745c5c3_Err
27993003 }
2800- var templ_7745c5c3_Var131 string
2801- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
3004+ var templ_7745c5c3_Var143 string
3005+ templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
28023006 if templ_7745c5c3_Err != nil {
2803- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 41}
3007+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 588, Col: 41}
28043008 }
2805- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)
3009+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
28063010 if templ_7745c5c3_Err != nil {
28073011 return templ_7745c5c3_Err
28083012 }
2809- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "\" data-merge-ok=\"")
3013+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "\" data-merge-ok=\"")
28103014 if templ_7745c5c3_Err != nil {
28113015 return templ_7745c5c3_Err
28123016 }
2813- var templ_7745c5c3_Var132 string
2814- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
3017+ var templ_7745c5c3_Var144 string
3018+ templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
28153019 if templ_7745c5c3_Err != nil {
2816- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 561, Col: 47}
3020+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 47}
28173021 }
2818- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var132)
3022+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
28193023 if templ_7745c5c3_Err != nil {
28203024 return templ_7745c5c3_Err
28213025 }
2822- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\" data-ff-reason=\"")
3026+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "\" data-ff-reason=\"")
28233027 if templ_7745c5c3_Err != nil {
28243028 return templ_7745c5c3_Err
28253029 }
2826- var templ_7745c5c3_Var133 string
2827- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
3030+ var templ_7745c5c3_Var145 string
3031+ templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
28283032 if templ_7745c5c3_Err != nil {
2829- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 562, Col: 29}
3033+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 590, Col: 29}
28303034 }
2831- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var133)
3035+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var145)
28323036 if templ_7745c5c3_Err != nil {
28333037 return templ_7745c5c3_Err
28343038 }
2835- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\" data-merge-reason=\"")
3039+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "\" data-merge-reason=\"")
28363040 if templ_7745c5c3_Err != nil {
28373041 return templ_7745c5c3_Err
28383042 }
2839- var templ_7745c5c3_Var134 string
2840- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
3043+ var templ_7745c5c3_Var146 string
3044+ templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
28413045 if templ_7745c5c3_Err != nil {
2842- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 35}
3046+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 591, Col: 35}
28433047 }
2844- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var134)
3048+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var146)
28453049 if templ_7745c5c3_Err != nil {
28463050 return templ_7745c5c3_Err
28473051 }
2848- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\"><span class=\"badge\" data-state=\"")
3052+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "\"><span class=\"badge\" data-state=\"")
28493053 if templ_7745c5c3_Err != nil {
28503054 return templ_7745c5c3_Err
28513055 }
2852- var templ_7745c5c3_Var135 string
2853- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
3056+ var templ_7745c5c3_Var147 string
3057+ templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
28543058 if templ_7745c5c3_Err != nil {
2855- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 42}
3059+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 42}
28563060 }
2857- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
3061+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)
28583062 if templ_7745c5c3_Err != nil {
28593063 return templ_7745c5c3_Err
28603064 }
2861- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\">")
3065+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "\">")
28623066 if templ_7745c5c3_Err != nil {
28633067 return templ_7745c5c3_Err
28643068 }
2865- var templ_7745c5c3_Var136 string
2866- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
3069+ var templ_7745c5c3_Var148 string
3070+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
28673071 if templ_7745c5c3_Err != nil {
2868- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 54}
3072+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 54}
28693073 }
2870- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
3074+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
28713075 if templ_7745c5c3_Err != nil {
28723076 return templ_7745c5c3_Err
28733077 }
2874- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "</span> ")
3078+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, "</span> ")
28753079 if templ_7745c5c3_Err != nil {
28763080 return templ_7745c5c3_Err
28773081 }
28783082 if v.Detail != "" {
2879- var templ_7745c5c3_Var137 string
2880- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
3083+ var templ_7745c5c3_Var149 string
3084+ templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
28813085 if templ_7745c5c3_Err != nil {
2882- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 567, Col: 8}
3086+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 8}
28833087 }
2884- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
3088+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))
28853089 if templ_7745c5c3_Err != nil {
28863090 return templ_7745c5c3_Err
28873091 }
2888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<span class=\"dim\">")
3092+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<span class=\"dim\">")
28893093 if templ_7745c5c3_Err != nil {
28903094 return templ_7745c5c3_Err
28913095 }
2892- var templ_7745c5c3_Var138 string
2893- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
3096+ var templ_7745c5c3_Var150 string
3097+ templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
28943098 if templ_7745c5c3_Err != nil {
2895- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 567, Col: 38}
3099+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 38}
28963100 }
2897- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))
3101+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))
28983102 if templ_7745c5c3_Err != nil {
28993103 return templ_7745c5c3_Err
29003104 }
2901- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</span>")
3105+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "</span>")
29023106 if templ_7745c5c3_Err != nil {
29033107 return templ_7745c5c3_Err
29043108 }
29053109 }
2906- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span> ")
3110+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "</span> ")
29073111 if templ_7745c5c3_Err != nil {
29083112 return templ_7745c5c3_Err
29093113 }
29103114 if len(v.Paths) > 0 {
2911- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")
3115+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")
29123116 if templ_7745c5c3_Err != nil {
29133117 return templ_7745c5c3_Err
29143118 }
29153119 for _, path := range v.Paths {
2916- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "<li><code>")
3120+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "<li><code>")
29173121 if templ_7745c5c3_Err != nil {
29183122 return templ_7745c5c3_Err
29193123 }
2920- var templ_7745c5c3_Var139 string
2921- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(path)
3124+ var templ_7745c5c3_Var151 string
3125+ templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(path)
29223126 if templ_7745c5c3_Err != nil {
2923- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 573, Col: 20}
3127+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 20}
29243128 }
2925- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))
3129+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
29263130 if templ_7745c5c3_Err != nil {
29273131 return templ_7745c5c3_Err
29283132 }
2929- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</code></li>")
3133+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "</code></li>")
29303134 if templ_7745c5c3_Err != nil {
29313135 return templ_7745c5c3_Err
29323136 }
29333137 }
2934- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</ul>")
3138+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</ul>")
29353139 if templ_7745c5c3_Err != nil {
29363140 return templ_7745c5c3_Err
29373141 }
29383142 }
29393143 if v.PathsNote != "" {
2940- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<p class=\"dim\">")
3144+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "<p class=\"dim\">")
29413145 if templ_7745c5c3_Err != nil {
29423146 return templ_7745c5c3_Err
29433147 }
2944- var templ_7745c5c3_Var140 string
2945- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
3148+ var templ_7745c5c3_Var152 string
3149+ templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
29463150 if templ_7745c5c3_Err != nil {
2947- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 30}
3151+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 606, Col: 30}
29483152 }
2949- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
3153+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
29503154 if templ_7745c5c3_Err != nil {
29513155 return templ_7745c5c3_Err
29523156 }
2953- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</p>")
3157+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "</p>")
29543158 if templ_7745c5c3_Err != nil {
29553159 return templ_7745c5c3_Err
29563160 }
@@ -2977,12 +3181,12 @@ func mrMergeabilitySlot(v MergeabilityView) templ.Component {
29773181 }()
29783182 }
29793183 ctx = templ.InitializeContext(ctx)
2980- templ_7745c5c3_Var141 := templ.GetChildren(ctx)
2981- if templ_7745c5c3_Var141 == nil {
2982- templ_7745c5c3_Var141 = templ.NopComponent
3184+ templ_7745c5c3_Var153 := templ.GetChildren(ctx)
3185+ if templ_7745c5c3_Var153 == nil {
3186+ templ_7745c5c3_Var153 = templ.NopComponent
29833187 }
29843188 ctx = templ.ClearChildren(ctx)
2985- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "<div id=\"mr-mergeability\" class=\"live-slot\" role=\"status\" data-slot>")
3189+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "<div id=\"mr-mergeability\" class=\"live-slot\" role=\"status\" data-slot>")
29863190 if templ_7745c5c3_Err != nil {
29873191 return templ_7745c5c3_Err
29883192 }
@@ -2990,7 +3194,7 @@ func mrMergeabilitySlot(v MergeabilityView) templ.Component {
29903194 if templ_7745c5c3_Err != nil {
29913195 return templ_7745c5c3_Err
29923196 }
2993- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "</div>")
3197+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "</div>")
29943198 if templ_7745c5c3_Err != nil {
29953199 return templ_7745c5c3_Err
29963200 }
@@ -3015,9 +3219,9 @@ func MRMergeabilityFragment(v MergeabilityView) templ.Component {
30153219 }()
30163220 }
30173221 ctx = templ.InitializeContext(ctx)
3018- templ_7745c5c3_Var142 := templ.GetChildren(ctx)
3019- if templ_7745c5c3_Var142 == nil {
3020- templ_7745c5c3_Var142 = templ.NopComponent
3222+ templ_7745c5c3_Var154 := templ.GetChildren(ctx)
3223+ if templ_7745c5c3_Var154 == nil {
3224+ templ_7745c5c3_Var154 = templ.NopComponent
30213225 }
30223226 ctx = templ.ClearChildren(ctx)
30233227 templ_7745c5c3_Err = mrMergeabilitySlot(v).Render(ctx, templ_7745c5c3_Buffer)
@@ -3046,128 +3250,128 @@ func moreMenu(p MRPage) templ.Component {
30463250 }()
30473251 }
30483252 ctx = templ.InitializeContext(ctx)
3049- templ_7745c5c3_Var143 := templ.GetChildren(ctx)
3050- if templ_7745c5c3_Var143 == nil {
3051- templ_7745c5c3_Var143 = templ.NopComponent
3253+ templ_7745c5c3_Var155 := templ.GetChildren(ctx)
3254+ if templ_7745c5c3_Var155 == nil {
3255+ templ_7745c5c3_Var155 = templ.NopComponent
30523256 }
30533257 ctx = templ.ClearChildren(ctx)
3054- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "<details class=\"menu omenu\"><summary class=\"btn icon\" aria-label=\"More actions\" title=\"more actions\">⋯</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"More actions\"><section class=\"merge-settings\"><h2>Merge settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
3258+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "<details class=\"menu omenu\"><summary class=\"btn icon\" aria-label=\"More actions\" title=\"more actions\">⋯</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"More actions\"><section class=\"merge-settings\"><h2>Merge settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
30553259 if templ_7745c5c3_Err != nil {
30563260 return templ_7745c5c3_Err
30573261 }
3058- var templ_7745c5c3_Var144 string
3059- templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
3262+ var templ_7745c5c3_Var156 string
3263+ templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
30603264 if templ_7745c5c3_Err != nil {
3061- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 38}
3265+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 633, Col: 38}
30623266 }
3063- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var144))
3267+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
30643268 if templ_7745c5c3_Err != nil {
30653269 return templ_7745c5c3_Err
30663270 }
3067- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</dd><dt>checks</dt><dd>")
3271+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "</dd><dt>checks</dt><dd>")
30683272 if templ_7745c5c3_Err != nil {
30693273 return templ_7745c5c3_Err
30703274 }
30713275 if p.ChecksBlock {
3072- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "failing ones block the merge")
3276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "failing ones block the merge")
30733277 if templ_7745c5c3_Err != nil {
30743278 return templ_7745c5c3_Err
30753279 }
30763280 } else {
3077- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "do not block the merge")
3281+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "do not block the merge")
30783282 if templ_7745c5c3_Err != nil {
30793283 return templ_7745c5c3_Err
30803284 }
30813285 }
3082- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</dd></dl></section>")
3286+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "</dd></dl></section>")
30833287 if templ_7745c5c3_Err != nil {
30843288 return templ_7745c5c3_Err
30853289 }
30863290 if p.Box != nil {
30873291 if p.Box.Signed {
3088- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "<p class=\"d-note\" id=\"mr-signing\">squash and merge commits are signed with ermite's own key (generated on first start). ")
3292+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "<p class=\"d-note\" id=\"mr-signing\">squash and merge commits are signed with ermite's own key (generated on first start). ")
30893293 if templ_7745c5c3_Err != nil {
30903294 return templ_7745c5c3_Err
30913295 }
30923296 if p.Box.Fingerprint != "" {
3093- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "fingerprint <code>")
3297+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "fingerprint <code>")
30943298 if templ_7745c5c3_Err != nil {
30953299 return templ_7745c5c3_Err
30963300 }
3097- var templ_7745c5c3_Var145 string
3098- templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
3301+ var templ_7745c5c3_Var157 string
3302+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
30993303 if templ_7745c5c3_Err != nil {
3100- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 621, Col: 44}
3304+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 44}
31013305 }
3102- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))
3306+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
31033307 if templ_7745c5c3_Err != nil {
31043308 return templ_7745c5c3_Err
31053309 }
3106- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "</code> ")
3310+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, "</code> ")
31073311 if templ_7745c5c3_Err != nil {
31083312 return templ_7745c5c3_Err
31093313 }
31103314 }
3111- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "<code>ermite signing-key show</code> prints it.</p>")
3315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "<code>ermite signing-key show</code> prints it.</p>")
31123316 if templ_7745c5c3_Err != nil {
31133317 return templ_7745c5c3_Err
31143318 }
31153319 }
3116- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, " ")
3320+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 311, " ")
31173321 if templ_7745c5c3_Err != nil {
31183322 return templ_7745c5c3_Err
31193323 }
31203324 if p.Box.CanMarkRead {
3121- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<form method=\"post\" action=\"")
3325+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 312, "<form method=\"post\" action=\"")
31223326 if templ_7745c5c3_Err != nil {
31233327 return templ_7745c5c3_Err
31243328 }
3125- var templ_7745c5c3_Var146 templ.SafeURL
3126- templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
3329+ var templ_7745c5c3_Var158 templ.SafeURL
3330+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
31273331 if templ_7745c5c3_Err != nil {
3128- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 627, Col: 65}
3332+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 655, Col: 65}
31293333 }
3130- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))
3334+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
31313335 if templ_7745c5c3_Err != nil {
31323336 return templ_7745c5c3_Err
31333337 }
3134- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
3338+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
31353339 if templ_7745c5c3_Err != nil {
31363340 return templ_7745c5c3_Err
31373341 }
3138- var templ_7745c5c3_Var147 string
3139- templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
3342+ var templ_7745c5c3_Var159 string
3343+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
31403344 if templ_7745c5c3_Err != nil {
3141- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 628, Col: 60}
3345+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 60}
31423346 }
3143- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)
3347+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var159)
31443348 if templ_7745c5c3_Err != nil {
31453349 return templ_7745c5c3_Err
31463350 }
3147- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\" aria-keyshortcuts=\"Shift+R\" data-read>Mark read <kbd aria-hidden=\"true\">R</kbd></button></form>")
3351+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "\"> <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>")
31483352 if templ_7745c5c3_Err != nil {
31493353 return templ_7745c5c3_Err
31503354 }
31513355 }
3152- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, " <form method=\"post\" action=\"")
3356+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, " <form method=\"post\" action=\"")
31533357 if templ_7745c5c3_Err != nil {
31543358 return templ_7745c5c3_Err
31553359 }
3156- var templ_7745c5c3_Var148 templ.SafeURL
3157- templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
3360+ var templ_7745c5c3_Var160 templ.SafeURL
3361+ templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
31583362 if templ_7745c5c3_Err != nil {
3159- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 632, Col: 65}
3363+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 660, Col: 65}
31603364 }
3161- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
3365+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
31623366 if templ_7745c5c3_Err != nil {
31633367 return templ_7745c5c3_Err
31643368 }
3165- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
3369+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
31663370 if templ_7745c5c3_Err != nil {
31673371 return templ_7745c5c3_Err
31683372 }
31693373 if !p.Box.HeadGone && p.Box.ReadyAction == "" {
3170- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
3374+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
31713375 if templ_7745c5c3_Err != nil {
31723376 return templ_7745c5c3_Err
31733377 }
@@ -3175,13 +3379,13 @@ func moreMenu(p MRPage) templ.Component {
31753379 if templ_7745c5c3_Err != nil {
31763380 return templ_7745c5c3_Err
31773381 }
3178- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</details>")
3382+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, "</details>")
31793383 if templ_7745c5c3_Err != nil {
31803384 return templ_7745c5c3_Err
31813385 }
31823386 }
31833387 }
3184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</div></details>")
3388+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 319, "</div></details>")
31853389 if templ_7745c5c3_Err != nil {
31863390 return templ_7745c5c3_Err
31873391 }
@@ -3205,12 +3409,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
32053409 }()
32063410 }
32073411 ctx = templ.InitializeContext(ctx)
3208- templ_7745c5c3_Var149 := templ.GetChildren(ctx)
3209- if templ_7745c5c3_Var149 == nil {
3210- templ_7745c5c3_Var149 = templ.NopComponent
3412+ templ_7745c5c3_Var161 := templ.GetChildren(ctx)
3413+ if templ_7745c5c3_Var161 == nil {
3414+ templ_7745c5c3_Var161 = templ.NopComponent
32113415 }
32123416 ctx = templ.ClearChildren(ctx)
3213- templ_7745c5c3_Var150 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3417+ templ_7745c5c3_Var162 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
32143418 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
32153419 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
32163420 if !templ_7745c5c3_IsBuffer {
@@ -3222,263 +3426,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
32223426 }()
32233427 }
32243428 ctx = templ.InitializeContext(ctx)
3225- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "<header class=\"show mr-h\"><h1>")
3429+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, "<header class=\"show mr-h\"><h1>")
32263430 if templ_7745c5c3_Err != nil {
32273431 return templ_7745c5c3_Err
32283432 }
3229- var templ_7745c5c3_Var151 string
3230- templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3433+ var templ_7745c5c3_Var163 string
3434+ templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
32313435 if templ_7745c5c3_Err != nil {
3232- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 17}
3436+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 17}
32333437 }
3234- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
3438+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var163))
32353439 if templ_7745c5c3_Err != nil {
32363440 return templ_7745c5c3_Err
32373441 }
3238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, " → ")
3442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " → ")
32393443 if templ_7745c5c3_Err != nil {
32403444 return templ_7745c5c3_Err
32413445 }
3242- var templ_7745c5c3_Var152 string
3243- templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3446+ var templ_7745c5c3_Var164 string
3447+ templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
32443448 if templ_7745c5c3_Err != nil {
3245- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 34}
3449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 34}
32463450 }
3247- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
3451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var164))
32483452 if templ_7745c5c3_Err != nil {
32493453 return templ_7745c5c3_Err
32503454 }
3251- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
3455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
32523456 if templ_7745c5c3_Err != nil {
32533457 return templ_7745c5c3_Err
32543458 }
3255- var templ_7745c5c3_Var153 templ.SafeURL
3256- templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
3459+ var templ_7745c5c3_Var165 templ.SafeURL
3460+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
32573461 if templ_7745c5c3_Err != nil {
3258- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 652, Col: 45}
3462+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 45}
32593463 }
3260- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))
3464+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
32613465 if templ_7745c5c3_Err != nil {
32623466 return templ_7745c5c3_Err
32633467 }
3264- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "\">")
3468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, "\">")
32653469 if templ_7745c5c3_Err != nil {
32663470 return templ_7745c5c3_Err
32673471 }
3268- var templ_7745c5c3_Var154 string
3269- templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3472+ var templ_7745c5c3_Var166 string
3473+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
32703474 if templ_7745c5c3_Err != nil {
3271- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 652, Col: 58}
3475+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 58}
32723476 }
3273- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))
3477+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
32743478 if templ_7745c5c3_Err != nil {
32753479 return templ_7745c5c3_Err
32763480 }
3277- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "</a></dd><dt>target</dt><dd><a href=\"")
3481+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, "</a></dd><dt>target</dt><dd><a href=\"")
32783482 if templ_7745c5c3_Err != nil {
32793483 return templ_7745c5c3_Err
32803484 }
3281- var templ_7745c5c3_Var155 templ.SafeURL
3282- templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
3485+ var templ_7745c5c3_Var167 templ.SafeURL
3486+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
32833487 if templ_7745c5c3_Err != nil {
3284- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 654, Col: 45}
3488+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 45}
32853489 }
3286- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))
3490+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
32873491 if templ_7745c5c3_Err != nil {
32883492 return templ_7745c5c3_Err
32893493 }
3290- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "\">")
3494+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, "\">")
32913495 if templ_7745c5c3_Err != nil {
32923496 return templ_7745c5c3_Err
32933497 }
3294- var templ_7745c5c3_Var156 string
3295- templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3498+ var templ_7745c5c3_Var168 string
3499+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
32963500 if templ_7745c5c3_Err != nil {
3297- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 654, Col: 58}
3501+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 58}
32983502 }
3299- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
3503+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
33003504 if templ_7745c5c3_Err != nil {
33013505 return templ_7745c5c3_Err
33023506 }
3303- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "</a></dd><dt>ahead</dt><dd>")
3507+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, "</a></dd><dt>ahead</dt><dd>")
33043508 if templ_7745c5c3_Err != nil {
33053509 return templ_7745c5c3_Err
33063510 }
3307- var templ_7745c5c3_Var157 string
3308- templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
3511+ var templ_7745c5c3_Var169 string
3512+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
33093513 if templ_7745c5c3_Err != nil {
3310- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 21}
3514+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 684, Col: 21}
33113515 }
3312- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
3516+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
33133517 if templ_7745c5c3_Err != nil {
33143518 return templ_7745c5c3_Err
33153519 }
3316- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "</dd></dl></header>")
3520+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, "</dd></dl></header>")
33173521 if templ_7745c5c3_Err != nil {
33183522 return templ_7745c5c3_Err
33193523 }
33203524 if p.Note != "" {
3321- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "<p class=\"notice\">")
3525+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "<p class=\"notice\">")
33223526 if templ_7745c5c3_Err != nil {
33233527 return templ_7745c5c3_Err
33243528 }
3325- var templ_7745c5c3_Var158 string
3326- templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
3529+ var templ_7745c5c3_Var170 string
3530+ templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
33273531 if templ_7745c5c3_Err != nil {
3328- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 660, Col: 29}
3532+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 688, Col: 29}
33293533 }
3330- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
3534+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170))
33313535 if templ_7745c5c3_Err != nil {
33323536 return templ_7745c5c3_Err
33333537 }
3334- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "</p>")
3538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, "</p>")
33353539 if templ_7745c5c3_Err != nil {
33363540 return templ_7745c5c3_Err
33373541 }
33383542 }
3339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, " ")
3543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, " ")
33403544 if templ_7745c5c3_Err != nil {
33413545 return templ_7745c5c3_Err
33423546 }
33433547 if p.Existing != nil {
3344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
3548+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
33453549 if templ_7745c5c3_Err != nil {
33463550 return templ_7745c5c3_Err
33473551 }
3348- var templ_7745c5c3_Var159 templ.SafeURL
3349- templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
3552+ var templ_7745c5c3_Var171 templ.SafeURL
3553+ templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
33503554 if templ_7745c5c3_Err != nil {
3351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 663, Col: 114}
3555+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 114}
33523556 }
3353- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))
3557+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
33543558 if templ_7745c5c3_Err != nil {
33553559 return templ_7745c5c3_Err
33563560 }
3357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 311, "\">")
3561+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, "\">")
33583562 if templ_7745c5c3_Err != nil {
33593563 return templ_7745c5c3_Err
33603564 }
3361- var templ_7745c5c3_Var160 string
3362- templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
3565+ var templ_7745c5c3_Var172 string
3566+ templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
33633567 if templ_7745c5c3_Err != nil {
3364- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 663, Col: 135}
3568+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 135}
33653569 }
3366- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
3570+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
33673571 if templ_7745c5c3_Err != nil {
33683572 return templ_7745c5c3_Err
33693573 }
3370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 312, "</a></p>")
3574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "</a></p>")
33713575 if templ_7745c5c3_Err != nil {
33723576 return templ_7745c5c3_Err
33733577 }
33743578 }
3375- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, " ")
3579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, " ")
33763580 if templ_7745c5c3_Err != nil {
33773581 return templ_7745c5c3_Err
33783582 }
33793583 if p.Create != nil {
3380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "<form class=\"cform create\" method=\"post\" action=\"")
3584+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "<form class=\"cform create\" method=\"post\" action=\"")
33813585 if templ_7745c5c3_Err != nil {
33823586 return templ_7745c5c3_Err
33833587 }
3384- var templ_7745c5c3_Var161 templ.SafeURL
3385- templ_7745c5c3_Var161, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
3588+ var templ_7745c5c3_Var173 templ.SafeURL
3589+ templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
33863590 if templ_7745c5c3_Err != nil {
3387- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 666, Col: 83}
3591+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 694, Col: 83}
33883592 }
3389- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var161))
3593+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
33903594 if templ_7745c5c3_Err != nil {
33913595 return templ_7745c5c3_Err
33923596 }
3393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, "\"><input type=\"hidden\" name=\"source\" value=\"")
3597+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "\"><input type=\"hidden\" name=\"source\" value=\"")
33943598 if templ_7745c5c3_Err != nil {
33953599 return templ_7745c5c3_Err
33963600 }
3397- var templ_7745c5c3_Var162 string
3398- templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
3601+ var templ_7745c5c3_Var174 string
3602+ templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
33993603 if templ_7745c5c3_Err != nil {
3400- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 667, Col: 62}
3604+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 62}
34013605 }
3402- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var162)
3606+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var174)
34033607 if templ_7745c5c3_Err != nil {
34043608 return templ_7745c5c3_Err
34053609 }
3406- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "\"> <input type=\"hidden\" name=\"target\" value=\"")
3610+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "\"> <input type=\"hidden\" name=\"target\" value=\"")
34073611 if templ_7745c5c3_Err != nil {
34083612 return templ_7745c5c3_Err
34093613 }
3410- var templ_7745c5c3_Var163 string
3411- templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
3614+ var templ_7745c5c3_Var175 string
3615+ templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
34123616 if templ_7745c5c3_Err != nil {
3413- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 668, Col: 62}
3617+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 696, Col: 62}
34143618 }
3415- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var163)
3619+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var175)
34163620 if templ_7745c5c3_Err != nil {
34173621 return templ_7745c5c3_Err
34183622 }
3419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
3623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
34203624 if templ_7745c5c3_Err != nil {
34213625 return templ_7745c5c3_Err
34223626 }
3423- var templ_7745c5c3_Var164 string
3424- templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
3627+ var templ_7745c5c3_Var176 string
3628+ templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
34253629 if templ_7745c5c3_Err != nil {
3426- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 670, Col: 97}
3630+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 698, Col: 97}
34273631 }
3428- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var164)
3632+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var176)
34293633 if templ_7745c5c3_Err != nil {
34303634 return templ_7745c5c3_Err
34313635 }
3432- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")
3636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, "\" 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>")
34333637 if templ_7745c5c3_Err != nil {
34343638 return templ_7745c5c3_Err
34353639 }
34363640 }
3437- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 319, " ")
3641+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, " ")
34383642 if templ_7745c5c3_Err != nil {
34393643 return templ_7745c5c3_Err
34403644 }
34413645 if p.HasDiff {
3442- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
3646+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
34433647 if templ_7745c5c3_Err != nil {
34443648 return templ_7745c5c3_Err
34453649 }
3446- var templ_7745c5c3_Var165 string
3447- templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
3650+ var templ_7745c5c3_Var177 string
3651+ templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
34483652 if templ_7745c5c3_Err != nil {
3449- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 57}
3653+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 57}
34503654 }
3451- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
3655+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))
34523656 if templ_7745c5c3_Err != nil {
34533657 return templ_7745c5c3_Err
34543658 }
3455- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " · <span class=\"plus\">+")
3659+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, " · <span class=\"plus\">+")
34563660 if templ_7745c5c3_Err != nil {
34573661 return templ_7745c5c3_Err
34583662 }
3459- var templ_7745c5c3_Var166 string
3460- templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
3663+ var templ_7745c5c3_Var178 string
3664+ templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
34613665 if templ_7745c5c3_Err != nil {
3462- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 97}
3666+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 97}
34633667 }
3464- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3668+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))
34653669 if templ_7745c5c3_Err != nil {
34663670 return templ_7745c5c3_Err
34673671 }
3468- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "</span> <span class=\"minus\">-")
3672+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "</span> <span class=\"minus\">-")
34693673 if templ_7745c5c3_Err != nil {
34703674 return templ_7745c5c3_Err
34713675 }
3472- var templ_7745c5c3_Var167 string
3473- templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
3676+ var templ_7745c5c3_Var179 string
3677+ templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
34743678 if templ_7745c5c3_Err != nil {
3475- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 142}
3679+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 142}
34763680 }
3477- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
3681+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
34783682 if templ_7745c5c3_Err != nil {
34793683 return templ_7745c5c3_Err
34803684 }
3481- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, "</span></span> <span class=\"grow\"></span>")
3685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "</span></span> <span class=\"grow\"></span>")
34823686 if templ_7745c5c3_Err != nil {
34833687 return templ_7745c5c3_Err
34843688 }
@@ -3486,64 +3690,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
34863690 if templ_7745c5c3_Err != nil {
34873691 return templ_7745c5c3_Err
34883692 }
3489- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, "</div>")
3693+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</div>")
34903694 if templ_7745c5c3_Err != nil {
34913695 return templ_7745c5c3_Err
34923696 }
34933697 if p.Invisible {
3494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\">&lt;U+XXXX&gt;</span> markers, never applied.</p>")
3698+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 346, "<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>")
34953699 if templ_7745c5c3_Err != nil {
34963700 return templ_7745c5c3_Err
34973701 }
34983702 }
3499- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, " <div id=\"diffs\"")
3703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 347, " <div id=\"diffs\"")
35003704 if templ_7745c5c3_Err != nil {
35013705 return templ_7745c5c3_Err
35023706 }
35033707 if p.Both {
3504- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, " data-diff=\"")
3708+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 348, " data-diff=\"")
35053709 if templ_7745c5c3_Err != nil {
35063710 return templ_7745c5c3_Err
35073711 }
3508- var templ_7745c5c3_Var168 string
3509- templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3712+ var templ_7745c5c3_Var180 string
3713+ templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
35103714 if templ_7745c5c3_Err != nil {
3511- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 693, Col: 23}
3715+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 721, Col: 23}
35123716 }
3513- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var168)
3717+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var180)
35143718 if templ_7745c5c3_Err != nil {
35153719 return templ_7745c5c3_Err
35163720 }
3517- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "\"")
3721+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 349, "\"")
35183722 if templ_7745c5c3_Err != nil {
35193723 return templ_7745c5c3_Err
35203724 }
35213725 } else {
3522- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, " data-view=\"")
3726+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 350, " data-view=\"")
35233727 if templ_7745c5c3_Err != nil {
35243728 return templ_7745c5c3_Err
35253729 }
3526- var templ_7745c5c3_Var169 string
3527- templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3730+ var templ_7745c5c3_Var181 string
3731+ templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
35283732 if templ_7745c5c3_Err != nil {
3529- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 23}
3733+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 723, Col: 23}
35303734 }
3531- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var169)
3735+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var181)
35323736 if templ_7745c5c3_Err != nil {
35333737 return templ_7745c5c3_Err
35343738 }
3535- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, "\"")
3739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 351, "\"")
35363740 if templ_7745c5c3_Err != nil {
35373741 return templ_7745c5c3_Err
35383742 }
35393743 }
35403744 if p.Both && !p.Pinned {
3541- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
3745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 352, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
35423746 if templ_7745c5c3_Err != nil {
35433747 return templ_7745c5c3_Err
35443748 }
35453749 }
3546- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, ">")
3750+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 353, ">")
35473751 if templ_7745c5c3_Err != nil {
35483752 return templ_7745c5c3_Err
35493753 }
@@ -3553,14 +3757,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
35533757 return templ_7745c5c3_Err
35543758 }
35553759 }
3556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "</div>")
3760+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 354, "</div>")
35573761 if templ_7745c5c3_Err != nil {
35583762 return templ_7745c5c3_Err
35593763 }
35603764 }
35613765 return nil
35623766 })
3563- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var150), templ_7745c5c3_Buffer)
3767+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var162), templ_7745c5c3_Buffer)
35643768 if templ_7745c5c3_Err != nil {
35653769 return templ_7745c5c3_Err
35663770 }
@@ -3584,173 +3788,173 @@ func compareRail(p ComparePage) templ.Component {
35843788 }()
35853789 }
35863790 ctx = templ.InitializeContext(ctx)
3587- templ_7745c5c3_Var170 := templ.GetChildren(ctx)
3588- if templ_7745c5c3_Var170 == nil {
3589- templ_7745c5c3_Var170 = templ.NopComponent
3791+ templ_7745c5c3_Var182 := templ.GetChildren(ctx)
3792+ if templ_7745c5c3_Var182 == nil {
3793+ templ_7745c5c3_Var182 = templ.NopComponent
35903794 }
35913795 ctx = templ.ClearChildren(ctx)
3592- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
3796+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 355, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
35933797 if templ_7745c5c3_Err != nil {
35943798 return templ_7745c5c3_Err
35953799 }
3596- var templ_7745c5c3_Var171 string
3597- templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
3800+ var templ_7745c5c3_Var183 string
3801+ templ_7745c5c3_Var183, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
35983802 if templ_7745c5c3_Err != nil {
3599- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 711, Col: 67}
3803+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 739, Col: 67}
36003804 }
3601- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
3805+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var183))
36023806 if templ_7745c5c3_Err != nil {
36033807 return templ_7745c5c3_Err
36043808 }
3605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "</span></h2><ul class=\"pcommits\">")
3809+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 356, "</span></h2><ul class=\"pcommits\">")
36063810 if templ_7745c5c3_Err != nil {
36073811 return templ_7745c5c3_Err
36083812 }
36093813 for _, c := range p.Ahead {
3610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "<li><a href=\"")
3814+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 357, "<li><a href=\"")
36113815 if templ_7745c5c3_Err != nil {
36123816 return templ_7745c5c3_Err
36133817 }
3614- var templ_7745c5c3_Var172 templ.SafeURL
3615- templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
3818+ var templ_7745c5c3_Var184 templ.SafeURL
3819+ templ_7745c5c3_Var184, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
36163820 if templ_7745c5c3_Err != nil {
3617- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 715, Col: 37}
3821+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 37}
36183822 }
3619- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
3823+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var184))
36203824 if templ_7745c5c3_Err != nil {
36213825 return templ_7745c5c3_Err
36223826 }
3623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "\">")
3827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 358, "\">")
36243828 if templ_7745c5c3_Err != nil {
36253829 return templ_7745c5c3_Err
36263830 }
3627- var templ_7745c5c3_Var173 string
3628- templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
3831+ var templ_7745c5c3_Var185 string
3832+ templ_7745c5c3_Var185, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
36293833 if templ_7745c5c3_Err != nil {
3630- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 715, Col: 49}
3834+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 49}
36313835 }
3632- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
3836+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var185))
36333837 if templ_7745c5c3_Err != nil {
36343838 return templ_7745c5c3_Err
36353839 }
3636- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "</a> <span class=\"s\">")
3840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 359, "</a> <span class=\"s\">")
36373841 if templ_7745c5c3_Err != nil {
36383842 return templ_7745c5c3_Err
36393843 }
3640- var templ_7745c5c3_Var174 string
3641- templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
3844+ var templ_7745c5c3_Var186 string
3845+ templ_7745c5c3_Var186, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
36423846 if templ_7745c5c3_Err != nil {
3643- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 716, Col: 32}
3847+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 32}
36443848 }
3645- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var174))
3849+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var186))
36463850 if templ_7745c5c3_Err != nil {
36473851 return templ_7745c5c3_Err
36483852 }
3649- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, "</span> <span class=\"mute\">")
3853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 360, "</span> <span class=\"mute\">")
36503854 if templ_7745c5c3_Err != nil {
36513855 return templ_7745c5c3_Err
36523856 }
3653- var templ_7745c5c3_Var175 string
3654- templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
3857+ var templ_7745c5c3_Var187 string
3858+ templ_7745c5c3_Var187, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
36553859 if templ_7745c5c3_Err != nil {
3656- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 718, Col: 16}
3860+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 746, Col: 16}
36573861 }
3658- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var175))
3862+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var187))
36593863 if templ_7745c5c3_Err != nil {
36603864 return templ_7745c5c3_Err
36613865 }
3662- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, " ")
3866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 361, " ")
36633867 if templ_7745c5c3_Err != nil {
36643868 return templ_7745c5c3_Err
36653869 }
36663870 if c.WhenAt != "" {
3667- var templ_7745c5c3_Var176 string
3668- templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
3871+ var templ_7745c5c3_Var188 string
3872+ templ_7745c5c3_Var188, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
36693873 if templ_7745c5c3_Err != nil {
3670- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 15}
3874+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 15}
36713875 }
3672- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var176))
3876+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var188))
36733877 if templ_7745c5c3_Err != nil {
36743878 return templ_7745c5c3_Err
36753879 }
3676- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, "<time datetime=\"")
3880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 362, "<time datetime=\"")
36773881 if templ_7745c5c3_Err != nil {
36783882 return templ_7745c5c3_Err
36793883 }
3680- var templ_7745c5c3_Var177 string
3681- templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
3884+ var templ_7745c5c3_Var189 string
3885+ templ_7745c5c3_Var189, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
36823886 if templ_7745c5c3_Err != nil {
3683- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 42}
3887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 42}
36843888 }
3685- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var177)
3889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var189)
36863890 if templ_7745c5c3_Err != nil {
36873891 return templ_7745c5c3_Err
36883892 }
3689- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, "\">")
3893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 363, "\">")
36903894 if templ_7745c5c3_Err != nil {
36913895 return templ_7745c5c3_Err
36923896 }
3693- var templ_7745c5c3_Var178 string
3694- templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
3897+ var templ_7745c5c3_Var190 string
3898+ templ_7745c5c3_Var190, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
36953899 if templ_7745c5c3_Err != nil {
3696- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 53}
3900+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 53}
36973901 }
3698- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))
3902+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var190))
36993903 if templ_7745c5c3_Err != nil {
37003904 return templ_7745c5c3_Err
37013905 }
3702- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "</time>")
3906+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 364, "</time>")
37033907 if templ_7745c5c3_Err != nil {
37043908 return templ_7745c5c3_Err
37053909 }
37063910 }
3707- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "</span></li>")
3911+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 365, "</span></li>")
37083912 if templ_7745c5c3_Err != nil {
37093913 return templ_7745c5c3_Err
37103914 }
37113915 }
3712- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</ul>")
3916+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 366, "</ul>")
37133917 if templ_7745c5c3_Err != nil {
37143918 return templ_7745c5c3_Err
37153919 }
37163920 if len(p.Ahead) == 0 {
3717- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 346, "<p class=\"dim\">no commit ahead of ")
3921+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 367, "<p class=\"dim\">no commit ahead of ")
37183922 if templ_7745c5c3_Err != nil {
37193923 return templ_7745c5c3_Err
37203924 }
3721- var templ_7745c5c3_Var179 string
3722- templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3925+ var templ_7745c5c3_Var191 string
3926+ templ_7745c5c3_Var191, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
37233927 if templ_7745c5c3_Err != nil {
3724- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 727, Col: 47}
3928+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 755, Col: 47}
37253929 }
3726- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
3930+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var191))
37273931 if templ_7745c5c3_Err != nil {
37283932 return templ_7745c5c3_Err
37293933 }
3730- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 347, ".</p>")
3934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 368, ".</p>")
37313935 if templ_7745c5c3_Err != nil {
37323936 return templ_7745c5c3_Err
37333937 }
37343938 }
3735- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 348, "</section>")
3939+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 369, "</section>")
37363940 if templ_7745c5c3_Err != nil {
37373941 return templ_7745c5c3_Err
37383942 }
37393943 if p.HasDiff {
3740- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 349, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
3944+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 370, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
37413945 if templ_7745c5c3_Err != nil {
37423946 return templ_7745c5c3_Err
37433947 }
3744- var templ_7745c5c3_Var180 string
3745- templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
3948+ var templ_7745c5c3_Var192 string
3949+ templ_7745c5c3_Var192, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
37463950 if templ_7745c5c3_Err != nil {
3747- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 732, Col: 66}
3951+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 760, Col: 66}
37483952 }
3749- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var180))
3953+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var192))
37503954 if templ_7745c5c3_Err != nil {
37513955 return templ_7745c5c3_Err
37523956 }
3753- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 350, "</span></h2>")
3957+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 371, "</span></h2>")
37543958 if templ_7745c5c3_Err != nil {
37553959 return templ_7745c5c3_Err
37563960 }
@@ -3758,12 +3962,12 @@ func compareRail(p ComparePage) templ.Component {
37583962 if templ_7745c5c3_Err != nil {
37593963 return templ_7745c5c3_Err
37603964 }
3761- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 351, "</section>")
3965+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 372, "</section>")
37623966 if templ_7745c5c3_Err != nil {
37633967 return templ_7745c5c3_Err
37643968 }
37653969 }
3766- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 352, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
3970+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 373, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
37673971 if templ_7745c5c3_Err != nil {
37683972 return templ_7745c5c3_Err
37693973 }
@@ -3771,7 +3975,7 @@ func compareRail(p ComparePage) templ.Component {
37713975 if templ_7745c5c3_Err != nil {
37723976 return templ_7745c5c3_Err
37733977 }
3774- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 353, "</section>")
3978+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 374, "</section>")
37753979 if templ_7745c5c3_Err != nil {
37763980 return templ_7745c5c3_Err
37773981 }
@@ -2571,185 +2571,389 @@ func mergeMenu(p MRPage) templ.Component {
2571 if templ_7745c5c3_Err != nil {2571 if templ_7745c5c3_Err != nil {
2572 return templ_7745c5c3_Err2572 return templ_7745c5c3_Err
2573 }2573 }
2574- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, " <form class=\"d-acts\" method=\"post\" action=\"")2574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, " ")
2575 if templ_7745c5c3_Err != nil {2575 if templ_7745c5c3_Err != nil {
2576 return templ_7745c5c3_Err2576 return templ_7745c5c3_Err
2577 }2577 }
2578- var templ_7745c5c3_Var120 templ.SafeURL2578+ if p.Box.AutoMerge != nil {
2579- templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))2579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "<form class=\"d-acts\" method=\"post\" action=\"")
2580+ if templ_7745c5c3_Err != nil {
2581+ return templ_7745c5c3_Err
2582+ }
2583+ var templ_7745c5c3_Var120 templ.SafeURL
2584+ templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CancelAutoAction))
2585+ if templ_7745c5c3_Err != nil {
2586+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 86}
2587+ }
2588+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))
2589+ if templ_7745c5c3_Err != nil {
2590+ return templ_7745c5c3_Err
2591+ }
2592+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "\"><p class=\"dim\" role=\"status\">Auto-merge: ")
2593+ if templ_7745c5c3_Err != nil {
2594+ return templ_7745c5c3_Err
2595+ }
2596+ var templ_7745c5c3_Var121 string
2597+ templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.Mode)
2598+ if templ_7745c5c3_Err != nil {
2599+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 514, Col: 41}
2600+ }
2601+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121))
2602+ if templ_7745c5c3_Err != nil {
2603+ return templ_7745c5c3_Err
2604+ }
2605+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, " when ready, enabled by ")
2606+ if templ_7745c5c3_Err != nil {
2607+ return templ_7745c5c3_Err
2608+ }
2609+ var templ_7745c5c3_Var122 string
2610+ templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.EnabledBy)
2611+ if templ_7745c5c3_Err != nil {
2612+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 514, Col: 94}
2613+ }
2614+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var122))
2615+ if templ_7745c5c3_Err != nil {
2616+ return templ_7745c5c3_Err
2617+ }
2618+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, " ")
2619+ if templ_7745c5c3_Err != nil {
2620+ return templ_7745c5c3_Err
2621+ }
2622+ if p.Box.AutoMerge.When != "" {
2623+ var templ_7745c5c3_Var123 string
2624+ templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2625+ if templ_7745c5c3_Err != nil {
2626+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 13}
2627+ }
2628+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var123))
2629+ if templ_7745c5c3_Err != nil {
2630+ return templ_7745c5c3_Err
2631+ }
2632+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<time datetime=\"")
2633+ if templ_7745c5c3_Err != nil {
2634+ return templ_7745c5c3_Err
2635+ }
2636+ var templ_7745c5c3_Var124 string
2637+ templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.AutoMerge.WhenAt)
2638+ if templ_7745c5c3_Err != nil {
2639+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 54}
2640+ }
2641+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124)
2642+ if templ_7745c5c3_Err != nil {
2643+ return templ_7745c5c3_Err
2644+ }
2645+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "\">")
2646+ if templ_7745c5c3_Err != nil {
2647+ return templ_7745c5c3_Err
2648+ }
2649+ var templ_7745c5c3_Var125 string
2650+ templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.When)
2651+ if templ_7745c5c3_Err != nil {
2652+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 516, Col: 79}
2653+ }
2654+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))
2655+ if templ_7745c5c3_Err != nil {
2656+ return templ_7745c5c3_Err
2657+ }
2658+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, "</time> ")
2659+ if templ_7745c5c3_Err != nil {
2660+ return templ_7745c5c3_Err
2661+ }
2662+ }
2663+ var templ_7745c5c3_Var126 string
2664+ templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2665+ if templ_7745c5c3_Err != nil {
2666+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 518, Col: 12}
2667+ }
2668+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))
2669+ if templ_7745c5c3_Err != nil {
2670+ return templ_7745c5c3_Err
2671+ }
2672+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "(head ")
2673+ if templ_7745c5c3_Err != nil {
2674+ return templ_7745c5c3_Err
2675+ }
2676+ var templ_7745c5c3_Var127 string
2677+ templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.AutoMerge.HeadShort)
2678+ if templ_7745c5c3_Err != nil {
2679+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 518, Col: 47}
2680+ }
2681+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127))
2682+ if templ_7745c5c3_Err != nil {
2683+ return templ_7745c5c3_Err
2684+ }
2685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, ")</p><div class=\"d-btns\"><button type=\"submit\" class=\"btn\">Cancel auto-merge</button></div></form>")
2686+ if templ_7745c5c3_Err != nil {
2687+ return templ_7745c5c3_Err
2688+ }
2689+ }
2690+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, " <form class=\"d-acts\" method=\"post\" action=\"")
2691+ if templ_7745c5c3_Err != nil {
2692+ return templ_7745c5c3_Err
2693+ }
2694+ var templ_7745c5c3_Var128 templ.SafeURL
2695+ templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction))
2580 if templ_7745c5c3_Err != nil {2696 if templ_7745c5c3_Err != nil {
2581- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 511, Col: 80}2697+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 525, Col: 80}
2582 }2698 }
2583- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120))2699+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))
2584 if templ_7745c5c3_Err != nil {2700 if templ_7745c5c3_Err != nil {
2585 return templ_7745c5c3_Err2701 return templ_7745c5c3_Err
2586 }2702 }
2587- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "\"><input type=\"hidden\" name=\"head_id\" value=\"")2703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2588 if templ_7745c5c3_Err != nil {2704 if templ_7745c5c3_Err != nil {
2589 return templ_7745c5c3_Err2705 return templ_7745c5c3_Err
2590 }2706 }
2591- var templ_7745c5c3_Var121 string2707+ var templ_7745c5c3_Var129 string
2592- templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)2708+ templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2593 if templ_7745c5c3_Err != nil {2709 if templ_7745c5c3_Err != nil {
2594- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 512, Col: 59}2710+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 526, Col: 59}
2595 }2711 }
2596- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var121)2712+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129)
2597 if templ_7745c5c3_Err != nil {2713 if templ_7745c5c3_Err != nil {
2598 return templ_7745c5c3_Err2714 return templ_7745c5c3_Err
2599 }2715 }
2600- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")2716+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "\"><fieldset class=\"d-modes\"><legend class=\"sr\">merge mode</legend> ")
2601 if templ_7745c5c3_Err != nil {2717 if templ_7745c5c3_Err != nil {
2602 return templ_7745c5c3_Err2718 return templ_7745c5c3_Err
2603 }2719 }
2604 for i, m := range p.Box.Modes {2720 for i, m := range p.Box.Modes {
2605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "<label><input type=\"radio\" name=\"mode\" value=\"")2721+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "<label><input type=\"radio\" name=\"mode\" value=\"")
2606 if templ_7745c5c3_Err != nil {2722 if templ_7745c5c3_Err != nil {
2607 return templ_7745c5c3_Err2723 return templ_7745c5c3_Err
2608 }2724 }
2609- var templ_7745c5c3_Var122 string2725+ var templ_7745c5c3_Var130 string
2610- templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)2726+ templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2611 if templ_7745c5c3_Err != nil {2727 if templ_7745c5c3_Err != nil {
2612- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 520, Col: 24}2728+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 534, Col: 24}
2613 }2729 }
2614- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122)2730+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)
2615 if templ_7745c5c3_Err != nil {2731 if templ_7745c5c3_Err != nil {
2616 return templ_7745c5c3_Err2732 return templ_7745c5c3_Err
2617 }2733 }
2618- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "\"")2734+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "\"")
2619 if templ_7745c5c3_Err != nil {2735 if templ_7745c5c3_Err != nil {
2620 return templ_7745c5c3_Err2736 return templ_7745c5c3_Err
2621 }2737 }
2622 if i == p.Box.Checked() {2738 if i == p.Box.Checked() {
2623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, " checked")2739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, " checked")
2624 if templ_7745c5c3_Err != nil {2740 if templ_7745c5c3_Err != nil {
2625 return templ_7745c5c3_Err2741 return templ_7745c5c3_Err
2626 }2742 }
2627 }2743 }
2628 if m.Disabled {2744 if m.Disabled {
2629- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, " disabled")2745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, " disabled")
2630 if templ_7745c5c3_Err != nil {2746 if templ_7745c5c3_Err != nil {
2631 return templ_7745c5c3_Err2747 return templ_7745c5c3_Err
2632 }2748 }
2633 }2749 }
2634- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 247, " title=\"")2750+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, " title=\"")
2635 if templ_7745c5c3_Err != nil {2751 if templ_7745c5c3_Err != nil {
2636 return templ_7745c5c3_Err2752 return templ_7745c5c3_Err
2637 }2753 }
2638- var templ_7745c5c3_Var123 string2754+ var templ_7745c5c3_Var131 string
2639- templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)2755+ templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Reason)
2640 if templ_7745c5c3_Err != nil {2756 if templ_7745c5c3_Err != nil {
2641- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 523, Col: 25}2757+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 537, Col: 25}
2642 }2758 }
2643- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123)2759+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)
2644 if templ_7745c5c3_Err != nil {2760 if templ_7745c5c3_Err != nil {
2645 return templ_7745c5c3_Err2761 return templ_7745c5c3_Err
2646 }2762 }
2647- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 248, "\" :disabled=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffOk !== true : q('[data-mergeability]').data.mergeOk !== true\" :title=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffReason : q('[data-mergeability]').data.mergeReason\"> ")2763+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, "\" :disabled=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffOk !== true : q('[data-mergeability]').data.mergeOk !== true\" :title=\"this.value === 'fast-forward' ? q('[data-mergeability]').data.ffReason : q('[data-mergeability]').data.mergeReason\"> ")
2648 if templ_7745c5c3_Err != nil {2764 if templ_7745c5c3_Err != nil {
2649 return templ_7745c5c3_Err2765 return templ_7745c5c3_Err
2650 }2766 }
2651- var templ_7745c5c3_Var124 string2767+ var templ_7745c5c3_Var132 string
2652- templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)2768+ templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2653 if templ_7745c5c3_Err != nil {2769 if templ_7745c5c3_Err != nil {
2654- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 527, Col: 17}2770+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 541, Col: 17}
2655 }2771 }
2656- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124))2772+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132))
2657 if templ_7745c5c3_Err != nil {2773 if templ_7745c5c3_Err != nil {
2658 return templ_7745c5c3_Err2774 return templ_7745c5c3_Err
2659 }2775 }
2660- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 249, " ")2776+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, " ")
2661 if templ_7745c5c3_Err != nil {2777 if templ_7745c5c3_Err != nil {
2662 return templ_7745c5c3_Err2778 return templ_7745c5c3_Err
2663 }2779 }
2664 if i < 9 {2780 if i < 9 {
2665- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 250, "<kbd aria-hidden=\"true\">")2781+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "<kbd aria-hidden=\"true\">")
2666 if templ_7745c5c3_Err != nil {2782 if templ_7745c5c3_Err != nil {
2667 return templ_7745c5c3_Err2783 return templ_7745c5c3_Err
2668 }2784 }
2669- var templ_7745c5c3_Var125 string2785+ var templ_7745c5c3_Var133 string
2670- templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))2786+ templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(i + 1))
2671 if templ_7745c5c3_Err != nil {2787 if templ_7745c5c3_Err != nil {
2672- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 529, Col: 46}2788+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 46}
2673 }2789 }
2674- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125))2790+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133))
2675 if templ_7745c5c3_Err != nil {2791 if templ_7745c5c3_Err != nil {
2676 return templ_7745c5c3_Err2792 return templ_7745c5c3_Err
2677 }2793 }
2678- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 251, "</kbd>")2794+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</kbd>")
2679 if templ_7745c5c3_Err != nil {2795 if templ_7745c5c3_Err != nil {
2680 return templ_7745c5c3_Err2796 return templ_7745c5c3_Err
2681 }2797 }
2682 }2798 }
2683- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 252, "</label>")2799+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "</label>")
2684 if templ_7745c5c3_Err != nil {2800 if templ_7745c5c3_Err != nil {
2685 return templ_7745c5c3_Err2801 return templ_7745c5c3_Err
2686 }2802 }
2687 }2803 }
2688- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 253, "</fieldset>")2804+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "</fieldset>")
2689 if templ_7745c5c3_Err != nil {2805 if templ_7745c5c3_Err != nil {
2690 return templ_7745c5c3_Err2806 return templ_7745c5c3_Err
2691 }2807 }
2692 if p.Box.CanDeleteBranch {2808 if p.Box.CanDeleteBranch {
2693- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 254, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")2809+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2694 if templ_7745c5c3_Err != nil {2810 if templ_7745c5c3_Err != nil {
2695 return templ_7745c5c3_Err2811 return templ_7745c5c3_Err
2696 }2812 }
2697- var templ_7745c5c3_Var126 string2813+ var templ_7745c5c3_Var134 string
2698- templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)2814+ templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2699 if templ_7745c5c3_Err != nil {2815 if templ_7745c5c3_Err != nil {
2700- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 535, Col: 102}2816+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 549, Col: 102}
2701 }2817 }
2702- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126))2818+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
2703 if templ_7745c5c3_Err != nil {2819 if templ_7745c5c3_Err != nil {
2704 return templ_7745c5c3_Err2820 return templ_7745c5c3_Err
2705 }2821 }
2706- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 255, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")2822+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, " after the merge <kbd aria-hidden=\"true\">D</kbd></label>")
2707 if templ_7745c5c3_Err != nil {2823 if templ_7745c5c3_Err != nil {
2708 return templ_7745c5c3_Err2824 return templ_7745c5c3_Err
2709 }2825 }
2710 }2826 }
2711- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 256, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")2827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "<div class=\"d-btns\"><button type=\"submit\" class=\"btn primary\" data-default")
2712 if templ_7745c5c3_Err != nil {2828 if templ_7745c5c3_Err != nil {
2713 return templ_7745c5c3_Err2829 return templ_7745c5c3_Err
2714 }2830 }
2715 if !p.Box.CanMerge() {2831 if !p.Box.CanMerge() {
2716- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 257, " disabled")2832+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, " disabled")
2717 if templ_7745c5c3_Err != nil {2833 if templ_7745c5c3_Err != nil {
2718 return templ_7745c5c3_Err2834 return templ_7745c5c3_Err
2719 }2835 }
2720 }2836 }
2721- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 258, " title=\"")2837+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, " title=\"")
2722 if templ_7745c5c3_Err != nil {2838 if templ_7745c5c3_Err != nil {
2723 return templ_7745c5c3_Err2839 return templ_7745c5c3_Err
2724 }2840 }
2725- var templ_7745c5c3_Var127 string2841+ var templ_7745c5c3_Var135 string
2726- templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())2842+ templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
2727 if templ_7745c5c3_Err != nil {2843 if templ_7745c5c3_Err != nil {
2728- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 543, Col: 33}2844+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 557, Col: 33}
2729 }2845 }
2730- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127)2846+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
2731 if templ_7745c5c3_Err != nil {2847 if templ_7745c5c3_Err != nil {
2732 return templ_7745c5c3_Err2848 return templ_7745c5c3_Err
2733 }2849 }
2734- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 259, "\" :disabled=\"!this.form.querySelector('input[name=mode]:checked:not(:disabled)')\" :title=\"(this.form.querySelector('input[name=mode]:checked') || {}).title || ''\">Merge into ")2850+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "\" :disabled=\"!this.form.querySelector('input[name=mode]:checked:not(:disabled)')\" :title=\"(this.form.querySelector('input[name=mode]:checked') || {}).title || ''\">Merge into ")
2735 if templ_7745c5c3_Err != nil {2851 if templ_7745c5c3_Err != nil {
2736 return templ_7745c5c3_Err2852 return templ_7745c5c3_Err
2737 }2853 }
2738- var templ_7745c5c3_Var128 string2854+ var templ_7745c5c3_Var136 string
2739- templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2855+ templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2740 if templ_7745c5c3_Err != nil {2856 if templ_7745c5c3_Err != nil {
2741- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 546, Col: 28}2857+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 28}
2742 }2858 }
2743- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var128))2859+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
2744 if templ_7745c5c3_Err != nil {2860 if templ_7745c5c3_Err != nil {
2745 return templ_7745c5c3_Err2861 return templ_7745c5c3_Err
2746 }2862 }
2747- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 260, "</button></div></form>")2863+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "</button></div></form>")
2748 if templ_7745c5c3_Err != nil {2864 if templ_7745c5c3_Err != nil {
2749 return templ_7745c5c3_Err2865 return templ_7745c5c3_Err
2750 }2866 }
2867+ if p.Box.CanAutoMerge {
2868+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "<form class=\"d-acts\" method=\"post\" action=\"")
2869+ if templ_7745c5c3_Err != nil {
2870+ return templ_7745c5c3_Err
2871+ }
2872+ var templ_7745c5c3_Var137 templ.SafeURL
2873+ templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.AutoAction))
2874+ if templ_7745c5c3_Err != nil {
2875+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 80}
2876+ }
2877+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
2878+ if templ_7745c5c3_Err != nil {
2879+ return templ_7745c5c3_Err
2880+ }
2881+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
2882+ if templ_7745c5c3_Err != nil {
2883+ return templ_7745c5c3_Err
2884+ }
2885+ var templ_7745c5c3_Var138 string
2886+ templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2887+ if templ_7745c5c3_Err != nil {
2888+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 60}
2889+ }
2890+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2891+ if templ_7745c5c3_Err != nil {
2892+ return templ_7745c5c3_Err
2893+ }
2894+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "\"><fieldset class=\"d-modes\"><legend>Auto-merge mode</legend> ")
2895+ if templ_7745c5c3_Err != nil {
2896+ return templ_7745c5c3_Err
2897+ }
2898+ for i, m := range p.Box.Modes {
2899+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "<label><input type=\"radio\" name=\"mode\" value=\"")
2900+ if templ_7745c5c3_Err != nil {
2901+ return templ_7745c5c3_Err
2902+ }
2903+ var templ_7745c5c3_Var139 string
2904+ templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2905+ if templ_7745c5c3_Err != nil {
2906+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 62}
2907+ }
2908+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var139)
2909+ if templ_7745c5c3_Err != nil {
2910+ return templ_7745c5c3_Err
2911+ }
2912+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "\"")
2913+ if templ_7745c5c3_Err != nil {
2914+ return templ_7745c5c3_Err
2915+ }
2916+ if i == 0 {
2917+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, " checked")
2918+ if templ_7745c5c3_Err != nil {
2919+ return templ_7745c5c3_Err
2920+ }
2921+ }
2922+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "> ")
2923+ if templ_7745c5c3_Err != nil {
2924+ return templ_7745c5c3_Err
2925+ }
2926+ var templ_7745c5c3_Var140 string
2927+ templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2928+ if templ_7745c5c3_Err != nil {
2929+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 96}
2930+ }
2931+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2932+ if templ_7745c5c3_Err != nil {
2933+ return templ_7745c5c3_Err
2934+ }
2935+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</label>")
2936+ if templ_7745c5c3_Err != nil {
2937+ return templ_7745c5c3_Err
2938+ }
2939+ }
2940+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</fieldset><div class=\"d-btns\"><button type=\"submit\" class=\"btn\"")
2941+ if templ_7745c5c3_Err != nil {
2942+ return templ_7745c5c3_Err
2943+ }
2944+ if len(p.Box.Modes) == 0 {
2945+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, " disabled")
2946+ if templ_7745c5c3_Err != nil {
2947+ return templ_7745c5c3_Err
2948+ }
2949+ }
2950+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, " title=\"merge once the checks pass\">Merge when ready</button></div></form>")
2951+ if templ_7745c5c3_Err != nil {
2952+ return templ_7745c5c3_Err
2953+ }
2954+ }
2751 }2955 }
2752- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 261, "</div></details>")2956+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</div></details>")
2753 if templ_7745c5c3_Err != nil {2957 if templ_7745c5c3_Err != nil {
2754 return templ_7745c5c3_Err2958 return templ_7745c5c3_Err
2755 }2959 }
@@ -2775,182 +2979,182 @@ func MRMergeability(v MergeabilityView) templ.Component {
2775 }()2979 }()
2776 }2980 }
2777 ctx = templ.InitializeContext(ctx)2981 ctx = templ.InitializeContext(ctx)
2778- templ_7745c5c3_Var129 := templ.GetChildren(ctx)2982+ templ_7745c5c3_Var141 := templ.GetChildren(ctx)
2779- if templ_7745c5c3_Var129 == nil {2983+ if templ_7745c5c3_Var141 == nil {
2780- templ_7745c5c3_Var129 = templ.NopComponent2984+ templ_7745c5c3_Var141 = templ.NopComponent
2781 }2985 }
2782 ctx = templ.ClearChildren(ctx)2986 ctx = templ.ClearChildren(ctx)
2783- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 262, "<span data-mergeability data-state=\"")2987+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "<span data-mergeability data-state=\"")
2784 if templ_7745c5c3_Err != nil {2988 if templ_7745c5c3_Err != nil {
2785 return templ_7745c5c3_Err2989 return templ_7745c5c3_Err
2786 }2990 }
2787- var templ_7745c5c3_Var130 string2991+ var templ_7745c5c3_Var142 string
2788- templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)2992+ templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2789 if templ_7745c5c3_Err != nil {2993 if templ_7745c5c3_Err != nil {
2790- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 559, Col: 22}2994+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 587, Col: 22}
2791 }2995 }
2792- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var130)2996+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
2793 if templ_7745c5c3_Err != nil {2997 if templ_7745c5c3_Err != nil {
2794 return templ_7745c5c3_Err2998 return templ_7745c5c3_Err
2795 }2999 }
2796- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 263, "\" data-ff-ok=\"")3000+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "\" data-ff-ok=\"")
2797 if templ_7745c5c3_Err != nil {3001 if templ_7745c5c3_Err != nil {
2798 return templ_7745c5c3_Err3002 return templ_7745c5c3_Err
2799 }3003 }
2800- var templ_7745c5c3_Var131 string3004+ var templ_7745c5c3_Var143 string
2801- templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))3005+ templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
2802 if templ_7745c5c3_Err != nil {3006 if templ_7745c5c3_Err != nil {
2803- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 41}3007+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 588, Col: 41}
2804 }3008 }
2805- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var131)3009+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
2806 if templ_7745c5c3_Err != nil {3010 if templ_7745c5c3_Err != nil {
2807 return templ_7745c5c3_Err3011 return templ_7745c5c3_Err
2808 }3012 }
2809- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "\" data-merge-ok=\"")3013+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "\" data-merge-ok=\"")
2810 if templ_7745c5c3_Err != nil {3014 if templ_7745c5c3_Err != nil {
2811 return templ_7745c5c3_Err3015 return templ_7745c5c3_Err
2812 }3016 }
2813- var templ_7745c5c3_Var132 string3017+ var templ_7745c5c3_Var144 string
2814- templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))3018+ templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
2815 if templ_7745c5c3_Err != nil {3019 if templ_7745c5c3_Err != nil {
2816- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 561, Col: 47}3020+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 47}
2817 }3021 }
2818- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var132)3022+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
2819 if templ_7745c5c3_Err != nil {3023 if templ_7745c5c3_Err != nil {
2820 return templ_7745c5c3_Err3024 return templ_7745c5c3_Err
2821 }3025 }
2822- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 265, "\" data-ff-reason=\"")3026+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "\" data-ff-reason=\"")
2823 if templ_7745c5c3_Err != nil {3027 if templ_7745c5c3_Err != nil {
2824 return templ_7745c5c3_Err3028 return templ_7745c5c3_Err
2825 }3029 }
2826- var templ_7745c5c3_Var133 string3030+ var templ_7745c5c3_Var145 string
2827- templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)3031+ templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
2828 if templ_7745c5c3_Err != nil {3032 if templ_7745c5c3_Err != nil {
2829- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 562, Col: 29}3033+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 590, Col: 29}
2830 }3034 }
2831- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var133)3035+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var145)
2832 if templ_7745c5c3_Err != nil {3036 if templ_7745c5c3_Err != nil {
2833 return templ_7745c5c3_Err3037 return templ_7745c5c3_Err
2834 }3038 }
2835- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 266, "\" data-merge-reason=\"")3039+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "\" data-merge-reason=\"")
2836 if templ_7745c5c3_Err != nil {3040 if templ_7745c5c3_Err != nil {
2837 return templ_7745c5c3_Err3041 return templ_7745c5c3_Err
2838 }3042 }
2839- var templ_7745c5c3_Var134 string3043+ var templ_7745c5c3_Var146 string
2840- templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)3044+ templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
2841 if templ_7745c5c3_Err != nil {3045 if templ_7745c5c3_Err != nil {
2842- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 563, Col: 35}3046+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 591, Col: 35}
2843 }3047 }
2844- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var134)3048+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var146)
2845 if templ_7745c5c3_Err != nil {3049 if templ_7745c5c3_Err != nil {
2846 return templ_7745c5c3_Err3050 return templ_7745c5c3_Err
2847 }3051 }
2848- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 267, "\"><span class=\"badge\" data-state=\"")3052+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "\"><span class=\"badge\" data-state=\"")
2849 if templ_7745c5c3_Err != nil {3053 if templ_7745c5c3_Err != nil {
2850 return templ_7745c5c3_Err3054 return templ_7745c5c3_Err
2851 }3055 }
2852- var templ_7745c5c3_Var135 string3056+ var templ_7745c5c3_Var147 string
2853- templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)3057+ templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2854 if templ_7745c5c3_Err != nil {3058 if templ_7745c5c3_Err != nil {
2855- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 42}3059+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 42}
2856 }3060 }
2857- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)3061+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)
2858 if templ_7745c5c3_Err != nil {3062 if templ_7745c5c3_Err != nil {
2859 return templ_7745c5c3_Err3063 return templ_7745c5c3_Err
2860 }3064 }
2861- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 268, "\">")3065+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "\">")
2862 if templ_7745c5c3_Err != nil {3066 if templ_7745c5c3_Err != nil {
2863 return templ_7745c5c3_Err3067 return templ_7745c5c3_Err
2864 }3068 }
2865- var templ_7745c5c3_Var136 string3069+ var templ_7745c5c3_Var148 string
2866- templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)3070+ templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
2867 if templ_7745c5c3_Err != nil {3071 if templ_7745c5c3_Err != nil {
2868- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 54}3072+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 54}
2869 }3073 }
2870- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))3074+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
2871 if templ_7745c5c3_Err != nil {3075 if templ_7745c5c3_Err != nil {
2872 return templ_7745c5c3_Err3076 return templ_7745c5c3_Err
2873 }3077 }
2874- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "</span> ")3078+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, "</span> ")
2875 if templ_7745c5c3_Err != nil {3079 if templ_7745c5c3_Err != nil {
2876 return templ_7745c5c3_Err3080 return templ_7745c5c3_Err
2877 }3081 }
2878 if v.Detail != "" {3082 if v.Detail != "" {
2879- var templ_7745c5c3_Var137 string3083+ var templ_7745c5c3_Var149 string
2880- templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(" ")3084+ templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
2881 if templ_7745c5c3_Err != nil {3085 if templ_7745c5c3_Err != nil {
2882- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 567, Col: 8}3086+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 8}
2883 }3087 }
2884- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))3088+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))
2885 if templ_7745c5c3_Err != nil {3089 if templ_7745c5c3_Err != nil {
2886 return templ_7745c5c3_Err3090 return templ_7745c5c3_Err
2887 }3091 }
2888- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<span class=\"dim\">")3092+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<span class=\"dim\">")
2889 if templ_7745c5c3_Err != nil {3093 if templ_7745c5c3_Err != nil {
2890 return templ_7745c5c3_Err3094 return templ_7745c5c3_Err
2891 }3095 }
2892- var templ_7745c5c3_Var138 string3096+ var templ_7745c5c3_Var150 string
2893- templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)3097+ templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
2894 if templ_7745c5c3_Err != nil {3098 if templ_7745c5c3_Err != nil {
2895- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 567, Col: 38}3099+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 38}
2896 }3100 }
2897- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138))3101+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))
2898 if templ_7745c5c3_Err != nil {3102 if templ_7745c5c3_Err != nil {
2899 return templ_7745c5c3_Err3103 return templ_7745c5c3_Err
2900 }3104 }
2901- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</span>")3105+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "</span>")
2902 if templ_7745c5c3_Err != nil {3106 if templ_7745c5c3_Err != nil {
2903 return templ_7745c5c3_Err3107 return templ_7745c5c3_Err
2904 }3108 }
2905 }3109 }
2906- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</span> ")3110+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "</span> ")
2907 if templ_7745c5c3_Err != nil {3111 if templ_7745c5c3_Err != nil {
2908 return templ_7745c5c3_Err3112 return templ_7745c5c3_Err
2909 }3113 }
2910 if len(v.Paths) > 0 {3114 if len(v.Paths) > 0 {
2911- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")3115+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, "<ul class=\"mg-paths\" aria-label=\"conflicting files\">")
2912 if templ_7745c5c3_Err != nil {3116 if templ_7745c5c3_Err != nil {
2913 return templ_7745c5c3_Err3117 return templ_7745c5c3_Err
2914 }3118 }
2915 for _, path := range v.Paths {3119 for _, path := range v.Paths {
2916- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "<li><code>")3120+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "<li><code>")
2917 if templ_7745c5c3_Err != nil {3121 if templ_7745c5c3_Err != nil {
2918 return templ_7745c5c3_Err3122 return templ_7745c5c3_Err
2919 }3123 }
2920- var templ_7745c5c3_Var139 string3124+ var templ_7745c5c3_Var151 string
2921- templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(path)3125+ templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(path)
2922 if templ_7745c5c3_Err != nil {3126 if templ_7745c5c3_Err != nil {
2923- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 573, Col: 20}3127+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 20}
2924 }3128 }
2925- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139))3129+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
2926 if templ_7745c5c3_Err != nil {3130 if templ_7745c5c3_Err != nil {
2927 return templ_7745c5c3_Err3131 return templ_7745c5c3_Err
2928 }3132 }
2929- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</code></li>")3133+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "</code></li>")
2930 if templ_7745c5c3_Err != nil {3134 if templ_7745c5c3_Err != nil {
2931 return templ_7745c5c3_Err3135 return templ_7745c5c3_Err
2932 }3136 }
2933 }3137 }
2934- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</ul>")3138+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</ul>")
2935 if templ_7745c5c3_Err != nil {3139 if templ_7745c5c3_Err != nil {
2936 return templ_7745c5c3_Err3140 return templ_7745c5c3_Err
2937 }3141 }
2938 }3142 }
2939 if v.PathsNote != "" {3143 if v.PathsNote != "" {
2940- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<p class=\"dim\">")3144+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "<p class=\"dim\">")
2941 if templ_7745c5c3_Err != nil {3145 if templ_7745c5c3_Err != nil {
2942 return templ_7745c5c3_Err3146 return templ_7745c5c3_Err
2943 }3147 }
2944- var templ_7745c5c3_Var140 string3148+ var templ_7745c5c3_Var152 string
2945- templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)3149+ templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
2946 if templ_7745c5c3_Err != nil {3150 if templ_7745c5c3_Err != nil {
2947- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 578, Col: 30}3151+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 606, Col: 30}
2948 }3152 }
2949- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))3153+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
2950 if templ_7745c5c3_Err != nil {3154 if templ_7745c5c3_Err != nil {
2951 return templ_7745c5c3_Err3155 return templ_7745c5c3_Err
2952 }3156 }
2953- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</p>")3157+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "</p>")
2954 if templ_7745c5c3_Err != nil {3158 if templ_7745c5c3_Err != nil {
2955 return templ_7745c5c3_Err3159 return templ_7745c5c3_Err
2956 }3160 }
@@ -2977,12 +3181,12 @@ func mrMergeabilitySlot(v MergeabilityView) templ.Component {
2977 }()3181 }()
2978 }3182 }
2979 ctx = templ.InitializeContext(ctx)3183 ctx = templ.InitializeContext(ctx)
2980- templ_7745c5c3_Var141 := templ.GetChildren(ctx)3184+ templ_7745c5c3_Var153 := templ.GetChildren(ctx)
2981- if templ_7745c5c3_Var141 == nil {3185+ if templ_7745c5c3_Var153 == nil {
2982- templ_7745c5c3_Var141 = templ.NopComponent3186+ templ_7745c5c3_Var153 = templ.NopComponent
2983 }3187 }
2984 ctx = templ.ClearChildren(ctx)3188 ctx = templ.ClearChildren(ctx)
2985- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "<div id=\"mr-mergeability\" class=\"live-slot\" role=\"status\" data-slot>")3189+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, "<div id=\"mr-mergeability\" class=\"live-slot\" role=\"status\" data-slot>")
2986 if templ_7745c5c3_Err != nil {3190 if templ_7745c5c3_Err != nil {
2987 return templ_7745c5c3_Err3191 return templ_7745c5c3_Err
2988 }3192 }
@@ -2990,7 +3194,7 @@ func mrMergeabilitySlot(v MergeabilityView) templ.Component {
2990 if templ_7745c5c3_Err != nil {3194 if templ_7745c5c3_Err != nil {
2991 return templ_7745c5c3_Err3195 return templ_7745c5c3_Err
2992 }3196 }
2993- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "</div>")3197+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "</div>")
2994 if templ_7745c5c3_Err != nil {3198 if templ_7745c5c3_Err != nil {
2995 return templ_7745c5c3_Err3199 return templ_7745c5c3_Err
2996 }3200 }
@@ -3015,9 +3219,9 @@ func MRMergeabilityFragment(v MergeabilityView) templ.Component {
3015 }()3219 }()
3016 }3220 }
3017 ctx = templ.InitializeContext(ctx)3221 ctx = templ.InitializeContext(ctx)
3018- templ_7745c5c3_Var142 := templ.GetChildren(ctx)3222+ templ_7745c5c3_Var154 := templ.GetChildren(ctx)
3019- if templ_7745c5c3_Var142 == nil {3223+ if templ_7745c5c3_Var154 == nil {
3020- templ_7745c5c3_Var142 = templ.NopComponent3224+ templ_7745c5c3_Var154 = templ.NopComponent
3021 }3225 }
3022 ctx = templ.ClearChildren(ctx)3226 ctx = templ.ClearChildren(ctx)
3023 templ_7745c5c3_Err = mrMergeabilitySlot(v).Render(ctx, templ_7745c5c3_Buffer)3227 templ_7745c5c3_Err = mrMergeabilitySlot(v).Render(ctx, templ_7745c5c3_Buffer)
@@ -3046,128 +3250,128 @@ func moreMenu(p MRPage) templ.Component {
3046 }()3250 }()
3047 }3251 }
3048 ctx = templ.InitializeContext(ctx)3252 ctx = templ.InitializeContext(ctx)
3049- templ_7745c5c3_Var143 := templ.GetChildren(ctx)3253+ templ_7745c5c3_Var155 := templ.GetChildren(ctx)
3050- if templ_7745c5c3_Var143 == nil {3254+ if templ_7745c5c3_Var155 == nil {
3051- templ_7745c5c3_Var143 = templ.NopComponent3255+ templ_7745c5c3_Var155 = templ.NopComponent
3052 }3256 }
3053 ctx = templ.ClearChildren(ctx)3257 ctx = templ.ClearChildren(ctx)
3054- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "<details class=\"menu omenu\"><summary class=\"btn icon\" aria-label=\"More actions\" title=\"more actions\">⋯</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"More actions\"><section class=\"merge-settings\"><h2>Merge settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")3258+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "<details class=\"menu omenu\"><summary class=\"btn icon\" aria-label=\"More actions\" title=\"more actions\">⋯</summary><div class=\"menu-pop\" role=\"group\" aria-label=\"More actions\"><section class=\"merge-settings\"><h2>Merge settings</h2><dl class=\"kv\"><dt>modes</dt><dd>")
3055 if templ_7745c5c3_Err != nil {3259 if templ_7745c5c3_Err != nil {
3056 return templ_7745c5c3_Err3260 return templ_7745c5c3_Err
3057 }3261 }
3058- var templ_7745c5c3_Var144 string3262+ var templ_7745c5c3_Var156 string
3059- templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))3263+ templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
3060 if templ_7745c5c3_Err != nil {3264 if templ_7745c5c3_Err != nil {
3061- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 605, Col: 38}3265+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 633, Col: 38}
3062 }3266 }
3063- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var144))3267+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
3064 if templ_7745c5c3_Err != nil {3268 if templ_7745c5c3_Err != nil {
3065 return templ_7745c5c3_Err3269 return templ_7745c5c3_Err
3066 }3270 }
3067- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</dd><dt>checks</dt><dd>")3271+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "</dd><dt>checks</dt><dd>")
3068 if templ_7745c5c3_Err != nil {3272 if templ_7745c5c3_Err != nil {
3069 return templ_7745c5c3_Err3273 return templ_7745c5c3_Err
3070 }3274 }
3071 if p.ChecksBlock {3275 if p.ChecksBlock {
3072- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "failing ones block the merge")3276+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "failing ones block the merge")
3073 if templ_7745c5c3_Err != nil {3277 if templ_7745c5c3_Err != nil {
3074 return templ_7745c5c3_Err3278 return templ_7745c5c3_Err
3075 }3279 }
3076 } else {3280 } else {
3077- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "do not block the merge")3281+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "do not block the merge")
3078 if templ_7745c5c3_Err != nil {3282 if templ_7745c5c3_Err != nil {
3079 return templ_7745c5c3_Err3283 return templ_7745c5c3_Err
3080 }3284 }
3081 }3285 }
3082- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</dd></dl></section>")3286+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "</dd></dl></section>")
3083 if templ_7745c5c3_Err != nil {3287 if templ_7745c5c3_Err != nil {
3084 return templ_7745c5c3_Err3288 return templ_7745c5c3_Err
3085 }3289 }
3086 if p.Box != nil {3290 if p.Box != nil {
3087 if p.Box.Signed {3291 if p.Box.Signed {
3088- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 286, "<p class=\"d-note\" id=\"mr-signing\">squash and merge commits are signed with ermite's own key (generated on first start). ")3292+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "<p class=\"d-note\" id=\"mr-signing\">squash and merge commits are signed with ermite's own key (generated on first start). ")
3089 if templ_7745c5c3_Err != nil {3293 if templ_7745c5c3_Err != nil {
3090 return templ_7745c5c3_Err3294 return templ_7745c5c3_Err
3091 }3295 }
3092 if p.Box.Fingerprint != "" {3296 if p.Box.Fingerprint != "" {
3093- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 287, "fingerprint <code>")3297+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "fingerprint <code>")
3094 if templ_7745c5c3_Err != nil {3298 if templ_7745c5c3_Err != nil {
3095 return templ_7745c5c3_Err3299 return templ_7745c5c3_Err
3096 }3300 }
3097- var templ_7745c5c3_Var145 string3301+ var templ_7745c5c3_Var157 string
3098- templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)3302+ templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
3099 if templ_7745c5c3_Err != nil {3303 if templ_7745c5c3_Err != nil {
3100- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 621, Col: 44}3304+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 44}
3101 }3305 }
3102- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var145))3306+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
3103 if templ_7745c5c3_Err != nil {3307 if templ_7745c5c3_Err != nil {
3104 return templ_7745c5c3_Err3308 return templ_7745c5c3_Err
3105 }3309 }
3106- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 288, "</code> ")3310+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, "</code> ")
3107 if templ_7745c5c3_Err != nil {3311 if templ_7745c5c3_Err != nil {
3108 return templ_7745c5c3_Err3312 return templ_7745c5c3_Err
3109 }3313 }
3110 }3314 }
3111- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 289, "<code>ermite signing-key show</code> prints it.</p>")3315+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "<code>ermite signing-key show</code> prints it.</p>")
3112 if templ_7745c5c3_Err != nil {3316 if templ_7745c5c3_Err != nil {
3113 return templ_7745c5c3_Err3317 return templ_7745c5c3_Err
3114 }3318 }
3115 }3319 }
3116- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 290, " ")3320+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 311, " ")
3117 if templ_7745c5c3_Err != nil {3321 if templ_7745c5c3_Err != nil {
3118 return templ_7745c5c3_Err3322 return templ_7745c5c3_Err
3119 }3323 }
3120 if p.Box.CanMarkRead {3324 if p.Box.CanMarkRead {
3121- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 291, "<form method=\"post\" action=\"")3325+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 312, "<form method=\"post\" action=\"")
3122 if templ_7745c5c3_Err != nil {3326 if templ_7745c5c3_Err != nil {
3123 return templ_7745c5c3_Err3327 return templ_7745c5c3_Err
3124 }3328 }
3125- var templ_7745c5c3_Var146 templ.SafeURL3329+ var templ_7745c5c3_Var158 templ.SafeURL
3126- templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))3330+ templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
3127 if templ_7745c5c3_Err != nil {3331 if templ_7745c5c3_Err != nil {
3128- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 627, Col: 65}3332+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 655, Col: 65}
3129 }3333 }
3130- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var146))3334+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
3131 if templ_7745c5c3_Err != nil {3335 if templ_7745c5c3_Err != nil {
3132 return templ_7745c5c3_Err3336 return templ_7745c5c3_Err
3133 }3337 }
3134- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 292, "\"><input type=\"hidden\" name=\"head_id\" value=\"")3338+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, "\"><input type=\"hidden\" name=\"head_id\" value=\"")
3135 if templ_7745c5c3_Err != nil {3339 if templ_7745c5c3_Err != nil {
3136 return templ_7745c5c3_Err3340 return templ_7745c5c3_Err
3137 }3341 }
3138- var templ_7745c5c3_Var147 string3342+ var templ_7745c5c3_Var159 string
3139- templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)3343+ templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
3140 if templ_7745c5c3_Err != nil {3344 if templ_7745c5c3_Err != nil {
3141- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 628, Col: 60}3345+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 60}
3142 }3346 }
3143- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)3347+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var159)
3144 if templ_7745c5c3_Err != nil {3348 if templ_7745c5c3_Err != nil {
3145 return templ_7745c5c3_Err3349 return templ_7745c5c3_Err
3146 }3350 }
3147- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 293, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\" aria-keyshortcuts=\"Shift+R\" data-read>Mark read <kbd aria-hidden=\"true\">R</kbd></button></form>")3351+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\" class=\"btn\" aria-keyshortcuts=\"Shift+R\" data-read>Mark read <kbd aria-hidden=\"true\">R</kbd></button></form>")
3148 if templ_7745c5c3_Err != nil {3352 if templ_7745c5c3_Err != nil {
3149 return templ_7745c5c3_Err3353 return templ_7745c5c3_Err
3150 }3354 }
3151 }3355 }
3152- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 294, " <form method=\"post\" action=\"")3356+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, " <form method=\"post\" action=\"")
3153 if templ_7745c5c3_Err != nil {3357 if templ_7745c5c3_Err != nil {
3154 return templ_7745c5c3_Err3358 return templ_7745c5c3_Err
3155 }3359 }
3156- var templ_7745c5c3_Var148 templ.SafeURL3360+ var templ_7745c5c3_Var160 templ.SafeURL
3157- templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))3361+ templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
3158 if templ_7745c5c3_Err != nil {3362 if templ_7745c5c3_Err != nil {
3159- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 632, Col: 65}3363+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 660, Col: 65}
3160 }3364 }
3161- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))3365+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
3162 if templ_7745c5c3_Err != nil {3366 if templ_7745c5c3_Err != nil {
3163 return templ_7745c5c3_Err3367 return templ_7745c5c3_Err
3164 }3368 }
3165- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 295, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")3369+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\" class=\"btn danger\">Close</button></form>")
3166 if templ_7745c5c3_Err != nil {3370 if templ_7745c5c3_Err != nil {
3167 return templ_7745c5c3_Err3371 return templ_7745c5c3_Err
3168 }3372 }
3169 if !p.Box.HeadGone && p.Box.ReadyAction == "" {3373 if !p.Box.HeadGone && p.Box.ReadyAction == "" {
3170- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 296, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")3374+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "<details class=\"d-or\"><summary class=\"mute\">or from a terminal</summary>")
3171 if templ_7745c5c3_Err != nil {3375 if templ_7745c5c3_Err != nil {
3172 return templ_7745c5c3_Err3376 return templ_7745c5c3_Err
3173 }3377 }
@@ -3175,13 +3379,13 @@ func moreMenu(p MRPage) templ.Component {
3175 if templ_7745c5c3_Err != nil {3379 if templ_7745c5c3_Err != nil {
3176 return templ_7745c5c3_Err3380 return templ_7745c5c3_Err
3177 }3381 }
3178- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 297, "</details>")3382+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, "</details>")
3179 if templ_7745c5c3_Err != nil {3383 if templ_7745c5c3_Err != nil {
3180 return templ_7745c5c3_Err3384 return templ_7745c5c3_Err
3181 }3385 }
3182 }3386 }
3183 }3387 }
3184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 298, "</div></details>")3388+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 319, "</div></details>")
3185 if templ_7745c5c3_Err != nil {3389 if templ_7745c5c3_Err != nil {
3186 return templ_7745c5c3_Err3390 return templ_7745c5c3_Err
3187 }3391 }
@@ -3205,12 +3409,12 @@ func Compare(l Layout, p ComparePage) templ.Component {
3205 }()3409 }()
3206 }3410 }
3207 ctx = templ.InitializeContext(ctx)3411 ctx = templ.InitializeContext(ctx)
3208- templ_7745c5c3_Var149 := templ.GetChildren(ctx)3412+ templ_7745c5c3_Var161 := templ.GetChildren(ctx)
3209- if templ_7745c5c3_Var149 == nil {3413+ if templ_7745c5c3_Var161 == nil {
3210- templ_7745c5c3_Var149 = templ.NopComponent3414+ templ_7745c5c3_Var161 = templ.NopComponent
3211 }3415 }
3212 ctx = templ.ClearChildren(ctx)3416 ctx = templ.ClearChildren(ctx)
3213- templ_7745c5c3_Var150 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {3417+ templ_7745c5c3_Var162 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3214 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context3418 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
3215 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)3419 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
3216 if !templ_7745c5c3_IsBuffer {3420 if !templ_7745c5c3_IsBuffer {
@@ -3222,263 +3426,263 @@ func Compare(l Layout, p ComparePage) templ.Component {
3222 }()3426 }()
3223 }3427 }
3224 ctx = templ.InitializeContext(ctx)3428 ctx = templ.InitializeContext(ctx)
3225- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 299, "<header class=\"show mr-h\"><h1>")3429+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, "<header class=\"show mr-h\"><h1>")
3226 if templ_7745c5c3_Err != nil {3430 if templ_7745c5c3_Err != nil {
3227 return templ_7745c5c3_Err3431 return templ_7745c5c3_Err
3228 }3432 }
3229- var templ_7745c5c3_Var151 string3433+ var templ_7745c5c3_Var163 string
3230- templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)3434+ templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3231 if templ_7745c5c3_Err != nil {3435 if templ_7745c5c3_Err != nil {
3232- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 17}3436+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 17}
3233 }3437 }
3234- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))3438+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var163))
3235 if templ_7745c5c3_Err != nil {3439 if templ_7745c5c3_Err != nil {
3236 return templ_7745c5c3_Err3440 return templ_7745c5c3_Err
3237 }3441 }
3238- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 300, " → ")3442+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " → ")
3239 if templ_7745c5c3_Err != nil {3443 if templ_7745c5c3_Err != nil {
3240 return templ_7745c5c3_Err3444 return templ_7745c5c3_Err
3241 }3445 }
3242- var templ_7745c5c3_Var152 string3446+ var templ_7745c5c3_Var164 string
3243- templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)3447+ templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3244 if templ_7745c5c3_Err != nil {3448 if templ_7745c5c3_Err != nil {
3245- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 34}3449+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 34}
3246 }3450 }
3247- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))3451+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var164))
3248 if templ_7745c5c3_Err != nil {3452 if templ_7745c5c3_Err != nil {
3249 return templ_7745c5c3_Err3453 return templ_7745c5c3_Err
3250 }3454 }
3251- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 301, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")3455+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"")
3252 if templ_7745c5c3_Err != nil {3456 if templ_7745c5c3_Err != nil {
3253 return templ_7745c5c3_Err3457 return templ_7745c5c3_Err
3254 }3458 }
3255- var templ_7745c5c3_Var153 templ.SafeURL3459+ var templ_7745c5c3_Var165 templ.SafeURL
3256- templ_7745c5c3_Var153, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))3460+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
3257 if templ_7745c5c3_Err != nil {3461 if templ_7745c5c3_Err != nil {
3258- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 652, Col: 45}3462+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 45}
3259 }3463 }
3260- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var153))3464+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
3261 if templ_7745c5c3_Err != nil {3465 if templ_7745c5c3_Err != nil {
3262 return templ_7745c5c3_Err3466 return templ_7745c5c3_Err
3263 }3467 }
3264- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 302, "\">")3468+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, "\">")
3265 if templ_7745c5c3_Err != nil {3469 if templ_7745c5c3_Err != nil {
3266 return templ_7745c5c3_Err3470 return templ_7745c5c3_Err
3267 }3471 }
3268- var templ_7745c5c3_Var154 string3472+ var templ_7745c5c3_Var166 string
3269- templ_7745c5c3_Var154, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)3473+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3270 if templ_7745c5c3_Err != nil {3474 if templ_7745c5c3_Err != nil {
3271- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 652, Col: 58}3475+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 58}
3272 }3476 }
3273- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var154))3477+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3274 if templ_7745c5c3_Err != nil {3478 if templ_7745c5c3_Err != nil {
3275 return templ_7745c5c3_Err3479 return templ_7745c5c3_Err
3276 }3480 }
3277- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 303, "</a></dd><dt>target</dt><dd><a href=\"")3481+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, "</a></dd><dt>target</dt><dd><a href=\"")
3278 if templ_7745c5c3_Err != nil {3482 if templ_7745c5c3_Err != nil {
3279 return templ_7745c5c3_Err3483 return templ_7745c5c3_Err
3280 }3484 }
3281- var templ_7745c5c3_Var155 templ.SafeURL3485+ var templ_7745c5c3_Var167 templ.SafeURL
3282- templ_7745c5c3_Var155, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))3486+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
3283 if templ_7745c5c3_Err != nil {3487 if templ_7745c5c3_Err != nil {
3284- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 654, Col: 45}3488+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 45}
3285 }3489 }
3286- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var155))3490+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
3287 if templ_7745c5c3_Err != nil {3491 if templ_7745c5c3_Err != nil {
3288 return templ_7745c5c3_Err3492 return templ_7745c5c3_Err
3289 }3493 }
3290- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "\">")3494+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, "\">")
3291 if templ_7745c5c3_Err != nil {3495 if templ_7745c5c3_Err != nil {
3292 return templ_7745c5c3_Err3496 return templ_7745c5c3_Err
3293 }3497 }
3294- var templ_7745c5c3_Var156 string3498+ var templ_7745c5c3_Var168 string
3295- templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)3499+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3296 if templ_7745c5c3_Err != nil {3500 if templ_7745c5c3_Err != nil {
3297- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 654, Col: 58}3501+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 58}
3298 }3502 }
3299- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))3503+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
3300 if templ_7745c5c3_Err != nil {3504 if templ_7745c5c3_Err != nil {
3301 return templ_7745c5c3_Err3505 return templ_7745c5c3_Err
3302 }3506 }
3303- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 305, "</a></dd><dt>ahead</dt><dd>")3507+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, "</a></dd><dt>ahead</dt><dd>")
3304 if templ_7745c5c3_Err != nil {3508 if templ_7745c5c3_Err != nil {
3305 return templ_7745c5c3_Err3509 return templ_7745c5c3_Err
3306 }3510 }
3307- var templ_7745c5c3_Var157 string3511+ var templ_7745c5c3_Var169 string
3308- templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)3512+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
3309 if templ_7745c5c3_Err != nil {3513 if templ_7745c5c3_Err != nil {
3310- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 21}3514+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 684, Col: 21}
3311 }3515 }
3312- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))3516+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
3313 if templ_7745c5c3_Err != nil {3517 if templ_7745c5c3_Err != nil {
3314 return templ_7745c5c3_Err3518 return templ_7745c5c3_Err
3315 }3519 }
3316- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 306, "</dd></dl></header>")3520+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, "</dd></dl></header>")
3317 if templ_7745c5c3_Err != nil {3521 if templ_7745c5c3_Err != nil {
3318 return templ_7745c5c3_Err3522 return templ_7745c5c3_Err
3319 }3523 }
3320 if p.Note != "" {3524 if p.Note != "" {
3321- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 307, "<p class=\"notice\">")3525+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "<p class=\"notice\">")
3322 if templ_7745c5c3_Err != nil {3526 if templ_7745c5c3_Err != nil {
3323 return templ_7745c5c3_Err3527 return templ_7745c5c3_Err
3324 }3528 }
3325- var templ_7745c5c3_Var158 string3529+ var templ_7745c5c3_Var170 string
3326- templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)3530+ templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
3327 if templ_7745c5c3_Err != nil {3531 if templ_7745c5c3_Err != nil {
3328- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 660, Col: 29}3532+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 688, Col: 29}
3329 }3533 }
3330- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))3534+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170))
3331 if templ_7745c5c3_Err != nil {3535 if templ_7745c5c3_Err != nil {
3332 return templ_7745c5c3_Err3536 return templ_7745c5c3_Err
3333 }3537 }
3334- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 308, "</p>")3538+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, "</p>")
3335 if templ_7745c5c3_Err != nil {3539 if templ_7745c5c3_Err != nil {
3336 return templ_7745c5c3_Err3540 return templ_7745c5c3_Err
3337 }3541 }
3338 }3542 }
3339- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 309, " ")3543+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, " ")
3340 if templ_7745c5c3_Err != nil {3544 if templ_7745c5c3_Err != nil {
3341 return templ_7745c5c3_Err3545 return templ_7745c5c3_Err
3342 }3546 }
3343 if p.Existing != nil {3547 if p.Existing != nil {
3344- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 310, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")3548+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"")
3345 if templ_7745c5c3_Err != nil {3549 if templ_7745c5c3_Err != nil {
3346 return templ_7745c5c3_Err3550 return templ_7745c5c3_Err
3347 }3551 }
3348- var templ_7745c5c3_Var159 templ.SafeURL3552+ var templ_7745c5c3_Var171 templ.SafeURL
3349- templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))3553+ templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
3350 if templ_7745c5c3_Err != nil {3554 if templ_7745c5c3_Err != nil {
3351- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 663, Col: 114}3555+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 114}
3352 }3556 }
3353- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var159))3557+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
3354 if templ_7745c5c3_Err != nil {3558 if templ_7745c5c3_Err != nil {
3355 return templ_7745c5c3_Err3559 return templ_7745c5c3_Err
3356 }3560 }
3357- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 311, "\">")3561+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, "\">")
3358 if templ_7745c5c3_Err != nil {3562 if templ_7745c5c3_Err != nil {
3359 return templ_7745c5c3_Err3563 return templ_7745c5c3_Err
3360 }3564 }
3361- var templ_7745c5c3_Var160 string3565+ var templ_7745c5c3_Var172 string
3362- templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)3566+ templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
3363 if templ_7745c5c3_Err != nil {3567 if templ_7745c5c3_Err != nil {
3364- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 663, Col: 135}3568+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 135}
3365 }3569 }
3366- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))3570+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
3367 if templ_7745c5c3_Err != nil {3571 if templ_7745c5c3_Err != nil {
3368 return templ_7745c5c3_Err3572 return templ_7745c5c3_Err
3369 }3573 }
3370- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 312, "</a></p>")3574+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "</a></p>")
3371 if templ_7745c5c3_Err != nil {3575 if templ_7745c5c3_Err != nil {
3372 return templ_7745c5c3_Err3576 return templ_7745c5c3_Err
3373 }3577 }
3374 }3578 }
3375- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 313, " ")3579+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, " ")
3376 if templ_7745c5c3_Err != nil {3580 if templ_7745c5c3_Err != nil {
3377 return templ_7745c5c3_Err3581 return templ_7745c5c3_Err
3378 }3582 }
3379 if p.Create != nil {3583 if p.Create != nil {
3380- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 314, "<form class=\"cform create\" method=\"post\" action=\"")3584+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "<form class=\"cform create\" method=\"post\" action=\"")
3381 if templ_7745c5c3_Err != nil {3585 if templ_7745c5c3_Err != nil {
3382 return templ_7745c5c3_Err3586 return templ_7745c5c3_Err
3383 }3587 }
3384- var templ_7745c5c3_Var161 templ.SafeURL3588+ var templ_7745c5c3_Var173 templ.SafeURL
3385- templ_7745c5c3_Var161, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))3589+ templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
3386 if templ_7745c5c3_Err != nil {3590 if templ_7745c5c3_Err != nil {
3387- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 666, Col: 83}3591+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 694, Col: 83}
3388 }3592 }
3389- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var161))3593+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
3390 if templ_7745c5c3_Err != nil {3594 if templ_7745c5c3_Err != nil {
3391 return templ_7745c5c3_Err3595 return templ_7745c5c3_Err
3392 }3596 }
3393- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 315, "\"><input type=\"hidden\" name=\"source\" value=\"")3597+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "\"><input type=\"hidden\" name=\"source\" value=\"")
3394 if templ_7745c5c3_Err != nil {3598 if templ_7745c5c3_Err != nil {
3395 return templ_7745c5c3_Err3599 return templ_7745c5c3_Err
3396 }3600 }
3397- var templ_7745c5c3_Var162 string3601+ var templ_7745c5c3_Var174 string
3398- templ_7745c5c3_Var162, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)3602+ templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
3399 if templ_7745c5c3_Err != nil {3603 if templ_7745c5c3_Err != nil {
3400- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 667, Col: 62}3604+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 62}
3401 }3605 }
3402- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var162)3606+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var174)
3403 if templ_7745c5c3_Err != nil {3607 if templ_7745c5c3_Err != nil {
3404 return templ_7745c5c3_Err3608 return templ_7745c5c3_Err
3405 }3609 }
3406- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 316, "\"> <input type=\"hidden\" name=\"target\" value=\"")3610+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "\"> <input type=\"hidden\" name=\"target\" value=\"")
3407 if templ_7745c5c3_Err != nil {3611 if templ_7745c5c3_Err != nil {
3408 return templ_7745c5c3_Err3612 return templ_7745c5c3_Err
3409 }3613 }
3410- var templ_7745c5c3_Var163 string3614+ var templ_7745c5c3_Var175 string
3411- templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)3615+ templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
3412 if templ_7745c5c3_Err != nil {3616 if templ_7745c5c3_Err != nil {
3413- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 668, Col: 62}3617+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 696, Col: 62}
3414 }3618 }
3415- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var163)3619+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var175)
3416 if templ_7745c5c3_Err != nil {3620 if templ_7745c5c3_Err != nil {
3417 return templ_7745c5c3_Err3621 return templ_7745c5c3_Err
3418 }3622 }
3419- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 317, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")3623+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"")
3420 if templ_7745c5c3_Err != nil {3624 if templ_7745c5c3_Err != nil {
3421 return templ_7745c5c3_Err3625 return templ_7745c5c3_Err
3422 }3626 }
3423- var templ_7745c5c3_Var164 string3627+ var templ_7745c5c3_Var176 string
3424- templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)3628+ templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
3425 if templ_7745c5c3_Err != nil {3629 if templ_7745c5c3_Err != nil {
3426- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 670, Col: 97}3630+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 698, Col: 97}
3427 }3631 }
3428- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var164)3632+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var176)
3429 if templ_7745c5c3_Err != nil {3633 if templ_7745c5c3_Err != nil {
3430 return templ_7745c5c3_Err3634 return templ_7745c5c3_Err
3431 }3635 }
3432- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 318, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"Markdown, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"btn primary\">Create merge request</button></div></form>")3636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, "\" 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>")
3433 if templ_7745c5c3_Err != nil {3637 if templ_7745c5c3_Err != nil {
3434 return templ_7745c5c3_Err3638 return templ_7745c5c3_Err
3435 }3639 }
3436 }3640 }
3437- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 319, " ")3641+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, " ")
3438 if templ_7745c5c3_Err != nil {3642 if templ_7745c5c3_Err != nil {
3439 return templ_7745c5c3_Err3643 return templ_7745c5c3_Err
3440 }3644 }
3441 if p.HasDiff {3645 if p.HasDiff {
3442- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 320, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")3646+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, "<div class=\"diff-tools\" role=\"group\" aria-label=\"Diff view\"><span class=\"dim\">")
3443 if templ_7745c5c3_Err != nil {3647 if templ_7745c5c3_Err != nil {
3444 return templ_7745c5c3_Err3648 return templ_7745c5c3_Err
3445 }3649 }
3446- var templ_7745c5c3_Var165 string3650+ var templ_7745c5c3_Var177 string
3447- templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))3651+ templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
3448 if templ_7745c5c3_Err != nil {3652 if templ_7745c5c3_Err != nil {
3449- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 57}3653+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 57}
3450 }3654 }
3451- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))3655+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))
3452 if templ_7745c5c3_Err != nil {3656 if templ_7745c5c3_Err != nil {
3453 return templ_7745c5c3_Err3657 return templ_7745c5c3_Err
3454 }3658 }
3455- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 321, " · <span class=\"plus\">+")3659+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, " · <span class=\"plus\">+")
3456 if templ_7745c5c3_Err != nil {3660 if templ_7745c5c3_Err != nil {
3457 return templ_7745c5c3_Err3661 return templ_7745c5c3_Err
3458 }3662 }
3459- var templ_7745c5c3_Var166 string3663+ var templ_7745c5c3_Var178 string
3460- templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))3664+ templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
3461 if templ_7745c5c3_Err != nil {3665 if templ_7745c5c3_Err != nil {
3462- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 97}3666+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 97}
3463 }3667 }
3464- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))3668+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))
3465 if templ_7745c5c3_Err != nil {3669 if templ_7745c5c3_Err != nil {
3466 return templ_7745c5c3_Err3670 return templ_7745c5c3_Err
3467 }3671 }
3468- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 322, "</span> <span class=\"minus\">-")3672+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "</span> <span class=\"minus\">-")
3469 if templ_7745c5c3_Err != nil {3673 if templ_7745c5c3_Err != nil {
3470 return templ_7745c5c3_Err3674 return templ_7745c5c3_Err
3471 }3675 }
3472- var templ_7745c5c3_Var167 string3676+ var templ_7745c5c3_Var179 string
3473- templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))3677+ templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
3474 if templ_7745c5c3_Err != nil {3678 if templ_7745c5c3_Err != nil {
3475- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 142}3679+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 142}
3476 }3680 }
3477- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))3681+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
3478 if templ_7745c5c3_Err != nil {3682 if templ_7745c5c3_Err != nil {
3479 return templ_7745c5c3_Err3683 return templ_7745c5c3_Err
3480 }3684 }
3481- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 323, "</span></span> <span class=\"grow\"></span>")3685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "</span></span> <span class=\"grow\"></span>")
3482 if templ_7745c5c3_Err != nil {3686 if templ_7745c5c3_Err != nil {
3483 return templ_7745c5c3_Err3687 return templ_7745c5c3_Err
3484 }3688 }
@@ -3486,64 +3690,64 @@ func Compare(l Layout, p ComparePage) templ.Component {
3486 if templ_7745c5c3_Err != nil {3690 if templ_7745c5c3_Err != nil {
3487 return templ_7745c5c3_Err3691 return templ_7745c5c3_Err
3488 }3692 }
3489- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 324, "</div>")3693+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</div>")
3490 if templ_7745c5c3_Err != nil {3694 if templ_7745c5c3_Err != nil {
3491 return templ_7745c5c3_Err3695 return templ_7745c5c3_Err
3492 }3696 }
3493 if p.Invisible {3697 if p.Invisible {
3494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 325, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\">&lt;U+XXXX&gt;</span> markers, never applied.</p>")3698+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 346, "<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>")
3495 if templ_7745c5c3_Err != nil {3699 if templ_7745c5c3_Err != nil {
3496 return templ_7745c5c3_Err3700 return templ_7745c5c3_Err
3497 }3701 }
3498 }3702 }
3499- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 326, " <div id=\"diffs\"")3703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 347, " <div id=\"diffs\"")
3500 if templ_7745c5c3_Err != nil {3704 if templ_7745c5c3_Err != nil {
3501 return templ_7745c5c3_Err3705 return templ_7745c5c3_Err
3502 }3706 }
3503 if p.Both {3707 if p.Both {
3504- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 327, " data-diff=\"")3708+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 348, " data-diff=\"")
3505 if templ_7745c5c3_Err != nil {3709 if templ_7745c5c3_Err != nil {
3506 return templ_7745c5c3_Err3710 return templ_7745c5c3_Err
3507 }3711 }
3508- var templ_7745c5c3_Var168 string3712+ var templ_7745c5c3_Var180 string
3509- templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)3713+ templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3510 if templ_7745c5c3_Err != nil {3714 if templ_7745c5c3_Err != nil {
3511- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 693, Col: 23}3715+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 721, Col: 23}
3512 }3716 }
3513- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var168)3717+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var180)
3514 if templ_7745c5c3_Err != nil {3718 if templ_7745c5c3_Err != nil {
3515 return templ_7745c5c3_Err3719 return templ_7745c5c3_Err
3516 }3720 }
3517- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 328, "\"")3721+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 349, "\"")
3518 if templ_7745c5c3_Err != nil {3722 if templ_7745c5c3_Err != nil {
3519 return templ_7745c5c3_Err3723 return templ_7745c5c3_Err
3520 }3724 }
3521 } else {3725 } else {
3522- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 329, " data-view=\"")3726+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 350, " data-view=\"")
3523 if templ_7745c5c3_Err != nil {3727 if templ_7745c5c3_Err != nil {
3524 return templ_7745c5c3_Err3728 return templ_7745c5c3_Err
3525 }3729 }
3526- var templ_7745c5c3_Var169 string3730+ var templ_7745c5c3_Var181 string
3527- templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)3731+ templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3528 if templ_7745c5c3_Err != nil {3732 if templ_7745c5c3_Err != nil {
3529- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 23}3733+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 723, Col: 23}
3530 }3734 }
3531- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var169)3735+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var181)
3532 if templ_7745c5c3_Err != nil {3736 if templ_7745c5c3_Err != nil {
3533 return templ_7745c5c3_Err3737 return templ_7745c5c3_Err
3534 }3738 }
3535- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 330, "\"")3739+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 351, "\"")
3536 if templ_7745c5c3_Err != nil {3740 if templ_7745c5c3_Err != nil {
3537 return templ_7745c5c3_Err3741 return templ_7745c5c3_Err
3538 }3742 }
3539 }3743 }
3540 if p.Both && !p.Pinned {3744 if p.Both && !p.Pinned {
3541- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 331, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")3745+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 352, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"")
3542 if templ_7745c5c3_Err != nil {3746 if templ_7745c5c3_Err != nil {
3543 return templ_7745c5c3_Err3747 return templ_7745c5c3_Err
3544 }3748 }
3545 }3749 }
3546- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 332, ">")3750+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 353, ">")
3547 if templ_7745c5c3_Err != nil {3751 if templ_7745c5c3_Err != nil {
3548 return templ_7745c5c3_Err3752 return templ_7745c5c3_Err
3549 }3753 }
@@ -3553,14 +3757,14 @@ func Compare(l Layout, p ComparePage) templ.Component {
3553 return templ_7745c5c3_Err3757 return templ_7745c5c3_Err
3554 }3758 }
3555 }3759 }
3556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 333, "</div>")3760+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 354, "</div>")
3557 if templ_7745c5c3_Err != nil {3761 if templ_7745c5c3_Err != nil {
3558 return templ_7745c5c3_Err3762 return templ_7745c5c3_Err
3559 }3763 }
3560 }3764 }
3561 return nil3765 return nil
3562 })3766 })
3563- templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var150), templ_7745c5c3_Buffer)3767+ templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var162), templ_7745c5c3_Buffer)
3564 if templ_7745c5c3_Err != nil {3768 if templ_7745c5c3_Err != nil {
3565 return templ_7745c5c3_Err3769 return templ_7745c5c3_Err
3566 }3770 }
@@ -3584,173 +3788,173 @@ func compareRail(p ComparePage) templ.Component {
3584 }()3788 }()
3585 }3789 }
3586 ctx = templ.InitializeContext(ctx)3790 ctx = templ.InitializeContext(ctx)
3587- templ_7745c5c3_Var170 := templ.GetChildren(ctx)3791+ templ_7745c5c3_Var182 := templ.GetChildren(ctx)
3588- if templ_7745c5c3_Var170 == nil {3792+ if templ_7745c5c3_Var182 == nil {
3589- templ_7745c5c3_Var170 = templ.NopComponent3793+ templ_7745c5c3_Var182 = templ.NopComponent
3590 }3794 }
3591 ctx = templ.ClearChildren(ctx)3795 ctx = templ.ClearChildren(ctx)
3592- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 334, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")3796+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 355, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">Commits <span class=\"count\">")
3593 if templ_7745c5c3_Err != nil {3797 if templ_7745c5c3_Err != nil {
3594 return templ_7745c5c3_Err3798 return templ_7745c5c3_Err
3595 }3799 }
3596- var templ_7745c5c3_Var171 string3800+ var templ_7745c5c3_Var183 string
3597- templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))3801+ templ_7745c5c3_Var183, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
3598 if templ_7745c5c3_Err != nil {3802 if templ_7745c5c3_Err != nil {
3599- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 711, Col: 67}3803+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 739, Col: 67}
3600 }3804 }
3601- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))3805+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var183))
3602 if templ_7745c5c3_Err != nil {3806 if templ_7745c5c3_Err != nil {
3603 return templ_7745c5c3_Err3807 return templ_7745c5c3_Err
3604 }3808 }
3605- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 335, "</span></h2><ul class=\"pcommits\">")3809+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 356, "</span></h2><ul class=\"pcommits\">")
3606 if templ_7745c5c3_Err != nil {3810 if templ_7745c5c3_Err != nil {
3607 return templ_7745c5c3_Err3811 return templ_7745c5c3_Err
3608 }3812 }
3609 for _, c := range p.Ahead {3813 for _, c := range p.Ahead {
3610- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 336, "<li><a href=\"")3814+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 357, "<li><a href=\"")
3611 if templ_7745c5c3_Err != nil {3815 if templ_7745c5c3_Err != nil {
3612 return templ_7745c5c3_Err3816 return templ_7745c5c3_Err
3613 }3817 }
3614- var templ_7745c5c3_Var172 templ.SafeURL3818+ var templ_7745c5c3_Var184 templ.SafeURL
3615- templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))3819+ templ_7745c5c3_Var184, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
3616 if templ_7745c5c3_Err != nil {3820 if templ_7745c5c3_Err != nil {
3617- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 715, Col: 37}3821+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 37}
3618 }3822 }
3619- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))3823+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var184))
3620 if templ_7745c5c3_Err != nil {3824 if templ_7745c5c3_Err != nil {
3621 return templ_7745c5c3_Err3825 return templ_7745c5c3_Err
3622 }3826 }
3623- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 337, "\">")3827+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 358, "\">")
3624 if templ_7745c5c3_Err != nil {3828 if templ_7745c5c3_Err != nil {
3625 return templ_7745c5c3_Err3829 return templ_7745c5c3_Err
3626 }3830 }
3627- var templ_7745c5c3_Var173 string3831+ var templ_7745c5c3_Var185 string
3628- templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)3832+ templ_7745c5c3_Var185, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
3629 if templ_7745c5c3_Err != nil {3833 if templ_7745c5c3_Err != nil {
3630- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 715, Col: 49}3834+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 49}
3631 }3835 }
3632- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))3836+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var185))
3633 if templ_7745c5c3_Err != nil {3837 if templ_7745c5c3_Err != nil {
3634 return templ_7745c5c3_Err3838 return templ_7745c5c3_Err
3635 }3839 }
3636- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 338, "</a> <span class=\"s\">")3840+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 359, "</a> <span class=\"s\">")
3637 if templ_7745c5c3_Err != nil {3841 if templ_7745c5c3_Err != nil {
3638 return templ_7745c5c3_Err3842 return templ_7745c5c3_Err
3639 }3843 }
3640- var templ_7745c5c3_Var174 string3844+ var templ_7745c5c3_Var186 string
3641- templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)3845+ templ_7745c5c3_Var186, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
3642 if templ_7745c5c3_Err != nil {3846 if templ_7745c5c3_Err != nil {
3643- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 716, Col: 32}3847+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 32}
3644 }3848 }
3645- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var174))3849+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var186))
3646 if templ_7745c5c3_Err != nil {3850 if templ_7745c5c3_Err != nil {
3647 return templ_7745c5c3_Err3851 return templ_7745c5c3_Err
3648 }3852 }
3649- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 339, "</span> <span class=\"mute\">")3853+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 360, "</span> <span class=\"mute\">")
3650 if templ_7745c5c3_Err != nil {3854 if templ_7745c5c3_Err != nil {
3651 return templ_7745c5c3_Err3855 return templ_7745c5c3_Err
3652 }3856 }
3653- var templ_7745c5c3_Var175 string3857+ var templ_7745c5c3_Var187 string
3654- templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)3858+ templ_7745c5c3_Var187, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
3655 if templ_7745c5c3_Err != nil {3859 if templ_7745c5c3_Err != nil {
3656- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 718, Col: 16}3860+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 746, Col: 16}
3657 }3861 }
3658- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var175))3862+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var187))
3659 if templ_7745c5c3_Err != nil {3863 if templ_7745c5c3_Err != nil {
3660 return templ_7745c5c3_Err3864 return templ_7745c5c3_Err
3661 }3865 }
3662- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 340, " ")3866+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 361, " ")
3663 if templ_7745c5c3_Err != nil {3867 if templ_7745c5c3_Err != nil {
3664 return templ_7745c5c3_Err3868 return templ_7745c5c3_Err
3665 }3869 }
3666 if c.WhenAt != "" {3870 if c.WhenAt != "" {
3667- var templ_7745c5c3_Var176 string3871+ var templ_7745c5c3_Var188 string
3668- templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")3872+ templ_7745c5c3_Var188, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
3669 if templ_7745c5c3_Err != nil {3873 if templ_7745c5c3_Err != nil {
3670- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 15}3874+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 15}
3671 }3875 }
3672- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var176))3876+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var188))
3673 if templ_7745c5c3_Err != nil {3877 if templ_7745c5c3_Err != nil {
3674 return templ_7745c5c3_Err3878 return templ_7745c5c3_Err
3675 }3879 }
3676- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 341, "<time datetime=\"")3880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 362, "<time datetime=\"")
3677 if templ_7745c5c3_Err != nil {3881 if templ_7745c5c3_Err != nil {
3678 return templ_7745c5c3_Err3882 return templ_7745c5c3_Err
3679 }3883 }
3680- var templ_7745c5c3_Var177 string3884+ var templ_7745c5c3_Var189 string
3681- templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)3885+ templ_7745c5c3_Var189, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
3682 if templ_7745c5c3_Err != nil {3886 if templ_7745c5c3_Err != nil {
3683- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 42}3887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 42}
3684 }3888 }
3685- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var177)3889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var189)
3686 if templ_7745c5c3_Err != nil {3890 if templ_7745c5c3_Err != nil {
3687 return templ_7745c5c3_Err3891 return templ_7745c5c3_Err
3688 }3892 }
3689- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 342, "\">")3893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 363, "\">")
3690 if templ_7745c5c3_Err != nil {3894 if templ_7745c5c3_Err != nil {
3691 return templ_7745c5c3_Err3895 return templ_7745c5c3_Err
3692 }3896 }
3693- var templ_7745c5c3_Var178 string3897+ var templ_7745c5c3_Var190 string
3694- templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)3898+ templ_7745c5c3_Var190, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
3695 if templ_7745c5c3_Err != nil {3899 if templ_7745c5c3_Err != nil {
3696- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 720, Col: 53}3900+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 53}
3697 }3901 }
3698- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))3902+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var190))
3699 if templ_7745c5c3_Err != nil {3903 if templ_7745c5c3_Err != nil {
3700 return templ_7745c5c3_Err3904 return templ_7745c5c3_Err
3701 }3905 }
3702- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 343, "</time>")3906+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 364, "</time>")
3703 if templ_7745c5c3_Err != nil {3907 if templ_7745c5c3_Err != nil {
3704 return templ_7745c5c3_Err3908 return templ_7745c5c3_Err
3705 }3909 }
3706 }3910 }
3707- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 344, "</span></li>")3911+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 365, "</span></li>")
3708 if templ_7745c5c3_Err != nil {3912 if templ_7745c5c3_Err != nil {
3709 return templ_7745c5c3_Err3913 return templ_7745c5c3_Err
3710 }3914 }
3711 }3915 }
3712- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 345, "</ul>")3916+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 366, "</ul>")
3713 if templ_7745c5c3_Err != nil {3917 if templ_7745c5c3_Err != nil {
3714 return templ_7745c5c3_Err3918 return templ_7745c5c3_Err
3715 }3919 }
3716 if len(p.Ahead) == 0 {3920 if len(p.Ahead) == 0 {
3717- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 346, "<p class=\"dim\">no commit ahead of ")3921+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 367, "<p class=\"dim\">no commit ahead of ")
3718 if templ_7745c5c3_Err != nil {3922 if templ_7745c5c3_Err != nil {
3719 return templ_7745c5c3_Err3923 return templ_7745c5c3_Err
3720 }3924 }
3721- var templ_7745c5c3_Var179 string3925+ var templ_7745c5c3_Var191 string
3722- templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)3926+ templ_7745c5c3_Var191, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3723 if templ_7745c5c3_Err != nil {3927 if templ_7745c5c3_Err != nil {
3724- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 727, Col: 47}3928+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 755, Col: 47}
3725 }3929 }
3726- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))3930+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var191))
3727 if templ_7745c5c3_Err != nil {3931 if templ_7745c5c3_Err != nil {
3728 return templ_7745c5c3_Err3932 return templ_7745c5c3_Err
3729 }3933 }
3730- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 347, ".</p>")3934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 368, ".</p>")
3731 if templ_7745c5c3_Err != nil {3935 if templ_7745c5c3_Err != nil {
3732 return templ_7745c5c3_Err3936 return templ_7745c5c3_Err
3733 }3937 }
3734 }3938 }
3735- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 348, "</section>")3939+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 369, "</section>")
3736 if templ_7745c5c3_Err != nil {3940 if templ_7745c5c3_Err != nil {
3737 return templ_7745c5c3_Err3941 return templ_7745c5c3_Err
3738 }3942 }
3739 if p.HasDiff {3943 if p.HasDiff {
3740- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 349, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")3944+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 370, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">Files <span class=\"count\">")
3741 if templ_7745c5c3_Err != nil {3945 if templ_7745c5c3_Err != nil {
3742 return templ_7745c5c3_Err3946 return templ_7745c5c3_Err
3743 }3947 }
3744- var templ_7745c5c3_Var180 string3948+ var templ_7745c5c3_Var192 string
3745- templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))3949+ templ_7745c5c3_Var192, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
3746 if templ_7745c5c3_Err != nil {3950 if templ_7745c5c3_Err != nil {
3747- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 732, Col: 66}3951+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 760, Col: 66}
3748 }3952 }
3749- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var180))3953+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var192))
3750 if templ_7745c5c3_Err != nil {3954 if templ_7745c5c3_Err != nil {
3751 return templ_7745c5c3_Err3955 return templ_7745c5c3_Err
3752 }3956 }
3753- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 350, "</span></h2>")3957+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 371, "</span></h2>")
3754 if templ_7745c5c3_Err != nil {3958 if templ_7745c5c3_Err != nil {
3755 return templ_7745c5c3_Err3959 return templ_7745c5c3_Err
3756 }3960 }
@@ -3758,12 +3962,12 @@ func compareRail(p ComparePage) templ.Component {
3758 if templ_7745c5c3_Err != nil {3962 if templ_7745c5c3_Err != nil {
3759 return templ_7745c5c3_Err3963 return templ_7745c5c3_Err
3760 }3964 }
3761- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 351, "</section>")3965+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 372, "</section>")
3762 if templ_7745c5c3_Err != nil {3966 if templ_7745c5c3_Err != nil {
3763 return templ_7745c5c3_Err3967 return templ_7745c5c3_Err
3764 }3968 }
3765 }3969 }
3766- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 352, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")3970+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 373, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">From a terminal</h2>")
3767 if templ_7745c5c3_Err != nil {3971 if templ_7745c5c3_Err != nil {
3768 return templ_7745c5c3_Err3972 return templ_7745c5c3_Err
3769 }3973 }
@@ -3771,7 +3975,7 @@ func compareRail(p ComparePage) templ.Component {
3771 if templ_7745c5c3_Err != nil {3975 if templ_7745c5c3_Err != nil {
3772 return templ_7745c5c3_Err3976 return templ_7745c5c3_Err
3773 }3977 }
3774- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 353, "</section>")3978+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 374, "</section>")
3775 if templ_7745c5c3_Err != nil {3979 if templ_7745c5c3_Err != nil {
3776 return templ_7745c5c3_Err3980 return templ_7745c5c3_Err
3777 }3981 }
M internal/adapters/web/mr_test.go +103 -1
@@ -4,8 +4,11 @@ package web
44
55 import (
66 "context"
7- "time"
7+ "net/http"
8+ "net/http/httptest"
9+ "strings"
810 "testing"
11+ "time"
912
1013 "github.com/stretchr/testify/assert"
1114 "github.com/stretchr/testify/mock"
@@ -195,3 +198,102 @@ func TestMRReadPresentationCommitCount(t *testing.T) {
195198 })
196199 }
197200 }
201+
202+func TestMergeBoxAutoMerge(t *testing.T) {
203+ t.Parallel()
204+ mg := mergeabilityView(domain.Mergeability{State: domain.MergeabilityUpToDate}, "main")
205+ mk := func(allow bool, runs ports.RunService) *MergeBox {
206+ v := repoWithModes(domain.MergeModes()...)
207+ v.Settings.AllowAutoMerge = allow
208+ return (&Server{cfg: Config{Runs: runs}}).mergeBox(&repoData{repo: v}, 1, "feat", "abc", false, mg)
209+ }
210+ assert.False(t, mk(false, mocks.NewRunService(t)).CanAutoMerge)
211+ assert.False(t, mk(true, nil).CanAutoMerge, "no CI, nothing to wait for")
212+ on := mk(true, mocks.NewRunService(t))
213+ assert.True(t, on.CanAutoMerge)
214+
215+ page := MRPage{Target: "main", Box: on}
216+ html := renderToString(t, mergeMenu(page))
217+ assert.Contains(t, html, "Merge when ready")
218+ assert.Contains(t, html, `action="`+on.AutoAction+`"`)
219+ assert.NotContains(t, html, "Cancel auto-merge")
220+ // Current mergeability must only gate the immediate form. Every allowed
221+ // mode remains selectable in the separate auto-merge form.
222+ for i := range on.Modes {
223+ on.Modes[i].Disabled = true
224+ }
225+ on.CanDeleteBranch = true
226+ page.Source = "feat"
227+ html = renderToString(t, mergeMenu(page))
228+ _, autoForm, found := strings.Cut(html, `action="`+on.AutoAction+`"`)
229+ require.True(t, found)
230+ autoForm, _, _ = strings.Cut(autoForm, "</form>")
231+ assert.NotContains(t, autoForm, "delete_branch")
232+ assert.NotContains(t, autoForm, "disabled")
233+ assert.Contains(t, autoForm, `name="head_id"`)
234+ for _, mode := range on.Modes {
235+ assert.Contains(t, autoForm, `value="`+mode.Value+`"`)
236+ }
237+ page.Box = mk(false, mocks.NewRunService(t))
238+ assert.NotContains(t, renderToString(t, mergeMenu(page)), "Merge when ready")
239+
240+ on.AutoMerge = &AutoMergeView{Mode: "squash", EnabledBy: "owner", HeadShort: "abc"}
241+ html = renderToString(t, mergeMenu(MRPage{Target: "main", Box: on}))
242+ assert.Contains(t, html, "Auto-merge: squash when ready, enabled by owner")
243+ assert.Contains(t, html, `action="`+on.CancelAutoAction+`"`)
244+ assert.Contains(t, html, "Cancel auto-merge")
245+}
246+
247+func autoMergeRequest(path, body string) *http.Request {
248+ r := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
249+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
250+ r.SetPathValue("repo", "demo")
251+ r.SetPathValue("n", "3")
252+ return r
253+}
254+
255+func TestAutoMergeHandlers(t *testing.T) {
256+ t.Parallel()
257+ name, err := domain.ParseRepoName("demo")
258+ require.NoError(t, err)
259+ ctx := context.Background()
260+
261+ head, err := domain.ParseObjectID(strings.Repeat("a", domain.ObjectIDLen))
262+ require.NoError(t, err)
263+ svc := mocks.NewMergeRequestService(t)
264+ svc.EXPECT().EnableAutoMerge(mock.Anything, mock.Anything, name, int64(3), ports.EnableAutoMergeParams{Mode: domain.MergeModeSquash, HeadID: head}).
265+ Return(domain.MergeRequest{}, nil).Once()
266+ svc.EXPECT().CancelAutoMerge(mock.Anything, mock.Anything, name, int64(3)).Return(domain.MergeRequest{}, nil).Once()
267+ s := &Server{cfg: Config{MergeRequests: svc}}
268+ to, _, err := s.mrAutoMerge(ctx, autoMergeRequest("/demo/merge-requests/3/auto-merge", "mode=squash&head_id="+head.String()))
269+ require.NoError(t, err)
270+ assert.Equal(t, mrHref("demo", 3), to)
271+ to, _, err = s.mrCancelAutoMerge(ctx, autoMergeRequest("/demo/merge-requests/3/auto-merge/cancel", ""))
272+ require.NoError(t, err)
273+ assert.Equal(t, mrHref("demo", 3), to)
274+
275+ // A bad mode is refused before the service.
276+ _, _, err = s.mrAutoMerge(ctx, autoMergeRequest("/x", "mode=rebase"))
277+ var re *rerender
278+ require.ErrorAs(t, err, &re)
279+ assert.Equal(t, http.StatusBadRequest, re.status)
280+
281+ // A refusal of the use case is shown in the box, like a refused merge.
282+ svc2 := mocks.NewMergeRequestService(t)
283+ svc2.EXPECT().EnableAutoMerge(mock.Anything, mock.Anything, name, int64(3), mock.Anything).
284+ Return(domain.MergeRequest{}, domain.ErrPreconditionFailed).Once()
285+ svc2.EXPECT().CancelAutoMerge(mock.Anything, mock.Anything, name, int64(3)).
286+ Return(domain.MergeRequest{}, domain.ErrNotFound).Once()
287+ s2 := &Server{cfg: Config{MergeRequests: svc2}}
288+ _, _, err = s2.mrAutoMerge(ctx, autoMergeRequest("/x", "mode=squash&head_id="+head.String()))
289+ require.ErrorAs(t, err, &re)
290+ assert.Equal(t, http.StatusConflict, re.status)
291+ _, _, err = s2.mrCancelAutoMerge(ctx, autoMergeRequest("/x", ""))
292+ require.ErrorIs(t, err, domain.ErrNotFound)
293+}
294+
295+func TestAutoMergeLiveEvents(t *testing.T) {
296+ t.Parallel()
297+ assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeEnabled{})))
298+ assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeCancelled{})))
299+}
@@ -4,8 +4,11 @@ package web
4 4
5 import (5 import (
6 "context"6 "context"
7- "time"7+ "net/http"
8+ "net/http/httptest"
9+ "strings"
8 "testing"10 "testing"
11+ "time"
9 12
10 "github.com/stretchr/testify/assert"13 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/mock"14 "github.com/stretchr/testify/mock"
@@ -195,3 +198,102 @@ func TestMRReadPresentationCommitCount(t *testing.T) {
195 })198 })
196 }199 }
197 }200 }
201+
202+func TestMergeBoxAutoMerge(t *testing.T) {
203+ t.Parallel()
204+ mg := mergeabilityView(domain.Mergeability{State: domain.MergeabilityUpToDate}, "main")
205+ mk := func(allow bool, runs ports.RunService) *MergeBox {
206+ v := repoWithModes(domain.MergeModes()...)
207+ v.Settings.AllowAutoMerge = allow
208+ return (&Server{cfg: Config{Runs: runs}}).mergeBox(&repoData{repo: v}, 1, "feat", "abc", false, mg)
209+ }
210+ assert.False(t, mk(false, mocks.NewRunService(t)).CanAutoMerge)
211+ assert.False(t, mk(true, nil).CanAutoMerge, "no CI, nothing to wait for")
212+ on := mk(true, mocks.NewRunService(t))
213+ assert.True(t, on.CanAutoMerge)
214+
215+ page := MRPage{Target: "main", Box: on}
216+ html := renderToString(t, mergeMenu(page))
217+ assert.Contains(t, html, "Merge when ready")
218+ assert.Contains(t, html, `action="`+on.AutoAction+`"`)
219+ assert.NotContains(t, html, "Cancel auto-merge")
220+ // Current mergeability must only gate the immediate form. Every allowed
221+ // mode remains selectable in the separate auto-merge form.
222+ for i := range on.Modes {
223+ on.Modes[i].Disabled = true
224+ }
225+ on.CanDeleteBranch = true
226+ page.Source = "feat"
227+ html = renderToString(t, mergeMenu(page))
228+ _, autoForm, found := strings.Cut(html, `action="`+on.AutoAction+`"`)
229+ require.True(t, found)
230+ autoForm, _, _ = strings.Cut(autoForm, "</form>")
231+ assert.NotContains(t, autoForm, "delete_branch")
232+ assert.NotContains(t, autoForm, "disabled")
233+ assert.Contains(t, autoForm, `name="head_id"`)
234+ for _, mode := range on.Modes {
235+ assert.Contains(t, autoForm, `value="`+mode.Value+`"`)
236+ }
237+ page.Box = mk(false, mocks.NewRunService(t))
238+ assert.NotContains(t, renderToString(t, mergeMenu(page)), "Merge when ready")
239+
240+ on.AutoMerge = &AutoMergeView{Mode: "squash", EnabledBy: "owner", HeadShort: "abc"}
241+ html = renderToString(t, mergeMenu(MRPage{Target: "main", Box: on}))
242+ assert.Contains(t, html, "Auto-merge: squash when ready, enabled by owner")
243+ assert.Contains(t, html, `action="`+on.CancelAutoAction+`"`)
244+ assert.Contains(t, html, "Cancel auto-merge")
245+}
246+
247+func autoMergeRequest(path, body string) *http.Request {
248+ r := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
249+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
250+ r.SetPathValue("repo", "demo")
251+ r.SetPathValue("n", "3")
252+ return r
253+}
254+
255+func TestAutoMergeHandlers(t *testing.T) {
256+ t.Parallel()
257+ name, err := domain.ParseRepoName("demo")
258+ require.NoError(t, err)
259+ ctx := context.Background()
260+
261+ head, err := domain.ParseObjectID(strings.Repeat("a", domain.ObjectIDLen))
262+ require.NoError(t, err)
263+ svc := mocks.NewMergeRequestService(t)
264+ svc.EXPECT().EnableAutoMerge(mock.Anything, mock.Anything, name, int64(3), ports.EnableAutoMergeParams{Mode: domain.MergeModeSquash, HeadID: head}).
265+ Return(domain.MergeRequest{}, nil).Once()
266+ svc.EXPECT().CancelAutoMerge(mock.Anything, mock.Anything, name, int64(3)).Return(domain.MergeRequest{}, nil).Once()
267+ s := &Server{cfg: Config{MergeRequests: svc}}
268+ to, _, err := s.mrAutoMerge(ctx, autoMergeRequest("/demo/merge-requests/3/auto-merge", "mode=squash&head_id="+head.String()))
269+ require.NoError(t, err)
270+ assert.Equal(t, mrHref("demo", 3), to)
271+ to, _, err = s.mrCancelAutoMerge(ctx, autoMergeRequest("/demo/merge-requests/3/auto-merge/cancel", ""))
272+ require.NoError(t, err)
273+ assert.Equal(t, mrHref("demo", 3), to)
274+
275+ // A bad mode is refused before the service.
276+ _, _, err = s.mrAutoMerge(ctx, autoMergeRequest("/x", "mode=rebase"))
277+ var re *rerender
278+ require.ErrorAs(t, err, &re)
279+ assert.Equal(t, http.StatusBadRequest, re.status)
280+
281+ // A refusal of the use case is shown in the box, like a refused merge.
282+ svc2 := mocks.NewMergeRequestService(t)
283+ svc2.EXPECT().EnableAutoMerge(mock.Anything, mock.Anything, name, int64(3), mock.Anything).
284+ Return(domain.MergeRequest{}, domain.ErrPreconditionFailed).Once()
285+ svc2.EXPECT().CancelAutoMerge(mock.Anything, mock.Anything, name, int64(3)).
286+ Return(domain.MergeRequest{}, domain.ErrNotFound).Once()
287+ s2 := &Server{cfg: Config{MergeRequests: svc2}}
288+ _, _, err = s2.mrAutoMerge(ctx, autoMergeRequest("/x", "mode=squash&head_id="+head.String()))
289+ require.ErrorAs(t, err, &re)
290+ assert.Equal(t, http.StatusConflict, re.status)
291+ _, _, err = s2.mrCancelAutoMerge(ctx, autoMergeRequest("/x", ""))
292+ require.ErrorIs(t, err, domain.ErrNotFound)
293+}
294+
295+func TestAutoMergeLiveEvents(t *testing.T) {
296+ t.Parallel()
297+ assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeEnabled{})))
298+ assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeCancelled{})))
299+}
M internal/adapters/web/web.go +2 -0
@@ -231,6 +231,8 @@ func (s *Server) routes() *http.ServeMux {
231231 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/edit", s.mrAction(s.mrEditComment))
232232 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/delete", s.mrAction(s.mrDeleteComment))
233233 mux.HandleFunc("POST /{repo}/merge-requests/{n}/merge", s.mrAction(s.mrMerge))
234+ mux.HandleFunc("POST /{repo}/merge-requests/{n}/auto-merge", s.mrAction(s.mrAutoMerge))
235+ mux.HandleFunc("POST /{repo}/merge-requests/{n}/auto-merge/cancel", s.mrAction(s.mrCancelAutoMerge))
234236 mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose))
235237 mux.HandleFunc("POST /{repo}/merge-requests/{n}/ready", s.mrAction(s.mrReady))
236238 mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead))
@@ -231,6 +231,8 @@ func (s *Server) routes() *http.ServeMux {
231 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/edit", s.mrAction(s.mrEditComment))231 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/edit", s.mrAction(s.mrEditComment))
232 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/delete", s.mrAction(s.mrDeleteComment))232 mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/delete", s.mrAction(s.mrDeleteComment))
233 mux.HandleFunc("POST /{repo}/merge-requests/{n}/merge", s.mrAction(s.mrMerge))233 mux.HandleFunc("POST /{repo}/merge-requests/{n}/merge", s.mrAction(s.mrMerge))
234+ mux.HandleFunc("POST /{repo}/merge-requests/{n}/auto-merge", s.mrAction(s.mrAutoMerge))
235+ mux.HandleFunc("POST /{repo}/merge-requests/{n}/auto-merge/cancel", s.mrAction(s.mrCancelAutoMerge))
234 mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose))236 mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose))
235 mux.HandleFunc("POST /{repo}/merge-requests/{n}/ready", s.mrAction(s.mrReady))237 mux.HandleFunc("POST /{repo}/merge-requests/{n}/ready", s.mrAction(s.mrReady))
236 mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead))238 mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead))
M internal/app/api_test.go +1 -1
@@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) {
196196 for _, who := range allPrincipals {
197197 resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{}))
198198 require.NoError(t, err, who)
199- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror"}, resp.Msg.GetFeatures(), who)
199+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, resp.Msg.GetFeatures(), who)
200200 assert.NotEmpty(t, resp.Msg.GetVersion())
201201 }
202202 })
@@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) {
196 for _, who := range allPrincipals {196 for _, who := range allPrincipals {
197 resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{}))197 resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{}))
198 require.NoError(t, err, who)198 require.NoError(t, err, who)
199- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror"}, resp.Msg.GetFeatures(), who)199+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, resp.Msg.GetFeatures(), who)
200 assert.NotEmpty(t, resp.Msg.GetVersion())200 assert.NotEmpty(t, resp.Msg.GetVersion())
201 }201 }
202 })202 })
A internal/app/automerge_e2e_test.go +163 -0
@@ -0,0 +1,163 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package app
4+
5+import (
6+ "testing"
7+ "time"
8+
9+ "connectrpc.com/connect"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+ "google.golang.org/protobuf/types/known/fieldmaskpb"
13+
14+ pb "bnema.dev/ermite-api/gen/ermite"
15+ "bnema.dev/ermite/internal/domain"
16+ "bnema.dev/ermite/internal/ports"
17+)
18+
19+// This test exercises the composition root through use cases and domain
20+// events; TestAutoMergeEndToEndAPI does the same through the RPC API.
21+func TestAutoMergeEndToEnd(t *testing.T) {
22+ c := newCIHarness(t, nil)
23+ runner := c.registerRunner("alpha", "ubuntu-latest")
24+ c.push("main", map[string]string{".github/workflows/ci.yml": `name: ci
25+on:
26+ push:
27+ branches: [feat]
28+ pull_request:
29+jobs:
30+ build:
31+ runs-on: ubuntu-latest
32+ steps:
33+ - run: echo hello
34+`}, "")
35+ head := c.push("feat", map[string]string{"change.txt": "feature\n"}, "")
36+ number := c.openMR(c.root, "feat")
37+ repo := c.name("proj")
38+ allow := true
39+ _, err := c.forge.Repos.Update(c.ctx, domain.Root(), repo, ports.UpdateRepoParams{AllowAutoMerge: &allow})
40+ require.NoError(t, err)
41+ headID, err := domain.ParseObjectID(head)
42+ require.NoError(t, err)
43+ events := c.forge.Bus.Subscribe(c.ctx)
44+ enabled, err := c.forge.MergeRequests.EnableAutoMerge(c.ctx, domain.Owner(), repo, number, ports.EnableAutoMergeParams{HeadID: headID})
45+ require.NoError(t, err)
46+ require.NotNil(t, enabled.AutoMerge)
47+ assert.Equal(t, head, enabled.AutoMerge.HeadID.String())
48+ enabledDeadline := time.After(5 * time.Second)
49+waitEnabled:
50+ for {
51+ select {
52+ case ev := <-events:
53+ if enabled, ok := ev.(domain.MergeRequestAutoMergeEnabled); ok && enabled.MergeRequest.Repo == repo && enabled.MergeRequest.Number == number {
54+ break waitEnabled
55+ }
56+ case <-enabledDeadline:
57+ t.Fatal("no enabled event")
58+ }
59+ }
60+ c.finish(runner)
61+ // A successful push check alone must not merge while the pull run is queued.
62+ got, err := c.forge.MergeRequests.Get(c.ctx, domain.Root(), repo, number, ports.GetMergeRequestOptions{})
63+ require.NoError(t, err)
64+ assert.True(t, got.MergeRequest.IsOpen())
65+ c.finish(runner)
66+ deadline := time.After(10 * time.Second)
67+ for {
68+ select {
69+ case ev := <-events:
70+ if merged, ok := ev.(domain.MergeRequestMerged); ok && merged.MergeRequest.Number == number {
71+ assert.True(t, merged.Auto)
72+ assert.True(t, merged.MergeRequest.AutoMerged)
73+ assert.Nil(t, merged.MergeRequest.AutoMerge)
74+ return
75+ }
76+ case <-deadline:
77+ t.Fatal("no automatic merged event")
78+ }
79+ }
80+}
81+
82+// TestAutoMergeEndToEndAPI enables auto-merge through the API client, lets CI
83+// succeed, and reads the Watch stream: enabled, then merged with auto=true.
84+func TestAutoMergeEndToEndAPI(t *testing.T) {
85+ c := newCIHarness(t, nil)
86+ runner := c.registerRunner("alpha", "ubuntu-latest")
87+ c.push("main", map[string]string{".github/workflows/ci.yml": `name: ci
88+on:
89+ push:
90+ branches: [feat]
91+ pull_request:
92+jobs:
93+ build:
94+ runs-on: ubuntu-latest
95+ steps:
96+ - run: echo hello
97+`}, "")
98+ head := c.push("feat", map[string]string{"change.txt": "feature\n"}, "")
99+ number := c.openMR(c.root, "feat")
100+
101+ // Off by default: the server refuses.
102+ _, err := c.root.mr.EnableAutoMerge(c.ctx, connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: c.repo, Number: number, HeadId: head}))
103+ require.Error(t, err)
104+ assert.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err))
105+
106+ ver, err := c.root.server.GetVersion(c.ctx, connect.NewRequest(&pb.GetVersionRequest{}))
107+ require.NoError(t, err)
108+ assert.Contains(t, ver.Msg.GetFeatures(), "merge_requests.auto_merge")
109+
110+ up, err := c.root.repo.UpdateRepo(c.ctx, connect.NewRequest(&pb.UpdateRepoRequest{
111+ Repo: c.repo,
112+ Settings: &pb.RepoSettings{AllowAutoMerge: true},
113+ UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allow_auto_merge"}},
114+ }))
115+ require.NoError(t, err)
116+ assert.True(t, up.Msg.GetRepo().GetSettings().GetAllowAutoMerge())
117+
118+ w := c.watch(c.root, "", "home")
119+ res, err := c.root.mr.EnableAutoMerge(c.ctx, connect.NewRequest(&pb.EnableAutoMergeRequest{
120+ Repo: c.repo, Number: number, Mode: pb.MergeMode_MERGE_MODE_SQUASH, HeadId: head,
121+ }))
122+ require.NoError(t, err)
123+ am := res.Msg.GetMergeRequest().GetAutoMerge()
124+ require.NotNil(t, am)
125+ assert.Equal(t, pb.MergeMode_MERGE_MODE_SQUASH, am.GetMode())
126+ assert.Equal(t, res.Msg.GetMergeRequest().GetHeadId(), am.GetHeadId())
127+ assert.NotEmpty(t, am.GetEnabledBy())
128+ assert.NotNil(t, am.GetEnabledAt())
129+
130+ got, err := c.root.mr.GetMergeRequest(c.ctx, connect.NewRequest(&pb.GetMergeRequestRequest{Repo: c.repo, Number: number}))
131+ require.NoError(t, err)
132+ assert.NotNil(t, got.Msg.GetMergeRequest().GetAutoMerge(), "Get reports the pending auto-merge")
133+
134+ c.finish(runner)
135+ c.finish(runner)
136+
137+ var sawEnabled bool
138+ deadline := time.After(15 * time.Second)
139+ for {
140+ select {
141+ case ev, ok := <-w.events:
142+ require.True(t, ok, "the stream ended: %v", w.err)
143+ if e := ev.GetMergeRequestAutoMergeEnabled(); e != nil {
144+ assert.EqualValues(t, number, e.GetMergeRequest().GetNumber())
145+ assert.NotNil(t, e.GetMergeRequest().GetAutoMerge())
146+ sawEnabled = true
147+ }
148+ if m := ev.GetMergeRequestMerged(); m != nil && m.GetMergeRequest().GetNumber() == number {
149+ assert.True(t, sawEnabled, "enabled comes before merged")
150+ assert.True(t, m.GetAuto())
151+ assert.True(t, m.GetMergeRequest().GetAutoMerged())
152+ assert.Nil(t, m.GetMergeRequest().GetAutoMerge())
153+ final, err := c.root.mr.GetMergeRequest(c.ctx, connect.NewRequest(&pb.GetMergeRequestRequest{Repo: c.repo, Number: number}))
154+ require.NoError(t, err)
155+ assert.Equal(t, pb.MergeRequestState_MERGE_REQUEST_STATE_MERGED, final.Msg.GetMergeRequest().GetState())
156+ assert.True(t, final.Msg.GetMergeRequest().GetAutoMerged())
157+ return
158+ }
159+ case <-deadline:
160+ t.Fatal("no automatic merged event")
161+ }
162+ }
163+}
@@ -0,0 +1,163 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package app
4+
5+import (
6+ "testing"
7+ "time"
8+
9+ "connectrpc.com/connect"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+ "google.golang.org/protobuf/types/known/fieldmaskpb"
13+
14+ pb "bnema.dev/ermite-api/gen/ermite"
15+ "bnema.dev/ermite/internal/domain"
16+ "bnema.dev/ermite/internal/ports"
17+)
18+
19+// This test exercises the composition root through use cases and domain
20+// events; TestAutoMergeEndToEndAPI does the same through the RPC API.
21+func TestAutoMergeEndToEnd(t *testing.T) {
22+ c := newCIHarness(t, nil)
23+ runner := c.registerRunner("alpha", "ubuntu-latest")
24+ c.push("main", map[string]string{".github/workflows/ci.yml": `name: ci
25+on:
26+ push:
27+ branches: [feat]
28+ pull_request:
29+jobs:
30+ build:
31+ runs-on: ubuntu-latest
32+ steps:
33+ - run: echo hello
34+`}, "")
35+ head := c.push("feat", map[string]string{"change.txt": "feature\n"}, "")
36+ number := c.openMR(c.root, "feat")
37+ repo := c.name("proj")
38+ allow := true
39+ _, err := c.forge.Repos.Update(c.ctx, domain.Root(), repo, ports.UpdateRepoParams{AllowAutoMerge: &allow})
40+ require.NoError(t, err)
41+ headID, err := domain.ParseObjectID(head)
42+ require.NoError(t, err)
43+ events := c.forge.Bus.Subscribe(c.ctx)
44+ enabled, err := c.forge.MergeRequests.EnableAutoMerge(c.ctx, domain.Owner(), repo, number, ports.EnableAutoMergeParams{HeadID: headID})
45+ require.NoError(t, err)
46+ require.NotNil(t, enabled.AutoMerge)
47+ assert.Equal(t, head, enabled.AutoMerge.HeadID.String())
48+ enabledDeadline := time.After(5 * time.Second)
49+waitEnabled:
50+ for {
51+ select {
52+ case ev := <-events:
53+ if enabled, ok := ev.(domain.MergeRequestAutoMergeEnabled); ok && enabled.MergeRequest.Repo == repo && enabled.MergeRequest.Number == number {
54+ break waitEnabled
55+ }
56+ case <-enabledDeadline:
57+ t.Fatal("no enabled event")
58+ }
59+ }
60+ c.finish(runner)
61+ // A successful push check alone must not merge while the pull run is queued.
62+ got, err := c.forge.MergeRequests.Get(c.ctx, domain.Root(), repo, number, ports.GetMergeRequestOptions{})
63+ require.NoError(t, err)
64+ assert.True(t, got.MergeRequest.IsOpen())
65+ c.finish(runner)
66+ deadline := time.After(10 * time.Second)
67+ for {
68+ select {
69+ case ev := <-events:
70+ if merged, ok := ev.(domain.MergeRequestMerged); ok && merged.MergeRequest.Number == number {
71+ assert.True(t, merged.Auto)
72+ assert.True(t, merged.MergeRequest.AutoMerged)
73+ assert.Nil(t, merged.MergeRequest.AutoMerge)
74+ return
75+ }
76+ case <-deadline:
77+ t.Fatal("no automatic merged event")
78+ }
79+ }
80+}
81+
82+// TestAutoMergeEndToEndAPI enables auto-merge through the API client, lets CI
83+// succeed, and reads the Watch stream: enabled, then merged with auto=true.
84+func TestAutoMergeEndToEndAPI(t *testing.T) {
85+ c := newCIHarness(t, nil)
86+ runner := c.registerRunner("alpha", "ubuntu-latest")
87+ c.push("main", map[string]string{".github/workflows/ci.yml": `name: ci
88+on:
89+ push:
90+ branches: [feat]
91+ pull_request:
92+jobs:
93+ build:
94+ runs-on: ubuntu-latest
95+ steps:
96+ - run: echo hello
97+`}, "")
98+ head := c.push("feat", map[string]string{"change.txt": "feature\n"}, "")
99+ number := c.openMR(c.root, "feat")
100+
101+ // Off by default: the server refuses.
102+ _, err := c.root.mr.EnableAutoMerge(c.ctx, connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: c.repo, Number: number, HeadId: head}))
103+ require.Error(t, err)
104+ assert.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err))
105+
106+ ver, err := c.root.server.GetVersion(c.ctx, connect.NewRequest(&pb.GetVersionRequest{}))
107+ require.NoError(t, err)
108+ assert.Contains(t, ver.Msg.GetFeatures(), "merge_requests.auto_merge")
109+
110+ up, err := c.root.repo.UpdateRepo(c.ctx, connect.NewRequest(&pb.UpdateRepoRequest{
111+ Repo: c.repo,
112+ Settings: &pb.RepoSettings{AllowAutoMerge: true},
113+ UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allow_auto_merge"}},
114+ }))
115+ require.NoError(t, err)
116+ assert.True(t, up.Msg.GetRepo().GetSettings().GetAllowAutoMerge())
117+
118+ w := c.watch(c.root, "", "home")
119+ res, err := c.root.mr.EnableAutoMerge(c.ctx, connect.NewRequest(&pb.EnableAutoMergeRequest{
120+ Repo: c.repo, Number: number, Mode: pb.MergeMode_MERGE_MODE_SQUASH, HeadId: head,
121+ }))
122+ require.NoError(t, err)
123+ am := res.Msg.GetMergeRequest().GetAutoMerge()
124+ require.NotNil(t, am)
125+ assert.Equal(t, pb.MergeMode_MERGE_MODE_SQUASH, am.GetMode())
126+ assert.Equal(t, res.Msg.GetMergeRequest().GetHeadId(), am.GetHeadId())
127+ assert.NotEmpty(t, am.GetEnabledBy())
128+ assert.NotNil(t, am.GetEnabledAt())
129+
130+ got, err := c.root.mr.GetMergeRequest(c.ctx, connect.NewRequest(&pb.GetMergeRequestRequest{Repo: c.repo, Number: number}))
131+ require.NoError(t, err)
132+ assert.NotNil(t, got.Msg.GetMergeRequest().GetAutoMerge(), "Get reports the pending auto-merge")
133+
134+ c.finish(runner)
135+ c.finish(runner)
136+
137+ var sawEnabled bool
138+ deadline := time.After(15 * time.Second)
139+ for {
140+ select {
141+ case ev, ok := <-w.events:
142+ require.True(t, ok, "the stream ended: %v", w.err)
143+ if e := ev.GetMergeRequestAutoMergeEnabled(); e != nil {
144+ assert.EqualValues(t, number, e.GetMergeRequest().GetNumber())
145+ assert.NotNil(t, e.GetMergeRequest().GetAutoMerge())
146+ sawEnabled = true
147+ }
148+ if m := ev.GetMergeRequestMerged(); m != nil && m.GetMergeRequest().GetNumber() == number {
149+ assert.True(t, sawEnabled, "enabled comes before merged")
150+ assert.True(t, m.GetAuto())
151+ assert.True(t, m.GetMergeRequest().GetAutoMerged())
152+ assert.Nil(t, m.GetMergeRequest().GetAutoMerge())
153+ final, err := c.root.mr.GetMergeRequest(c.ctx, connect.NewRequest(&pb.GetMergeRequestRequest{Repo: c.repo, Number: number}))
154+ require.NoError(t, err)
155+ assert.Equal(t, pb.MergeRequestState_MERGE_REQUEST_STATE_MERGED, final.Msg.GetMergeRequest().GetState())
156+ assert.True(t, final.Msg.GetMergeRequest().GetAutoMerged())
157+ return
158+ }
159+ case <-deadline:
160+ t.Fatal("no automatic merged event")
161+ }
162+ }
163+}
M internal/app/cli_e2e_test.go +2 -2
@@ -203,7 +203,7 @@ type eventOut struct {
203203 MergeRequest *mrOut `json:"merge_request"`
204204 }
205205
206-var repoKeys = []string{"created_at", "default_branch", "description", "failing_checks_block", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"}
206+var repoKeys = []string{"allow_auto_merge", "created_at", "default_branch", "description", "failing_checks_block", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"}
207207
208208 // AC6: the CLI end to end against a real forge.
209209 func TestAC6CLIEndToEnd(t *testing.T) {
@@ -350,7 +350,7 @@ func TestAC6CLIEndToEnd(t *testing.T) {
350350 ver := decode[versionOut](t, res.stdout)
351351 assert.Equal(t, c.url, ver.Server.URL)
352352 assert.NotEmpty(t, ver.Server.Version)
353- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror"}, ver.Server.Features)
353+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, ver.Server.Features)
354354 assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures)
355355 stdout, stderr, code = c.bin(t, secret, "version")
356356 require.Equal(t, 0, code, stderr)
@@ -203,7 +203,7 @@ type eventOut struct {
203 MergeRequest *mrOut `json:"merge_request"`203 MergeRequest *mrOut `json:"merge_request"`
204 }204 }
205 205
206-var repoKeys = []string{"created_at", "default_branch", "description", "failing_checks_block", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"}206+var repoKeys = []string{"allow_auto_merge", "created_at", "default_branch", "description", "failing_checks_block", "issues", "merge_modes", "name", "pushed_at", "topics", "visibility"}
207 207
208 // AC6: the CLI end to end against a real forge.208 // AC6: the CLI end to end against a real forge.
209 func TestAC6CLIEndToEnd(t *testing.T) {209 func TestAC6CLIEndToEnd(t *testing.T) {
@@ -350,7 +350,7 @@ func TestAC6CLIEndToEnd(t *testing.T) {
350 ver := decode[versionOut](t, res.stdout)350 ver := decode[versionOut](t, res.stdout)
351 assert.Equal(t, c.url, ver.Server.URL)351 assert.Equal(t, c.url, ver.Server.URL)
352 assert.NotEmpty(t, ver.Server.Version)352 assert.NotEmpty(t, ver.Server.Version)
353- assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror"}, ver.Server.Features)353+ assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge"}, ver.Server.Features)
354 assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures)354 assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures)
355 stdout, stderr, code = c.bin(t, secret, "version")355 stdout, stderr, code = c.bin(t, secret, "version")
356 require.Equal(t, 0, code, stderr)356 require.Equal(t, 0, code, stderr)
M internal/app/cli_fake_test.go +8 -0
M internal/app/cli_mr.go +95 -3
M internal/app/cli_mr_e2e_test.go +1 -1
M internal/app/cli_mr_test.go +112 -1
M internal/app/cli_repo.go +17 -6
M internal/app/cli_repo_test.go +34 -4
M internal/app/cli_version.go +1 -0
M internal/app/cli_version_test.go +5 -5
M internal/app/cli_watch.go +25 -3
M internal/app/cli_watch_test.go +55 -1
M internal/app/forge.go +13 -0
M internal/domain/event.go +28 -0
M internal/domain/event_test.go +7 -0
M internal/domain/mergerequest.go +21 -0
M internal/domain/mergerequest_test.go +6 -0
M internal/ports/ci.go +20 -0
M internal/ports/dto.go +11 -0
A internal/ports/mocks/AutoMergeTrigger.go +140 -0
A internal/ports/mocks/CIActivity.go +111 -0
A internal/ports/mocks/MergeGate.go +111 -0
M internal/ports/mocks/MergeRequestService.go +162 -0
M internal/ports/ports.go +7 -0
M internal/ports/services.go +5 -0
A internal/usecase/automerge.go +413 -0
A internal/usecase/automerge_index_test.go +97 -0
A internal/usecase/automerge_test.go +453 -0
M internal/usecase/ci.go +61 -2
M internal/usecase/ci_actions.go +22 -14
A internal/usecase/ci_activity_test.go +72 -0
M internal/usecase/ci_github_test.go +21 -0
M internal/usecase/ci_githubapi.go +1 -1
M internal/usecase/ci_pull.go +6 -0
M internal/usecase/ci_trigger.go +41 -6
M internal/usecase/events.go +9 -0
M internal/usecase/export_test.go +48 -0
M internal/usecase/issue_links_test.go +27 -0
M internal/usecase/mergeability.go +18 -6
M internal/usecase/mergerequests.go +93 -18
A internal/usecase/mergerequests_auto.go +168 -0
M internal/usecase/repos.go +14 -6
M internal/usecase/repos_write.go +19 -4
M internal/usecase/repos_write_test.go +17 -0
M proto/ermite/storage/merge_request.proto +15 -0
M skills/ermite/CLI.md +4 -2
M skills/ermite/SKILL.md +2 -0

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 3

Tags 41

↑↓ moveenter openesc close