Commit dc10160221

dc10160221836dabdd5ef8febe0b4f7f13051041

parent: b1f4bf1aaf

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 06:51 UTC

repo settings: required contexts turn the checks gate on, pending until reported

Ref #258

Layout: unified · split

cmd/gitbay/main.go +1
@@ -618,6 +618,7 @@ func repoCmd() *cobra.Command {
618 pass("require-resolved", passOpts{server: []string{"repo", "settings", "require-resolved"}, needsRepo: true}), 618 pass("require-resolved", passOpts{server: []string{"repo", "settings", "require-resolved"}, needsRepo: true}),
619 pass("require-codeowners", passOpts{server: []string{"repo", "settings", "require-codeowners"}, needsRepo: true}), 619 pass("require-codeowners", passOpts{server: []string{"repo", "settings", "require-codeowners"}, needsRepo: true}),
620 pass("require-checks", passOpts{server: []string{"repo", "settings", "require-checks"}, needsRepo: true}), 620 pass("require-checks", passOpts{server: []string{"repo", "settings", "require-checks"}, needsRepo: true}),
621 pass("require-contexts", passOpts{server: []string{"repo", "settings", "require-contexts"}, needsRepo: true}),
621 pass("visibility", passOpts{server: []string{"repo", "settings", "visibility"}, needsRepo: true}), 622 pass("visibility", passOpts{server: []string{"repo", "settings", "visibility"}, needsRepo: true}),
622 pass("require-signed", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}), 623 pass("require-signed", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}),
623 pass("require-mr", passOpts{server: []string{"repo", "settings", "require-mr"}, needsRepo: true}), 624 pass("require-mr", passOpts{server: []string{"repo", "settings", "require-mr"}, needsRepo: true}),
cmd/gitbay/summaries_gen.go +1
@@ -187,6 +187,7 @@ var summaries = map[string]string{
187 "repo settings require-approvals": "require N fresh approvals to merge", 187 "repo settings require-approvals": "require N fresh approvals to merge",
188 "repo settings require-checks": "gate merges on green statuses", 188 "repo settings require-checks": "gate merges on green statuses",
189 "repo settings require-codeowners": "require an owner's approval for every file CODEOWNERS covers", 189 "repo settings require-codeowners": "require an owner's approval for every file CODEOWNERS covers",
190 "repo settings require-contexts": "name the statuses the checks gate waits for, and turn the gate on",
190 "repo settings require-mr": "protected branches take changes through merge requests only", 191 "repo settings require-mr": "protected branches take changes through merge requests only",
191 "repo settings require-resolved": "require all review threads resolved to merge", 192 "repo settings require-resolved": "require all review threads resolved to merge",
192 "repo settings require-signed": "require verified commit signatures", 193 "repo settings require-signed": "require verified commit signatures",
e2e/settingsweb_test.go +7 −2
@@ -55,7 +55,12 @@ func TestRepoSettingsWeb(t *testing.T) {
55 55
56 post(url.Values{"field": {"description"}, "description": {"a fine tool"}}) 56 post(url.Values{"field": {"description"}, "description": {"a fine tool"}})
57 post(url.Values{"field": {"website"}, "website": {"https://tool.example"}}) 57 post(url.Values{"field": {"website"}, "website": {"https://tool.example"}})
58 post(url.Values{"field": {"require-checks"}, "require-checks": {"on"}}) 58 // Saving required contexts turns the checks gate on, and the page
59 // shows it ticked with the contexts in its hint (#258).
60 if body := post(url.Values{"field": {"require-contexts"}, "contexts": {"ext/deploy lint"}}); !strings.Contains(body, `id="require-checks" name="require-checks" value="on" checked`) ||
61 !strings.Contains(body, `Also waits for <code>ext/deploy</code>, <code>lint</code> until they report.`) {
62 t.Fatalf("required contexts did not show as turning required checks on:\n%s", body)
63 }
59 post(url.Values{"field": {"require-approvals"}, "approvals": {"2"}}) 64 post(url.Values{"field": {"require-approvals"}, "approvals": {"2"}})
60 post(url.Values{"field": {"protect"}, "branch": {"main"}}) 65 post(url.Values{"field": {"protect"}, "branch": {"main"}})
61 66
@@ -66,7 +71,7 @@ func TestRepoSettingsWeb(t *testing.T) {
66 } 71 }
67 } 72 }
68 out, _, _ = inst.ssh(t, aliceKey, "", "repo", "settings", "show", "alice/app", "--json") 73 out, _, _ = inst.ssh(t, aliceKey, "", "repo", "settings", "show", "alice/app", "--json")
69 for _, want := range []string{`"require_checks":true`, `"require_approvals":2`, `"main"`} { 74 for _, want := range []string{`"require_checks":true`, `"required_contexts":["ext/deploy","lint"]`, `"require_approvals":2`, `"main"`} {
70 if !strings.Contains(out, want) { 75 if !strings.Contains(out, want) {
71 t.Fatalf("settings show missing %q:\n%s", want, out) 76 t.Fatalf("settings show missing %q:\n%s", want, out)
72 } 77 }
internal/control/checksgate_test.go +55 −7
@@ -3,6 +3,7 @@ package control
3import ( 3import (
4 "os" 4 "os"
5 "path/filepath" 5 "path/filepath"
6 "slices"
6 "strings" 7 "strings"
7 "testing" 8 "testing"
8 9
@@ -18,9 +19,30 @@ func gatesForHead(t *testing.T, ciYML string) GatesOut {
18} 19}
19 20
20func gatesForHeadSeeded(t *testing.T, ciYML string, seed bool) GatesOut { 21func gatesForHeadSeeded(t *testing.T, ciYML string, seed bool) GatesOut {
22 return gatesFor(t, ciYML, nil, func(st *store.Store, repoID, uid int64, targetSHA, _ string) {
23 if !seed {
24 return
25 }
26 if err := st.SetCommitStatus(repoID, targetSHA, "lint", "success", "", "", uid); err != nil {
27 t.Fatal(err)
28 }
29 })
30}
31
32// gatesFor builds a repository with require_checks on and set applied to
33// its settings, a bare dir holding the given .gitbay/ci.yml (empty
34// string for none), and one MR; seed records statuses before the gates
35// are computed.
36func gatesFor(t *testing.T, ciYML string, set func(*store.RepoSettings),
37 seed func(st *store.Store, repoID, uid int64, targetSHA, headSHA string)) GatesOut {
21 t.Helper() 38 t.Helper()
22 st, repo, uid := newQueueTestRepo(t) 39 st, repo, uid := newQueueTestRepo(t)
23 if _, err := st.UpdateRepoSettings(repo.ID, func(set *store.RepoSettings) { set.RequireChecks = true }); err != nil { 40 if _, err := st.UpdateRepoSettings(repo.ID, func(s *store.RepoSettings) {
41 s.RequireChecks = true
42 if set != nil {
43 set(s)
44 }
45 }); err != nil {
24 t.Fatal(err) 46 t.Fatal(err)
25 } 47 }
26 repo, err := st.RepoByID(repo.ID) 48 repo, err := st.RepoByID(repo.ID)
@@ -58,13 +80,9 @@ func gatesForHeadSeeded(t *testing.T, ciYML string, seed bool) GatesOut {
58 if err != nil { 80 if err != nil {
59 t.Fatal(err) 81 t.Fatal(err)
60 } 82 }
61 83 if seed != nil {
62 if seed { 84 seed(st, repo.ID, uid, targetSHA, headSHA)
63 if err := st.SetCommitStatus(repo.ID, targetSHA, "lint", "success", "", "", uid); err != nil {
64 t.Fatal(err)
65 }
66 } 85 }
67
68 g, err := MergeGates(st, repo, mr, dir, targetSHA, headSHA) 86 g, err := MergeGates(st, repo, mr, dir, targetSHA, headSHA)
69 if err != nil { 87 if err != nil {
70 t.Fatal(err) 88 t.Fatal(err)
@@ -118,3 +136,33 @@ func TestRequireChecksRefusesSilentHeadInReportingRepo(t *testing.T) {
118 t.Fatalf("allowed a silent head in a repository that reports statuses: %v", g.Unmet) 136 t.Fatalf("allowed a silent head in a repository that reports statuses: %v", g.Unmet)
119 } 137 }
120} 138}
139
140// A required context that has not reported holds the merge as pending,
141// even when every status that did report is green (#258).
142func TestRequiredContextMissingIsPending(t *testing.T) {
143 g := gatesFor(t, "", func(s *store.RepoSettings) { s.RequiredContexts = []string{"ext/deploy", "lint"} },
144 func(st *store.Store, repoID, uid int64, _, headSHA string) {
145 if err := st.SetCommitStatus(repoID, headSHA, "lint", "success", "", "", uid); err != nil {
146 t.Fatal(err)
147 }
148 })
149 if g.Checks != "pending" || !slices.Equal(g.ChecksMissing, []string{"ext/deploy"}) {
150 t.Fatalf("checks %q, missing %v", g.Checks, g.ChecksMissing)
151 }
152 if !checksUnmet(g) || !strings.Contains(strings.Join(g.Unmet, "\n"), "ext/deploy=missing") {
153 t.Fatalf("unmet: %v", g.Unmet)
154 }
155}
156
157// Every required context reported green: nothing is held.
158func TestRequiredContextsReportedPass(t *testing.T) {
159 g := gatesFor(t, "", func(s *store.RepoSettings) { s.RequiredContexts = []string{"lint"} },
160 func(st *store.Store, repoID, uid int64, _, headSHA string) {
161 if err := st.SetCommitStatus(repoID, headSHA, "lint", "success", "", "", uid); err != nil {
162 t.Fatal(err)
163 }
164 })
165 if checksUnmet(g) || len(g.ChecksMissing) != 0 || g.Checks != "success" {
166 t.Fatalf("checks %q, missing %v, unmet %v", g.Checks, g.ChecksMissing, g.Unmet)
167 }
168}
internal/control/mr.go +72 −1
@@ -47,6 +47,11 @@ func init() {
47 Usage: "repo settings require-checks <owner/name> on|off", 47 Usage: "repo settings require-checks <owner/name> on|off",
48 Examples: []string{"repo settings require-checks krz/gitbay on"}, 48 Examples: []string{"repo settings require-checks krz/gitbay on"},
49 Run: runRequireChecks}) 49 Run: runRequireChecks})
50 register(Command{Path: []string{"repo", "settings", "require-contexts"},
51 Summary: "name the statuses the checks gate waits for, and turn the gate on",
52 Usage: "repo settings require-contexts <owner/name> [<context>...] (none clears the list)",
53 Examples: []string{"repo settings require-contexts krz/gitbay ci/build ci/test"},
54 Run: runRequireContexts})
50 register(Command{Path: []string{"repo", "settings", "require-mr"}, 55 register(Command{Path: []string{"repo", "settings", "require-mr"},
51 Summary: "protected branches take changes through merge requests only", 56 Summary: "protected branches take changes through merge requests only",
52 Usage: "repo settings require-mr <owner/name> on|off", 57 Usage: "repo settings require-mr <owner/name> on|off",
@@ -340,6 +345,54 @@ func runRequireChecks(c *Ctx, args []string) int {
340 }) 345 })
341} 346}
342 347
348// maxRequiredContexts bounds the list: a gate naming more checks than
349// this is a configuration mistake.
350const maxRequiredContexts = 20
351
352func runRequireContexts(c *Ctx, args []string) int {
353 if len(args) < 1 {
354 return c.usage()
355 }
356 var contexts []string
357 for _, ctx := range args[1:] {
358 if ctx == "" || len(ctx) > 100 || strings.ContainsAny(ctx, " \t\r\n") {
359 return c.fail(protocol.ExitUsage, "a context is 1 to 100 characters with no whitespace: %q", ctx)
360 }
361 if !slices.Contains(contexts, ctx) {
362 contexts = append(contexts, ctx)
363 }
364 }
365 if len(contexts) > maxRequiredContexts {
366 return c.fail(protocol.ExitUsage, "at most %d required contexts", maxRequiredContexts)
367 }
368 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
369 if code >= 0 {
370 return code
371 }
372 // Naming contexts asks for the gate, so it turns require_checks on in
373 // the same update. Clearing the list leaves the gate as it was.
374 s, err := c.Store.UpdateRepoSettings(repo.ID, func(s *store.RepoSettings) {
375 s.RequiredContexts = contexts
376 if len(contexts) > 0 {
377 s.RequireChecks = true
378 }
379 })
380 if err != nil {
381 return c.fail(protocol.ExitFailure, "%v", err)
382 }
383 return c.emit(s, func(w io.Writer) {
384 if len(contexts) > 0 {
385 fmt.Fprintf(w, "required contexts on %s: %s; require_checks on\n", repo.Path(), strings.Join(contexts, ", "))
386 return
387 }
388 gate := "off"
389 if s.RequireChecks {
390 gate = "on"
391 }
392 fmt.Fprintf(w, "required contexts cleared on %s; require_checks %s\n", repo.Path(), gate)
393 })
394}
395
343func runRequireMR(c *Ctx, args []string) int { 396func runRequireMR(c *Ctx, args []string) int {
344 if len(args) != 2 || (args[1] != "on" && args[1] != "off") { 397 if len(args) != 2 || (args[1] != "on" && args[1] != "off") {
345 return c.usage() 398 return c.usage()
@@ -1577,13 +1630,28 @@ func MergeGates(st *store.Store, repo store.Repo, mr store.MR, dir, targetSHA, h
1577 } 1630 }
1578 1631
1579 // Checks: with require_checks, every status the head carries must be 1632 // Checks: with require_checks, every status the head carries must be
1580 // green, and a head something was going to report on must carry some. 1633 // green, a head something was going to report on must carry some, and
1634 // every required context must have reported: one that has not is
1635 // pending whatever the others say (#258). Setting contexts turns
1636 // require_checks on; turned off again, the list is kept and unread.
1581 statuses, err := st.ListCommitStatuses(repo.ID, headSHA) 1637 statuses, err := st.ListCommitStatuses(repo.ID, headSHA)
1582 if err != nil { 1638 if err != nil {
1583 return g, err 1639 return g, err
1584 } 1640 }
1585 g.Checks = store.CombinedStatus(statuses) 1641 g.Checks = store.CombinedStatus(statuses)
1586 if set.RequireChecks { 1642 if set.RequireChecks {
1643 reported := map[string]bool{}
1644 for _, s := range statuses {
1645 reported[s.Context] = true
1646 }
1647 for _, want := range set.RequiredContexts {
1648 if !reported[want] {
1649 g.ChecksMissing = append(g.ChecksMissing, want)
1650 }
1651 }
1652 if len(g.ChecksMissing) > 0 && (g.Checks == "" || g.Checks == "success") {
1653 g.Checks = "pending"
1654 }
1587 switch g.Checks { 1655 switch g.Checks {
1588 case "success": 1656 case "success":
1589 case "": 1657 case "":
@@ -1597,6 +1665,9 @@ func MergeGates(st *store.Store, repo store.Repo, mr store.MR, dir, targetSHA, h
1597 bad = append(bad, st.Context+"="+st.State) 1665 bad = append(bad, st.Context+"="+st.State)
1598 } 1666 }
1599 } 1667 }
1668 for _, m := range g.ChecksMissing {
1669 bad = append(bad, m+"=missing")
1670 }
1600 g.Unmet = append(g.Unmet, fmt.Sprintf("%s requires green checks; %.10s has %s", repo.Path(), headSHA, strings.Join(bad, ", "))) 1671 g.Unmet = append(g.Unmet, fmt.Sprintf("%s requires green checks; %.10s has %s", repo.Path(), headSHA, strings.Join(bad, ", ")))
1601 } 1672 }
1602 } 1673 }
internal/control/mr_test.go +75
@@ -5,6 +5,7 @@ import (
5 "encoding/json" 5 "encoding/json"
6 "os" 6 "os"
7 "path/filepath" 7 "path/filepath"
8 "slices"
8 "strconv" 9 "strconv"
9 "strings" 10 "strings"
10 "testing" 11 "testing"
@@ -262,3 +263,77 @@ func TestMRShowPluralizesMultiRowSections(t *testing.T) {
262 } 263 }
263 } 264 }
264} 265}
266
267// require-contexts stores a deduplicated list and turns require_checks
268// on with it; an empty list clears the contexts and leaves
269// require_checks as it was. A context with whitespace is refused (#258).
270func TestRequireContextsSetsAndClears(t *testing.T) {
271 st, repo, uid := newQueueTestRepo(t)
272 alice := store.User{ID: uid, Username: "alice"}
273 dispatch := func(args ...string) int {
274 t.Helper()
275 c, _, _ := mrTestCtx(st, alice)
276 return Dispatch(c, args)
277 }
278 contexts := func(names ...string) int {
279 t.Helper()
280 return dispatch(append([]string{"repo", "settings", "require-contexts", repo.Path()}, names...)...)
281 }
282 settings := func() store.RepoSettings {
283 t.Helper()
284 got, err := st.RepoByID(repo.ID)
285 if err != nil {
286 t.Fatal(err)
287 }
288 return got.Settings
289 }
290
291 if settings().RequireChecks {
292 t.Fatal("require_checks on in a new repository")
293 }
294 if code := contexts("lint", "ext/deploy", "lint"); code != protocol.ExitOK {
295 t.Fatalf("set: exit %d", code)
296 }
297 if s := settings(); !slices.Equal(s.RequiredContexts, []string{"lint", "ext/deploy"}) || !s.RequireChecks {
298 t.Fatalf("stored %v, require_checks %v; want [lint ext/deploy], on", s.RequiredContexts, s.RequireChecks)
299 }
300 if code := contexts("bad context"); code != protocol.ExitUsage {
301 t.Fatalf("a context with a space: exit %d", code)
302 }
303 if code := contexts(); code != protocol.ExitOK {
304 t.Fatalf("clear: exit %d", code)
305 }
306 if s := settings(); len(s.RequiredContexts) != 0 || !s.RequireChecks {
307 t.Fatalf("after clearing: contexts %v, require_checks %v; want none, still on", s.RequiredContexts, s.RequireChecks)
308 }
309 if code := dispatch("repo", "settings", "require-checks", repo.Path(), "off"); code != protocol.ExitOK {
310 t.Fatalf("require-checks off: exit %d", code)
311 }
312 if code := contexts(); code != protocol.ExitOK {
313 t.Fatalf("clear again: exit %d", code)
314 }
315 if settings().RequireChecks {
316 t.Fatal("clearing the list turned require_checks on")
317 }
318}
319
320// settings show prints the checks gate beside the contexts it waits for,
321// so a list that turned the gate on is visible where the gate is (#258).
322func TestSettingsShowRequiredContexts(t *testing.T) {
323 st, repo, uid := newQueueTestRepo(t)
324 alice := store.User{ID: uid, Username: "alice"}
325 c, _, _ := mrTestCtx(st, alice)
326 if code := Dispatch(c, []string{"repo", "settings", "require-contexts", repo.Path(), "ext/deploy", "lint"}); code != protocol.ExitOK {
327 t.Fatalf("require-contexts: exit %d", code)
328 }
329 c, out, _ := mrTestCtx(st, alice)
330 if code := Dispatch(c, []string{"repo", "settings", "show", repo.Path()}); code != protocol.ExitOK {
331 t.Fatalf("settings show: exit %d", code)
332 }
333 got := strings.Join(strings.Fields(out.String()), " ")
334 for _, want := range []string{"require checks true", "required contexts ext/deploy, lint"} {
335 if !strings.Contains(got, want) {
336 t.Errorf("settings show lacks %q:\n%s", want, out.String())
337 }
338 }
339}
internal/control/output.go +2 −1
@@ -57,7 +57,8 @@ type GatesOut struct {
57 ResolvedRequired bool `json:"resolved_required"` 57 ResolvedRequired bool `json:"resolved_required"`
58 OpenThreads int `json:"open_threads"` 58 OpenThreads int `json:"open_threads"`
59 ChecksRequired bool `json:"checks_required"` 59 ChecksRequired bool `json:"checks_required"`
60 Checks string `json:"checks,omitempty"` // combined status; "" when none reported 60 Checks string `json:"checks,omitempty"` // combined status; "" when none reported
61 ChecksMissing []string `json:"checks_missing,omitempty"` // required contexts not reported
61 FastForward bool `json:"fast_forward"` 62 FastForward bool `json:"fast_forward"`
62 Unmet []string `json:"unmet,omitempty"` 63 Unmet []string `json:"unmet,omitempty"`
63} 64}
internal/control/repo.go +2
@@ -711,6 +711,8 @@ func runSettingsShow(c *Ctx, args []string) int {
711 "protected branches", strings.Join(repo.Settings.ProtectedBranches, ", "), 711 "protected branches", strings.Join(repo.Settings.ProtectedBranches, ", "),
712 "protected tags", strings.Join(repo.Settings.ProtectedTags, ", "), 712 "protected tags", strings.Join(repo.Settings.ProtectedTags, ", "),
713 "require mr", strconv.FormatBool(repo.Settings.RequireMR), 713 "require mr", strconv.FormatBool(repo.Settings.RequireMR),
714 "require checks", strconv.FormatBool(repo.Settings.RequireChecks),
715 "required contexts", strings.Join(repo.Settings.RequiredContexts, ", "),
714 "require signed commits", strconv.FormatBool(repo.Settings.RequireSignedCommits), 716 "require signed commits", strconv.FormatBool(repo.Settings.RequireSignedCommits),
715 "git daemon", strconv.FormatBool(repo.Settings.GitDaemon), 717 "git daemon", strconv.FormatBool(repo.Settings.GitDaemon),
716 "archived", strconv.FormatBool(repo.Settings.Archived), 718 "archived", strconv.FormatBool(repo.Settings.Archived),
internal/httpd/mrpage_test.go +3
@@ -195,6 +195,9 @@ func TestMRGatesRender(t *testing.T) {
195 if out := render(&control.GatesOut{FastForward: true}); !strings.Contains(out, "All gates met") || !strings.Contains(out, "fast-forward possible") { 195 if out := render(&control.GatesOut{FastForward: true}); !strings.Contains(out, "All gates met") || !strings.Contains(out, "fast-forward possible") {
196 t.Errorf("met gates not rendered:\n%s", out) 196 t.Errorf("met gates not rendered:\n%s", out)
197 } 197 }
198 if out := render(&control.GatesOut{Checks: "pending", ChecksMissing: []string{"ext/deploy"}}); !strings.Contains(out, "waiting on <code>ext/deploy</code>") {
199 t.Errorf("missing required context not rendered:\n%s", out)
200 }
198 if out := render(nil); strings.Contains(out, "Merge gates") { 201 if out := render(nil); strings.Contains(out, "Merge gates") {
199 t.Errorf("gates block on a merge request without gates:\n%s", out) 202 t.Errorf("gates block on a merge request without gates:\n%s", out)
200 } 203 }
internal/httpd/settings.go +4
@@ -105,6 +105,8 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request, u store.
105 argv = []string{"repo", "settings", "git-daemon", repo, onOff(v("git-daemon"))} 105 argv = []string{"repo", "settings", "git-daemon", repo, onOff(v("git-daemon"))}
106 case "require-checks": 106 case "require-checks":
107 argv = []string{"repo", "settings", "require-checks", repo, onOff(v("require-checks"))} 107 argv = []string{"repo", "settings", "require-checks", repo, onOff(v("require-checks"))}
108 case "require-contexts":
109 argv = append([]string{"repo", "settings", "require-contexts", repo}, strings.Fields(v("contexts"))...)
108 case "require-resolved": 110 case "require-resolved":
109 argv = []string{"repo", "settings", "require-resolved", repo, onOff(v("require-resolved"))} 111 argv = []string{"repo", "settings", "require-resolved", repo, onOff(v("require-resolved"))}
110 case "require-codeowners": 112 case "require-codeowners":
@@ -227,6 +229,8 @@ func fieldLabel(field string) string {
227 return "git:// serving" 229 return "git:// serving"
228 case "require-checks": 230 case "require-checks":
229 return "required checks" 231 return "required checks"
232 case "require-contexts":
233 return "required contexts"
230 case "require-approvals": 234 case "require-approvals":
231 return "approvals" 235 return "approvals"
232 case "require-resolved": 236 case "require-resolved":
internal/store/repos.go +11 −7
@@ -27,13 +27,17 @@ type RepoSettings struct {
27 ProtectedTags []string `json:"protected_tags,omitempty"` // path.Match globs 27 ProtectedTags []string `json:"protected_tags,omitempty"` // path.Match globs
28 RequireSignedCommits bool `json:"require_signed_commits,omitempty"` 28 RequireSignedCommits bool `json:"require_signed_commits,omitempty"`
29 RequireChecks bool `json:"require_checks,omitempty"` 29 RequireChecks bool `json:"require_checks,omitempty"`
30 RequireApprovals int `json:"require_approvals,omitempty"` 30 // RequiredContexts are statuses require_checks waits for whether or
31 RequireResolved bool `json:"require_resolved,omitempty"` 31 // not they have reported; one that has not is pending. Setting a
32 RequireCodeowners bool `json:"require_codeowners,omitempty"` 32 // non-empty list turns RequireChecks on (#258).
33 RequireMR bool `json:"require_mr,omitempty"` 33 RequiredContexts []string `json:"required_contexts,omitempty"`
34 GitDaemon bool `json:"git_daemon,omitempty"` 34 RequireApprovals int `json:"require_approvals,omitempty"`
35 Archived bool `json:"archived,omitempty"` 35 RequireResolved bool `json:"require_resolved,omitempty"`
36 Website string `json:"website,omitempty"` 36 RequireCodeowners bool `json:"require_codeowners,omitempty"`
37 RequireMR bool `json:"require_mr,omitempty"`
38 GitDaemon bool `json:"git_daemon,omitempty"`
39 Archived bool `json:"archived,omitempty"`
40 Website string `json:"website,omitempty"`
37} 41}
38 42
39// Path returns the canonical owner/name form. 43// Path returns the canonical owner/name form.
internal/web/templates/mr.html +1
@@ -122,6 +122,7 @@
122 {{else}}<p class="row"><span class="dot ok"></span>All gates met</p>{{end}} 122 {{else}}<p class="row"><span class="dot ok"></span>All gates met</p>{{end}}
123 {{if .ApprovalsRequired}}<p class="row none">approvals: {{len .Approvals}} of {{.ApprovalsRequired}}{{if .Approvals}} ({{range $i, $a := .Approvals}}{{if $i}}, {{end}}{{$a}}{{end}}){{end}}</p>{{end}} 123 {{if .ApprovalsRequired}}<p class="row none">approvals: {{len .Approvals}} of {{.ApprovalsRequired}}{{if .Approvals}} ({{range $i, $a := .Approvals}}{{if $i}}, {{end}}{{$a}}{{end}}){{end}}</p>{{end}}
124 {{range .OwnersOutstanding}}<p class="row none">waiting on {{range $i, $o := .Owners}}{{if $i}} or {{end}}<a href="/{{$o}}">{{$o}}</a>{{end}} for {{range $i, $f := .Files}}{{if $i}}, {{end}}<code>{{$f}}</code>{{end}}</p>{{end}} 124 {{range .OwnersOutstanding}}<p class="row none">waiting on {{range $i, $o := .Owners}}{{if $i}} or {{end}}<a href="/{{$o}}">{{$o}}</a>{{end}} for {{range $i, $f := .Files}}{{if $i}}, {{end}}<code>{{$f}}</code>{{end}}</p>{{end}}
125 {{range .ChecksMissing}}<p class="row none">waiting on <code>{{.}}</code>, not yet reported</p>{{end}}
125 <p class="row none">{{if .FastForward}}fast-forward possible{{else}}not a fast-forward: rebase, or merge with a merge commit{{end}}</p> 126 <p class="row none">{{if .FastForward}}fast-forward possible{{else}}not a fast-forward: rebase, or merge with a merge commit{{end}}</p>
126 </div>{{end}} 127 </div>{{end}}
127 <div class="grp"> 128 <div class="grp">
internal/web/templates/settings.html +7 −1
@@ -72,10 +72,16 @@
72<p class="meta">Checked before a merge, in this order: checks, approvals, resolved threads, signatures.</p> 72<p class="meta">Checked before a merge, in this order: checks, approvals, resolved threads, signatures.</p>
73<form method="post" action="{{$base}}" class="setform"> 73<form method="post" action="{{$base}}" class="setform">
74 <input type="hidden" name="field" value="require-checks"> 74 <input type="hidden" name="field" value="require-checks">
75 <div><label for="require-checks">Required checks</label><p class="hint">Requires CI to succeed.</p></div> 75 <div><label for="require-checks">Required checks</label><p class="hint">Requires CI to succeed.{{with .Repo.Settings.RequiredContexts}} Also waits for {{range $i, $c := .}}{{if $i}}, {{end}}<code>{{$c}}</code>{{end}} until they report{{if not $.Repo.Settings.RequireChecks}}, once this is on{{end}}.{{end}}</p></div>
76 <div class="check"><input type="checkbox" id="require-checks" name="require-checks" value="on"{{if .Repo.Settings.RequireChecks}} checked{{end}}></div> 76 <div class="check"><input type="checkbox" id="require-checks" name="require-checks" value="on"{{if .Repo.Settings.RequireChecks}} checked{{end}}></div>
77 <div><button type="submit" class="btn">Save</button></div> 77 <div><button type="submit" class="btn">Save</button></div>
78</form> 78</form>
79<form method="post" action="{{$base}}" class="setform">
80 <input type="hidden" name="field" value="require-contexts">
81 <div><label for="contexts">Required contexts</label><p class="hint">Statuses the checks gate waits for until they report, separated by spaces. Saving any turns required checks on; saving none leaves it as it is.</p></div>
82 <div><input type="text" id="contexts" name="contexts" value="{{range $i, $c := .Repo.Settings.RequiredContexts}}{{if $i}} {{end}}{{$c}}{{end}}" autocomplete="off"></div>
83 <div><button type="submit" class="btn">Save</button></div>
84</form>
79<form method="post" action="{{$base}}" class="setform"> 85<form method="post" action="{{$base}}" class="setform">
80 <input type="hidden" name="field" value="require-approvals"> 86 <input type="hidden" name="field" value="require-approvals">
81 <div><label for="approvals">Approvals</label><p class="hint">Approvals from anyone with write access. Zero means none required.</p></div> 87 <div><label for="approvals">Approvals</label><p class="hint">Approvals from anyone with write access. Zero means none required.</p></div>