Commit 63a1edda14
Unsigned
Layout: unified · split
gitbay/Repos/RepoSettings.swift +2
| @@ -8,6 +8,7 @@ nonisolated struct RepoSettings: Decodable, Sendable, Hashable { | |||
| 8 | let requireChecks: Bool? | 8 | let requireChecks: Bool? |
| 9 | let requireApprovals: Int? | 9 | let requireApprovals: Int? |
| 10 | let requireResolved: Bool? | 10 | let requireResolved: Bool? |
| 11 | let requireCodeowners: Bool? | ||
| 11 | let gitDaemon: Bool? | 12 | let gitDaemon: Bool? |
| 12 | let archived: Bool? | 13 | let archived: Bool? |
| 13 | let website: String? | 14 | let website: String? |
| @@ -18,6 +19,7 @@ nonisolated struct RepoSettings: Decodable, Sendable, Hashable { | |||
| 18 | case requireChecks = "require_checks" | 19 | case requireChecks = "require_checks" |
| 19 | case requireApprovals = "require_approvals" | 20 | case requireApprovals = "require_approvals" |
| 20 | case requireResolved = "require_resolved" | 21 | case requireResolved = "require_resolved" |
| 22 | case requireCodeowners = "require_codeowners" | ||
| 21 | case gitDaemon = "git_daemon" | 23 | case gitDaemon = "git_daemon" |
| 22 | case archived, website | 24 | case archived, website |
| 23 | } | 25 | } |
gitbay/Repos/RepoSettingsViewModel.swift +4
| @@ -107,6 +107,10 @@ final class RepoSettingsViewModel { | |||
| 107 | await perform(["repo", "settings", "require-signed", repoPath, on ? "on" : "off"]) | 107 | await perform(["repo", "settings", "require-signed", repoPath, on ? "on" : "off"]) |
| 108 | } | 108 | } |
| 109 | 109 | ||
| 110 | func setRequireCodeowners(_ on: Bool) async { | ||
| 111 | await perform(["repo", "settings", "require-codeowners", repoPath, on ? "on" : "off"]) | ||
| 112 | } | ||
| 113 | |||
| 110 | func addTopic(_ topic: String) async { | 114 | func addTopic(_ topic: String) async { |
| 111 | await perform(["repo", "topics", "add", repoPath, topic]) | 115 | await perform(["repo", "topics", "add", repoPath, topic]) |
| 112 | } | 116 | } |
gitbay/Views/Repos/RepoSettingsView.swift +3
| @@ -210,6 +210,9 @@ struct RepoSettingsView: View { | |||
| 210 | toggle("Require signed commits", loaded.settings.requireSignedCommits ?? false) { | 210 | toggle("Require signed commits", loaded.settings.requireSignedCommits ?? false) { |
| 211 | await model.setRequireSigned($0) | 211 | await model.setRequireSigned($0) |
| 212 | } | 212 | } |
| 213 | toggle("Require code owner review", loaded.settings.requireCodeowners ?? false) { | ||
| 214 | await model.setRequireCodeowners($0) | ||
| 215 | } | ||
| 213 | } header: { | 216 | } header: { |
| 214 | Text("Merge requirements") | 217 | Text("Merge requirements") |
| 215 | } footer: { | 218 | } footer: { |
gitbayTests/RepoManagementTests.swift +5 −2
| @@ -21,7 +21,7 @@ private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"# | |||
| 21 | 21 | ||
| 22 | private let settingsShowJSON = """ | 22 | private let settingsShowJSON = """ |
| 23 | {"protocol_version":1,"data":{"protected_branches":["main"],"require_approvals":1,\ | 23 | {"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} |
| 25 | """ | 25 | """ |
| 26 | 26 | ||
| 27 | private let repoShowJSON = """ | 27 | private let repoShowJSON = """ |
| @@ -50,6 +50,7 @@ struct RepoSettingsViewModelTests { | |||
| 50 | #expect(loaded.settings.requireResolved == true) | 50 | #expect(loaded.settings.requireResolved == true) |
| 51 | // Absent means default, not unknown. | 51 | // Absent means default, not unknown. |
| 52 | #expect(loaded.settings.requireChecks == nil) | 52 | #expect(loaded.settings.requireChecks == nil) |
| 53 | #expect(loaded.settings.requireCodeowners == true) | ||
| 53 | #expect(loaded.detail.topics == ["git"]) | 54 | #expect(loaded.detail.topics == ["git"]) |
| 54 | } | 55 | } |
| 55 | 56 | ||
| @@ -72,7 +73,7 @@ struct RepoSettingsViewModelTests { | |||
| 72 | 73 | ||
| 73 | @Test func everyKnobSendsItsOwnCommand() async throws { | 74 | @Test func everyKnobSendsItsOwnCommand() async throws { |
| 74 | let (model, stub) = try await loadedModel() | 75 | let (model, stub) = try await loadedModel() |
| 75 | for _ in 0..<12 { | 76 | for _ in 0..<13 { |
| 76 | stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) | 77 | stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) |
| 77 | stub.enqueue(.init(status: 200, json: settingsShowJSON, match: "argv=settings")) | 78 | stub.enqueue(.init(status: 200, json: settingsShowJSON, match: "argv=settings")) |
| 78 | stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz")) | 79 | stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz")) |
| @@ -88,6 +89,7 @@ struct RepoSettingsViewModelTests { | |||
| 88 | await model.setRequireResolved(false) | 89 | await model.setRequireResolved(false) |
| 89 | await model.setRequireChecks(true) | 90 | await model.setRequireChecks(true) |
| 90 | await model.setRequireSigned(true) | 91 | await model.setRequireSigned(true) |
| 92 | await model.setRequireCodeowners(true) | ||
| 91 | await model.addTopic("ios") | 93 | await model.addTopic("ios") |
| 92 | await model.removeTopic("git") | 94 | await model.removeTopic("git") |
| 93 | 95 | ||
| @@ -103,6 +105,7 @@ struct RepoSettingsViewModelTests { | |||
| 103 | ["repo", "settings", "require-resolved", "krz/gitbay", "off"], | 105 | ["repo", "settings", "require-resolved", "krz/gitbay", "off"], |
| 104 | ["repo", "settings", "require-checks", "krz/gitbay", "on"], | 106 | ["repo", "settings", "require-checks", "krz/gitbay", "on"], |
| 105 | ["repo", "settings", "require-signed", "krz/gitbay", "on"], | 107 | ["repo", "settings", "require-signed", "krz/gitbay", "on"], |
| 108 | ["repo", "settings", "require-codeowners", "krz/gitbay", "on"], | ||
| 106 | ["repo", "topics", "add", "krz/gitbay", "ios"], | 109 | ["repo", "topics", "add", "krz/gitbay", "ios"], |
| 107 | ["repo", "topics", "remove", "krz/gitbay", "git"], | 110 | ["repo", "topics", "remove", "krz/gitbay", "git"], |
| 108 | ]) | 111 | ]) |
gitbayUITests/LiveSmokeUITests.swift +16
| @@ -479,6 +479,22 @@ extension LiveSmokeUITests { | |||
| 479 | control() | 479 | control() |
| 480 | XCTAssertTrue(waitForValue(resolved, "0", timeout: 10), "toggle did not persist off") | 480 | XCTAssertTrue(waitForValue(resolved, "0", timeout: 10), "toggle did not persist off") |
| 481 | 481 | ||
| 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 | |||
| 482 | back() // settings -> repo | 498 | back() // settings -> repo |
| 483 | 499 | ||
| 484 | // --- nothing to trigger without a job file --- | 500 | // --- nothing to trigger without a job file --- |