Commit bf403d4e28

bf403d4e281d8ae8c83d9f7c634a9720ed4207f9

parent: c266338b03

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-14 05:20 UTC

Required contexts: say whether the gate is on; keep a refused name

The heading under Require green checks follows the toggle, so named
checks do not read as required while the gate is off. Adding waits for
the server and clears the field only once it takes the name, and a
second add cannot start while one is in flight.

Ref #25

Layout: unified · split

gitbay/Repos/RepoSettingsViewModel.swift +7 −2
@@ -113,11 +113,16 @@ final class RepoSettingsViewModel {
113113
114114 /// `repo settings require-contexts` replaces the list, so the whole
115115 /// list goes each time. Naming one turns the checks gate on.
116 func addRequiredContext(_ context: String) async {
116 /// Returns whether the field can be cleared: the server took it, or
117 /// it was already listed. A refused name stays in the field to fix.
118 @discardableResult
119 func addRequiredContext(_ context: String) async -> Bool {
117120 let context = context.trimmingCharacters(in: .whitespaces)
118121 let current = state.value?.settings.contexts ?? []
119 guard !context.isEmpty, !current.contains(context) else { return }
122 guard !context.isEmpty, !working else { return false }
123 guard !current.contains(context) else { return true }
120124 await setRequiredContexts(current + [context])
125 return actionError == nil
121126 }
122127
123128 /// Removing the last one leaves the gate as it was, waiting on every check.
gitbay/Views/Repos/RepoSettingsView.swift +12 −6
@@ -353,9 +353,7 @@ struct RepoSettingsView: View {
353353 await model.setRequireChecks($0)
354354 }
355355 VStack(alignment: .leading, spacing: 6) {
356 Text(loaded.settings.contexts.isEmpty
357 ? "Checks required: every check on the head"
358 : "Checks required by name")
356 Text(contextsHeading(loaded.settings))
359357 .font(.gbSans(.subheadline))
360358 RemovableChips(items: loaded.settings.contexts, idPrefix: "settings-context",
361359 disabled: model.working,
@@ -365,7 +363,7 @@ struct RepoSettingsView: View {
365363 .font(.gbMono(.subheadline))
366364 .autocorrectionDisabled()
367365 .textInputAutocapitalization(.never)
368 .onSubmit { addContext() }
366 .onSubmit { if !model.working { addContext() } }
369367 .accessibilityIdentifier("settings-context-field")
370368 Button {
371369 addContext()
@@ -392,8 +390,16 @@ struct RepoSettingsView: View {
392390
393391 private func addContext() {
394392 let context = newContext
395 newContext = ""
396 Task { await model.addRequiredContext(context) }
393 Task {
394 if await model.addRequiredContext(context), newContext == context { newContext = "" }
395 }
396 }
397
398 private func contextsHeading(_ settings: RepoSettings) -> String {
399 if !(settings.requireChecks ?? false) {
400 return settings.contexts.isEmpty ? "Name a check to require it" : "Named checks, not required while the gate is off"
401 }
402 return settings.contexts.isEmpty ? "Every check on the head must pass" : "These checks must pass"
397403 }
398404
399405 private func daemonSection(_ loaded: RepoSettingsViewModel.Loaded) -> some View {
gitbayTests/RequiredContextsTests.swift +12 −1
@@ -52,8 +52,9 @@ struct RequiredContextsTests {
5252 let (model, stub) = try await loaded()
5353 stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
5454
55 await model.addRequiredContext(" ci/build ")
55 let cleared = await model.addRequiredContext(" ci/build ")
5656
57 #expect(cleared)
5758 #expect(try postedArgv(stub) == [["repo", "settings", "require-contexts", "krz/gitbay",
5859 "ci/test", "ext/lint", "ci/build"]])
5960 }
@@ -66,6 +67,16 @@ struct RequiredContextsTests {
6667 #expect(try postedArgv(stub).isEmpty)
6768 }
6869
70 @Test func aRefusedContextIsNotClearedFromTheField() async throws {
71 let (model, stub) = try await loaded()
72 stub.enqueue(.init(status: 400, json: #"{"protocol_version":1,"error":"at most 20 required contexts","exit_code":2}"#, match: "cmd"))
73
74 let cleared = await model.addRequiredContext("ci/more")
75
76 #expect(!cleared)
77 #expect(model.actionError != nil)
78 }
79
6980 @Test func removingTheLastContextSendsAnEmptyList() async throws {
7081 let (client, stub) = try makeClient()
7182 stub.enqueue(.init(status: 200, json: """