detached f61e6a6

#23 feat(merge): mr ready, passing checks to merge, mr:force-merge and --ignore-checks

state
merged · f61e6a6 · fast-forward
branch
feat/mr-ready-merge-checks → main
opened
bnema ·
updated

Closes #22.

  • ermite mr ready <repo> <n> (MarkReady RPC on the existing use case): removes the WIP:/[WIP] prefix; prints the URL, --json.
  • Every merge needs passing checks on the head: none failed, queued or running; a head with no checks can merge. Auto-merge is unchanged.
  • New scope mr:force-merge: ermite mr merge --ignore-checks (not with --auto/--cancel-auto), and an "Ignore checks" box on the owner's merge form when checks block (refreshed live, owner streams only). cli preset has it, agent does not.
  • Storage schema 5 gives mr:force-merge to unrestricted full tokens so they keep the owner rights (login, session revocation).
  • failing_checks_block removed (setting, repo edit flag, storage field reserved).
  • Docs and skill updated.

Depends on ermite-api!10. Before merge: tag ermite-api v0.15.0 and replace the local replace in go.mod with it.

CommitSubjectAuthorDateSignatureStatus
f61e6a6fix(store): keep failing_checks_block as a deprecated storage field, run the new subtests in parallelbriceGPG Verified
923f868build(deps): use ermite-api v0.15.0briceGPG Verified
eba8ed7fix(scopes): keep the owner rights of full tokens with mr:force-merge, refresh the owner's merge check controls livebriceGPG Verified
7348bb2feat(web): offer ignore checks on the merge form, document mr ready and the merge check rulesbriceGPG Verified
5d0c190feat(cli): add mr ready and mr merge --ignore-checks, implement the MarkReady RPCbriceGPG Verified
cfc292cfeat(merge): require passing checks to merge, add mr:force-merge and ignore_checks, drop failing_checks_blockbriceGPG Verified

Checks 8

