Commit 63a1edda14

63a1edda1414eb84776aa050c718197c1917d153

parent: 1dab2a3e20

Unsigned

cmc <hello@cleberg.net> · 2026-09-07 17:11 UTC

Require code owner review toggle in repo settings (!66)

Decodes require_codeowners and dispatches repo settings require-codeowners on|off beside the other merge rules. Unit test covers the decode and the argv; the live suite flips the toggle.

Layout: unified · split

gitbay/Repos/RepoSettings.swift +2
@@ -8,6 +8,7 @@ nonisolated struct RepoSettings: Decodable, Sendable, Hashable {
88 let requireChecks: Bool?
99 let requireApprovals: Int?
1010 let requireResolved: Bool?
11 let requireCodeowners: Bool?
1112 let gitDaemon: Bool?
1213 let archived: Bool?
1314 let website: String?
@@ -18,6 +19,7 @@ nonisolated struct RepoSettings: Decodable, Sendable, Hashable {
1819 case requireChecks = "require_checks"
1920 case requireApprovals = "require_approvals"
2021 case requireResolved = "require_resolved"
22 case requireCodeowners = "require_codeowners"
2123 case gitDaemon = "git_daemon"
2224 case archived, website
2325 }
gitbay/Repos/RepoSettingsViewModel.swift +4
@@ -107,6 +107,10 @@ final class RepoSettingsViewModel {
107107 await perform(["repo", "settings", "require-signed", repoPath, on ? "on" : "off"])
108108 }
109109
110 func setRequireCodeowners(_ on: Bool) async {
111 await perform(["repo", "settings", "require-codeowners", repoPath, on ? "on" : "off"])
112 }
113
110114 func addTopic(_ topic: String) async {
111115 await perform(["repo", "topics", "add", repoPath, topic])
112116 }
gitbay/Views/Repos/RepoSettingsView.swift +3
@@ -210,6 +210,9 @@ struct RepoSettingsView: View {
210210 toggle("Require signed commits", loaded.settings.requireSignedCommits ?? false) {
211211 await model.setRequireSigned($0)
212212 }
213 toggle("Require code owner review", loaded.settings.requireCodeowners ?? false) {
214 await model.setRequireCodeowners($0)
215 }
213216 } header: {
214217 Text("Merge requirements")
215218 } footer: {
gitbayTests/RepoManagementTests.swift +5 −2
@@ -21,7 +21,7 @@ private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
2121
2222private let settingsShowJSON = """
2323 {"protocol_version":1,"data":{"protected_branches":["main"],"require_approvals":1,\
24 "require_resolved":true,"website":"https://gitbay.org"},"exit_code":0}
24 "require_resolved":true,"require_codeowners":true,"website":"https://gitbay.org"},"exit_code":0}
2525 """
2626
2727private let repoShowJSON = """
@@ -50,6 +50,7 @@ struct RepoSettingsViewModelTests {
5050 #expect(loaded.settings.requireResolved == true)
5151 // Absent means default, not unknown.
5252 #expect(loaded.settings.requireChecks == nil)
53 #expect(loaded.settings.requireCodeowners == true)
5354 #expect(loaded.detail.topics == ["git"])
5455 }
5556
@@ -72,7 +73,7 @@ struct RepoSettingsViewModelTests {
7273
7374 @Test func everyKnobSendsItsOwnCommand() async throws {
7475 let (model, stub) = try await loadedModel()
75 for _ in 0..<12 {
76 for _ in 0..<13 {
7677 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
7778 stub.enqueue(.init(status: 200, json: settingsShowJSON, match: "argv=settings"))
7879 stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz"))
@@ -88,6 +89,7 @@ struct RepoSettingsViewModelTests {
8889 await model.setRequireResolved(false)
8990 await model.setRequireChecks(true)
9091 await model.setRequireSigned(true)
92 await model.setRequireCodeowners(true)
9193 await model.addTopic("ios")
9294 await model.removeTopic("git")
9395
@@ -103,6 +105,7 @@ struct RepoSettingsViewModelTests {
103105 ["repo", "settings", "require-resolved", "krz/gitbay", "off"],
104106 ["repo", "settings", "require-checks", "krz/gitbay", "on"],
105107 ["repo", "settings", "require-signed", "krz/gitbay", "on"],
108 ["repo", "settings", "require-codeowners", "krz/gitbay", "on"],
106109 ["repo", "topics", "add", "krz/gitbay", "ios"],
107110 ["repo", "topics", "remove", "krz/gitbay", "git"],
108111 ])
gitbayUITests/LiveSmokeUITests.swift +16
@@ -479,6 +479,22 @@ extension LiveSmokeUITests {
479479 control()
480480 XCTAssertTrue(waitForValue(resolved, "0", timeout: 10), "toggle did not persist off")
481481
482 // The code-owner rule is one more toggle in the same section.
483 let codeowners = app.switches["Require code owner review"].firstMatch
484 XCTAssertTrue(scrollTo(codeowners), "code owner toggle not reachable")
485 let innerCodeowners = codeowners.switches.firstMatch
486 let flipCodeowners: () -> Void = {
487 if innerCodeowners.exists && innerCodeowners != codeowners {
488 innerCodeowners.tap()
489 } else {
490 codeowners.coordinate(withNormalizedOffset: CGVector(dx: 0.93, dy: 0.5)).tap()
491 }
492 }
493 flipCodeowners()
494 XCTAssertTrue(waitForValue(codeowners, "1", timeout: 10), "code owner toggle did not persist on")
495 flipCodeowners()
496 XCTAssertTrue(waitForValue(codeowners, "0", timeout: 10), "code owner toggle did not persist off")
497
482498 back() // settings -> repo
483499
484500 // --- nothing to trigger without a job file ---