fix(web): give merge when ready its own mode choice and drop branch deletion from it
- commit
- b6e937407800bd6d1100a2aeac0d41cca931ce47
- author
- brice · · 2026-10-20 17:15 UTC
- parent
- 355f5f4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +394 -317
M docs/merge-requests.md +1 -1
| @@ -44,7 +44,7 @@ The merge box of the web page and the output of `mr merge --mode squash|merge` r | |||
| 44 | 44 | A merge request can merge by itself once its checks passed. | |
| 45 | 45 | ||
| 46 | 46 | - The repository setting `allow_auto_merge` is off by default: `ermite repo edit <repo> --allow-auto-merge` (`=false` to turn it off). | |
| 47 | - | - Enable it with `ermite mr merge <repo> <n> --head <commit> --auto --mode squash` (`--head` is required, as for a plain merge; a head that moved is refused with "branch moved"), or with **Merge when ready** in the web merge box. It needs `mr:merge` and a server with CI. Re-enabling replaces the previous mode and enabler. | |
| 47 | + | - Enable it with `ermite mr merge <repo> <n> --head <commit> --auto --mode squash` (`--head` is required, as for a plain merge; a head that moved is refused with "branch moved"), or with **Merge when ready** in the web merge box. Its separate mode choice offers every repository-allowed mode, even while the head conflicts or is behind. Auto-merge keeps the source branch; the delete-source checkbox applies only to an immediate merge. It needs `mr:merge` and a server with CI. Re-enabling replaces the previous mode and enabler. | |
| 48 | 48 | - The server merges when CI has settled on the head with every run complete and at least one check passed. A failing or unfinished check always blocks, whatever `failing_checks_block` says. The mode must still be allowed. | |
| 49 | 49 | - The merge is made as the person who enabled it. Their token is checked at every evaluation, even while checks are pending, and again at merge time under the merge lock: it must still be valid and allow `mr:merge` on the repository and the target branch. A token that no longer qualifies cancels the auto-merge. | |
| 50 | 50 | - A pending auto-merge shows in `mr show` (`auto_merge`), in the web merge box and as `merge_request_auto_merge_enabled` in `ermite watch`. A merge request merged this way has `auto_merged` true. | |
| @@ -44,7 +44,7 @@ The merge box of the web page and the output of `mr merge --mode squash|merge` r | |||||
| 44 | A merge request can merge by itself once its checks passed. | 44 | A merge request can merge by itself once its checks passed. | ||
| 45 | 45 | ||||
| 46 | - The repository setting `allow_auto_merge` is off by default: `ermite repo edit <repo> --allow-auto-merge` (`=false` to turn it off). | 46 | - The repository setting `allow_auto_merge` is off by default: `ermite repo edit <repo> --allow-auto-merge` (`=false` to turn it off). | ||
| 47 | - | - Enable it with `ermite mr merge <repo> <n> --head <commit> --auto --mode squash` (`--head` is required, as for a plain merge; a head that moved is refused with "branch moved"), or with **Merge when ready** in the web merge box. It needs `mr:merge` and a server with CI. Re-enabling replaces the previous mode and enabler. | 47 | + | - Enable it with `ermite mr merge <repo> <n> --head <commit> --auto --mode squash` (`--head` is required, as for a plain merge; a head that moved is refused with "branch moved"), or with **Merge when ready** in the web merge box. Its separate mode choice offers every repository-allowed mode, even while the head conflicts or is behind. Auto-merge keeps the source branch; the delete-source checkbox applies only to an immediate merge. It needs `mr:merge` and a server with CI. Re-enabling replaces the previous mode and enabler. |
| 48 | - The server merges when CI has settled on the head with every run complete and at least one check passed. A failing or unfinished check always blocks, whatever `failing_checks_block` says. The mode must still be allowed. | 48 | - The server merges when CI has settled on the head with every run complete and at least one check passed. A failing or unfinished check always blocks, whatever `failing_checks_block` says. The mode must still be allowed. | ||
| 49 | - The merge is made as the person who enabled it. Their token is checked at every evaluation, even while checks are pending, and again at merge time under the merge lock: it must still be valid and allow `mr:merge` on the repository and the target branch. A token that no longer qualifies cancels the auto-merge. | 49 | - The merge is made as the person who enabled it. Their token is checked at every evaluation, even while checks are pending, and again at merge time under the merge lock: it must still be valid and allow `mr:merge` on the repository and the target branch. A token that no longer qualifies cancels the auto-merge. | ||
| 50 | - A pending auto-merge shows in `mr show` (`auto_merge`), in the web merge box and as `merge_request_auto_merge_enabled` in `ermite watch`. A merge request merged this way has `auto_merged` true. | 50 | - A pending auto-merge shows in `mr show` (`auto_merge`), in the web merge box and as `merge_request_auto_merge_enabled` in `ermite watch`. A merge request merged this way has `auto_merged` true. | ||
M internal/adapters/web/mr.templ +14 -9
| @@ -558,17 +558,22 @@ templ mergeMenu(p MRPage) { | |||
| 558 | 558 | :disabled="!this.form.querySelector('input[name=mode]:checked:not(:disabled)')" | |
| 559 | 559 | :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''" | |
| 560 | 560 | >Merge into { p.Target }</button> | |
| 561 | - | if p.Box.CanAutoMerge { | |
| 562 | - | <button | |
| 563 | - | type="submit" | |
| 564 | - | class="btn" | |
| 565 | - | formaction={ templ.SafeURL(p.Box.AutoAction) } | |
| 566 | - | disabled?={ len(p.Box.Modes) == 0 } | |
| 567 | - | title="merge once the checks pass" | |
| 568 | - | >Merge when ready</button> | |
| 569 | - | } | |
| 570 | 561 | </div> | |
| 571 | 562 | </form> | |
| 563 | + | if p.Box.CanAutoMerge { | |
| 564 | + | <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.AutoAction) }> | |
| 565 | + | <input type="hidden" name="head_id" value={ p.Box.Head }/> | |
| 566 | + | <fieldset class="d-modes"> | |
| 567 | + | <legend>Auto-merge mode</legend> | |
| 568 | + | for i, m := range p.Box.Modes { | |
| 569 | + | <label><input type="radio" name="mode" value={ m.Value } checked?={ i == 0 }/> { m.Label }</label> | |
| 570 | + | } | |
| 571 | + | </fieldset> | |
| 572 | + | <div class="d-btns"> | |
| 573 | + | <button type="submit" class="btn" disabled?={ len(p.Box.Modes) == 0 } title="merge once the checks pass">Merge when ready</button> | |
| 574 | + | </div> | |
| 575 | + | </form> | |
| 576 | + | } | |
| 572 | 577 | } | |
| 573 | 578 | </div> | |
| 574 | 579 | </details> | |
| @@ -558,17 +558,22 @@ templ mergeMenu(p MRPage) { | |||||
| 558 | :disabled="!this.form.querySelector('input[name=mode]:checked:not(:disabled)')" | 558 | :disabled="!this.form.querySelector('input[name=mode]:checked:not(:disabled)')" | ||
| 559 | :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''" | 559 | :title="(this.form.querySelector('input[name=mode]:checked') || {}).title || ''" | ||
| 560 | >Merge into { p.Target }</button> | 560 | >Merge into { p.Target }</button> | ||
| 561 | - | if p.Box.CanAutoMerge { | |||
| 562 | - | <button | |||
| 563 | - | type="submit" | |||
| 564 | - | class="btn" | |||
| 565 | - | formaction={ templ.SafeURL(p.Box.AutoAction) } | |||
| 566 | - | disabled?={ len(p.Box.Modes) == 0 } | |||
| 567 | - | title="merge once the checks pass" | |||
| 568 | - | >Merge when ready</button> | |||
| 569 | - | } | |||
| 570 | </div> | 561 | </div> | ||
| 571 | </form> | 562 | </form> | ||
| 563 | + | if p.Box.CanAutoMerge { | |||
| 564 | + | <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.AutoAction) }> | |||
| 565 | + | <input type="hidden" name="head_id" value={ p.Box.Head }/> | |||
| 566 | + | <fieldset class="d-modes"> | |||
| 567 | + | <legend>Auto-merge mode</legend> | |||
| 568 | + | for i, m := range p.Box.Modes { | |||
| 569 | + | <label><input type="radio" name="mode" value={ m.Value } checked?={ i == 0 }/> { m.Label }</label> | |||
| 570 | + | } | |||
| 571 | + | </fieldset> | |||
| 572 | + | <div class="d-btns"> | |||
| 573 | + | <button type="submit" class="btn" disabled?={ len(p.Box.Modes) == 0 } title="merge once the checks pass">Merge when ready</button> | |||
| 574 | + | </div> | |||
| 575 | + | </form> | |||
| 576 | + | } | |||
| 572 | } | 577 | } | ||
| 573 | </div> | 578 | </div> | ||
| 574 | </details> | 579 | </details> | ||
M internal/adapters/web/mr_templ.go +361 -306 generated file: collapsed
M internal/adapters/web/mr_test.go +18 -1
| @@ -215,8 +215,25 @@ func TestMergeBoxAutoMerge(t *testing.T) { | |||
| 215 | 215 | page := MRPage{Target: "main", Box: on} | |
| 216 | 216 | html := renderToString(t, mergeMenu(page)) | |
| 217 | 217 | assert.Contains(t, html, "Merge when ready") | |
| 218 | - | assert.Contains(t, html, `formaction="`+on.AutoAction+`"`) | |
| 218 | + | assert.Contains(t, html, `action="`+on.AutoAction+`"`) | |
| 219 | 219 | assert.NotContains(t, html, "Cancel auto-merge") | |
| 220 | + | // Current mergeability must only gate the immediate form. Every allowed | |
| 221 | + | // mode remains selectable in the separate auto-merge form. | |
| 222 | + | for i := range on.Modes { | |
| 223 | + | on.Modes[i].Disabled = true | |
| 224 | + | } | |
| 225 | + | on.CanDeleteBranch = true | |
| 226 | + | page.Source = "feat" | |
| 227 | + | html = renderToString(t, mergeMenu(page)) | |
| 228 | + | _, autoForm, found := strings.Cut(html, `action="`+on.AutoAction+`"`) | |
| 229 | + | require.True(t, found) | |
| 230 | + | autoForm, _, _ = strings.Cut(autoForm, "</form>") | |
| 231 | + | assert.NotContains(t, autoForm, "delete_branch") | |
| 232 | + | assert.NotContains(t, autoForm, "disabled") | |
| 233 | + | assert.Contains(t, autoForm, `name="head_id"`) | |
| 234 | + | for _, mode := range on.Modes { | |
| 235 | + | assert.Contains(t, autoForm, `value="`+mode.Value+`"`) | |
| 236 | + | } | |
| 220 | 237 | page.Box = mk(false, mocks.NewRunService(t)) | |
| 221 | 238 | assert.NotContains(t, renderToString(t, mergeMenu(page)), "Merge when ready") | |
| 222 | 239 | ||
| @@ -215,8 +215,25 @@ func TestMergeBoxAutoMerge(t *testing.T) { | |||||
| 215 | page := MRPage{Target: "main", Box: on} | 215 | page := MRPage{Target: "main", Box: on} | ||
| 216 | html := renderToString(t, mergeMenu(page)) | 216 | html := renderToString(t, mergeMenu(page)) | ||
| 217 | assert.Contains(t, html, "Merge when ready") | 217 | assert.Contains(t, html, "Merge when ready") | ||
| 218 | - | assert.Contains(t, html, `formaction="`+on.AutoAction+`"`) | 218 | + | assert.Contains(t, html, `action="`+on.AutoAction+`"`) |
| 219 | assert.NotContains(t, html, "Cancel auto-merge") | 219 | assert.NotContains(t, html, "Cancel auto-merge") | ||
| 220 | + | // Current mergeability must only gate the immediate form. Every allowed | |||
| 221 | + | // mode remains selectable in the separate auto-merge form. | |||
| 222 | + | for i := range on.Modes { | |||
| 223 | + | on.Modes[i].Disabled = true | |||
| 224 | + | } | |||
| 225 | + | on.CanDeleteBranch = true | |||
| 226 | + | page.Source = "feat" | |||
| 227 | + | html = renderToString(t, mergeMenu(page)) | |||
| 228 | + | _, autoForm, found := strings.Cut(html, `action="`+on.AutoAction+`"`) | |||
| 229 | + | require.True(t, found) | |||
| 230 | + | autoForm, _, _ = strings.Cut(autoForm, "</form>") | |||
| 231 | + | assert.NotContains(t, autoForm, "delete_branch") | |||
| 232 | + | assert.NotContains(t, autoForm, "disabled") | |||
| 233 | + | assert.Contains(t, autoForm, `name="head_id"`) | |||
| 234 | + | for _, mode := range on.Modes { | |||
| 235 | + | assert.Contains(t, autoForm, `value="`+mode.Value+`"`) | |||
| 236 | + | } | |||
| 220 | page.Box = mk(false, mocks.NewRunService(t)) | 237 | page.Box = mk(false, mocks.NewRunService(t)) | ||
| 221 | assert.NotContains(t, renderToString(t, mergeMenu(page)), "Merge when ready") | 238 | assert.NotContains(t, renderToString(t, mergeMenu(page)), "Merge when ready") | ||
| 222 | 239 | ||||