56 files · +645 -265
8/8
M docs/ci.md +1 -1
M docs/cli.md +4 -2
M docs/merge-requests.md +5 -3
M gen/ermite/storage/merge_request.pb.go +9 -5
M go.mod +1 -1
M go.sum +2 -2
M internal/adapters/client/features.go +4 -0
M internal/adapters/client/scopes.go +3 -0
M internal/adapters/client/scopes_test.go +1 -0
M internal/adapters/kvstore/issues_test.go +53 -6
M internal/adapters/kvstore/mergerequests.go +2 -2
M internal/adapters/kvstore/mergerequests_test.go +2 -2
M internal/adapters/kvstore/store.go +36 -8
M internal/adapters/rpc/api.go +1 -1
M internal/adapters/rpc/convert.go +2 -1
M internal/adapters/rpc/convert_test.go +9 -9
M internal/adapters/rpc/mergerequest.go +13 -1
M internal/adapters/rpc/mergerequest_test.go +31 -1
M internal/adapters/rpc/repo.go +2 -5
M internal/adapters/rpc/repo_test.go +1 -7
M internal/adapters/web/checks.go +1 -3
M internal/adapters/web/fragments.go +18 -1
M internal/adapters/web/mr.go +1 -1
M internal/adapters/web/mr.templ +13 -1
M internal/adapters/web/mr_post.go +1 -0
M internal/adapters/web/mr_templ.go +111 -53
@@ -2805,6 +2805,10 @@ func mergeMenu(p MRPage) templ.Component {
28052805 if templ_7745c5c3_Err != nil {
28062806 return templ_7745c5c3_Err
28072807 }
2808+ templ_7745c5c3_Err = MRMergeChecks(p.ChecksSum).Render(ctx, templ_7745c5c3_Buffer)
2809+ if templ_7745c5c3_Err != nil {
2810+ return templ_7745c5c3_Err
2811+ }
28082812 if p.Box.CanDeleteBranch {
28092813 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
28102814 if templ_7745c5c3_Err != nil {
@@ -2813,7 +2817,7 @@ func mergeMenu(p MRPage) templ.Component {
28132817 var templ_7745c5c3_Var134 string
28142818 templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
28152819 if templ_7745c5c3_Err != nil {
2816- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 549, Col: 102}
2820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 550, Col: 102}
28172821 }
28182822 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
28192823 if templ_7745c5c3_Err != nil {
@@ -2841,7 +2845,7 @@ func mergeMenu(p MRPage) templ.Component {
28412845 var templ_7745c5c3_Var135 string
28422846 templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
28432847 if templ_7745c5c3_Err != nil {
2844- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 557, Col: 33}
2848+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 558, Col: 33}
28452849 }
28462850 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
28472851 if templ_7745c5c3_Err != nil {
@@ -2854,7 +2858,7 @@ func mergeMenu(p MRPage) templ.Component {
28542858 var templ_7745c5c3_Var136 string
28552859 templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
28562860 if templ_7745c5c3_Err != nil {
2857- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 28}
2861+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 561, Col: 28}
28582862 }
28592863 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
28602864 if templ_7745c5c3_Err != nil {
@@ -2872,7 +2876,7 @@ func mergeMenu(p MRPage) templ.Component {
28722876 var templ_7745c5c3_Var137 templ.SafeURL
28732877 templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.AutoAction))
28742878 if templ_7745c5c3_Err != nil {
2875- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 80}
2879+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 80}
28762880 }
28772881 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
28782882 if templ_7745c5c3_Err != nil {
@@ -2885,7 +2889,7 @@ func mergeMenu(p MRPage) templ.Component {
28852889 var templ_7745c5c3_Var138 string
28862890 templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
28872891 if templ_7745c5c3_Err != nil {
2888- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 60}
2892+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 566, Col: 60}
28892893 }
28902894 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
28912895 if templ_7745c5c3_Err != nil {
@@ -2903,7 +2907,7 @@ func mergeMenu(p MRPage) templ.Component {
29032907 var templ_7745c5c3_Var139 string
29042908 templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
29052909 if templ_7745c5c3_Err != nil {
2906- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 62}
2910+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 570, Col: 62}
29072911 }
29082912 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var139)
29092913 if templ_7745c5c3_Err != nil {
@@ -2926,7 +2930,7 @@ func mergeMenu(p MRPage) templ.Component {
29262930 var templ_7745c5c3_Var140 string
29272931 templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
29282932 if templ_7745c5c3_Err != nil {
2929- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 96}
2933+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 570, Col: 96}
29302934 }
29312935 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
29322936 if templ_7745c5c3_Err != nil {
@@ -2991,7 +2995,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
29912995 var templ_7745c5c3_Var142 string
29922996 templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
29932997 if templ_7745c5c3_Err != nil {
2994- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 587, Col: 22}
2998+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 588, Col: 22}
29952999 }
29963000 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
29973001 if templ_7745c5c3_Err != nil {
@@ -3004,7 +3008,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30043008 var templ_7745c5c3_Var143 string
30053009 templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
30063010 if templ_7745c5c3_Err != nil {
3007- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 588, Col: 41}
3011+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 41}
30083012 }
30093013 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
30103014 if templ_7745c5c3_Err != nil {
@@ -3017,7 +3021,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30173021 var templ_7745c5c3_Var144 string
30183022 templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
30193023 if templ_7745c5c3_Err != nil {
3020- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 47}
3024+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 590, Col: 47}
30213025 }
30223026 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
30233027 if templ_7745c5c3_Err != nil {
@@ -3030,7 +3034,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30303034 var templ_7745c5c3_Var145 string
30313035 templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
30323036 if templ_7745c5c3_Err != nil {
3033- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 590, Col: 29}
3037+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 591, Col: 29}
30343038 }
30353039 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var145)
30363040 if templ_7745c5c3_Err != nil {
@@ -3043,7 +3047,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30433047 var templ_7745c5c3_Var146 string
30443048 templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
30453049 if templ_7745c5c3_Err != nil {
3046- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 591, Col: 35}
3050+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 592, Col: 35}
30473051 }
30483052 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var146)
30493053 if templ_7745c5c3_Err != nil {
@@ -3056,7 +3060,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30563060 var templ_7745c5c3_Var147 string
30573061 templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
30583062 if templ_7745c5c3_Err != nil {
3059- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 42}
3063+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 42}
30603064 }
30613065 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)
30623066 if templ_7745c5c3_Err != nil {
@@ -3069,7 +3073,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30693073 var templ_7745c5c3_Var148 string
30703074 templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
30713075 if templ_7745c5c3_Err != nil {
3072- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 54}
3076+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 54}
30733077 }
30743078 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
30753079 if templ_7745c5c3_Err != nil {
@@ -3083,7 +3087,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30833087 var templ_7745c5c3_Var149 string
30843088 templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
30853089 if templ_7745c5c3_Err != nil {
3086- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 8}
3090+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 8}
30873091 }
30883092 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))
30893093 if templ_7745c5c3_Err != nil {
@@ -3096,7 +3100,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
30963100 var templ_7745c5c3_Var150 string
30973101 templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
30983102 if templ_7745c5c3_Err != nil {
3099- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 38}
3103+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 38}
31003104 }
31013105 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))
31023106 if templ_7745c5c3_Err != nil {
@@ -3124,7 +3128,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
31243128 var templ_7745c5c3_Var151 string
31253129 templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(path)
31263130 if templ_7745c5c3_Err != nil {
3127- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 20}
3131+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 602, Col: 20}
31283132 }
31293133 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
31303134 if templ_7745c5c3_Err != nil {
@@ -3148,7 +3152,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
31483152 var templ_7745c5c3_Var152 string
31493153 templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
31503154 if templ_7745c5c3_Err != nil {
3151- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 606, Col: 30}
3155+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 607, Col: 30}
31523156 }
31533157 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
31543158 if templ_7745c5c3_Err != nil {
@@ -3262,7 +3266,7 @@ func moreMenu(p MRPage) templ.Component {
32623266 var templ_7745c5c3_Var156 string
32633267 templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
32643268 if templ_7745c5c3_Err != nil {
3265- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 633, Col: 38}
3269+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 634, Col: 38}
32663270 }
32673271 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
32683272 if templ_7745c5c3_Err != nil {
@@ -3273,7 +3277,7 @@ func moreMenu(p MRPage) templ.Component {
32733277 return templ_7745c5c3_Err
32743278 }
32753279 if p.ChecksBlock {
3276- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "failing ones block the merge")
3280+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "failing or unfinished checks block the merge")
32773281 if templ_7745c5c3_Err != nil {
32783282 return templ_7745c5c3_Err
32793283 }
@@ -3301,7 +3305,7 @@ func moreMenu(p MRPage) templ.Component {
33013305 var templ_7745c5c3_Var157 string
33023306 templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
33033307 if templ_7745c5c3_Err != nil {
3304- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 44}
3308+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 650, Col: 44}
33053309 }
33063310 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
33073311 if templ_7745c5c3_Err != nil {
@@ -3329,7 +3333,7 @@ func moreMenu(p MRPage) templ.Component {
33293333 var templ_7745c5c3_Var158 templ.SafeURL
33303334 templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
33313335 if templ_7745c5c3_Err != nil {
3332- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 655, Col: 65}
3336+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 65}
33333337 }
33343338 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
33353339 if templ_7745c5c3_Err != nil {
@@ -3342,7 +3346,7 @@ func moreMenu(p MRPage) templ.Component {
33423346 var templ_7745c5c3_Var159 string
33433347 templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
33443348 if templ_7745c5c3_Err != nil {
3345- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 60}
3349+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 657, Col: 60}
33463350 }
33473351 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var159)
33483352 if templ_7745c5c3_Err != nil {
@@ -3360,7 +3364,7 @@ func moreMenu(p MRPage) templ.Component {
33603364 var templ_7745c5c3_Var160 templ.SafeURL
33613365 templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
33623366 if templ_7745c5c3_Err != nil {
3363- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 660, Col: 65}
3367+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 661, Col: 65}
33643368 }
33653369 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
33663370 if templ_7745c5c3_Err != nil {
@@ -3433,7 +3437,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
34333437 var templ_7745c5c3_Var163 string
34343438 templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
34353439 if templ_7745c5c3_Err != nil {
3436- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 17}
3440+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 678, Col: 17}
34373441 }
34383442 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var163))
34393443 if templ_7745c5c3_Err != nil {
@@ -3446,7 +3450,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
34463450 var templ_7745c5c3_Var164 string
34473451 templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
34483452 if templ_7745c5c3_Err != nil {
3449- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 34}
3453+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 678, Col: 34}
34503454 }
34513455 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var164))
34523456 if templ_7745c5c3_Err != nil {
@@ -3459,7 +3463,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
34593463 var templ_7745c5c3_Var165 templ.SafeURL
34603464 templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
34613465 if templ_7745c5c3_Err != nil {
3462- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 45}
3466+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 681, Col: 45}
34633467 }
34643468 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
34653469 if templ_7745c5c3_Err != nil {
@@ -3472,7 +3476,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
34723476 var templ_7745c5c3_Var166 string
34733477 templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
34743478 if templ_7745c5c3_Err != nil {
3475- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 58}
3479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 681, Col: 58}
34763480 }
34773481 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
34783482 if templ_7745c5c3_Err != nil {
@@ -3485,7 +3489,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
34853489 var templ_7745c5c3_Var167 templ.SafeURL
34863490 templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
34873491 if templ_7745c5c3_Err != nil {
3488- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 45}
3492+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 683, Col: 45}
34893493 }
34903494 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
34913495 if templ_7745c5c3_Err != nil {
@@ -3498,7 +3502,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
34983502 var templ_7745c5c3_Var168 string
34993503 templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
35003504 if templ_7745c5c3_Err != nil {
3501- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 58}
3505+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 683, Col: 58}
35023506 }
35033507 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
35043508 if templ_7745c5c3_Err != nil {
@@ -3511,7 +3515,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
35113515 var templ_7745c5c3_Var169 string
35123516 templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
35133517 if templ_7745c5c3_Err != nil {
3514- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 684, Col: 21}
3518+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 685, Col: 21}
35153519 }
35163520 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
35173521 if templ_7745c5c3_Err != nil {
@@ -3529,7 +3533,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
35293533 var templ_7745c5c3_Var170 string
35303534 templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
35313535 if templ_7745c5c3_Err != nil {
3532- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 688, Col: 29}
3536+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 689, Col: 29}
35333537 }
35343538 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170))
35353539 if templ_7745c5c3_Err != nil {
@@ -3552,7 +3556,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
35523556 var templ_7745c5c3_Var171 templ.SafeURL
35533557 templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
35543558 if templ_7745c5c3_Err != nil {
3555- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 114}
3559+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 692, Col: 114}
35563560 }
35573561 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
35583562 if templ_7745c5c3_Err != nil {
@@ -3565,7 +3569,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
35653569 var templ_7745c5c3_Var172 string
35663570 templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
35673571 if templ_7745c5c3_Err != nil {
3568- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 135}
3572+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 692, Col: 135}
35693573 }
35703574 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
35713575 if templ_7745c5c3_Err != nil {
@@ -3588,7 +3592,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
35883592 var templ_7745c5c3_Var173 templ.SafeURL
35893593 templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
35903594 if templ_7745c5c3_Err != nil {
3591- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 694, Col: 83}
3595+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 83}
35923596 }
35933597 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
35943598 if templ_7745c5c3_Err != nil {
@@ -3601,7 +3605,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
36013605 var templ_7745c5c3_Var174 string
36023606 templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
36033607 if templ_7745c5c3_Err != nil {
3604- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 62}
3608+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 696, Col: 62}
36053609 }
36063610 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var174)
36073611 if templ_7745c5c3_Err != nil {
@@ -3614,7 +3618,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
36143618 var templ_7745c5c3_Var175 string
36153619 templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
36163620 if templ_7745c5c3_Err != nil {
3617- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 696, Col: 62}
3621+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 697, Col: 62}
36183622 }
36193623 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var175)
36203624 if templ_7745c5c3_Err != nil {
@@ -3627,7 +3631,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
36273631 var templ_7745c5c3_Var176 string
36283632 templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
36293633 if templ_7745c5c3_Err != nil {
3630- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 698, Col: 97}
3634+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 699, Col: 97}
36313635 }
36323636 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var176)
36333637 if templ_7745c5c3_Err != nil {
@@ -3650,7 +3654,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
36503654 var templ_7745c5c3_Var177 string
36513655 templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
36523656 if templ_7745c5c3_Err != nil {
3653- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 57}
3657+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 709, Col: 57}
36543658 }
36553659 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))
36563660 if templ_7745c5c3_Err != nil {
@@ -3663,7 +3667,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
36633667 var templ_7745c5c3_Var178 string
36643668 templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
36653669 if templ_7745c5c3_Err != nil {
3666- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 97}
3670+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 709, Col: 97}
36673671 }
36683672 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))
36693673 if templ_7745c5c3_Err != nil {
@@ -3676,7 +3680,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
36763680 var templ_7745c5c3_Var179 string
36773681 templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
36783682 if templ_7745c5c3_Err != nil {
3679- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 142}
3683+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 709, Col: 142}
36803684 }
36813685 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
36823686 if templ_7745c5c3_Err != nil {
@@ -3712,7 +3716,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
37123716 var templ_7745c5c3_Var180 string
37133717 templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
37143718 if templ_7745c5c3_Err != nil {
3715- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 721, Col: 23}
3719+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 722, Col: 23}
37163720 }
37173721 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var180)
37183722 if templ_7745c5c3_Err != nil {
@@ -3730,7 +3734,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
37303734 var templ_7745c5c3_Var181 string
37313735 templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
37323736 if templ_7745c5c3_Err != nil {
3733- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 723, Col: 23}
3737+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 724, Col: 23}
37343738 }
37353739 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var181)
37363740 if templ_7745c5c3_Err != nil {
@@ -3800,7 +3804,7 @@ func compareRail(p ComparePage) templ.Component {
38003804 var templ_7745c5c3_Var183 string
38013805 templ_7745c5c3_Var183, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
38023806 if templ_7745c5c3_Err != nil {
3803- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 739, Col: 67}
3807+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 740, Col: 67}
38043808 }
38053809 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var183))
38063810 if templ_7745c5c3_Err != nil {
@@ -3818,7 +3822,7 @@ func compareRail(p ComparePage) templ.Component {
38183822 var templ_7745c5c3_Var184 templ.SafeURL
38193823 templ_7745c5c3_Var184, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
38203824 if templ_7745c5c3_Err != nil {
3821- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 37}
3825+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 37}
38223826 }
38233827 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var184))
38243828 if templ_7745c5c3_Err != nil {
@@ -3831,7 +3835,7 @@ func compareRail(p ComparePage) templ.Component {
38313835 var templ_7745c5c3_Var185 string
38323836 templ_7745c5c3_Var185, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
38333837 if templ_7745c5c3_Err != nil {
3834- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 49}
3838+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 49}
38353839 }
38363840 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var185))
38373841 if templ_7745c5c3_Err != nil {
@@ -3844,7 +3848,7 @@ func compareRail(p ComparePage) templ.Component {
38443848 var templ_7745c5c3_Var186 string
38453849 templ_7745c5c3_Var186, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
38463850 if templ_7745c5c3_Err != nil {
3847- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 32}
3851+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 745, Col: 32}
38483852 }
38493853 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var186))
38503854 if templ_7745c5c3_Err != nil {
@@ -3857,7 +3861,7 @@ func compareRail(p ComparePage) templ.Component {
38573861 var templ_7745c5c3_Var187 string
38583862 templ_7745c5c3_Var187, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
38593863 if templ_7745c5c3_Err != nil {
3860- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 746, Col: 16}
3864+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 747, Col: 16}
38613865 }
38623866 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var187))
38633867 if templ_7745c5c3_Err != nil {
@@ -3871,7 +3875,7 @@ func compareRail(p ComparePage) templ.Component {
38713875 var templ_7745c5c3_Var188 string
38723876 templ_7745c5c3_Var188, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
38733877 if templ_7745c5c3_Err != nil {
3874- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 15}
3878+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 749, Col: 15}
38753879 }
38763880 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var188))
38773881 if templ_7745c5c3_Err != nil {
@@ -3884,7 +3888,7 @@ func compareRail(p ComparePage) templ.Component {
38843888 var templ_7745c5c3_Var189 string
38853889 templ_7745c5c3_Var189, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
38863890 if templ_7745c5c3_Err != nil {
3887- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 42}
3891+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 749, Col: 42}
38883892 }
38893893 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var189)
38903894 if templ_7745c5c3_Err != nil {
@@ -3897,7 +3901,7 @@ func compareRail(p ComparePage) templ.Component {
38973901 var templ_7745c5c3_Var190 string
38983902 templ_7745c5c3_Var190, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
38993903 if templ_7745c5c3_Err != nil {
3900- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 53}
3904+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 749, Col: 53}
39013905 }
39023906 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var190))
39033907 if templ_7745c5c3_Err != nil {
@@ -3925,7 +3929,7 @@ func compareRail(p ComparePage) templ.Component {
39253929 var templ_7745c5c3_Var191 string
39263930 templ_7745c5c3_Var191, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
39273931 if templ_7745c5c3_Err != nil {
3928- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 755, Col: 47}
3932+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 756, Col: 47}
39293933 }
39303934 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var191))
39313935 if templ_7745c5c3_Err != nil {
@@ -3948,7 +3952,7 @@ func compareRail(p ComparePage) templ.Component {
39483952 var templ_7745c5c3_Var192 string
39493953 templ_7745c5c3_Var192, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
39503954 if templ_7745c5c3_Err != nil {
3951- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 760, Col: 66}
3955+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 761, Col: 66}
39523956 }
39533957 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var192))
39543958 if templ_7745c5c3_Err != nil {
@@ -3983,4 +3987,58 @@ func compareRail(p ComparePage) templ.Component {
39833987 })
39843988 }
39853989
3990+// MRMergeChecks is the part of the owner's merge form that depends on the
3991+// checks: a live update replaces it and keeps "Ignore checks" ticked.
3992+func MRMergeChecks(c ChecksSummary) templ.Component {
3993+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3994+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
3995+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
3996+ return templ_7745c5c3_CtxErr
3997+ }
3998+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
3999+ if !templ_7745c5c3_IsBuffer {
4000+ defer func() {
4001+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
4002+ if templ_7745c5c3_Err == nil {
4003+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
4004+ }
4005+ }()
4006+ }
4007+ ctx = templ.InitializeContext(ctx)
4008+ templ_7745c5c3_Var193 := templ.GetChildren(ctx)
4009+ if templ_7745c5c3_Var193 == nil {
4010+ templ_7745c5c3_Var193 = templ.NopComponent
4011+ }
4012+ ctx = templ.ClearChildren(ctx)
4013+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 375, "<div id=\"mr-merge-checks\" data-keep-checks>")
4014+ if templ_7745c5c3_Err != nil {
4015+ return templ_7745c5c3_Err
4016+ }
4017+ if c.Kind == "bad" || c.Kind == "running" {
4018+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 376, "<p class=\"d-note\">")
4019+ if templ_7745c5c3_Err != nil {
4020+ return templ_7745c5c3_Err
4021+ }
4022+ var templ_7745c5c3_Var194 string
4023+ templ_7745c5c3_Var194, templ_7745c5c3_Err = templ.JoinStringErrs(c.State + ": a failing or unfinished check blocks the merge.")
4024+ if templ_7745c5c3_Err != nil {
4025+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 776, Col: 84}
4026+ }
4027+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var194))
4028+ if templ_7745c5c3_Err != nil {
4029+ return templ_7745c5c3_Err
4030+ }
4031+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 377, "</p><label class=\"d-check\"><input type=\"checkbox\" name=\"ignore_checks\"> Ignore checks</label>")
4032+ if templ_7745c5c3_Err != nil {
4033+ return templ_7745c5c3_Err
4034+ }
4035+ }
4036+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 378, "</div>")
4037+ if templ_7745c5c3_Err != nil {
4038+ return templ_7745c5c3_Err
4039+ }
4040+ return nil
4041+ })
4042+}
4043+
39864044 var _ = templruntime.GeneratedTemplate
@@ -2805,6 +2805,10 @@ func mergeMenu(p MRPage) templ.Component {
2805 if templ_7745c5c3_Err != nil {2805 if templ_7745c5c3_Err != nil {
2806 return templ_7745c5c3_Err2806 return templ_7745c5c3_Err
2807 }2807 }
2808+ templ_7745c5c3_Err = MRMergeChecks(p.ChecksSum).Render(ctx, templ_7745c5c3_Buffer)
2809+ if templ_7745c5c3_Err != nil {
2810+ return templ_7745c5c3_Err
2811+ }
2808 if p.Box.CanDeleteBranch {2812 if p.Box.CanDeleteBranch {
2809 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")2813 templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 264, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ")
2810 if templ_7745c5c3_Err != nil {2814 if templ_7745c5c3_Err != nil {
@@ -2813,7 +2817,7 @@ func mergeMenu(p MRPage) templ.Component {
2813 var templ_7745c5c3_Var134 string2817 var templ_7745c5c3_Var134 string
2814 templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)2818 templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
2815 if templ_7745c5c3_Err != nil {2819 if templ_7745c5c3_Err != nil {
2816- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 549, Col: 102}2820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 550, Col: 102}
2817 }2821 }
2818 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))2822 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134))
2819 if templ_7745c5c3_Err != nil {2823 if templ_7745c5c3_Err != nil {
@@ -2841,7 +2845,7 @@ func mergeMenu(p MRPage) templ.Component {
2841 var templ_7745c5c3_Var135 string2845 var templ_7745c5c3_Var135 string
2842 templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())2846 templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.MergeTitle())
2843 if templ_7745c5c3_Err != nil {2847 if templ_7745c5c3_Err != nil {
2844- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 557, Col: 33}2848+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 558, Col: 33}
2845 }2849 }
2846 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)2850 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var135)
2847 if templ_7745c5c3_Err != nil {2851 if templ_7745c5c3_Err != nil {
@@ -2854,7 +2858,7 @@ func mergeMenu(p MRPage) templ.Component {
2854 var templ_7745c5c3_Var136 string2858 var templ_7745c5c3_Var136 string
2855 templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)2859 templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
2856 if templ_7745c5c3_Err != nil {2860 if templ_7745c5c3_Err != nil {
2857- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 560, Col: 28}2861+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 561, Col: 28}
2858 }2862 }
2859 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))2863 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136))
2860 if templ_7745c5c3_Err != nil {2864 if templ_7745c5c3_Err != nil {
@@ -2872,7 +2876,7 @@ func mergeMenu(p MRPage) templ.Component {
2872 var templ_7745c5c3_Var137 templ.SafeURL2876 var templ_7745c5c3_Var137 templ.SafeURL
2873 templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.AutoAction))2877 templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.AutoAction))
2874 if templ_7745c5c3_Err != nil {2878 if templ_7745c5c3_Err != nil {
2875- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 564, Col: 80}2879+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 80}
2876 }2880 }
2877 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))2881 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137))
2878 if templ_7745c5c3_Err != nil {2882 if templ_7745c5c3_Err != nil {
@@ -2885,7 +2889,7 @@ func mergeMenu(p MRPage) templ.Component {
2885 var templ_7745c5c3_Var138 string2889 var templ_7745c5c3_Var138 string
2886 templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)2890 templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
2887 if templ_7745c5c3_Err != nil {2891 if templ_7745c5c3_Err != nil {
2888- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 565, Col: 60}2892+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 566, Col: 60}
2889 }2893 }
2890 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)2894 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var138)
2891 if templ_7745c5c3_Err != nil {2895 if templ_7745c5c3_Err != nil {
@@ -2903,7 +2907,7 @@ func mergeMenu(p MRPage) templ.Component {
2903 var templ_7745c5c3_Var139 string2907 var templ_7745c5c3_Var139 string
2904 templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)2908 templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
2905 if templ_7745c5c3_Err != nil {2909 if templ_7745c5c3_Err != nil {
2906- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 62}2910+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 570, Col: 62}
2907 }2911 }
2908 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var139)2912 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var139)
2909 if templ_7745c5c3_Err != nil {2913 if templ_7745c5c3_Err != nil {
@@ -2926,7 +2930,7 @@ func mergeMenu(p MRPage) templ.Component {
2926 var templ_7745c5c3_Var140 string2930 var templ_7745c5c3_Var140 string
2927 templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)2931 templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
2928 if templ_7745c5c3_Err != nil {2932 if templ_7745c5c3_Err != nil {
2929- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 569, Col: 96}2933+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 570, Col: 96}
2930 }2934 }
2931 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))2935 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140))
2932 if templ_7745c5c3_Err != nil {2936 if templ_7745c5c3_Err != nil {
@@ -2991,7 +2995,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
2991 var templ_7745c5c3_Var142 string2995 var templ_7745c5c3_Var142 string
2992 templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)2996 templ_7745c5c3_Var142, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
2993 if templ_7745c5c3_Err != nil {2997 if templ_7745c5c3_Err != nil {
2994- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 587, Col: 22}2998+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 588, Col: 22}
2995 }2999 }
2996 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)3000 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var142)
2997 if templ_7745c5c3_Err != nil {3001 if templ_7745c5c3_Err != nil {
@@ -3004,7 +3008,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3004 var templ_7745c5c3_Var143 string3008 var templ_7745c5c3_Var143 string
3005 templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))3009 templ_7745c5c3_Var143, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.FFOk))
3006 if templ_7745c5c3_Err != nil {3010 if templ_7745c5c3_Err != nil {
3007- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 588, Col: 41}3011+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 41}
3008 }3012 }
3009 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)3013 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var143)
3010 if templ_7745c5c3_Err != nil {3014 if templ_7745c5c3_Err != nil {
@@ -3017,7 +3021,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3017 var templ_7745c5c3_Var144 string3021 var templ_7745c5c3_Var144 string
3018 templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))3022 templ_7745c5c3_Var144, templ_7745c5c3_Err = templ.ResolveAttributeValue(strconv.FormatBool(v.MergeOk))
3019 if templ_7745c5c3_Err != nil {3023 if templ_7745c5c3_Err != nil {
3020- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 589, Col: 47}3024+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 590, Col: 47}
3021 }3025 }
3022 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)3026 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var144)
3023 if templ_7745c5c3_Err != nil {3027 if templ_7745c5c3_Err != nil {
@@ -3030,7 +3034,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3030 var templ_7745c5c3_Var145 string3034 var templ_7745c5c3_Var145 string
3031 templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)3035 templ_7745c5c3_Var145, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.FFReason)
3032 if templ_7745c5c3_Err != nil {3036 if templ_7745c5c3_Err != nil {
3033- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 590, Col: 29}3037+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 591, Col: 29}
3034 }3038 }
3035 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var145)3039 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var145)
3036 if templ_7745c5c3_Err != nil {3040 if templ_7745c5c3_Err != nil {
@@ -3043,7 +3047,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3043 var templ_7745c5c3_Var146 string3047 var templ_7745c5c3_Var146 string
3044 templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)3048 templ_7745c5c3_Var146, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.MergeReason)
3045 if templ_7745c5c3_Err != nil {3049 if templ_7745c5c3_Err != nil {
3046- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 591, Col: 35}3050+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 592, Col: 35}
3047 }3051 }
3048 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var146)3052 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var146)
3049 if templ_7745c5c3_Err != nil {3053 if templ_7745c5c3_Err != nil {
@@ -3056,7 +3060,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3056 var templ_7745c5c3_Var147 string3060 var templ_7745c5c3_Var147 string
3057 templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)3061 templ_7745c5c3_Var147, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.State)
3058 if templ_7745c5c3_Err != nil {3062 if templ_7745c5c3_Err != nil {
3059- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 42}3063+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 42}
3060 }3064 }
3061 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)3065 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var147)
3062 if templ_7745c5c3_Err != nil {3066 if templ_7745c5c3_Err != nil {
@@ -3069,7 +3073,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3069 var templ_7745c5c3_Var148 string3073 var templ_7745c5c3_Var148 string
3070 templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)3074 templ_7745c5c3_Var148, templ_7745c5c3_Err = templ.JoinStringErrs(v.Label)
3071 if templ_7745c5c3_Err != nil {3075 if templ_7745c5c3_Err != nil {
3072- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 593, Col: 54}3076+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 594, Col: 54}
3073 }3077 }
3074 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))3078 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var148))
3075 if templ_7745c5c3_Err != nil {3079 if templ_7745c5c3_Err != nil {
@@ -3083,7 +3087,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3083 var templ_7745c5c3_Var149 string3087 var templ_7745c5c3_Var149 string
3084 templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinStringErrs(" ")3088 templ_7745c5c3_Var149, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
3085 if templ_7745c5c3_Err != nil {3089 if templ_7745c5c3_Err != nil {
3086- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 8}3090+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 8}
3087 }3091 }
3088 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))3092 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var149))
3089 if templ_7745c5c3_Err != nil {3093 if templ_7745c5c3_Err != nil {
@@ -3096,7 +3100,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3096 var templ_7745c5c3_Var150 string3100 var templ_7745c5c3_Var150 string
3097 templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)3101 templ_7745c5c3_Var150, templ_7745c5c3_Err = templ.JoinStringErrs(v.Detail)
3098 if templ_7745c5c3_Err != nil {3102 if templ_7745c5c3_Err != nil {
3099- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 595, Col: 38}3103+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 596, Col: 38}
3100 }3104 }
3101 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))3105 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var150))
3102 if templ_7745c5c3_Err != nil {3106 if templ_7745c5c3_Err != nil {
@@ -3124,7 +3128,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3124 var templ_7745c5c3_Var151 string3128 var templ_7745c5c3_Var151 string
3125 templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(path)3129 templ_7745c5c3_Var151, templ_7745c5c3_Err = templ.JoinStringErrs(path)
3126 if templ_7745c5c3_Err != nil {3130 if templ_7745c5c3_Err != nil {
3127- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 601, Col: 20}3131+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 602, Col: 20}
3128 }3132 }
3129 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))3133 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var151))
3130 if templ_7745c5c3_Err != nil {3134 if templ_7745c5c3_Err != nil {
@@ -3148,7 +3152,7 @@ func MRMergeability(v MergeabilityView) templ.Component {
3148 var templ_7745c5c3_Var152 string3152 var templ_7745c5c3_Var152 string
3149 templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)3153 templ_7745c5c3_Var152, templ_7745c5c3_Err = templ.JoinStringErrs(v.PathsNote)
3150 if templ_7745c5c3_Err != nil {3154 if templ_7745c5c3_Err != nil {
3151- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 606, Col: 30}3155+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 607, Col: 30}
3152 }3156 }
3153 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))3157 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var152))
3154 if templ_7745c5c3_Err != nil {3158 if templ_7745c5c3_Err != nil {
@@ -3262,7 +3266,7 @@ func moreMenu(p MRPage) templ.Component {
3262 var templ_7745c5c3_Var156 string3266 var templ_7745c5c3_Var156 string
3263 templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))3267 templ_7745c5c3_Var156, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", "))
3264 if templ_7745c5c3_Err != nil {3268 if templ_7745c5c3_Err != nil {
3265- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 633, Col: 38}3269+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 634, Col: 38}
3266 }3270 }
3267 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))3271 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var156))
3268 if templ_7745c5c3_Err != nil {3272 if templ_7745c5c3_Err != nil {
@@ -3273,7 +3277,7 @@ func moreMenu(p MRPage) templ.Component {
3273 return templ_7745c5c3_Err3277 return templ_7745c5c3_Err
3274 }3278 }
3275 if p.ChecksBlock {3279 if p.ChecksBlock {
3276- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "failing ones block the merge")3280+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 304, "failing or unfinished checks block the merge")
3277 if templ_7745c5c3_Err != nil {3281 if templ_7745c5c3_Err != nil {
3278 return templ_7745c5c3_Err3282 return templ_7745c5c3_Err
3279 }3283 }
@@ -3301,7 +3305,7 @@ func moreMenu(p MRPage) templ.Component {
3301 var templ_7745c5c3_Var157 string3305 var templ_7745c5c3_Var157 string
3302 templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)3306 templ_7745c5c3_Var157, templ_7745c5c3_Err = templ.JoinStringErrs(p.Box.Fingerprint)
3303 if templ_7745c5c3_Err != nil {3307 if templ_7745c5c3_Err != nil {
3304- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 649, Col: 44}3308+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 650, Col: 44}
3305 }3309 }
3306 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))3310 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var157))
3307 if templ_7745c5c3_Err != nil {3311 if templ_7745c5c3_Err != nil {
@@ -3329,7 +3333,7 @@ func moreMenu(p MRPage) templ.Component {
3329 var templ_7745c5c3_Var158 templ.SafeURL3333 var templ_7745c5c3_Var158 templ.SafeURL
3330 templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))3334 templ_7745c5c3_Var158, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction))
3331 if templ_7745c5c3_Err != nil {3335 if templ_7745c5c3_Err != nil {
3332- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 655, Col: 65}3336+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 65}
3333 }3337 }
3334 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))3338 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var158))
3335 if templ_7745c5c3_Err != nil {3339 if templ_7745c5c3_Err != nil {
@@ -3342,7 +3346,7 @@ func moreMenu(p MRPage) templ.Component {
3342 var templ_7745c5c3_Var159 string3346 var templ_7745c5c3_Var159 string
3343 templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)3347 templ_7745c5c3_Var159, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head)
3344 if templ_7745c5c3_Err != nil {3348 if templ_7745c5c3_Err != nil {
3345- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 656, Col: 60}3349+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 657, Col: 60}
3346 }3350 }
3347 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var159)3351 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var159)
3348 if templ_7745c5c3_Err != nil {3352 if templ_7745c5c3_Err != nil {
@@ -3360,7 +3364,7 @@ func moreMenu(p MRPage) templ.Component {
3360 var templ_7745c5c3_Var160 templ.SafeURL3364 var templ_7745c5c3_Var160 templ.SafeURL
3361 templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))3365 templ_7745c5c3_Var160, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction))
3362 if templ_7745c5c3_Err != nil {3366 if templ_7745c5c3_Err != nil {
3363- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 660, Col: 65}3367+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 661, Col: 65}
3364 }3368 }
3365 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))3369 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var160))
3366 if templ_7745c5c3_Err != nil {3370 if templ_7745c5c3_Err != nil {
@@ -3433,7 +3437,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3433 var templ_7745c5c3_Var163 string3437 var templ_7745c5c3_Var163 string
3434 templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)3438 templ_7745c5c3_Var163, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3435 if templ_7745c5c3_Err != nil {3439 if templ_7745c5c3_Err != nil {
3436- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 17}3440+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 678, Col: 17}
3437 }3441 }
3438 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var163))3442 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var163))
3439 if templ_7745c5c3_Err != nil {3443 if templ_7745c5c3_Err != nil {
@@ -3446,7 +3450,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3446 var templ_7745c5c3_Var164 string3450 var templ_7745c5c3_Var164 string
3447 templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)3451 templ_7745c5c3_Var164, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3448 if templ_7745c5c3_Err != nil {3452 if templ_7745c5c3_Err != nil {
3449- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 677, Col: 34}3453+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 678, Col: 34}
3450 }3454 }
3451 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var164))3455 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var164))
3452 if templ_7745c5c3_Err != nil {3456 if templ_7745c5c3_Err != nil {
@@ -3459,7 +3463,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3459 var templ_7745c5c3_Var165 templ.SafeURL3463 var templ_7745c5c3_Var165 templ.SafeURL
3460 templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))3464 templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref))
3461 if templ_7745c5c3_Err != nil {3465 if templ_7745c5c3_Err != nil {
3462- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 45}3466+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 681, Col: 45}
3463 }3467 }
3464 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))3468 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
3465 if templ_7745c5c3_Err != nil {3469 if templ_7745c5c3_Err != nil {
@@ -3472,7 +3476,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3472 var templ_7745c5c3_Var166 string3476 var templ_7745c5c3_Var166 string
3473 templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)3477 templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source)
3474 if templ_7745c5c3_Err != nil {3478 if templ_7745c5c3_Err != nil {
3475- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 680, Col: 58}3479+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 681, Col: 58}
3476 }3480 }
3477 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))3481 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3478 if templ_7745c5c3_Err != nil {3482 if templ_7745c5c3_Err != nil {
@@ -3485,7 +3489,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3485 var templ_7745c5c3_Var167 templ.SafeURL3489 var templ_7745c5c3_Var167 templ.SafeURL
3486 templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))3490 templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref))
3487 if templ_7745c5c3_Err != nil {3491 if templ_7745c5c3_Err != nil {
3488- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 45}3492+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 683, Col: 45}
3489 }3493 }
3490 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))3494 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
3491 if templ_7745c5c3_Err != nil {3495 if templ_7745c5c3_Err != nil {
@@ -3498,7 +3502,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3498 var templ_7745c5c3_Var168 string3502 var templ_7745c5c3_Var168 string
3499 templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)3503 templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3500 if templ_7745c5c3_Err != nil {3504 if templ_7745c5c3_Err != nil {
3501- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 682, Col: 58}3505+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 683, Col: 58}
3502 }3506 }
3503 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))3507 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
3504 if templ_7745c5c3_Err != nil {3508 if templ_7745c5c3_Err != nil {
@@ -3511,7 +3515,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3511 var templ_7745c5c3_Var169 string3515 var templ_7745c5c3_Var169 string
3512 templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)3516 templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText)
3513 if templ_7745c5c3_Err != nil {3517 if templ_7745c5c3_Err != nil {
3514- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 684, Col: 21}3518+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 685, Col: 21}
3515 }3519 }
3516 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))3520 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
3517 if templ_7745c5c3_Err != nil {3521 if templ_7745c5c3_Err != nil {
@@ -3529,7 +3533,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3529 var templ_7745c5c3_Var170 string3533 var templ_7745c5c3_Var170 string
3530 templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)3534 templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note)
3531 if templ_7745c5c3_Err != nil {3535 if templ_7745c5c3_Err != nil {
3532- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 688, Col: 29}3536+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 689, Col: 29}
3533 }3537 }
3534 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170))3538 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170))
3535 if templ_7745c5c3_Err != nil {3539 if templ_7745c5c3_Err != nil {
@@ -3552,7 +3556,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3552 var templ_7745c5c3_Var171 templ.SafeURL3556 var templ_7745c5c3_Var171 templ.SafeURL
3553 templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))3557 templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href))
3554 if templ_7745c5c3_Err != nil {3558 if templ_7745c5c3_Err != nil {
3555- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 114}3559+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 692, Col: 114}
3556 }3560 }
3557 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))3561 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
3558 if templ_7745c5c3_Err != nil {3562 if templ_7745c5c3_Err != nil {
@@ -3565,7 +3569,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3565 var templ_7745c5c3_Var172 string3569 var templ_7745c5c3_Var172 string
3566 templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)3570 templ_7745c5c3_Var172, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label)
3567 if templ_7745c5c3_Err != nil {3571 if templ_7745c5c3_Err != nil {
3568- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 691, Col: 135}3572+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 692, Col: 135}
3569 }3573 }
3570 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))3574 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var172))
3571 if templ_7745c5c3_Err != nil {3575 if templ_7745c5c3_Err != nil {
@@ -3588,7 +3592,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3588 var templ_7745c5c3_Var173 templ.SafeURL3592 var templ_7745c5c3_Var173 templ.SafeURL
3589 templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))3593 templ_7745c5c3_Var173, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action))
3590 if templ_7745c5c3_Err != nil {3594 if templ_7745c5c3_Err != nil {
3591- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 694, Col: 83}3595+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 83}
3592 }3596 }
3593 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))3597 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var173))
3594 if templ_7745c5c3_Err != nil {3598 if templ_7745c5c3_Err != nil {
@@ -3601,7 +3605,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3601 var templ_7745c5c3_Var174 string3605 var templ_7745c5c3_Var174 string
3602 templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)3606 templ_7745c5c3_Var174, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source)
3603 if templ_7745c5c3_Err != nil {3607 if templ_7745c5c3_Err != nil {
3604- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 695, Col: 62}3608+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 696, Col: 62}
3605 }3609 }
3606 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var174)3610 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var174)
3607 if templ_7745c5c3_Err != nil {3611 if templ_7745c5c3_Err != nil {
@@ -3614,7 +3618,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3614 var templ_7745c5c3_Var175 string3618 var templ_7745c5c3_Var175 string
3615 templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)3619 templ_7745c5c3_Var175, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target)
3616 if templ_7745c5c3_Err != nil {3620 if templ_7745c5c3_Err != nil {
3617- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 696, Col: 62}3621+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 697, Col: 62}
3618 }3622 }
3619 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var175)3623 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var175)
3620 if templ_7745c5c3_Err != nil {3624 if templ_7745c5c3_Err != nil {
@@ -3627,7 +3631,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3627 var templ_7745c5c3_Var176 string3631 var templ_7745c5c3_Var176 string
3628 templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)3632 templ_7745c5c3_Var176, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title)
3629 if templ_7745c5c3_Err != nil {3633 if templ_7745c5c3_Err != nil {
3630- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 698, Col: 97}3634+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 699, Col: 97}
3631 }3635 }
3632 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var176)3636 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var176)
3633 if templ_7745c5c3_Err != nil {3637 if templ_7745c5c3_Err != nil {
@@ -3650,7 +3654,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3650 var templ_7745c5c3_Var177 string3654 var templ_7745c5c3_Var177 string
3651 templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))3655 templ_7745c5c3_Var177, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files"))
3652 if templ_7745c5c3_Err != nil {3656 if templ_7745c5c3_Err != nil {
3653- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 57}3657+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 709, Col: 57}
3654 }3658 }
3655 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))3659 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var177))
3656 if templ_7745c5c3_Err != nil {3660 if templ_7745c5c3_Err != nil {
@@ -3663,7 +3667,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3663 var templ_7745c5c3_Var178 string3667 var templ_7745c5c3_Var178 string
3664 templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))3668 templ_7745c5c3_Var178, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds))
3665 if templ_7745c5c3_Err != nil {3669 if templ_7745c5c3_Err != nil {
3666- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 97}3670+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 709, Col: 97}
3667 }3671 }
3668 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))3672 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var178))
3669 if templ_7745c5c3_Err != nil {3673 if templ_7745c5c3_Err != nil {
@@ -3676,7 +3680,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3676 var templ_7745c5c3_Var179 string3680 var templ_7745c5c3_Var179 string
3677 templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))3681 templ_7745c5c3_Var179, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels))
3678 if templ_7745c5c3_Err != nil {3682 if templ_7745c5c3_Err != nil {
3679- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 708, Col: 142}3683+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 709, Col: 142}
3680 }3684 }
3681 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))3685 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var179))
3682 if templ_7745c5c3_Err != nil {3686 if templ_7745c5c3_Err != nil {
@@ -3712,7 +3716,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3712 var templ_7745c5c3_Var180 string3716 var templ_7745c5c3_Var180 string
3713 templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)3717 templ_7745c5c3_Var180, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3714 if templ_7745c5c3_Err != nil {3718 if templ_7745c5c3_Err != nil {
3715- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 721, Col: 23}3719+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 722, Col: 23}
3716 }3720 }
3717 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var180)3721 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var180)
3718 if templ_7745c5c3_Err != nil {3722 if templ_7745c5c3_Err != nil {
@@ -3730,7 +3734,7 @@ func Compare(l Layout, p ComparePage) templ.Component {
3730 var templ_7745c5c3_Var181 string3734 var templ_7745c5c3_Var181 string
3731 templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)3735 templ_7745c5c3_Var181, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View)
3732 if templ_7745c5c3_Err != nil {3736 if templ_7745c5c3_Err != nil {
3733- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 723, Col: 23}3737+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 724, Col: 23}
3734 }3738 }
3735 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var181)3739 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var181)
3736 if templ_7745c5c3_Err != nil {3740 if templ_7745c5c3_Err != nil {
@@ -3800,7 +3804,7 @@ func compareRail(p ComparePage) templ.Component {
3800 var templ_7745c5c3_Var183 string3804 var templ_7745c5c3_Var183 string
3801 templ_7745c5c3_Var183, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))3805 templ_7745c5c3_Var183, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead)))
3802 if templ_7745c5c3_Err != nil {3806 if templ_7745c5c3_Err != nil {
3803- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 739, Col: 67}3807+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 740, Col: 67}
3804 }3808 }
3805 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var183))3809 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var183))
3806 if templ_7745c5c3_Err != nil {3810 if templ_7745c5c3_Err != nil {
@@ -3818,7 +3822,7 @@ func compareRail(p ComparePage) templ.Component {
3818 var templ_7745c5c3_Var184 templ.SafeURL3822 var templ_7745c5c3_Var184 templ.SafeURL
3819 templ_7745c5c3_Var184, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))3823 templ_7745c5c3_Var184, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref))
3820 if templ_7745c5c3_Err != nil {3824 if templ_7745c5c3_Err != nil {
3821- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 37}3825+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 37}
3822 }3826 }
3823 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var184))3827 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var184))
3824 if templ_7745c5c3_Err != nil {3828 if templ_7745c5c3_Err != nil {
@@ -3831,7 +3835,7 @@ func compareRail(p ComparePage) templ.Component {
3831 var templ_7745c5c3_Var185 string3835 var templ_7745c5c3_Var185 string
3832 templ_7745c5c3_Var185, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)3836 templ_7745c5c3_Var185, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short)
3833 if templ_7745c5c3_Err != nil {3837 if templ_7745c5c3_Err != nil {
3834- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 743, Col: 49}3838+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 49}
3835 }3839 }
3836 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var185))3840 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var185))
3837 if templ_7745c5c3_Err != nil {3841 if templ_7745c5c3_Err != nil {
@@ -3844,7 +3848,7 @@ func compareRail(p ComparePage) templ.Component {
3844 var templ_7745c5c3_Var186 string3848 var templ_7745c5c3_Var186 string
3845 templ_7745c5c3_Var186, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)3849 templ_7745c5c3_Var186, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject)
3846 if templ_7745c5c3_Err != nil {3850 if templ_7745c5c3_Err != nil {
3847- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 744, Col: 32}3851+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 745, Col: 32}
3848 }3852 }
3849 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var186))3853 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var186))
3850 if templ_7745c5c3_Err != nil {3854 if templ_7745c5c3_Err != nil {
@@ -3857,7 +3861,7 @@ func compareRail(p ComparePage) templ.Component {
3857 var templ_7745c5c3_Var187 string3861 var templ_7745c5c3_Var187 string
3858 templ_7745c5c3_Var187, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)3862 templ_7745c5c3_Var187, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author)
3859 if templ_7745c5c3_Err != nil {3863 if templ_7745c5c3_Err != nil {
3860- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 746, Col: 16}3864+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 747, Col: 16}
3861 }3865 }
3862 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var187))3866 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var187))
3863 if templ_7745c5c3_Err != nil {3867 if templ_7745c5c3_Err != nil {
@@ -3871,7 +3875,7 @@ func compareRail(p ComparePage) templ.Component {
3871 var templ_7745c5c3_Var188 string3875 var templ_7745c5c3_Var188 string
3872 templ_7745c5c3_Var188, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")3876 templ_7745c5c3_Var188, templ_7745c5c3_Err = templ.JoinStringErrs(" · ")
3873 if templ_7745c5c3_Err != nil {3877 if templ_7745c5c3_Err != nil {
3874- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 15}3878+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 749, Col: 15}
3875 }3879 }
3876 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var188))3880 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var188))
3877 if templ_7745c5c3_Err != nil {3881 if templ_7745c5c3_Err != nil {
@@ -3884,7 +3888,7 @@ func compareRail(p ComparePage) templ.Component {
3884 var templ_7745c5c3_Var189 string3888 var templ_7745c5c3_Var189 string
3885 templ_7745c5c3_Var189, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)3889 templ_7745c5c3_Var189, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt)
3886 if templ_7745c5c3_Err != nil {3890 if templ_7745c5c3_Err != nil {
3887- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 42}3891+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 749, Col: 42}
3888 }3892 }
3889 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var189)3893 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var189)
3890 if templ_7745c5c3_Err != nil {3894 if templ_7745c5c3_Err != nil {
@@ -3897,7 +3901,7 @@ func compareRail(p ComparePage) templ.Component {
3897 var templ_7745c5c3_Var190 string3901 var templ_7745c5c3_Var190 string
3898 templ_7745c5c3_Var190, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)3902 templ_7745c5c3_Var190, templ_7745c5c3_Err = templ.JoinStringErrs(c.When)
3899 if templ_7745c5c3_Err != nil {3903 if templ_7745c5c3_Err != nil {
3900- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 748, Col: 53}3904+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 749, Col: 53}
3901 }3905 }
3902 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var190))3906 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var190))
3903 if templ_7745c5c3_Err != nil {3907 if templ_7745c5c3_Err != nil {
@@ -3925,7 +3929,7 @@ func compareRail(p ComparePage) templ.Component {
3925 var templ_7745c5c3_Var191 string3929 var templ_7745c5c3_Var191 string
3926 templ_7745c5c3_Var191, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)3930 templ_7745c5c3_Var191, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target)
3927 if templ_7745c5c3_Err != nil {3931 if templ_7745c5c3_Err != nil {
3928- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 755, Col: 47}3932+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 756, Col: 47}
3929 }3933 }
3930 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var191))3934 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var191))
3931 if templ_7745c5c3_Err != nil {3935 if templ_7745c5c3_Err != nil {
@@ -3948,7 +3952,7 @@ func compareRail(p ComparePage) templ.Component {
3948 var templ_7745c5c3_Var192 string3952 var templ_7745c5c3_Var192 string
3949 templ_7745c5c3_Var192, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))3953 templ_7745c5c3_Var192, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files)))
3950 if templ_7745c5c3_Err != nil {3954 if templ_7745c5c3_Err != nil {
3951- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 760, Col: 66}3955+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 761, Col: 66}
3952 }3956 }
3953 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var192))3957 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var192))
3954 if templ_7745c5c3_Err != nil {3958 if templ_7745c5c3_Err != nil {
@@ -3983,4 +3987,58 @@ func compareRail(p ComparePage) templ.Component {
3983 })3987 })
3984 }3988 }
3985 3989
3990+// MRMergeChecks is the part of the owner's merge form that depends on the
3991+// checks: a live update replaces it and keeps "Ignore checks" ticked.
3992+func MRMergeChecks(c ChecksSummary) templ.Component {
3993+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
3994+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
3995+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
3996+ return templ_7745c5c3_CtxErr
3997+ }
3998+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
3999+ if !templ_7745c5c3_IsBuffer {
4000+ defer func() {
4001+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
4002+ if templ_7745c5c3_Err == nil {
4003+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
4004+ }
4005+ }()
4006+ }
4007+ ctx = templ.InitializeContext(ctx)
4008+ templ_7745c5c3_Var193 := templ.GetChildren(ctx)
4009+ if templ_7745c5c3_Var193 == nil {
4010+ templ_7745c5c3_Var193 = templ.NopComponent
4011+ }
4012+ ctx = templ.ClearChildren(ctx)
4013+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 375, "<div id=\"mr-merge-checks\" data-keep-checks>")
4014+ if templ_7745c5c3_Err != nil {
4015+ return templ_7745c5c3_Err
4016+ }
4017+ if c.Kind == "bad" || c.Kind == "running" {
4018+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 376, "<p class=\"d-note\">")
4019+ if templ_7745c5c3_Err != nil {
4020+ return templ_7745c5c3_Err
4021+ }
4022+ var templ_7745c5c3_Var194 string
4023+ templ_7745c5c3_Var194, templ_7745c5c3_Err = templ.JoinStringErrs(c.State + ": a failing or unfinished check blocks the merge.")
4024+ if templ_7745c5c3_Err != nil {
4025+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 776, Col: 84}
4026+ }
4027+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var194))
4028+ if templ_7745c5c3_Err != nil {
4029+ return templ_7745c5c3_Err
4030+ }
4031+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 377, "</p><label class=\"d-check\"><input type=\"checkbox\" name=\"ignore_checks\"> Ignore checks</label>")
4032+ if templ_7745c5c3_Err != nil {
4033+ return templ_7745c5c3_Err
4034+ }
4035+ }
4036+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 378, "</div>")
4037+ if templ_7745c5c3_Err != nil {
4038+ return templ_7745c5c3_Err
4039+ }
4040+ return nil
4041+ })
4042+}
4043+
3986 var _ = templruntime.GeneratedTemplate4044 var _ = templruntime.GeneratedTemplate
M internal/adapters/web/mr_test.go +35 -1
@@ -161,7 +161,7 @@ func TestMRPanelsAndMenus(t *testing.T) {
161161 assert.NotContains(t, rail, text)
162162 }
163163 more := renderToString(t, moreMenu(p))
164- for _, text := range []string{"Merge settings", "squash, merge", "failing ones block", `id="mr-signing"`, "fingerprint", "ermite signing-key show"} {
164+ for _, text := range []string{"Merge settings", "squash, merge", "failing or unfinished checks block", `id="mr-signing"`, "fingerprint", "ermite signing-key show"} {
165165 assert.Contains(t, more, text)
166166 }
167167 merge := renderToString(t, mergeMenu(p))
@@ -297,3 +297,37 @@ func TestAutoMergeLiveEvents(t *testing.T) {
297297 assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeEnabled{})))
298298 assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeCancelled{})))
299299 }
300+
301+func TestMergeIgnoreChecks(t *testing.T) {
302+ t.Parallel()
303+ for _, kind := range []string{"bad", "running", "ok", ""} {
304+ t.Run(kind, func(t *testing.T) {
305+ t.Parallel()
306+ box := (&Server{}).mergeBox(&repoData{repo: repoWithModes(domain.MergeModes()...)}, 3, "feat", "abc", true, mergeabilityView(domain.Mergeability{State: domain.MergeabilityUpToDate}, "main"))
307+ html := renderToString(t, mergeMenu(MRPage{Box: box, ChecksSum: ChecksSummary{Kind: kind}}))
308+ if kind == "bad" || kind == "running" {
309+ assert.Contains(t, html, `name="ignore_checks"`)
310+ assert.Contains(t, html, "Ignore checks")
311+ } else {
312+ assert.NotContains(t, html, `name="ignore_checks"`)
313+ }
314+ assert.True(t, box.CanMerge())
315+ })
316+ }
317+ name, err := domain.ParseRepoName("demo")
318+ require.NoError(t, err)
319+ head, err := domain.ParseObjectID(strings.Repeat("a", domain.ObjectIDLen))
320+ require.NoError(t, err)
321+ for _, ignore := range []bool{false, true} {
322+ svc := mocks.NewMergeRequestService(t)
323+ svc.EXPECT().Merge(mock.Anything, mock.Anything, name, int64(3), ports.MergeParams{Mode: domain.MergeModeSquash, HeadID: head, IgnoreChecks: ignore}).Return(ports.MergeResult{}, nil).Once()
324+ body := "mode=squash&head_id=" + head.String()
325+ if ignore {
326+ body += "&ignore_checks=on"
327+ }
328+ s := &Server{cfg: Config{MergeRequests: svc}}
329+ to, _, err := s.mrMerge(context.Background(), autoMergeRequest("/demo/merge-requests/3/merge", body))
330+ require.NoError(t, err)
331+ assert.Equal(t, mrHref("demo", 3), to)
332+ }
333+}
@@ -161,7 +161,7 @@ func TestMRPanelsAndMenus(t *testing.T) {
161 assert.NotContains(t, rail, text)161 assert.NotContains(t, rail, text)
162 }162 }
163 more := renderToString(t, moreMenu(p))163 more := renderToString(t, moreMenu(p))
164- for _, text := range []string{"Merge settings", "squash, merge", "failing ones block", `id="mr-signing"`, "fingerprint", "ermite signing-key show"} {164+ for _, text := range []string{"Merge settings", "squash, merge", "failing or unfinished checks block", `id="mr-signing"`, "fingerprint", "ermite signing-key show"} {
165 assert.Contains(t, more, text)165 assert.Contains(t, more, text)
166 }166 }
167 merge := renderToString(t, mergeMenu(p))167 merge := renderToString(t, mergeMenu(p))
@@ -297,3 +297,37 @@ func TestAutoMergeLiveEvents(t *testing.T) {
297 assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeEnabled{})))297 assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeEnabled{})))
298 assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeCancelled{})))298 assert.True(t, liveEvent(watchOf(domain.MergeRequestAutoMergeCancelled{})))
299 }299 }
300+
301+func TestMergeIgnoreChecks(t *testing.T) {
302+ t.Parallel()
303+ for _, kind := range []string{"bad", "running", "ok", ""} {
304+ t.Run(kind, func(t *testing.T) {
305+ t.Parallel()
306+ box := (&Server{}).mergeBox(&repoData{repo: repoWithModes(domain.MergeModes()...)}, 3, "feat", "abc", true, mergeabilityView(domain.Mergeability{State: domain.MergeabilityUpToDate}, "main"))
307+ html := renderToString(t, mergeMenu(MRPage{Box: box, ChecksSum: ChecksSummary{Kind: kind}}))
308+ if kind == "bad" || kind == "running" {
309+ assert.Contains(t, html, `name="ignore_checks"`)
310+ assert.Contains(t, html, "Ignore checks")
311+ } else {
312+ assert.NotContains(t, html, `name="ignore_checks"`)
313+ }
314+ assert.True(t, box.CanMerge())
315+ })
316+ }
317+ name, err := domain.ParseRepoName("demo")
318+ require.NoError(t, err)
319+ head, err := domain.ParseObjectID(strings.Repeat("a", domain.ObjectIDLen))
320+ require.NoError(t, err)
321+ for _, ignore := range []bool{false, true} {
322+ svc := mocks.NewMergeRequestService(t)
323+ svc.EXPECT().Merge(mock.Anything, mock.Anything, name, int64(3), ports.MergeParams{Mode: domain.MergeModeSquash, HeadID: head, IgnoreChecks: ignore}).Return(ports.MergeResult{}, nil).Once()
324+ body := "mode=squash&head_id=" + head.String()
325+ if ignore {
326+ body += "&ignore_checks=on"
327+ }
328+ s := &Server{cfg: Config{MergeRequests: svc}}
329+ to, _, err := s.mrMerge(context.Background(), autoMergeRequest("/demo/merge-requests/3/merge", body))
330+ require.NoError(t, err)
331+ assert.Equal(t, mrHref("demo", 3), to)
332+ }
333+}
M internal/adapters/web/static/js/ermite.js +7 -0
@@ -966,6 +966,13 @@ window.ermiteLive = (() => {
966966 continue;
967967 }
968968 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');
969+ // data-keep-checks: the checkboxes the user ticked stay ticked.
970+ if (old.hasAttribute('data-keep-checks')) {
971+ for (const box of old.querySelectorAll('input[type=checkbox][name]:checked')) {
972+ const same = el.querySelector(`input[type=checkbox][name="${CSS.escape(box.name)}"]`);
973+ if (same) same.checked = true;
974+ }
975+ }
969976 old.replaceWith(el);
970977 const rows = document.getElementById('repo-rows');
971978 if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' &&
@@ -966,6 +966,13 @@ window.ermiteLive = (() => {
966 continue;966 continue;
967 }967 }
968 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');968 if (old.hasAttribute('data-cursor')) el.setAttribute('data-cursor', '');
969+ // data-keep-checks: the checkboxes the user ticked stay ticked.
970+ if (old.hasAttribute('data-keep-checks')) {
971+ for (const box of old.querySelectorAll('input[type=checkbox][name]:checked')) {
972+ const same = el.querySelector(`input[type=checkbox][name="${CSS.escape(box.name)}"]`);
973+ if (same) same.checked = true;
974+ }
975+ }
969 old.replaceWith(el);976 old.replaceWith(el);
970 const rows = document.getElementById('repo-rows');977 const rows = document.getElementById('repo-rows');
971 if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' &&978 if (rows && el.parentElement === rows && rows.dataset.sort === 'activity' &&
M internal/adapters/web/summary_actions_test.go +53 -0
@@ -4,6 +4,7 @@ package web
44
55 import (
66 "bytes"
7+ "context"
78 "errors"
89 "strings"
910 "testing"
@@ -125,3 +126,55 @@ func TestRunUpdatedOnSummaryWhenCIIsNotReadable(t *testing.T) {
125126 assert.Contains(t, got[0].html, "hidden", "the block empties and hides, no run leaks")
126127 assert.NotContains(t, got[0].html, "rr-name")
127128 }
129+
130+func TestMRChecksLiveMergeControls(t *testing.T) {
131+ t.Parallel()
132+ repo := repoName(t, "proj")
133+ head := strings.Repeat("a", domain.ObjectIDLen)
134+ for _, kind := range []string{"bad", "running", "ok", ""} {
135+ t.Run(kind, func(t *testing.T) {
136+ t.Parallel()
137+ runs := mocks.NewRunService(t)
138+ var checks []ports.CheckView
139+ if kind != "" {
140+ check := domain.CICheck{Name: "ci", Status: domain.CICompleted, Conclusion: domain.CISuccess}
141+ if kind == "bad" {
142+ check.Conclusion = domain.CIFailure
143+ }
144+ if kind == "running" {
145+ check.Status, check.Conclusion = domain.CIQueued, ""
146+ }
147+ checks = []ports.CheckView{{CICheck: check}}
148+ }
149+ runs.EXPECT().ListChecks(mock.Anything, mock.Anything, repo, head).Return(ports.ChecksResult{Checks: checks}, nil)
150+ srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: mocks.NewBrowseService(t), Runs: runs})
151+
152+ // A visitor's stream never carries the owner's merge form.
153+ visitor, err := (&liveStream{s: srv, repo: "proj", head: head, view: "mr"}).mrChecks(t.Context())
154+ require.NoError(t, err)
155+ require.Len(t, visitor, 2)
156+ for _, f := range visitor {
157+ assert.NotContains(t, f.html, `mr-merge-checks`)
158+ }
159+
160+ owner := context.WithValue(t.Context(), signedInKey{}, struct{}{})
161+ ls := &liveStream{s: srv, repo: "proj", head: head, view: "mr"}
162+ got, err := ls.mrChecks(owner)
163+ require.NoError(t, err)
164+ require.Len(t, got, 3)
165+ assert.Contains(t, got[2].html, `data-keep-checks`, "a swap keeps the ticked box")
166+ assert.Equal(t, "ci-run", got[2].event)
167+ assert.Contains(t, got[2].html, `id="mr-merge-checks"`)
168+ assert.Equal(t, renderToString(t, MRMergeChecks(checksSummary(repo, head, checks))), got[2].html)
169+ if kind == "bad" || kind == "running" {
170+ assert.Contains(t, got[2].html, `name="ignore_checks"`)
171+ } else {
172+ assert.NotContains(t, got[2].html, `name="ignore_checks"`)
173+ }
174+ // An unrelated job event with identical checks must not reset the checkbox.
175+ got, err = ls.mrChecks(owner)
176+ require.NoError(t, err)
177+ require.Len(t, got, 2)
178+ })
179+ }
180+}
@@ -4,6 +4,7 @@ package web
4 4
5 import (5 import (
6 "bytes"6 "bytes"
7+ "context"
7 "errors"8 "errors"
8 "strings"9 "strings"
9 "testing"10 "testing"
@@ -125,3 +126,55 @@ func TestRunUpdatedOnSummaryWhenCIIsNotReadable(t *testing.T) {
125 assert.Contains(t, got[0].html, "hidden", "the block empties and hides, no run leaks")126 assert.Contains(t, got[0].html, "hidden", "the block empties and hides, no run leaks")
126 assert.NotContains(t, got[0].html, "rr-name")127 assert.NotContains(t, got[0].html, "rr-name")
127 }128 }
129+
130+func TestMRChecksLiveMergeControls(t *testing.T) {
131+ t.Parallel()
132+ repo := repoName(t, "proj")
133+ head := strings.Repeat("a", domain.ObjectIDLen)
134+ for _, kind := range []string{"bad", "running", "ok", ""} {
135+ t.Run(kind, func(t *testing.T) {
136+ t.Parallel()
137+ runs := mocks.NewRunService(t)
138+ var checks []ports.CheckView
139+ if kind != "" {
140+ check := domain.CICheck{Name: "ci", Status: domain.CICompleted, Conclusion: domain.CISuccess}
141+ if kind == "bad" {
142+ check.Conclusion = domain.CIFailure
143+ }
144+ if kind == "running" {
145+ check.Status, check.Conclusion = domain.CIQueued, ""
146+ }
147+ checks = []ports.CheckView{{CICheck: check}}
148+ }
149+ runs.EXPECT().ListChecks(mock.Anything, mock.Anything, repo, head).Return(ports.ChecksResult{Checks: checks}, nil)
150+ srv := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: mocks.NewBrowseService(t), Runs: runs})
151+
152+ // A visitor's stream never carries the owner's merge form.
153+ visitor, err := (&liveStream{s: srv, repo: "proj", head: head, view: "mr"}).mrChecks(t.Context())
154+ require.NoError(t, err)
155+ require.Len(t, visitor, 2)
156+ for _, f := range visitor {
157+ assert.NotContains(t, f.html, `mr-merge-checks`)
158+ }
159+
160+ owner := context.WithValue(t.Context(), signedInKey{}, struct{}{})
161+ ls := &liveStream{s: srv, repo: "proj", head: head, view: "mr"}
162+ got, err := ls.mrChecks(owner)
163+ require.NoError(t, err)
164+ require.Len(t, got, 3)
165+ assert.Contains(t, got[2].html, `data-keep-checks`, "a swap keeps the ticked box")
166+ assert.Equal(t, "ci-run", got[2].event)
167+ assert.Contains(t, got[2].html, `id="mr-merge-checks"`)
168+ assert.Equal(t, renderToString(t, MRMergeChecks(checksSummary(repo, head, checks))), got[2].html)
169+ if kind == "bad" || kind == "running" {
170+ assert.Contains(t, got[2].html, `name="ignore_checks"`)
171+ } else {
172+ assert.NotContains(t, got[2].html, `name="ignore_checks"`)
173+ }
174+ // An unrelated job event with identical checks must not reset the checkbox.
175+ got, err = ls.mrChecks(owner)
176+ require.NoError(t, err)
177+ require.Len(t, got, 2)
178+ })
179+ }
180+}
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", "merge_requests.auto_merge"}, 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", "merge_requests.ignore_checks", "merge_requests.ready"}, 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", "merge_requests.auto_merge"}, 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", "merge_requests.ignore_checks", "merge_requests.ready"}, resp.Msg.GetFeatures(), who)
200 assert.NotEmpty(t, resp.Msg.GetVersion())200 assert.NotEmpty(t, resp.Msg.GetVersion())
201 }201 }
202 })202 })
M internal/app/ci_e2e_test.go +3 -9
@@ -13,7 +13,6 @@ import (
1313 "connectrpc.com/connect"
1414 "github.com/stretchr/testify/assert"
1515 "github.com/stretchr/testify/require"
16- "google.golang.org/protobuf/types/known/fieldmaskpb"
1716 "google.golang.org/protobuf/types/known/timestamppb"
1817
1918 pb "bnema.dev/ermite-api/gen/ermite"
@@ -271,7 +270,7 @@ func TestCIPushRunsAWorkflowOnARunner(t *testing.T) {
271270 }
272271
273272 // AC5: merge refused while checks are pending or failing, allowed when they
274-// pass, and when failing_checks_block is off.
273+// pass, or when explicitly bypassed by an authorized caller.
275274 func TestCIMergeIsBlockedByChecks(t *testing.T) {
276275 c := newCIHarness(t, nil)
277276 runner := c.registerRunner("alpha", "ubuntu-latest")
@@ -302,14 +301,9 @@ func TestCIMergeIsBlockedByChecks(t *testing.T) {
302301 assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err))
303302 assert.Contains(t, err.Error(), "failing checks block the merge")
304303
305- // Off: the merge goes through.
306- _, err = c.root.repo.UpdateRepo(c.ctx, connect.NewRequest(&pb.UpdateRepoRequest{
307- Repo: "proj",
308- Settings: &pb.RepoSettings{FailingChecksBlock: false},
309- UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.failing_checks_block"}},
310- }))
304+ // An explicit override by root allows the merge despite failure.
305+ _, err = c.root.mr.MergeMergeRequest(c.ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{Repo: "proj", Number: mr.Msg.GetMergeRequest().GetNumber(), HeadId: feat, IgnoreChecks: true}))
311306 require.NoError(t, err)
312- require.NoError(t, merge())
313307 }
314308
315309 // AC2: a job token works for its job only, and not once the job has ended.
@@ -13,7 +13,6 @@ import (
13 "connectrpc.com/connect"13 "connectrpc.com/connect"
14 "github.com/stretchr/testify/assert"14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
16- "google.golang.org/protobuf/types/known/fieldmaskpb"
17 "google.golang.org/protobuf/types/known/timestamppb"16 "google.golang.org/protobuf/types/known/timestamppb"
18 17
19 pb "bnema.dev/ermite-api/gen/ermite"18 pb "bnema.dev/ermite-api/gen/ermite"
@@ -271,7 +270,7 @@ func TestCIPushRunsAWorkflowOnARunner(t *testing.T) {
271 }270 }
272 271
273 // AC5: merge refused while checks are pending or failing, allowed when they272 // AC5: merge refused while checks are pending or failing, allowed when they
274-// pass, and when failing_checks_block is off.273+// pass, or when explicitly bypassed by an authorized caller.
275 func TestCIMergeIsBlockedByChecks(t *testing.T) {274 func TestCIMergeIsBlockedByChecks(t *testing.T) {
276 c := newCIHarness(t, nil)275 c := newCIHarness(t, nil)
277 runner := c.registerRunner("alpha", "ubuntu-latest")276 runner := c.registerRunner("alpha", "ubuntu-latest")
@@ -302,14 +301,9 @@ func TestCIMergeIsBlockedByChecks(t *testing.T) {
302 assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err))301 assert.Equal(t, connect.CodeFailedPrecondition, ciErrCode(err))
303 assert.Contains(t, err.Error(), "failing checks block the merge")302 assert.Contains(t, err.Error(), "failing checks block the merge")
304 303
305- // Off: the merge goes through.304+ // An explicit override by root allows the merge despite failure.
306- _, err = c.root.repo.UpdateRepo(c.ctx, connect.NewRequest(&pb.UpdateRepoRequest{305+ _, err = c.root.mr.MergeMergeRequest(c.ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{Repo: "proj", Number: mr.Msg.GetMergeRequest().GetNumber(), HeadId: feat, IgnoreChecks: true}))
307- Repo: "proj",
308- Settings: &pb.RepoSettings{FailingChecksBlock: false},
309- UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.failing_checks_block"}},
310- }))
311 require.NoError(t, err)306 require.NoError(t, err)
312- require.NoError(t, merge())
313 }307 }
314 308
315 // AC2: a job token works for its job only, and not once the job has ended.309 // AC2: a job token works for its job only, and not once the job has ended.
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{"allow_auto_merge", "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", "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", "merge_requests.auto_merge"}, 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", "merge_requests.ignore_checks", "merge_requests.ready"}, 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{"allow_auto_merge", "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", "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", "merge_requests.auto_merge"}, 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", "merge_requests.ignore_checks", "merge_requests.ready"}, 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_issue_test.go +1 -1
@@ -202,7 +202,7 @@ func TestRepoShowIssues(t *testing.T) {
202202 }}
203203 url := f.serve(t)
204204 res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t)
205- assert.Contains(t, res.stdout, "issues owner\n")
205+ assert.Contains(t, res.stdout, "issues owner\n")
206206 res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t)
207207 assert.Contains(t, res.stdout, `"issues": "owner"`)
208208 }
@@ -202,7 +202,7 @@ func TestRepoShowIssues(t *testing.T) {
202 }}202 }}
203 url := f.serve(t)203 url := f.serve(t)
204 res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t)204 res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t)
205- assert.Contains(t, res.stdout, "issues owner\n")205+ assert.Contains(t, res.stdout, "issues owner\n")
206 res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t)206 res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t)
207 assert.Contains(t, res.stdout, `"issues": "owner"`)207 assert.Contains(t, res.stdout, `"issues": "owner"`)
208 }208 }
M internal/app/cli_mr.go +45 -3
@@ -291,7 +291,7 @@ func newMRCmd(env *cliEnv) *cobra.Command {
291291 }
292292 mr.AddCommand(
293293 newMRCreateCmd(env), newMRListCmd(env), newMRShowCmd(env), newMRCommentCmd(env),
294- newMRResolveCmd(env), newMREditCommentCmd(env), newMRDeleteCommentCmd(env), newMRMergeCmd(env), newMRCloseCmd(env),
294+ newMRResolveCmd(env), newMREditCommentCmd(env), newMRDeleteCommentCmd(env), newMRMergeCmd(env), newMRCloseCmd(env), newMRReadyCmd(env),
295295 )
296296 return mr
297297 }
@@ -1085,7 +1085,7 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
10851085 var mode string
10861086 var deleteBranch bool
10871087 var head string
1088- var auto, cancelAuto bool
1088+ var auto, cancelAuto, ignoreChecks bool
10891089 cmd := &cobra.Command{
10901090 Use: "merge <repo> <number>",
10911091 Short: "Merge a merge request",
@@ -1105,6 +1105,9 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
11051105 "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" +
11061106 "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" +
11071107 "mr:merge scope.\n\n" +
1108+ "A merge needs the checks on the head to pass: none failed, none queued or running. A head\n" +
1109+ "without checks can be merged. --ignore-checks merges anyway and needs the mr:force-merge scope\n" +
1110+ "(the cli preset has it, the agent preset does not).\n\n" +
11081111 "The merge answers as soon as the target branch has moved. --delete-branch asks the server to delete\n" +
11091112 "the source branch right after, in the background. The merge is refused when the branch is the\n" +
11101113 "default branch or used by another open merge request. If the deletion later fails (the branch\n" +
@@ -1123,6 +1126,9 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
11231126 cmd.Flags().BoolVar(&deleteBranch, "delete-branch", false, "delete the source branch after the merge")
11241127 cmd.Flags().BoolVar(&auto, "auto", false, "merge by itself once the checks passed (needs `repo edit --allow-auto-merge`)")
11251128 cmd.Flags().BoolVar(&cancelAuto, "cancel-auto", false, "cancel the pending auto-merge")
1129+ cmd.Flags().BoolVar(&ignoreChecks, "ignore-checks", false, "merge despite failing or unfinished checks (needs mr:force-merge)")
1130+ cmd.MarkFlagsMutuallyExclusive("ignore-checks", "auto")
1131+ cmd.MarkFlagsMutuallyExclusive("ignore-checks", "cancel-auto")
11261132 cmd.MarkFlagsMutuallyExclusive("auto", "cancel-auto")
11271133 cmd.MarkFlagsMutuallyExclusive("cancel-auto", "delete-branch")
11281134 cmd.MarkFlagsMutuallyExclusive("cancel-auto", "head")
@@ -1156,13 +1162,18 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
11561162 return err
11571163 }
11581164 ctx := env.ctx(cmd)
1165+ if ignoreChecks {
1166+ if err := requireFeature(ctx, c, client.FeatureIgnoreChecks, "cannot bypass checks (--ignore-checks)"); err != nil {
1167+ return err
1168+ }
1169+ }
11591170 if pbMode == ermite.MergeMode_MERGE_MODE_MERGE_COMMIT {
11601171 if err := requireFeature(ctx, c, client.FeatureMergeability, "has no merge commit mode (--mode merge)"); err != nil {
11611172 return err
11621173 }
11631174 }
11641175 res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{
1165- Repo: args[0], Number: n, Mode: pbMode, HeadId: head, DeleteSourceBranch: deleteBranch,
1176+ Repo: args[0], Number: n, Mode: pbMode, HeadId: head, DeleteSourceBranch: deleteBranch, IgnoreChecks: ignoreChecks,
11661177 }))
11671178 if err != nil {
11681179 return c.Describe(err)
@@ -1282,3 +1293,34 @@ func newMRCloseCmd(env *cliEnv) *cobra.Command {
12821293 }
12831294 return cmd
12841295 }
1296+
1297+func newMRReadyCmd(env *cliEnv) *cobra.Command {
1298+ cmd := &cobra.Command{
1299+ Use: "ready <repo> <number>",
1300+ Short: "Mark a draft merge request ready for review",
1301+ Long: "Remove the WIP: or [WIP] prefix from the title so the merge request can be merged.\nFails if it is not open or not a draft. Needs the mr:write scope.\nPrints a confirmation, then the full web URL on its own line." + jsonMRHelp,
1302+ Args: cobra.ExactArgs(2),
1303+ ValidArgsFunction: completeMRRepoThenNumber(env),
1304+ }
1305+ asJSON := addJSONFlag(cmd)
1306+ cmd.RunE = func(cmd *cobra.Command, args []string) error {
1307+ n, err := parseNumber(args[1])
1308+ if err != nil {
1309+ return err
1310+ }
1311+ c, err := env.client(cmd)
1312+ if err != nil {
1313+ return err
1314+ }
1315+ ctx := env.ctx(cmd)
1316+ if err := requireFeature(ctx, c, client.FeatureMarkReady, "cannot mark drafts ready (mr ready)"); err != nil {
1317+ return err
1318+ }
1319+ res, err := c.MergeRequests().MarkReady(ctx, connect.NewRequest(&ermite.MarkReadyRequest{Repo: args[0], Number: n}))
1320+ if err != nil {
1321+ return c.Describe(err)
1322+ }
1323+ return outputMR(cmd, res.Msg.GetMergeRequest(), *asJSON, "marked ready", c.URL())
1324+ }
1325+ return cmd
1326+}
@@ -291,7 +291,7 @@ func newMRCmd(env *cliEnv) *cobra.Command {
291 }291 }
292 mr.AddCommand(292 mr.AddCommand(
293 newMRCreateCmd(env), newMRListCmd(env), newMRShowCmd(env), newMRCommentCmd(env),293 newMRCreateCmd(env), newMRListCmd(env), newMRShowCmd(env), newMRCommentCmd(env),
294- newMRResolveCmd(env), newMREditCommentCmd(env), newMRDeleteCommentCmd(env), newMRMergeCmd(env), newMRCloseCmd(env),294+ newMRResolveCmd(env), newMREditCommentCmd(env), newMRDeleteCommentCmd(env), newMRMergeCmd(env), newMRCloseCmd(env), newMRReadyCmd(env),
295 )295 )
296 return mr296 return mr
297 }297 }
@@ -1085,7 +1085,7 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
1085 var mode string1085 var mode string
1086 var deleteBranch bool1086 var deleteBranch bool
1087 var head string1087 var head string
1088- var auto, cancelAuto bool1088+ var auto, cancelAuto, ignoreChecks bool
1089 cmd := &cobra.Command{1089 cmd := &cobra.Command{
1090 Use: "merge <repo> <number>",1090 Use: "merge <repo> <number>",
1091 Short: "Merge a merge request",1091 Short: "Merge a merge request",
@@ -1105,6 +1105,9 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
1105 "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" +1105 "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" +
1106 "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" +1106 "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" +
1107 "mr:merge scope.\n\n" +1107 "mr:merge scope.\n\n" +
1108+ "A merge needs the checks on the head to pass: none failed, none queued or running. A head\n" +
1109+ "without checks can be merged. --ignore-checks merges anyway and needs the mr:force-merge scope\n" +
1110+ "(the cli preset has it, the agent preset does not).\n\n" +
1108 "The merge answers as soon as the target branch has moved. --delete-branch asks the server to delete\n" +1111 "The merge answers as soon as the target branch has moved. --delete-branch asks the server to delete\n" +
1109 "the source branch right after, in the background. The merge is refused when the branch is the\n" +1112 "the source branch right after, in the background. The merge is refused when the branch is the\n" +
1110 "default branch or used by another open merge request. If the deletion later fails (the branch\n" +1113 "default branch or used by another open merge request. If the deletion later fails (the branch\n" +
@@ -1123,6 +1126,9 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
1123 cmd.Flags().BoolVar(&deleteBranch, "delete-branch", false, "delete the source branch after the merge")1126 cmd.Flags().BoolVar(&deleteBranch, "delete-branch", false, "delete the source branch after the merge")
1124 cmd.Flags().BoolVar(&auto, "auto", false, "merge by itself once the checks passed (needs `repo edit --allow-auto-merge`)")1127 cmd.Flags().BoolVar(&auto, "auto", false, "merge by itself once the checks passed (needs `repo edit --allow-auto-merge`)")
1125 cmd.Flags().BoolVar(&cancelAuto, "cancel-auto", false, "cancel the pending auto-merge")1128 cmd.Flags().BoolVar(&cancelAuto, "cancel-auto", false, "cancel the pending auto-merge")
1129+ cmd.Flags().BoolVar(&ignoreChecks, "ignore-checks", false, "merge despite failing or unfinished checks (needs mr:force-merge)")
1130+ cmd.MarkFlagsMutuallyExclusive("ignore-checks", "auto")
1131+ cmd.MarkFlagsMutuallyExclusive("ignore-checks", "cancel-auto")
1126 cmd.MarkFlagsMutuallyExclusive("auto", "cancel-auto")1132 cmd.MarkFlagsMutuallyExclusive("auto", "cancel-auto")
1127 cmd.MarkFlagsMutuallyExclusive("cancel-auto", "delete-branch")1133 cmd.MarkFlagsMutuallyExclusive("cancel-auto", "delete-branch")
1128 cmd.MarkFlagsMutuallyExclusive("cancel-auto", "head")1134 cmd.MarkFlagsMutuallyExclusive("cancel-auto", "head")
@@ -1156,13 +1162,18 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command {
1156 return err1162 return err
1157 }1163 }
1158 ctx := env.ctx(cmd)1164 ctx := env.ctx(cmd)
1165+ if ignoreChecks {
1166+ if err := requireFeature(ctx, c, client.FeatureIgnoreChecks, "cannot bypass checks (--ignore-checks)"); err != nil {
1167+ return err
1168+ }
1169+ }
1159 if pbMode == ermite.MergeMode_MERGE_MODE_MERGE_COMMIT {1170 if pbMode == ermite.MergeMode_MERGE_MODE_MERGE_COMMIT {
1160 if err := requireFeature(ctx, c, client.FeatureMergeability, "has no merge commit mode (--mode merge)"); err != nil {1171 if err := requireFeature(ctx, c, client.FeatureMergeability, "has no merge commit mode (--mode merge)"); err != nil {
1161 return err1172 return err
1162 }1173 }
1163 }1174 }
1164 res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{1175 res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{
1165- Repo: args[0], Number: n, Mode: pbMode, HeadId: head, DeleteSourceBranch: deleteBranch,1176+ Repo: args[0], Number: n, Mode: pbMode, HeadId: head, DeleteSourceBranch: deleteBranch, IgnoreChecks: ignoreChecks,
1166 }))1177 }))
1167 if err != nil {1178 if err != nil {
1168 return c.Describe(err)1179 return c.Describe(err)
@@ -1282,3 +1293,34 @@ func newMRCloseCmd(env *cliEnv) *cobra.Command {
1282 }1293 }
1283 return cmd1294 return cmd
1284 }1295 }
1296+
1297+func newMRReadyCmd(env *cliEnv) *cobra.Command {
1298+ cmd := &cobra.Command{
1299+ Use: "ready <repo> <number>",
1300+ Short: "Mark a draft merge request ready for review",
1301+ Long: "Remove the WIP: or [WIP] prefix from the title so the merge request can be merged.\nFails if it is not open or not a draft. Needs the mr:write scope.\nPrints a confirmation, then the full web URL on its own line." + jsonMRHelp,
1302+ Args: cobra.ExactArgs(2),
1303+ ValidArgsFunction: completeMRRepoThenNumber(env),
1304+ }
1305+ asJSON := addJSONFlag(cmd)
1306+ cmd.RunE = func(cmd *cobra.Command, args []string) error {
1307+ n, err := parseNumber(args[1])
1308+ if err != nil {
1309+ return err
1310+ }
1311+ c, err := env.client(cmd)
1312+ if err != nil {
1313+ return err
1314+ }
1315+ ctx := env.ctx(cmd)
1316+ if err := requireFeature(ctx, c, client.FeatureMarkReady, "cannot mark drafts ready (mr ready)"); err != nil {
1317+ return err
1318+ }
1319+ res, err := c.MergeRequests().MarkReady(ctx, connect.NewRequest(&ermite.MarkReadyRequest{Repo: args[0], Number: n}))
1320+ if err != nil {
1321+ return c.Describe(err)
1322+ }
1323+ return outputMR(cmd, res.Msg.GetMergeRequest(), *asJSON, "marked ready", c.URL())
1324+ }
1325+ return cmd
1326+}
M internal/app/cli_mr_e2e_test.go +38 -14
@@ -382,29 +382,17 @@ func TestRepoEditMergeSettingsCLI(t *testing.T) {
382382 c.seed("proj", domain.VisibilityPublic)
383383 res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t)
384384 repo := decode[struct {
385- MergeModes []string `json:"merge_modes"`
386- FailingChecksBlock bool `json:"failing_checks_block"`
385+ MergeModes []string `json:"merge_modes"`
387386 }](t, res.stdout)
388387 assert.Equal(t, []string{"fast-forward", "squash", "merge"}, repo.MergeModes)
389- assert.True(t, repo.FailingChecksBlock)
390388
391389 res = c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "squash", "--json").ok(t)
392390 repo = decode[struct {
393- MergeModes []string `json:"merge_modes"`
394- FailingChecksBlock bool `json:"failing_checks_block"`
391+ MergeModes []string `json:"merge_modes"`
395392 }](t, res.stdout)
396393 assert.Equal(t, []string{"squash"}, repo.MergeModes)
397- assert.True(t, repo.FailingChecksBlock, "untouched")
398- res = c.cli(t, adminToken, "repo", "edit", "proj", "--failing-checks-block=false", "--json").ok(t)
399- repo = decode[struct {
400- MergeModes []string `json:"merge_modes"`
401- FailingChecksBlock bool `json:"failing_checks_block"`
402- }](t, res.stdout)
403- assert.Equal(t, []string{"squash"}, repo.MergeModes, "untouched")
404- assert.False(t, repo.FailingChecksBlock)
405394 res = c.cli(t, adminToken, "repo", "show", "proj").ok(t)
406395 assert.Regexp(t, `(?m)^merge modes\s+squash$`, res.stdout)
407- assert.Regexp(t, `(?m)^failing checks block\s+false$`, res.stdout)
408396
409397 require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "rebase").err)
410398 require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "").err)
@@ -497,3 +485,39 @@ func TestMergeRequestNotesCLIEndToEnd(t *testing.T) {
497485 require.Error(t, c.cli(t, adminToken, "mr", "edit-comment", "notes", "1", root.ID, "-m", "late").err)
498486 require.Error(t, c.cli(t, adminToken, "mr", "delete-comment", "notes", "1", root.ID).err)
499487 }
488+
489+func TestMRReadyAndIgnoreChecksCLIEndToEnd(t *testing.T) {
490+ c := newCLIHarness(t)
491+ secret := strings.TrimSpace(c.cli(t, adminToken, "token", "create", "daily", "--preset", "cli").ok(t).stdout)
492+ agent := strings.TrimSpace(c.cli(t, adminToken, "token", "create", "agent", "--preset", "agent", "--scope", "mr:merge").ok(t).stdout)
493+ c.cli(t, secret, "repo", "create", "proj", "--visibility", "public").ok(t)
494+ wt := c.workTree("main", "README.md")
495+ url := c.urlAuth(secret, "/proj.git")
496+ c.mustGit(wt, "push", "-q", url, "main")
497+ c.mustGit(wt, "checkout", "-q", "-b", "feat/ready")
498+ c.writeFile(wt, "README.md", []byte("ready\n"))
499+ c.mustGit(wt, "push", "-q", url, "feat/ready")
500+ head := c.bareGit("proj", "rev-parse", "feat/ready")
501+ c.cli(t, agent, "mr", "create", "proj", "--source", "feat/ready", "--title", "WIP: Add feature").ok(t)
502+ ready := c.cli(t, agent, "mr", "ready", "proj", "1", "--json").ok(t)
503+ assert.Equal(t, mrKeys, keys(t, ready.stdout))
504+ assert.Equal(t, "Add feature", decode[mrOut](t, ready.stdout).Title)
505+ res := c.cli(t, agent, "mr", "ready", "proj", "1")
506+ require.Error(t, res.err)
507+ assert.Contains(t, res.err.Error(), "not a draft")
508+ res = c.cli(t, secret, "mr", "merge", "proj", "1", "--head", head, "--ignore-checks", "--auto")
509+ require.Error(t, res.err)
510+ assert.Contains(t, res.err.Error(), "ignore-checks")
511+ res = c.cli(t, agent, "mr", "merge", "proj", "1", "--head", head, "--ignore-checks")
512+ require.Error(t, res.err)
513+ assert.Contains(t, res.err.Error(), "mr:force-merge")
514+ c.cli(t, secret, "mr", "merge", "proj", "1", "--head", head, "--ignore-checks").ok(t)
515+ c.mustGit(wt, "checkout", "-q", "-b", "feat/second")
516+ c.writeFile(wt, "README.md", []byte("second\n"))
517+ c.mustGit(wt, "push", "-q", url, "feat/second")
518+ head = c.bareGit("proj", "rev-parse", "feat/second")
519+ c.cli(t, agent, "mr", "create", "proj", "--source", "feat/second", "--title", "[WIP] Second").ok(t)
520+ res = c.cli(t, agent, "mr", "ready", "proj", "2").ok(t)
521+ assert.Contains(t, res.stdout, "marked ready proj!2 (feat/second into main)\n"+c.url+"/proj/merge-requests/2\n")
522+ c.cli(t, secret, "mr", "merge", "proj", "2", "--head", head).ok(t)
523+}
@@ -382,29 +382,17 @@ func TestRepoEditMergeSettingsCLI(t *testing.T) {
382 c.seed("proj", domain.VisibilityPublic)382 c.seed("proj", domain.VisibilityPublic)
383 res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t)383 res := c.cli(t, adminToken, "repo", "show", "proj", "--json").ok(t)
384 repo := decode[struct {384 repo := decode[struct {
385- MergeModes []string `json:"merge_modes"`385+ MergeModes []string `json:"merge_modes"`
386- FailingChecksBlock bool `json:"failing_checks_block"`
387 }](t, res.stdout)386 }](t, res.stdout)
388 assert.Equal(t, []string{"fast-forward", "squash", "merge"}, repo.MergeModes)387 assert.Equal(t, []string{"fast-forward", "squash", "merge"}, repo.MergeModes)
389- assert.True(t, repo.FailingChecksBlock)
390 388
391 res = c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "squash", "--json").ok(t)389 res = c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "squash", "--json").ok(t)
392 repo = decode[struct {390 repo = decode[struct {
393- MergeModes []string `json:"merge_modes"`391+ MergeModes []string `json:"merge_modes"`
394- FailingChecksBlock bool `json:"failing_checks_block"`
395 }](t, res.stdout)392 }](t, res.stdout)
396 assert.Equal(t, []string{"squash"}, repo.MergeModes)393 assert.Equal(t, []string{"squash"}, repo.MergeModes)
397- assert.True(t, repo.FailingChecksBlock, "untouched")
398- res = c.cli(t, adminToken, "repo", "edit", "proj", "--failing-checks-block=false", "--json").ok(t)
399- repo = decode[struct {
400- MergeModes []string `json:"merge_modes"`
401- FailingChecksBlock bool `json:"failing_checks_block"`
402- }](t, res.stdout)
403- assert.Equal(t, []string{"squash"}, repo.MergeModes, "untouched")
404- assert.False(t, repo.FailingChecksBlock)
405 res = c.cli(t, adminToken, "repo", "show", "proj").ok(t)394 res = c.cli(t, adminToken, "repo", "show", "proj").ok(t)
406 assert.Regexp(t, `(?m)^merge modes\s+squash$`, res.stdout)395 assert.Regexp(t, `(?m)^merge modes\s+squash$`, res.stdout)
407- assert.Regexp(t, `(?m)^failing checks block\s+false$`, res.stdout)
408 396
409 require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "rebase").err)397 require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "rebase").err)
410 require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "").err)398 require.Error(t, c.cli(t, adminToken, "repo", "edit", "proj", "--merge-modes", "").err)
@@ -497,3 +485,39 @@ func TestMergeRequestNotesCLIEndToEnd(t *testing.T) {
497 require.Error(t, c.cli(t, adminToken, "mr", "edit-comment", "notes", "1", root.ID, "-m", "late").err)485 require.Error(t, c.cli(t, adminToken, "mr", "edit-comment", "notes", "1", root.ID, "-m", "late").err)
498 require.Error(t, c.cli(t, adminToken, "mr", "delete-comment", "notes", "1", root.ID).err)486 require.Error(t, c.cli(t, adminToken, "mr", "delete-comment", "notes", "1", root.ID).err)
499 }487 }
488+
489+func TestMRReadyAndIgnoreChecksCLIEndToEnd(t *testing.T) {
490+ c := newCLIHarness(t)
491+ secret := strings.TrimSpace(c.cli(t, adminToken, "token", "create", "daily", "--preset", "cli").ok(t).stdout)
492+ agent := strings.TrimSpace(c.cli(t, adminToken, "token", "create", "agent", "--preset", "agent", "--scope", "mr:merge").ok(t).stdout)
493+ c.cli(t, secret, "repo", "create", "proj", "--visibility", "public").ok(t)
494+ wt := c.workTree("main", "README.md")
495+ url := c.urlAuth(secret, "/proj.git")
496+ c.mustGit(wt, "push", "-q", url, "main")
497+ c.mustGit(wt, "checkout", "-q", "-b", "feat/ready")
498+ c.writeFile(wt, "README.md", []byte("ready\n"))
499+ c.mustGit(wt, "push", "-q", url, "feat/ready")
500+ head := c.bareGit("proj", "rev-parse", "feat/ready")
501+ c.cli(t, agent, "mr", "create", "proj", "--source", "feat/ready", "--title", "WIP: Add feature").ok(t)
502+ ready := c.cli(t, agent, "mr", "ready", "proj", "1", "--json").ok(t)
503+ assert.Equal(t, mrKeys, keys(t, ready.stdout))
504+ assert.Equal(t, "Add feature", decode[mrOut](t, ready.stdout).Title)
505+ res := c.cli(t, agent, "mr", "ready", "proj", "1")
506+ require.Error(t, res.err)
507+ assert.Contains(t, res.err.Error(), "not a draft")
508+ res = c.cli(t, secret, "mr", "merge", "proj", "1", "--head", head, "--ignore-checks", "--auto")
509+ require.Error(t, res.err)
510+ assert.Contains(t, res.err.Error(), "ignore-checks")
511+ res = c.cli(t, agent, "mr", "merge", "proj", "1", "--head", head, "--ignore-checks")
512+ require.Error(t, res.err)
513+ assert.Contains(t, res.err.Error(), "mr:force-merge")
514+ c.cli(t, secret, "mr", "merge", "proj", "1", "--head", head, "--ignore-checks").ok(t)
515+ c.mustGit(wt, "checkout", "-q", "-b", "feat/second")
516+ c.writeFile(wt, "README.md", []byte("second\n"))
517+ c.mustGit(wt, "push", "-q", url, "feat/second")
518+ head = c.bareGit("proj", "rev-parse", "feat/second")
519+ c.cli(t, agent, "mr", "create", "proj", "--source", "feat/second", "--title", "[WIP] Second").ok(t)
520+ res = c.cli(t, agent, "mr", "ready", "proj", "2").ok(t)
521+ assert.Contains(t, res.stdout, "marked ready proj!2 (feat/second into main)\n"+c.url+"/proj/merge-requests/2\n")
522+ c.cli(t, secret, "mr", "merge", "proj", "2", "--head", head).ok(t)
523+}
M internal/app/cli_repo.go +17 -29
@@ -34,9 +34,8 @@ type jsonRepo struct {
3434 CreatedAt *string `json:"created_at"`
3535 PushedAt *string `json:"pushed_at"`
3636 // MergeModes are the allowed merge modes ("fast-forward", "squash", "merge").
37- MergeModes []string `json:"merge_modes"`
38- FailingChecksBlock bool `json:"failing_checks_block"`
39- AllowAutoMerge bool `json:"allow_auto_merge"`
37+ MergeModes []string `json:"merge_modes"`
38+ AllowAutoMerge bool `json:"allow_auto_merge"`
4039 // Issues is the issue mode ("off", "owner" or "public").
4140 Issues string `json:"issues"`
4241 }
@@ -47,17 +46,16 @@ func toJSONRepo(r *ermite.Repo) jsonRepo {
4746 modes = append(modes, modeName(m))
4847 }
4948 return jsonRepo{
50- MergeModes: modes,
51- FailingChecksBlock: r.GetSettings().GetFailingChecksBlock(),
52- AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(),
53- Issues: issuesModeName(r.GetSettings().GetIssues()),
54- Name: r.GetName(),
55- Description: r.GetDescription(),
56- Topics: append([]string{}, r.GetTopics()...),
57- Visibility: visibilityName(r.GetVisibility()),
58- DefaultBranch: r.GetDefaultBranch(),
59- CreatedAt: jsonTime(r.GetCreatedAt()),
60- PushedAt: jsonTime(r.GetPushedAt()),
49+ MergeModes: modes,
50+ AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(),
51+ Issues: issuesModeName(r.GetSettings().GetIssues()),
52+ Name: r.GetName(),
53+ Description: r.GetDescription(),
54+ Topics: append([]string{}, r.GetTopics()...),
55+ Visibility: visibilityName(r.GetVisibility()),
56+ DefaultBranch: r.GetDefaultBranch(),
57+ CreatedAt: jsonTime(r.GetCreatedAt()),
58+ PushedAt: jsonTime(r.GetPushedAt()),
6159 }
6260 }
6361
@@ -117,7 +115,7 @@ const jsonRepoHelp = `
117115 JSON fields of a repository (--json): name, description, visibility ("public" or
118116 "private"), default_branch, created_at and pushed_at (RFC 3339 UTC; pushed_at is
119117 null until the first push), merge_modes (array of the allowed merge modes, "fast-forward", "squash" and/or
120-"merge"), failing_checks_block (whether a failing check prevents a merge), allow_auto_merge (whether
118+"merge"), allow_auto_merge (whether
121119 auto-merge can be enabled) and issues (the issue mode: "off", "owner" or "public").`
122120
123121 func newRepoCmd(env *cliEnv) *cobra.Command {
@@ -230,7 +228,6 @@ func printRepo(w io.Writer, r *ermite.Repo) error {
230228 {"created", shortTime(r.GetCreatedAt(), "-")},
231229 {"last push", shortTime(r.GetPushedAt(), "never")},
232230 {"merge modes", orDash(strings.Join(modes, ", "))},
233- {"failing checks block", strconv.FormatBool(r.GetSettings().GetFailingChecksBlock())},
234231 {"allow auto-merge", strconv.FormatBool(r.GetSettings().GetAllowAutoMerge())},
235232 {"issues", issuesModeName(r.GetSettings().GetIssues())},
236233 })
@@ -311,7 +308,7 @@ func buildCreateRepo(name, description string, topics []string, visibility strin
311308 func newRepoEditCmd(env *cliEnv) *cobra.Command {
312309 var description, visibility, defaultBranch string
313310 var topics, mergeModes []string
314- var failingChecksBlock, allowAutoMerge bool
311+ var allowAutoMerge bool
315312 var issues string
316313 cmd := &cobra.Command{
317314 Use: "edit <name>",
@@ -321,7 +318,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
321318 "may be merged with (fast-forward, squash, merge; every mode is allowed by default, so to disable one,\n" +
322319 "list the others: --merge-modes fast-forward,squash disables the merge commit; squash and merge\n" +
323320 "commits are signed with the server's key, see `ermite signing-key show`), and\n" +
324- "--failing-checks-block says whether a failing check prevents a merge (a failed or unfinished CI check of the head; see README, CI).\n" +
325321 "--issues sets who may use issues: off (default), owner (the owner and tokens with issue:write) or\n" +
326322 "public (visitors of a public repository may also propose issues through a moderated form).\nNeeds a server with the issues feature.\n" +
327323 "Needs the repo:admin scope." + jsonRepoHelp,
@@ -333,7 +329,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
333329 cmd.Flags().StringVar(&visibility, "visibility", "", "new visibility: public or private")
334330 cmd.Flags().StringVar(&defaultBranch, "default-branch", "", "short name of the new default branch")
335331 cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash, merge (all by default; list the ones to keep)")
336- cmd.Flags().BoolVar(&failingChecksBlock, "failing-checks-block", true, "a failing check prevents a merge (--failing-checks-block=false to allow it)")
337332 cmd.Flags().BoolVar(&allowAutoMerge, "allow-auto-merge", false, "allow auto-merge on merge requests (--allow-auto-merge=false to forbid it)")
338333 cmd.Flags().StringVar(&issues, "issues", "", "issue mode: off, owner or public")
339334 _ = cmd.RegisterFlagCompletionFunc("issues", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
@@ -345,7 +340,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
345340 })
346341 asJSON := addJSONFlag(cmd)
347342 cmd.RunE = func(cmd *cobra.Command, args []string) error {
348- req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, failingChecksBlock, allowAutoMerge, issues)
343+ req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, allowAutoMerge, issues)
349344 if err != nil {
350345 return err
351346 }
@@ -369,7 +364,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
369364
370365 // buildUpdateRepo maps the flags of `repo edit` to the request: only the flags
371366 // the user passed go into the update mask.
372-func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, failingChecksBlock, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) {
367+func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) {
373368 req := &ermite.UpdateRepoRequest{Repo: name}
374369 var paths []string
375370 if cmd.Flags().Changed("description") {
@@ -411,13 +406,6 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri
411406 req.Settings = &ermite.RepoSettings{AllowedMergeModes: modes}
412407 paths = append(paths, "settings.allowed_merge_modes")
413408 }
414- if cmd.Flags().Changed("failing-checks-block") {
415- if req.Settings == nil {
416- req.Settings = &ermite.RepoSettings{}
417- }
418- req.Settings.FailingChecksBlock = failingChecksBlock
419- paths = append(paths, "settings.failing_checks_block")
420- }
421409 if cmd.Flags().Changed("allow-auto-merge") {
422410 if req.Settings == nil {
423411 req.Settings = &ermite.RepoSettings{}
@@ -434,7 +422,7 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri
434422 req.Issues = &mode
435423 }
436424 if len(paths) == 0 && req.Issues == nil {
437- return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --failing-checks-block, --allow-auto-merge, --issues")
425+ return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --allow-auto-merge, --issues")
438426 }
439427 req.UpdateMask = &fieldmaskpb.FieldMask{Paths: paths}
440428 return req, nil
@@ -34,9 +34,8 @@ type jsonRepo struct {
34 CreatedAt *string `json:"created_at"`34 CreatedAt *string `json:"created_at"`
35 PushedAt *string `json:"pushed_at"`35 PushedAt *string `json:"pushed_at"`
36 // MergeModes are the allowed merge modes ("fast-forward", "squash", "merge").36 // MergeModes are the allowed merge modes ("fast-forward", "squash", "merge").
37- MergeModes []string `json:"merge_modes"`37+ MergeModes []string `json:"merge_modes"`
38- FailingChecksBlock bool `json:"failing_checks_block"`38+ AllowAutoMerge bool `json:"allow_auto_merge"`
39- AllowAutoMerge bool `json:"allow_auto_merge"`
40 // Issues is the issue mode ("off", "owner" or "public").39 // Issues is the issue mode ("off", "owner" or "public").
41 Issues string `json:"issues"`40 Issues string `json:"issues"`
42 }41 }
@@ -47,17 +46,16 @@ func toJSONRepo(r *ermite.Repo) jsonRepo {
47 modes = append(modes, modeName(m))46 modes = append(modes, modeName(m))
48 }47 }
49 return jsonRepo{48 return jsonRepo{
50- MergeModes: modes,49+ MergeModes: modes,
51- FailingChecksBlock: r.GetSettings().GetFailingChecksBlock(),50+ AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(),
52- AllowAutoMerge: r.GetSettings().GetAllowAutoMerge(),51+ Issues: issuesModeName(r.GetSettings().GetIssues()),
53- Issues: issuesModeName(r.GetSettings().GetIssues()),52+ Name: r.GetName(),
54- Name: r.GetName(),53+ Description: r.GetDescription(),
55- Description: r.GetDescription(),54+ Topics: append([]string{}, r.GetTopics()...),
56- Topics: append([]string{}, r.GetTopics()...),55+ Visibility: visibilityName(r.GetVisibility()),
57- Visibility: visibilityName(r.GetVisibility()),56+ DefaultBranch: r.GetDefaultBranch(),
58- DefaultBranch: r.GetDefaultBranch(),57+ CreatedAt: jsonTime(r.GetCreatedAt()),
59- CreatedAt: jsonTime(r.GetCreatedAt()),58+ PushedAt: jsonTime(r.GetPushedAt()),
60- PushedAt: jsonTime(r.GetPushedAt()),
61 }59 }
62 }60 }
63 61
@@ -117,7 +115,7 @@ const jsonRepoHelp = `
117 JSON fields of a repository (--json): name, description, visibility ("public" or115 JSON fields of a repository (--json): name, description, visibility ("public" or
118 "private"), default_branch, created_at and pushed_at (RFC 3339 UTC; pushed_at is116 "private"), default_branch, created_at and pushed_at (RFC 3339 UTC; pushed_at is
119 null until the first push), merge_modes (array of the allowed merge modes, "fast-forward", "squash" and/or117 null until the first push), merge_modes (array of the allowed merge modes, "fast-forward", "squash" and/or
120-"merge"), failing_checks_block (whether a failing check prevents a merge), allow_auto_merge (whether118+"merge"), allow_auto_merge (whether
121 auto-merge can be enabled) and issues (the issue mode: "off", "owner" or "public").`119 auto-merge can be enabled) and issues (the issue mode: "off", "owner" or "public").`
122 120
123 func newRepoCmd(env *cliEnv) *cobra.Command {121 func newRepoCmd(env *cliEnv) *cobra.Command {
@@ -230,7 +228,6 @@ func printRepo(w io.Writer, r *ermite.Repo) error {
230 {"created", shortTime(r.GetCreatedAt(), "-")},228 {"created", shortTime(r.GetCreatedAt(), "-")},
231 {"last push", shortTime(r.GetPushedAt(), "never")},229 {"last push", shortTime(r.GetPushedAt(), "never")},
232 {"merge modes", orDash(strings.Join(modes, ", "))},230 {"merge modes", orDash(strings.Join(modes, ", "))},
233- {"failing checks block", strconv.FormatBool(r.GetSettings().GetFailingChecksBlock())},
234 {"allow auto-merge", strconv.FormatBool(r.GetSettings().GetAllowAutoMerge())},231 {"allow auto-merge", strconv.FormatBool(r.GetSettings().GetAllowAutoMerge())},
235 {"issues", issuesModeName(r.GetSettings().GetIssues())},232 {"issues", issuesModeName(r.GetSettings().GetIssues())},
236 })233 })
@@ -311,7 +308,7 @@ func buildCreateRepo(name, description string, topics []string, visibility strin
311 func newRepoEditCmd(env *cliEnv) *cobra.Command {308 func newRepoEditCmd(env *cliEnv) *cobra.Command {
312 var description, visibility, defaultBranch string309 var description, visibility, defaultBranch string
313 var topics, mergeModes []string310 var topics, mergeModes []string
314- var failingChecksBlock, allowAutoMerge bool311+ var allowAutoMerge bool
315 var issues string312 var issues string
316 cmd := &cobra.Command{313 cmd := &cobra.Command{
317 Use: "edit <name>",314 Use: "edit <name>",
@@ -321,7 +318,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
321 "may be merged with (fast-forward, squash, merge; every mode is allowed by default, so to disable one,\n" +318 "may be merged with (fast-forward, squash, merge; every mode is allowed by default, so to disable one,\n" +
322 "list the others: --merge-modes fast-forward,squash disables the merge commit; squash and merge\n" +319 "list the others: --merge-modes fast-forward,squash disables the merge commit; squash and merge\n" +
323 "commits are signed with the server's key, see `ermite signing-key show`), and\n" +320 "commits are signed with the server's key, see `ermite signing-key show`), and\n" +
324- "--failing-checks-block says whether a failing check prevents a merge (a failed or unfinished CI check of the head; see README, CI).\n" +
325 "--issues sets who may use issues: off (default), owner (the owner and tokens with issue:write) or\n" +321 "--issues sets who may use issues: off (default), owner (the owner and tokens with issue:write) or\n" +
326 "public (visitors of a public repository may also propose issues through a moderated form).\nNeeds a server with the issues feature.\n" +322 "public (visitors of a public repository may also propose issues through a moderated form).\nNeeds a server with the issues feature.\n" +
327 "Needs the repo:admin scope." + jsonRepoHelp,323 "Needs the repo:admin scope." + jsonRepoHelp,
@@ -333,7 +329,6 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
333 cmd.Flags().StringVar(&visibility, "visibility", "", "new visibility: public or private")329 cmd.Flags().StringVar(&visibility, "visibility", "", "new visibility: public or private")
334 cmd.Flags().StringVar(&defaultBranch, "default-branch", "", "short name of the new default branch")330 cmd.Flags().StringVar(&defaultBranch, "default-branch", "", "short name of the new default branch")
335 cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash, merge (all by default; list the ones to keep)")331 cmd.Flags().StringSliceVar(&mergeModes, "merge-modes", nil, "allowed merge modes, comma-separated: fast-forward, squash, merge (all by default; list the ones to keep)")
336- cmd.Flags().BoolVar(&failingChecksBlock, "failing-checks-block", true, "a failing check prevents a merge (--failing-checks-block=false to allow it)")
337 cmd.Flags().BoolVar(&allowAutoMerge, "allow-auto-merge", false, "allow auto-merge on merge requests (--allow-auto-merge=false to forbid it)")332 cmd.Flags().BoolVar(&allowAutoMerge, "allow-auto-merge", false, "allow auto-merge on merge requests (--allow-auto-merge=false to forbid it)")
338 cmd.Flags().StringVar(&issues, "issues", "", "issue mode: off, owner or public")333 cmd.Flags().StringVar(&issues, "issues", "", "issue mode: off, owner or public")
339 _ = cmd.RegisterFlagCompletionFunc("issues", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {334 _ = cmd.RegisterFlagCompletionFunc("issues", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
@@ -345,7 +340,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
345 })340 })
346 asJSON := addJSONFlag(cmd)341 asJSON := addJSONFlag(cmd)
347 cmd.RunE = func(cmd *cobra.Command, args []string) error {342 cmd.RunE = func(cmd *cobra.Command, args []string) error {
348- req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, failingChecksBlock, allowAutoMerge, issues)343+ req, err := buildUpdateRepo(cmd, args[0], description, topics, visibility, defaultBranch, mergeModes, allowAutoMerge, issues)
349 if err != nil {344 if err != nil {
350 return err345 return err
351 }346 }
@@ -369,7 +364,7 @@ func newRepoEditCmd(env *cliEnv) *cobra.Command {
369 364
370 // buildUpdateRepo maps the flags of `repo edit` to the request: only the flags365 // buildUpdateRepo maps the flags of `repo edit` to the request: only the flags
371 // the user passed go into the update mask.366 // the user passed go into the update mask.
372-func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, failingChecksBlock, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) {367+func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []string, visibility, defaultBranch string, mergeModes []string, allowAutoMerge bool, issues string) (*ermite.UpdateRepoRequest, error) {
373 req := &ermite.UpdateRepoRequest{Repo: name}368 req := &ermite.UpdateRepoRequest{Repo: name}
374 var paths []string369 var paths []string
375 if cmd.Flags().Changed("description") {370 if cmd.Flags().Changed("description") {
@@ -411,13 +406,6 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri
411 req.Settings = &ermite.RepoSettings{AllowedMergeModes: modes}406 req.Settings = &ermite.RepoSettings{AllowedMergeModes: modes}
412 paths = append(paths, "settings.allowed_merge_modes")407 paths = append(paths, "settings.allowed_merge_modes")
413 }408 }
414- if cmd.Flags().Changed("failing-checks-block") {
415- if req.Settings == nil {
416- req.Settings = &ermite.RepoSettings{}
417- }
418- req.Settings.FailingChecksBlock = failingChecksBlock
419- paths = append(paths, "settings.failing_checks_block")
420- }
421 if cmd.Flags().Changed("allow-auto-merge") {409 if cmd.Flags().Changed("allow-auto-merge") {
422 if req.Settings == nil {410 if req.Settings == nil {
423 req.Settings = &ermite.RepoSettings{}411 req.Settings = &ermite.RepoSettings{}
@@ -434,7 +422,7 @@ func buildUpdateRepo(cmd *cobra.Command, name, description string, topics []stri
434 req.Issues = &mode422 req.Issues = &mode
435 }423 }
436 if len(paths) == 0 && req.Issues == nil {424 if len(paths) == 0 && req.Issues == nil {
437- return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --failing-checks-block, --allow-auto-merge, --issues")425+ return nil, errors.New("nothing to change: give at least one of --description, --topics, --visibility, --default-branch, --merge-modes, --allow-auto-merge, --issues")
438 }426 }
439 req.UpdateMask = &fieldmaskpb.FieldMask{Paths: paths}427 req.UpdateMask = &fieldmaskpb.FieldMask{Paths: paths}
440 return req, nil428 return req, nil
M internal/app/cli_repo_test.go +17 -27
@@ -19,8 +19,7 @@ var (
1919 Name: "alpha", Description: "The first one", Topics: []string{"cli", "go"}, Visibility: ermite.Visibility_VISIBILITY_PUBLIC,
2020 DefaultBranch: "main", CreatedAt: ts("2026-01-02T03:04:05Z"), PushedAt: ts("2026-03-04T05:06:07Z"),
2121 Settings: &ermite.RepoSettings{
22- AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH},
23- FailingChecksBlock: true,
22+ AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH},
2423 },
2524 }
2625 repoBeta = &ermite.Repo{
@@ -54,8 +53,8 @@ func TestRepoList(t *testing.T) {
5453 {
5554 name: "json", repos: both, args: []string{"repo", "list", "--json"}, wantJSON: true,
5655 wantStdout: `[
57- {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"},
58- {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}
56+ {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"},
57+ {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}
5958 ]`,
6059 },
6160 {
@@ -107,22 +106,21 @@ func TestRepoShow(t *testing.T) {
107106 url := f.serve(t)
108107 res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t)
109108 assert.Equal(t, ""+
110- "name alpha\n"+
111- "visibility public\n"+
112- "default branch main\n"+
113- "description The first one\n"+
114- "topics cli, go\n"+
115- "created 2026-01-02 03:04\n"+
116- "last push 2026-03-04 05:06\n"+
117- "merge modes fast-forward, squash\n"+
118- "failing checks block true\n"+
119- "allow auto-merge false\n"+
120- "issues off\n", res.stdout)
109+ "name alpha\n"+
110+ "visibility public\n"+
111+ "default branch main\n"+
112+ "description The first one\n"+
113+ "topics cli, go\n"+
114+ "created 2026-01-02 03:04\n"+
115+ "last push 2026-03-04 05:06\n"+
116+ "merge modes fast-forward, squash\n"+
117+ "allow auto-merge false\n"+
118+ "issues off\n", res.stdout)
121119 _, reqs := f.snapshot()
122120 assert.Equal(t, "alpha", reqs[0].(*ermite.GetRepoRequest).GetRepo())
123121
124122 res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t)
125- assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"}`, res.stdout)
123+ assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout)
126124 }
127125
128126 func TestRepoShowNeedsOneArg(t *testing.T) {
@@ -178,7 +176,7 @@ func TestRepoCreateJSON(t *testing.T) {
178176 return &ermite.CreateRepoResponse{Repo: repoBeta}, nil
179177 }}
180178 res := runCLI(t, envFor(f.serve(t)), "", "repo", "create", "beta", "--json").ok(t)
181- assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}`, res.stdout)
179+ assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout)
182180 }
183181
184182 func TestRepoEditMask(t *testing.T) {
@@ -209,16 +207,8 @@ func TestRepoEditMask(t *testing.T) {
209207 {"merge modes", []string{"--merge-modes", "squash,ff"}, []string{"settings.allowed_merge_modes"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
210208 assert.Equal(t, []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH, ermite.MergeMode_MERGE_MODE_FAST_FORWARD}, r.GetSettings().GetAllowedMergeModes())
211209 }},
212- {"failing checks do not block", []string{"--failing-checks-block=false"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
213- assert.False(t, r.GetSettings().GetFailingChecksBlock())
214- assert.Empty(t, r.GetSettings().GetAllowedMergeModes())
215- }},
216- {"failing checks block", []string{"--failing-checks-block"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
217- assert.True(t, r.GetSettings().GetFailingChecksBlock())
218- }},
219210 {"allow auto-merge", []string{"--allow-auto-merge"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
220211 assert.True(t, r.GetSettings().GetAllowAutoMerge())
221- assert.False(t, r.GetSettings().GetFailingChecksBlock())
222212 }},
223213 {"forbid auto-merge", []string{"--allow-auto-merge=false"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
224214 assert.False(t, r.GetSettings().GetAllowAutoMerge())
@@ -460,9 +450,9 @@ func TestRepoEditAllowAutoMerge(t *testing.T) {
460450 assert.Equal(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths())
461451 assert.True(t, got.GetSettings().GetAllowAutoMerge())
462452
463- res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--failing-checks-block=false", "--json").ok(t)
453+ res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--json").ok(t)
464454 assert.Contains(t, res.stdout, `"allow_auto_merge": true`)
465455 _, reqs = f.snapshot()
466456 got = reqs[1].(*ermite.UpdateRepoRequest)
467- assert.ElementsMatch(t, []string{"settings.allow_auto_merge", "settings.failing_checks_block"}, got.GetUpdateMask().GetPaths())
457+ assert.ElementsMatch(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths())
468458 }
@@ -19,8 +19,7 @@ var (
19 Name: "alpha", Description: "The first one", Topics: []string{"cli", "go"}, Visibility: ermite.Visibility_VISIBILITY_PUBLIC,19 Name: "alpha", Description: "The first one", Topics: []string{"cli", "go"}, Visibility: ermite.Visibility_VISIBILITY_PUBLIC,
20 DefaultBranch: "main", CreatedAt: ts("2026-01-02T03:04:05Z"), PushedAt: ts("2026-03-04T05:06:07Z"),20 DefaultBranch: "main", CreatedAt: ts("2026-01-02T03:04:05Z"), PushedAt: ts("2026-03-04T05:06:07Z"),
21 Settings: &ermite.RepoSettings{21 Settings: &ermite.RepoSettings{
22- AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH},22+ AllowedMergeModes: []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_FAST_FORWARD, ermite.MergeMode_MERGE_MODE_SQUASH},
23- FailingChecksBlock: true,
24 },23 },
25 }24 }
26 repoBeta = &ermite.Repo{25 repoBeta = &ermite.Repo{
@@ -54,8 +53,8 @@ func TestRepoList(t *testing.T) {
54 {53 {
55 name: "json", repos: both, args: []string{"repo", "list", "--json"}, wantJSON: true,54 name: "json", repos: both, args: []string{"repo", "list", "--json"}, wantJSON: true,
56 wantStdout: `[55 wantStdout: `[
57- {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"},56+ {"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"},
58- {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}57+ {"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}
59 ]`,58 ]`,
60 },59 },
61 {60 {
@@ -107,22 +106,21 @@ func TestRepoShow(t *testing.T) {
107 url := f.serve(t)106 url := f.serve(t)
108 res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t)107 res := runCLI(t, envFor(url), "", "repo", "show", "alpha").ok(t)
109 assert.Equal(t, ""+108 assert.Equal(t, ""+
110- "name alpha\n"+109+ "name alpha\n"+
111- "visibility public\n"+110+ "visibility public\n"+
112- "default branch main\n"+111+ "default branch main\n"+
113- "description The first one\n"+112+ "description The first one\n"+
114- "topics cli, go\n"+113+ "topics cli, go\n"+
115- "created 2026-01-02 03:04\n"+114+ "created 2026-01-02 03:04\n"+
116- "last push 2026-03-04 05:06\n"+115+ "last push 2026-03-04 05:06\n"+
117- "merge modes fast-forward, squash\n"+116+ "merge modes fast-forward, squash\n"+
118- "failing checks block true\n"+117+ "allow auto-merge false\n"+
119- "allow auto-merge false\n"+118+ "issues off\n", res.stdout)
120- "issues off\n", res.stdout)
121 _, reqs := f.snapshot()119 _, reqs := f.snapshot()
122 assert.Equal(t, "alpha", reqs[0].(*ermite.GetRepoRequest).GetRepo())120 assert.Equal(t, "alpha", reqs[0].(*ermite.GetRepoRequest).GetRepo())
123 121
124 res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t)122 res = runCLI(t, envFor(url), "", "repo", "show", "alpha", "--json").ok(t)
125- assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"failing_checks_block":true,"allow_auto_merge":false,"issues":"off"}`, res.stdout)123+ assert.JSONEq(t, `{"name":"alpha","description":"The first one","topics":["cli","go"],"visibility":"public","default_branch":"main","created_at":"2026-01-02T03:04:05Z","pushed_at":"2026-03-04T05:06:07Z","merge_modes":["fast-forward","squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout)
126 }124 }
127 125
128 func TestRepoShowNeedsOneArg(t *testing.T) {126 func TestRepoShowNeedsOneArg(t *testing.T) {
@@ -178,7 +176,7 @@ func TestRepoCreateJSON(t *testing.T) {
178 return &ermite.CreateRepoResponse{Repo: repoBeta}, nil176 return &ermite.CreateRepoResponse{Repo: repoBeta}, nil
179 }}177 }}
180 res := runCLI(t, envFor(f.serve(t)), "", "repo", "create", "beta", "--json").ok(t)178 res := runCLI(t, envFor(f.serve(t)), "", "repo", "create", "beta", "--json").ok(t)
181- assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}`, res.stdout)179+ assert.JSONEq(t, `{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}`, res.stdout)
182 }180 }
183 181
184 func TestRepoEditMask(t *testing.T) {182 func TestRepoEditMask(t *testing.T) {
@@ -209,16 +207,8 @@ func TestRepoEditMask(t *testing.T) {
209 {"merge modes", []string{"--merge-modes", "squash,ff"}, []string{"settings.allowed_merge_modes"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {207 {"merge modes", []string{"--merge-modes", "squash,ff"}, []string{"settings.allowed_merge_modes"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
210 assert.Equal(t, []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH, ermite.MergeMode_MERGE_MODE_FAST_FORWARD}, r.GetSettings().GetAllowedMergeModes())208 assert.Equal(t, []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH, ermite.MergeMode_MERGE_MODE_FAST_FORWARD}, r.GetSettings().GetAllowedMergeModes())
211 }},209 }},
212- {"failing checks do not block", []string{"--failing-checks-block=false"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
213- assert.False(t, r.GetSettings().GetFailingChecksBlock())
214- assert.Empty(t, r.GetSettings().GetAllowedMergeModes())
215- }},
216- {"failing checks block", []string{"--failing-checks-block"}, []string{"settings.failing_checks_block"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
217- assert.True(t, r.GetSettings().GetFailingChecksBlock())
218- }},
219 {"allow auto-merge", []string{"--allow-auto-merge"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {210 {"allow auto-merge", []string{"--allow-auto-merge"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
220 assert.True(t, r.GetSettings().GetAllowAutoMerge())211 assert.True(t, r.GetSettings().GetAllowAutoMerge())
221- assert.False(t, r.GetSettings().GetFailingChecksBlock())
222 }},212 }},
223 {"forbid auto-merge", []string{"--allow-auto-merge=false"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {213 {"forbid auto-merge", []string{"--allow-auto-merge=false"}, []string{"settings.allow_auto_merge"}, func(t *testing.T, r *ermite.UpdateRepoRequest) {
224 assert.False(t, r.GetSettings().GetAllowAutoMerge())214 assert.False(t, r.GetSettings().GetAllowAutoMerge())
@@ -460,9 +450,9 @@ func TestRepoEditAllowAutoMerge(t *testing.T) {
460 assert.Equal(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths())450 assert.Equal(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths())
461 assert.True(t, got.GetSettings().GetAllowAutoMerge())451 assert.True(t, got.GetSettings().GetAllowAutoMerge())
462 452
463- res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--failing-checks-block=false", "--json").ok(t)453+ res = runCLI(t, envFor(url), "", "repo", "edit", "alpha", "--allow-auto-merge", "--json").ok(t)
464 assert.Contains(t, res.stdout, `"allow_auto_merge": true`)454 assert.Contains(t, res.stdout, `"allow_auto_merge": true`)
465 _, reqs = f.snapshot()455 _, reqs = f.snapshot()
466 got = reqs[1].(*ermite.UpdateRepoRequest)456 got = reqs[1].(*ermite.UpdateRepoRequest)
467- assert.ElementsMatch(t, []string{"settings.allow_auto_merge", "settings.failing_checks_block"}, got.GetUpdateMask().GetPaths())457+ assert.ElementsMatch(t, []string{"settings.allow_auto_merge"}, got.GetUpdateMask().GetPaths())
468 }458 }
M internal/app/cli_watch_test.go +1 -1
@@ -95,7 +95,7 @@ func TestWatchNDJSON(t *testing.T) {
9595 assert.True(t, json.Valid([]byte(sc.Text())), "one JSON value per line: %s", sc.Text())
9696 }
9797 require.Len(t, lines, 5)
98- assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}}`, lines[0])
98+ assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}}`, lines[0])
9999 assert.JSONEq(t, `{"id":"3","time":"2026-10-10T12:00:00Z","type":"pushed","repo":"alpha","ref_updates":[
100100 {"ref":"refs/heads/main","old_id":"1111111111111111","new_id":"2222222222222222"},
101101 {"ref":"refs/heads/new","old_id":"","new_id":"3333333333333333"},
@@ -95,7 +95,7 @@ func TestWatchNDJSON(t *testing.T) {
95 assert.True(t, json.Valid([]byte(sc.Text())), "one JSON value per line: %s", sc.Text())95 assert.True(t, json.Valid([]byte(sc.Text())), "one JSON value per line: %s", sc.Text())
96 }96 }
97 require.Len(t, lines, 5)97 require.Len(t, lines, 5)
98- assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"failing_checks_block":false,"allow_auto_merge":false,"issues":"off"}}`, lines[0])98+ assert.JSONEq(t, `{"id":"1","time":"2026-10-10T12:00:00Z","type":"repo_created","repo":"beta","repository":{"name":"beta","description":"","topics":[],"visibility":"private","default_branch":"trunk","created_at":"2026-02-02T00:00:00Z","pushed_at":null,"merge_modes":["squash"],"allow_auto_merge":false,"issues":"off"}}`, lines[0])
99 assert.JSONEq(t, `{"id":"3","time":"2026-10-10T12:00:00Z","type":"pushed","repo":"alpha","ref_updates":[99 assert.JSONEq(t, `{"id":"3","time":"2026-10-10T12:00:00Z","type":"pushed","repo":"alpha","ref_updates":[
100 {"ref":"refs/heads/main","old_id":"1111111111111111","new_id":"2222222222222222"},100 {"ref":"refs/heads/main","old_id":"1111111111111111","new_id":"2222222222222222"},
101 {"ref":"refs/heads/new","old_id":"","new_id":"3333333333333333"},101 {"ref":"refs/heads/new","old_id":"","new_id":"3333333333333333"},
M internal/app/issue_api_test.go +0 -1
@@ -221,7 +221,6 @@ func TestIssuesFeatureAndSettingOverTheAPI(t *testing.T) {
221221 updated, err := update(r.root, pb.IssuesMode_ISSUES_MODE_OWNER)
222222 require.NoError(t, err)
223223 assert.Equal(t, pb.IssuesMode_ISSUES_MODE_OWNER, updated.GetSettings().GetIssues())
224- assert.True(t, updated.GetSettings().GetFailingChecksBlock(), "the other settings are kept")
225224 _, err = r.root.issue.CreateIssue(ctx, connect.NewRequest(&pb.CreateIssueRequest{Repo: "priv", Title: "t"}))
226225 require.NoError(t, err)
227226
@@ -221,7 +221,6 @@ func TestIssuesFeatureAndSettingOverTheAPI(t *testing.T) {
221 updated, err := update(r.root, pb.IssuesMode_ISSUES_MODE_OWNER)221 updated, err := update(r.root, pb.IssuesMode_ISSUES_MODE_OWNER)
222 require.NoError(t, err)222 require.NoError(t, err)
223 assert.Equal(t, pb.IssuesMode_ISSUES_MODE_OWNER, updated.GetSettings().GetIssues())223 assert.Equal(t, pb.IssuesMode_ISSUES_MODE_OWNER, updated.GetSettings().GetIssues())
224- assert.True(t, updated.GetSettings().GetFailingChecksBlock(), "the other settings are kept")
225 _, err = r.root.issue.CreateIssue(ctx, connect.NewRequest(&pb.CreateIssueRequest{Repo: "priv", Title: "t"}))224 _, err = r.root.issue.CreateIssue(ctx, connect.NewRequest(&pb.CreateIssueRequest{Repo: "priv", Title: "t"}))
226 require.NoError(t, err)225 require.NoError(t, err)
227 226
M internal/app/mr_api_test.go +9 -9
@@ -299,7 +299,7 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) {
299299 got, err := h.api("").repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"}))
300300 require.NoError(t, err)
301301 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes())
302- assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock())
302+ assert.False(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge())
303303
304304 update := func(c *clients, paths []string, s *pb.RepoSettings) (*pb.Repo, error) {
305305 res, err := c.repo.UpdateRepo(ctx, connect.NewRequest(&pb.UpdateRepoRequest{
@@ -311,23 +311,23 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) {
311311 return res.Msg.GetRepo(), nil
312312 }
313313 repo, err := update(root, []string{"settings.allowed_merge_modes"}, &pb.RepoSettings{
314- AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: false,
314+ AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: false,
315315 })
316316 require.NoError(t, err)
317317 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes())
318- assert.True(t, repo.GetSettings().GetFailingChecksBlock(), "not named in the mask: unchanged")
318+ assert.False(t, repo.GetSettings().GetAllowAutoMerge(), "not named in the mask: unchanged")
319319
320- repo, err = update(root, []string{"settings.failing_checks_block"}, &pb.RepoSettings{FailingChecksBlock: false})
320+ repo, err = update(root, []string{"settings.allow_auto_merge"}, &pb.RepoSettings{AllowAutoMerge: false})
321321 require.NoError(t, err)
322- assert.False(t, repo.GetSettings().GetFailingChecksBlock())
322+ assert.False(t, repo.GetSettings().GetAllowAutoMerge())
323323 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "not named in the mask: unchanged")
324324
325325 repo, err = update(root, []string{"settings"}, &pb.RepoSettings{
326- AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: true,
326+ AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: true,
327327 })
328328 require.NoError(t, err)
329329 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "canonical order, no duplicates")
330- assert.True(t, repo.GetSettings().GetFailingChecksBlock())
330+ assert.True(t, repo.GetSettings().GetAllowAutoMerge())
331331
332332 for name, s := range map[string]*pb.RepoSettings{
333333 "no modes": {},
@@ -341,11 +341,11 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) {
341341 assert.Equal(t, "invalid_argument", codeName(err))
342342
343343 // repo:admin is needed.
344- _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.failing_checks_block"}, &pb.RepoSettings{})
344+ _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.allow_auto_merge"}, &pb.RepoSettings{})
345345 assert.Equal(t, "permission_denied", codeName(err))
346346 got, err = root.repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"}))
347347 require.NoError(t, err)
348- assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock())
348+ assert.True(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge())
349349 }
350350
351351 // AC4 over the API: a mode the settings do not allow is refused.
@@ -299,7 +299,7 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) {
299 got, err := h.api("").repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"}))299 got, err := h.api("").repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"}))
300 require.NoError(t, err)300 require.NoError(t, err)
301 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes())301 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_MERGE_COMMIT}, got.Msg.GetRepo().GetSettings().GetAllowedMergeModes())
302- assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock())302+ assert.False(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge())
303 303
304 update := func(c *clients, paths []string, s *pb.RepoSettings) (*pb.Repo, error) {304 update := func(c *clients, paths []string, s *pb.RepoSettings) (*pb.Repo, error) {
305 res, err := c.repo.UpdateRepo(ctx, connect.NewRequest(&pb.UpdateRepoRequest{305 res, err := c.repo.UpdateRepo(ctx, connect.NewRequest(&pb.UpdateRepoRequest{
@@ -311,23 +311,23 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) {
311 return res.Msg.GetRepo(), nil311 return res.Msg.GetRepo(), nil
312 }312 }
313 repo, err := update(root, []string{"settings.allowed_merge_modes"}, &pb.RepoSettings{313 repo, err := update(root, []string{"settings.allowed_merge_modes"}, &pb.RepoSettings{
314- AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: false,314+ AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: false,
315 })315 })
316 require.NoError(t, err)316 require.NoError(t, err)
317 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes())317 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes())
318- assert.True(t, repo.GetSettings().GetFailingChecksBlock(), "not named in the mask: unchanged")318+ assert.False(t, repo.GetSettings().GetAllowAutoMerge(), "not named in the mask: unchanged")
319 319
320- repo, err = update(root, []string{"settings.failing_checks_block"}, &pb.RepoSettings{FailingChecksBlock: false})320+ repo, err = update(root, []string{"settings.allow_auto_merge"}, &pb.RepoSettings{AllowAutoMerge: false})
321 require.NoError(t, err)321 require.NoError(t, err)
322- assert.False(t, repo.GetSettings().GetFailingChecksBlock())322+ assert.False(t, repo.GetSettings().GetAllowAutoMerge())
323 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "not named in the mask: unchanged")323 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "not named in the mask: unchanged")
324 324
325 repo, err = update(root, []string{"settings"}, &pb.RepoSettings{325 repo, err = update(root, []string{"settings"}, &pb.RepoSettings{
326- AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, FailingChecksBlock: true,326+ AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH, pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, AllowAutoMerge: true,
327 })327 })
328 require.NoError(t, err)328 require.NoError(t, err)
329 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "canonical order, no duplicates")329 assert.Equal(t, []pb.MergeMode{pb.MergeMode_MERGE_MODE_FAST_FORWARD, pb.MergeMode_MERGE_MODE_SQUASH}, repo.GetSettings().GetAllowedMergeModes(), "canonical order, no duplicates")
330- assert.True(t, repo.GetSettings().GetFailingChecksBlock())330+ assert.True(t, repo.GetSettings().GetAllowAutoMerge())
331 331
332 for name, s := range map[string]*pb.RepoSettings{332 for name, s := range map[string]*pb.RepoSettings{
333 "no modes": {},333 "no modes": {},
@@ -341,11 +341,11 @@ func TestUpdateRepoSettingsOverTheAPI(t *testing.T) {
341 assert.Equal(t, "invalid_argument", codeName(err))341 assert.Equal(t, "invalid_argument", codeName(err))
342 342
343 // repo:admin is needed.343 // repo:admin is needed.
344- _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.failing_checks_block"}, &pb.RepoSettings{})344+ _, err = update(h.api(h.token(scopes(domain.ScopeRepoRead, domain.ScopeMRMerge))), []string{"settings.allow_auto_merge"}, &pb.RepoSettings{})
345 assert.Equal(t, "permission_denied", codeName(err))345 assert.Equal(t, "permission_denied", codeName(err))
346 got, err = root.repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"}))346 got, err = root.repo.GetRepo(ctx, connect.NewRequest(&pb.GetRepoRequest{Repo: "proj"}))
347 require.NoError(t, err)347 require.NoError(t, err)
348- assert.True(t, got.Msg.GetRepo().GetSettings().GetFailingChecksBlock())348+ assert.True(t, got.Msg.GetRepo().GetSettings().GetAllowAutoMerge())
349 }349 }
350 350
351 // AC4 over the API: a mode the settings do not allow is refused.351 // AC4 over the API: a mode the settings do not allow is refused.
M internal/domain/ci.go +1 -1
@@ -42,7 +42,7 @@ const (
4242 )
4343
4444 // PassesMerge reports whether a check that ended this way lets a merge go
45-// through under FailingChecksBlock: success, skipped and neutral do.
45+// through under the merge checks policy: success, skipped and neutral do.
4646 func (c CIConclusion) PassesMerge() bool {
4747 return c == CISuccess || c == CISkipped || c == CINeutral
4848 }
@@ -42,7 +42,7 @@ const (
42 )42 )
43 43
44 // PassesMerge reports whether a check that ended this way lets a merge go44 // PassesMerge reports whether a check that ended this way lets a merge go
45-// through under FailingChecksBlock: success, skipped and neutral do.45+// through under the merge checks policy: success, skipped and neutral do.
46 func (c CIConclusion) PassesMerge() bool {46 func (c CIConclusion) PassesMerge() bool {
47 return c == CISuccess || c == CISkipped || c == CINeutral47 return c == CISuccess || c == CISkipped || c == CINeutral
48 }48 }
M internal/domain/mergerequest.go +3 -5
@@ -46,8 +46,6 @@ type RepoSettings struct {
4646 // AllowedMergeModes is never empty, in canonical order, without
4747 // duplicates.
4848 AllowedMergeModes []MergeMode
49- // FailingChecksBlock says that a failing check prevents the merge.
50- FailingChecksBlock bool
5149 // Issues says who may write issues; the empty mode means IssuesOff.
5250 Issues IssuesMode
5351 // AllowAutoMerge says that merge requests may be set to merge themselves
@@ -55,10 +53,10 @@ type RepoSettings struct {
5553 AllowAutoMerge bool
5654 }
5755
58-// DefaultRepoSettings are the settings of a new repository: every mode, and
59-// failing checks block the merge.
56+// DefaultRepoSettings are the settings of a new repository: every merge mode,
57+// with issues and auto-merge disabled.
6058 func DefaultRepoSettings() RepoSettings {
61- return RepoSettings{AllowedMergeModes: MergeModes(), FailingChecksBlock: true, Issues: IssuesOff}
59+ return RepoSettings{AllowedMergeModes: MergeModes(), Issues: IssuesOff}
6260 }
6361
6462 // IsZero reports whether s holds no mode, which only the zero value does.
@@ -46,8 +46,6 @@ type RepoSettings struct {
46 // AllowedMergeModes is never empty, in canonical order, without46 // AllowedMergeModes is never empty, in canonical order, without
47 // duplicates.47 // duplicates.
48 AllowedMergeModes []MergeMode48 AllowedMergeModes []MergeMode
49- // FailingChecksBlock says that a failing check prevents the merge.
50- FailingChecksBlock bool
51 // Issues says who may write issues; the empty mode means IssuesOff.49 // Issues says who may write issues; the empty mode means IssuesOff.
52 Issues IssuesMode50 Issues IssuesMode
53 // AllowAutoMerge says that merge requests may be set to merge themselves51 // AllowAutoMerge says that merge requests may be set to merge themselves
@@ -55,10 +53,10 @@ type RepoSettings struct {
55 AllowAutoMerge bool53 AllowAutoMerge bool
56 }54 }
57 55
58-// DefaultRepoSettings are the settings of a new repository: every mode, and56+// DefaultRepoSettings are the settings of a new repository: every merge mode,
59-// failing checks block the merge.57+// with issues and auto-merge disabled.
60 func DefaultRepoSettings() RepoSettings {58 func DefaultRepoSettings() RepoSettings {
61- return RepoSettings{AllowedMergeModes: MergeModes(), FailingChecksBlock: true, Issues: IssuesOff}59+ return RepoSettings{AllowedMergeModes: MergeModes(), Issues: IssuesOff}
62 }60 }
63 61
64 // IsZero reports whether s holds no mode, which only the zero value does.62 // IsZero reports whether s holds no mode, which only the zero value does.
M internal/domain/mergerequest_test.go +0 -1
@@ -25,7 +25,6 @@ func TestDefaultRepoSettings(t *testing.T) {
2525 t.Parallel()
2626 s := domain.DefaultRepoSettings()
2727 assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge}, s.AllowedMergeModes)
28- assert.True(t, s.FailingChecksBlock)
2928 assert.True(t, s.Allows(domain.MergeModeSquash))
3029 assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash))
3130 assert.True(t, domain.RepoSettings{}.IsZero())
@@ -25,7 +25,6 @@ func TestDefaultRepoSettings(t *testing.T) {
25 t.Parallel()25 t.Parallel()
26 s := domain.DefaultRepoSettings()26 s := domain.DefaultRepoSettings()
27 assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge}, s.AllowedMergeModes)27 assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash, domain.MergeModeMerge}, s.AllowedMergeModes)
28- assert.True(t, s.FailingChecksBlock)
29 assert.True(t, s.Allows(domain.MergeModeSquash))28 assert.True(t, s.Allows(domain.MergeModeSquash))
30 assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash))29 assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash))
31 assert.True(t, domain.RepoSettings{}.IsZero())30 assert.True(t, domain.RepoSettings{}.IsZero())
M internal/domain/token.go +4 -2
@@ -30,6 +30,8 @@ const (
3030 ScopeMRWrite Scope = "mr:write"
3131 // ScopeMRMerge merges, and deletes the merged source branch on request.
3232 ScopeMRMerge Scope = "mr:merge"
33+ // ScopeMRForceMerge bypasses failing or unfinished merge checks.
34+ ScopeMRForceMerge Scope = "mr:force-merge"
3335 // ScopeMRReview sets a verdict on a merge request.
3436 ScopeMRReview Scope = "mr:review"
3537 // ScopeCIWrite re-runs or cancels CI runs.
@@ -45,11 +47,11 @@ const (
4547 ScopeIssueWrite Scope = "issue:write"
4648 )
4749
48-// AllScopes returns the eleven scopes in a stable order.
50+// AllScopes returns the twelve scopes in a stable order.
4951 func AllScopes() []Scope {
5052 return []Scope{
5153 ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,
52- ScopeMRWrite, ScopeMRMerge, ScopeMRReview,
54+ ScopeMRWrite, ScopeMRMerge, ScopeMRForceMerge, ScopeMRReview,
5355 ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,
5456 ScopeTokenAdmin,
5557 ScopeIssueWrite,
@@ -30,6 +30,8 @@ const (
30 ScopeMRWrite Scope = "mr:write"30 ScopeMRWrite Scope = "mr:write"
31 // ScopeMRMerge merges, and deletes the merged source branch on request.31 // ScopeMRMerge merges, and deletes the merged source branch on request.
32 ScopeMRMerge Scope = "mr:merge"32 ScopeMRMerge Scope = "mr:merge"
33+ // ScopeMRForceMerge bypasses failing or unfinished merge checks.
34+ ScopeMRForceMerge Scope = "mr:force-merge"
33 // ScopeMRReview sets a verdict on a merge request.35 // ScopeMRReview sets a verdict on a merge request.
34 ScopeMRReview Scope = "mr:review"36 ScopeMRReview Scope = "mr:review"
35 // ScopeCIWrite re-runs or cancels CI runs.37 // ScopeCIWrite re-runs or cancels CI runs.
@@ -45,11 +47,11 @@ const (
45 ScopeIssueWrite Scope = "issue:write"47 ScopeIssueWrite Scope = "issue:write"
46 )48 )
47 49
48-// AllScopes returns the eleven scopes in a stable order.50+// AllScopes returns the twelve scopes in a stable order.
49 func AllScopes() []Scope {51 func AllScopes() []Scope {
50 return []Scope{52 return []Scope{
51 ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,53 ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,
52- ScopeMRWrite, ScopeMRMerge, ScopeMRReview,54+ ScopeMRWrite, ScopeMRMerge, ScopeMRForceMerge, ScopeMRReview,
53 ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,55 ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,
54 ScopeTokenAdmin,56 ScopeTokenAdmin,
55 ScopeIssueWrite,57 ScopeIssueWrite,
M internal/domain/token_test.go +1 -1
@@ -16,7 +16,7 @@ import (
1616 func TestScopes(t *testing.T) {
1717 t.Parallel()
1818 all := domain.AllScopes()
19- assert.Len(t, all, 11)
19+ assert.Len(t, all, 12)
2020 assert.Contains(t, all, domain.ScopeIssueWrite)
2121 got, err := domain.ParseScope("issue:write")
2222 require.NoError(t, err)
@@ -16,7 +16,7 @@ import (
16 func TestScopes(t *testing.T) {16 func TestScopes(t *testing.T) {
17 t.Parallel()17 t.Parallel()
18 all := domain.AllScopes()18 all := domain.AllScopes()
19- assert.Len(t, all, 11)19+ assert.Len(t, all, 12)
20 assert.Contains(t, all, domain.ScopeIssueWrite)20 assert.Contains(t, all, domain.ScopeIssueWrite)
21 got, err := domain.ParseScope("issue:write")21 got, err := domain.ParseScope("issue:write")
22 require.NoError(t, err)22 require.NoError(t, err)
M internal/ports/dto.go +2 -2
@@ -47,8 +47,6 @@ type UpdateRepoParams struct {
4747 // AllowedMergeModes replaces the allowed merge modes; it must hold at
4848 // least one known mode.
4949 AllowedMergeModes *[]domain.MergeMode
50- // FailingChecksBlock replaces the setting of that name.
51- FailingChecksBlock *bool
5250 // Issues replaces the issues mode.
5351 Issues *domain.IssuesMode
5452 // AllowAutoMerge replaces the setting of that name.
@@ -381,6 +379,8 @@ type AddCommentParams struct {
381379
382380 // MergeParams describes the merge to perform.
383381 type MergeParams struct {
382+ // IgnoreChecks bypasses checks and requires mr:force-merge.
383+ IgnoreChecks bool
384384 // Mode defaults to fast-forward.
385385 Mode domain.MergeMode
386386 // HeadID is the commit the caller reviewed; the source branch must still
@@ -47,8 +47,6 @@ type UpdateRepoParams struct {
47 // AllowedMergeModes replaces the allowed merge modes; it must hold at47 // AllowedMergeModes replaces the allowed merge modes; it must hold at
48 // least one known mode.48 // least one known mode.
49 AllowedMergeModes *[]domain.MergeMode49 AllowedMergeModes *[]domain.MergeMode
50- // FailingChecksBlock replaces the setting of that name.
51- FailingChecksBlock *bool
52 // Issues replaces the issues mode.50 // Issues replaces the issues mode.
53 Issues *domain.IssuesMode51 Issues *domain.IssuesMode
54 // AllowAutoMerge replaces the setting of that name.52 // AllowAutoMerge replaces the setting of that name.
@@ -381,6 +379,8 @@ type AddCommentParams struct {
381 379
382 // MergeParams describes the merge to perform.380 // MergeParams describes the merge to perform.
383 type MergeParams struct {381 type MergeParams struct {
382+ // IgnoreChecks bypasses checks and requires mr:force-merge.
383+ IgnoreChecks bool
384 // Mode defaults to fast-forward.384 // Mode defaults to fast-forward.
385 Mode domain.MergeMode385 Mode domain.MergeMode
386 // HeadID is the commit the caller reviewed; the source branch must still386 // HeadID is the commit the caller reviewed; the source branch must still
M internal/usecase/access.go +1 -1
@@ -36,7 +36,7 @@ func canReadAs(p domain.Principal, name domain.RepoName, vis domain.Visibility)
3636 // repo:admin on it, and no branch prefixes. A token limited to some branches
3737 // may not delete the repository (that deletes every branch, those outside its
3838 // prefixes included), nor change its default branch, visibility, description,
39-// merge modes or failing-checks rule: these decide what happens to branches the
39+// merge modes or auto-merge settings: these decide what happens to branches the
4040 // token may not write. A caller that may not administer it learns nothing it
4141 // could not already read: not found when it cannot see the repository,
4242 // unauthenticated when it is anonymous, forbidden otherwise.
@@ -36,7 +36,7 @@ func canReadAs(p domain.Principal, name domain.RepoName, vis domain.Visibility)
36 // repo:admin on it, and no branch prefixes. A token limited to some branches36 // repo:admin on it, and no branch prefixes. A token limited to some branches
37 // may not delete the repository (that deletes every branch, those outside its37 // may not delete the repository (that deletes every branch, those outside its
38 // prefixes included), nor change its default branch, visibility, description,38 // prefixes included), nor change its default branch, visibility, description,
39-// merge modes or failing-checks rule: these decide what happens to branches the39+// merge modes or auto-merge settings: these decide what happens to branches the
40 // token may not write. A caller that may not administer it learns nothing it40 // token may not write. A caller that may not administer it learns nothing it
41 // could not already read: not found when it cannot see the repository,41 // could not already read: not found when it cannot see the repository,
42 // unauthenticated when it is anonymous, forbidden otherwise.42 // unauthenticated when it is anonymous, forbidden otherwise.
M internal/usecase/automerge_test.go +1 -4
@@ -159,7 +159,7 @@ func TestAutoMergeMergesAsEnablerWhenAChecksPassed(t *testing.T) {
159159 require.True(t, found)
160160 }
161161
162-func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) {
162+func TestAutoMergeRequiresPassingChecks(t *testing.T) {
163163 for _, tc := range []struct {
164164 name string
165165 check domain.CICheck
@@ -170,8 +170,6 @@ func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) {
170170 t.Run(tc.name, func(t *testing.T) {
171171 r, mr, checks := autoRig(t)
172172 enableAuto(t, r, mr)
173- _, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{FailingChecksBlock: ptr(false)})
174- require.NoError(t, err)
175173 checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return([]domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CISuccess}, tc.check}, nil)
176174 a := autoEvaluator(r, checks, nil, nil)
177175 defer a.Close(r.ctx)
@@ -388,7 +386,6 @@ func TestMergeGateSettingsUpdateWaits(t *testing.T) {
388386 for _, params := range []ports.UpdateRepoParams{
389387 {AllowAutoMerge: ptr(false)},
390388 {AllowedMergeModes: ptr([]domain.MergeMode{domain.MergeModeSquash})},
391- {FailingChecksBlock: ptr(false)},
392389 } {
393390 r, _, _ := autoRig(t)
394391 r.repos.WithMergeGate(r.mrs)
@@ -159,7 +159,7 @@ func TestAutoMergeMergesAsEnablerWhenAChecksPassed(t *testing.T) {
159 require.True(t, found)159 require.True(t, found)
160 }160 }
161 161
162-func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) {162+func TestAutoMergeRequiresPassingChecks(t *testing.T) {
163 for _, tc := range []struct {163 for _, tc := range []struct {
164 name string164 name string
165 check domain.CICheck165 check domain.CICheck
@@ -170,8 +170,6 @@ func TestAutoMergeIgnoresFailingChecksBlockOff(t *testing.T) {
170 t.Run(tc.name, func(t *testing.T) {170 t.Run(tc.name, func(t *testing.T) {
171 r, mr, checks := autoRig(t)171 r, mr, checks := autoRig(t)
172 enableAuto(t, r, mr)172 enableAuto(t, r, mr)
173- _, err := r.repos.Update(r.ctx, root, r.repo, ports.UpdateRepoParams{FailingChecksBlock: ptr(false)})
174- require.NoError(t, err)
175 checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return([]domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CISuccess}, tc.check}, nil)173 checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return([]domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CISuccess}, tc.check}, nil)
176 a := autoEvaluator(r, checks, nil, nil)174 a := autoEvaluator(r, checks, nil, nil)
177 defer a.Close(r.ctx)175 defer a.Close(r.ctx)
@@ -388,7 +386,6 @@ func TestMergeGateSettingsUpdateWaits(t *testing.T) {
388 for _, params := range []ports.UpdateRepoParams{386 for _, params := range []ports.UpdateRepoParams{
389 {AllowAutoMerge: ptr(false)},387 {AllowAutoMerge: ptr(false)},
390 {AllowedMergeModes: ptr([]domain.MergeMode{domain.MergeModeSquash})},388 {AllowedMergeModes: ptr([]domain.MergeMode{domain.MergeModeSquash})},
391- {FailingChecksBlock: ptr(false)},
392 } {389 } {
393 r, _, _ := autoRig(t)390 r, _, _ := autoRig(t)
394 r.repos.WithMergeGate(r.mrs)391 r.repos.WithMergeGate(r.mrs)
M internal/usecase/ci_unit_test.go +37 -6
@@ -24,7 +24,7 @@ type mergeUnit struct {
2424 head domain.ObjectID
2525 }
2626
27-func newMergeUnit(t *testing.T, block bool) *mergeUnit {
27+func newMergeUnit(t *testing.T) *mergeUnit {
2828 t.Helper()
2929 u := &mrUnit{
3030 meta: mocks.NewRepoMetaStore(t),
@@ -34,7 +34,6 @@ func newMergeUnit(t *testing.T, block bool) *mergeUnit {
3434 repo: domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPrivate},
3535 }
3636 u.repo.Settings = domain.DefaultRepoSettings()
37- u.repo.Settings.FailingChecksBlock = block
3837 clock := mocks.NewClock(t)
3938 clock.EXPECT().Now().Return(now).Maybe()
4039 sched := mocks.NewMaintenanceScheduler(t)
@@ -63,8 +62,8 @@ func (m *mergeUnit) merge() error {
6362 return err
6463 }
6564
66-// AC5: the merge rule under FailingChecksBlock.
67-func TestMergeRuleOfFailingChecksBlock(t *testing.T) {
65+// AC5: the merge rule under ChecksPolicy.
66+func TestMergeRuleOfChecksPolicy(t *testing.T) {
6867 t.Parallel()
6968 check := func(status domain.CIStatus, c domain.CIConclusion, name string) domain.CICheck {
7069 return domain.CICheck{Name: name, Status: status, Conclusion: c}
@@ -84,7 +83,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) {
8483 for _, tt := range tests {
8584 t.Run(tt.name, func(t *testing.T) {
8685 t.Parallel()
87- m := newMergeUnit(t, true)
86+ m := newMergeUnit(t)
8887 m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(tt.checks, nil)
8988 // The merge stops at the rule: the branch mock expects no move.
9089 err := m.merge()
@@ -94,7 +93,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) {
9493 }
9594 t.Run("too many checks to tell", func(t *testing.T) {
9695 t.Parallel()
97- m := newMergeUnit(t, true)
96+ m := newMergeUnit(t)
9897 m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(nil, domain.ErrResourceExhausted)
9998 err := m.merge()
10099 require.ErrorIs(t, err, domain.ErrPreconditionFailed)
@@ -174,3 +173,35 @@ func TestTokenShapes(t *testing.T) {
174173 assert.False(t, domain.IsJobToken("ermite_"+good[10:]))
175174 assert.False(t, domain.IsJobToken(good+"="))
176175 }
176+
177+func TestMergeChecksAndOverride(t *testing.T) {
178+ for _, tc := range []struct {
179+ name string
180+ principal domain.Principal
181+ checks []domain.CICheck
182+ ignore bool
183+ want error
184+ }{
185+ {name: "no checks allowed", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge)},
186+ {name: "failure refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CICompleted, Conclusion: domain.CIFailure}}, want: domain.ErrPreconditionFailed},
187+ {name: "pending refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CIQueued}}, want: domain.ErrPreconditionFailed},
188+ {name: "force scope bypasses failure", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge, domain.ScopeMRForceMerge), checks: []domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CIFailure}}, ignore: true},
189+ {name: "missing force scope denied", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge), ignore: true, want: domain.ErrForbidden},
190+ {name: "force scope alone insufficient", principal: writer(domain.ScopeRepoRead, domain.ScopeMRForceMerge), ignore: true, want: domain.ErrForbidden},
191+ {name: "root override", principal: domain.Root(), ignore: true},
192+ {name: "owner override", principal: domain.Owner(), ignore: true},
193+ } {
194+ t.Run(tc.name, func(t *testing.T) {
195+ r, mr, checks := autoRig(t)
196+ if !tc.ignore {
197+ checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return(tc.checks, nil).Once()
198+ }
199+ _, err := r.mrs.Merge(r.ctx, tc.principal, r.repo, mr.Number, ports.MergeParams{HeadID: r.head(), IgnoreChecks: tc.ignore})
200+ if tc.want != nil {
201+ require.ErrorIs(t, err, tc.want)
202+ } else {
203+ require.NoError(t, err)
204+ }
205+ })
206+ }
207+}
@@ -24,7 +24,7 @@ type mergeUnit struct {
24 head domain.ObjectID24 head domain.ObjectID
25 }25 }
26 26
27-func newMergeUnit(t *testing.T, block bool) *mergeUnit {27+func newMergeUnit(t *testing.T) *mergeUnit {
28 t.Helper()28 t.Helper()
29 u := &mrUnit{29 u := &mrUnit{
30 meta: mocks.NewRepoMetaStore(t),30 meta: mocks.NewRepoMetaStore(t),
@@ -34,7 +34,6 @@ func newMergeUnit(t *testing.T, block bool) *mergeUnit {
34 repo: domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPrivate},34 repo: domain.Repo{Name: name(t, "proj"), Visibility: domain.VisibilityPrivate},
35 }35 }
36 u.repo.Settings = domain.DefaultRepoSettings()36 u.repo.Settings = domain.DefaultRepoSettings()
37- u.repo.Settings.FailingChecksBlock = block
38 clock := mocks.NewClock(t)37 clock := mocks.NewClock(t)
39 clock.EXPECT().Now().Return(now).Maybe()38 clock.EXPECT().Now().Return(now).Maybe()
40 sched := mocks.NewMaintenanceScheduler(t)39 sched := mocks.NewMaintenanceScheduler(t)
@@ -63,8 +62,8 @@ func (m *mergeUnit) merge() error {
63 return err62 return err
64 }63 }
65 64
66-// AC5: the merge rule under FailingChecksBlock.65+// AC5: the merge rule under ChecksPolicy.
67-func TestMergeRuleOfFailingChecksBlock(t *testing.T) {66+func TestMergeRuleOfChecksPolicy(t *testing.T) {
68 t.Parallel()67 t.Parallel()
69 check := func(status domain.CIStatus, c domain.CIConclusion, name string) domain.CICheck {68 check := func(status domain.CIStatus, c domain.CIConclusion, name string) domain.CICheck {
70 return domain.CICheck{Name: name, Status: status, Conclusion: c}69 return domain.CICheck{Name: name, Status: status, Conclusion: c}
@@ -84,7 +83,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) {
84 for _, tt := range tests {83 for _, tt := range tests {
85 t.Run(tt.name, func(t *testing.T) {84 t.Run(tt.name, func(t *testing.T) {
86 t.Parallel()85 t.Parallel()
87- m := newMergeUnit(t, true)86+ m := newMergeUnit(t)
88 m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(tt.checks, nil)87 m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(tt.checks, nil)
89 // The merge stops at the rule: the branch mock expects no move.88 // The merge stops at the rule: the branch mock expects no move.
90 err := m.merge()89 err := m.merge()
@@ -94,7 +93,7 @@ func TestMergeRuleOfFailingChecksBlock(t *testing.T) {
94 }93 }
95 t.Run("too many checks to tell", func(t *testing.T) {94 t.Run("too many checks to tell", func(t *testing.T) {
96 t.Parallel()95 t.Parallel()
97- m := newMergeUnit(t, true)96+ m := newMergeUnit(t)
98 m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(nil, domain.ErrResourceExhausted)97 m.checks.EXPECT().CommitChecks(mock.Anything, m.repo.Name, m.head.String()).Return(nil, domain.ErrResourceExhausted)
99 err := m.merge()98 err := m.merge()
100 require.ErrorIs(t, err, domain.ErrPreconditionFailed)99 require.ErrorIs(t, err, domain.ErrPreconditionFailed)
@@ -174,3 +173,35 @@ func TestTokenShapes(t *testing.T) {
174 assert.False(t, domain.IsJobToken("ermite_"+good[10:]))173 assert.False(t, domain.IsJobToken("ermite_"+good[10:]))
175 assert.False(t, domain.IsJobToken(good+"="))174 assert.False(t, domain.IsJobToken(good+"="))
176 }175 }
176+
177+func TestMergeChecksAndOverride(t *testing.T) {
178+ for _, tc := range []struct {
179+ name string
180+ principal domain.Principal
181+ checks []domain.CICheck
182+ ignore bool
183+ want error
184+ }{
185+ {name: "no checks allowed", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge)},
186+ {name: "failure refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CICompleted, Conclusion: domain.CIFailure}}, want: domain.ErrPreconditionFailed},
187+ {name: "pending refused", principal: root, checks: []domain.CICheck{{Name: "build", Status: domain.CIQueued}}, want: domain.ErrPreconditionFailed},
188+ {name: "force scope bypasses failure", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge, domain.ScopeMRForceMerge), checks: []domain.CICheck{{Status: domain.CICompleted, Conclusion: domain.CIFailure}}, ignore: true},
189+ {name: "missing force scope denied", principal: writer(domain.ScopeRepoRead, domain.ScopeMRMerge), ignore: true, want: domain.ErrForbidden},
190+ {name: "force scope alone insufficient", principal: writer(domain.ScopeRepoRead, domain.ScopeMRForceMerge), ignore: true, want: domain.ErrForbidden},
191+ {name: "root override", principal: domain.Root(), ignore: true},
192+ {name: "owner override", principal: domain.Owner(), ignore: true},
193+ } {
194+ t.Run(tc.name, func(t *testing.T) {
195+ r, mr, checks := autoRig(t)
196+ if !tc.ignore {
197+ checks.EXPECT().CommitChecks(mock.Anything, r.repo, r.head().String()).Return(tc.checks, nil).Once()
198+ }
199+ _, err := r.mrs.Merge(r.ctx, tc.principal, r.repo, mr.Number, ports.MergeParams{HeadID: r.head(), IgnoreChecks: tc.ignore})
200+ if tc.want != nil {
201+ require.ErrorIs(t, err, tc.want)
202+ } else {
203+ require.NoError(t, err)
204+ }
205+ })
206+ }
207+}
M internal/usecase/mergerequests.go +12 -5
@@ -995,8 +995,9 @@ func (m *MergeRequests) MarkRead(ctx context.Context, p domain.Principal, name d
995995 // allowed by the settings of the repository, the source branch no longer
996996 // points to the head ("branch moved, read again") or is gone, the target is
997997 // gone, the mode's own condition does not hold (see below), or the branch
998-// brings nothing. Checks come with CI (CI-1): there are none yet, so the
999-// setting that lets failing checks block the merge has nothing to act on.
998+// brings nothing. Checks on the head must pass: none may be failed, queued
999+// or running; a head with no checks may be merged. params.IgnoreChecks bypasses
1000+// this requirement and needs mr:force-merge.
10001001 //
10011002 // The conditions are always checked with Git, under the lock, never from the
10021003 // stored mergeability. A fast-forward needs the target to be an ancestor of
@@ -1075,9 +1076,15 @@ var errAutoMergeWait = errors.New("auto-merge is waiting for checks")
10751076
10761077 func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams, opts mergeOpts) (domain.MergeRequest, error) {
10771078 if opts.auto == nil {
1078- if _, err := m.writable(ctx, p, name, domain.ScopeMRMerge); err != nil {
1079+ repo, err := m.writable(ctx, p, name, domain.ScopeMRMerge)
1080+ if err != nil {
10791081 return domain.MergeRequest{}, err
10801082 }
1083+ if params.IgnoreChecks {
1084+ if err := scopeAccess(p, repo, domain.ScopeMRForceMerge); err != nil {
1085+ return domain.MergeRequest{}, err
1086+ }
1087+ }
10811088 }
10821089 mode := params.Mode
10831090 if mode == "" {
@@ -1179,7 +1186,7 @@ func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name do
11791186 if opts.auto != nil && !settings.AllowAutoMerge {
11801187 return domain.MergeRequest{}, fmt.Errorf("%w: auto-merge is off in %s", domain.ErrPreconditionFailed, name)
11811188 }
1182- if settings.FailingChecksBlock || opts.auto != nil {
1189+ if !params.IgnoreChecks || opts.auto != nil {
11831190 checks, err := m.readMergeChecks(ctx, name, head)
11841191 if err == nil {
11851192 err = checksPolicy(checks)
@@ -1362,7 +1369,7 @@ func checksPolicy(checks []domain.CICheck) error {
13621369 }
13631370 switch {
13641371 case len(failing) > 0:
1365- return fmt.Errorf("%w: failing checks block the merge: %s (turn off failing_checks_block to merge anyway)", domain.ErrPreconditionFailed, strings.Join(failing, ", "))
1372+ return fmt.Errorf("%w: failing checks block the merge: %s (merge with --ignore-checks, needs the mr:force-merge scope)", domain.ErrPreconditionFailed, strings.Join(failing, ", "))
13661373 case len(pending) > 0:
13671374 return fmt.Errorf("%w: checks have not finished: %s", domain.ErrPreconditionFailed, strings.Join(pending, ", "))
13681375 }
@@ -995,8 +995,9 @@ func (m *MergeRequests) MarkRead(ctx context.Context, p domain.Principal, name d
995 // allowed by the settings of the repository, the source branch no longer995 // allowed by the settings of the repository, the source branch no longer
996 // points to the head ("branch moved, read again") or is gone, the target is996 // points to the head ("branch moved, read again") or is gone, the target is
997 // gone, the mode's own condition does not hold (see below), or the branch997 // gone, the mode's own condition does not hold (see below), or the branch
998-// brings nothing. Checks come with CI (CI-1): there are none yet, so the998+// brings nothing. Checks on the head must pass: none may be failed, queued
999-// setting that lets failing checks block the merge has nothing to act on.999+// or running; a head with no checks may be merged. params.IgnoreChecks bypasses
1000+// this requirement and needs mr:force-merge.
1000 //1001 //
1001 // The conditions are always checked with Git, under the lock, never from the1002 // The conditions are always checked with Git, under the lock, never from the
1002 // stored mergeability. A fast-forward needs the target to be an ancestor of1003 // stored mergeability. A fast-forward needs the target to be an ancestor of
@@ -1075,9 +1076,15 @@ var errAutoMergeWait = errors.New("auto-merge is waiting for checks")
1075 1076
1076 func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams, opts mergeOpts) (domain.MergeRequest, error) {1077 func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params ports.MergeParams, opts mergeOpts) (domain.MergeRequest, error) {
1077 if opts.auto == nil {1078 if opts.auto == nil {
1078- if _, err := m.writable(ctx, p, name, domain.ScopeMRMerge); err != nil {1079+ repo, err := m.writable(ctx, p, name, domain.ScopeMRMerge)
1080+ if err != nil {
1079 return domain.MergeRequest{}, err1081 return domain.MergeRequest{}, err
1080 }1082 }
1083+ if params.IgnoreChecks {
1084+ if err := scopeAccess(p, repo, domain.ScopeMRForceMerge); err != nil {
1085+ return domain.MergeRequest{}, err
1086+ }
1087+ }
1081 }1088 }
1082 mode := params.Mode1089 mode := params.Mode
1083 if mode == "" {1090 if mode == "" {
@@ -1179,7 +1186,7 @@ func (m *MergeRequests) mergeAs(ctx context.Context, p domain.Principal, name do
1179 if opts.auto != nil && !settings.AllowAutoMerge {1186 if opts.auto != nil && !settings.AllowAutoMerge {
1180 return domain.MergeRequest{}, fmt.Errorf("%w: auto-merge is off in %s", domain.ErrPreconditionFailed, name)1187 return domain.MergeRequest{}, fmt.Errorf("%w: auto-merge is off in %s", domain.ErrPreconditionFailed, name)
1181 }1188 }
1182- if settings.FailingChecksBlock || opts.auto != nil {1189+ if !params.IgnoreChecks || opts.auto != nil {
1183 checks, err := m.readMergeChecks(ctx, name, head)1190 checks, err := m.readMergeChecks(ctx, name, head)
1184 if err == nil {1191 if err == nil {
1185 err = checksPolicy(checks)1192 err = checksPolicy(checks)
@@ -1362,7 +1369,7 @@ func checksPolicy(checks []domain.CICheck) error {
1362 }1369 }
1363 switch {1370 switch {
1364 case len(failing) > 0:1371 case len(failing) > 0:
1365- return fmt.Errorf("%w: failing checks block the merge: %s (turn off failing_checks_block to merge anyway)", domain.ErrPreconditionFailed, strings.Join(failing, ", "))1372+ return fmt.Errorf("%w: failing checks block the merge: %s (merge with --ignore-checks, needs the mr:force-merge scope)", domain.ErrPreconditionFailed, strings.Join(failing, ", "))
1366 case len(pending) > 0:1373 case len(pending) > 0:
1367 return fmt.Errorf("%w: checks have not finished: %s", domain.ErrPreconditionFailed, strings.Join(pending, ", "))1374 return fmt.Errorf("%w: checks have not finished: %s", domain.ErrPreconditionFailed, strings.Join(pending, ", "))
1368 }1375 }
M internal/usecase/mergerequests_integration_test.go +5 -5
M internal/usecase/repos_write.go +4 -7
M internal/usecase/repos_write_test.go +1 -3
M proto/ermite/storage/merge_request.proto +2 -1
M skills/ermite/CLI.md +10 -4
M skills/ermite/SKILL.md +5 -3

Comments2 open / 2

bnema outdated

CI fails on purpose for now: go.mod has a local replace for ermite-api (ermite-api!10). Once ermite-api!10 is merged and tagged v0.15.0, I replace it with the tag and CI should go green. ermite-api keeps failing_checks_block as a deprecated, ignored field (buf breaking).

bnema

go.mod now uses ermite-api v0.15.0 (no local replace). The storage field failing_checks_block is kept as deprecated (buf breaking). CI green (run 159).

Keyboard shortcuts

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

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

Branches 2

Tags 44

↑↓ moveenter openesc close