Commit d2860af98a

d2860af98aa72c64374b7ef3b50f29aa0f9288ab

parent: 4ba5e2812c

Unsigned

cmc <hello@cleberg.net> · 2026-09-08 03:13 UTC

Access section shows where access comes from (!88)

repo access list reports effective access since v1.16.0 (krz/gitbay#200) with a source per row: owner, direct, org admin, org member, or team. The row shows a source chip for anything but a direct grant, and only a direct grant offers revoke. An older server's rows decode with no source and stay revocable.

Layout: unified · split

gitbay/Repos/RepoSettings.swift +6 −1
@@ -27,9 +27,14 @@ nonisolated struct RepoSettings: Decodable, Sendable, Hashable {
2727 var branches: [String] { protectedBranches ?? [] }
2828}
2929
30/// One row of `repo access list`: a user granted read, write or admin.
30/// One row of `repo access list`: an account and its effective role.
31/// Since v1.16.0 the row says where the access comes from — owner,
32/// direct, org admin, org member, or team; only a direct grant can be
33/// revoked here. An older server lists direct grants and says nothing.
3134nonisolated struct AccessGrant: Decodable, Sendable, Hashable, Identifiable {
3235 let user: String
3336 let role: String
37 let source: String?
3438 var id: String { user }
39 var isDirect: Bool { source == nil || source == "direct" }
3540}
gitbay/Views/Repos/RepoSettingsView.swift +8 −3
@@ -267,6 +267,9 @@ struct RepoSettingsView: View {
267267 .font(.gbSans(.subheadline))
268268 }
269269 Spacer()
270 if let source = grant.source, source != "direct" {
271 GBChip(source, .secondary)
272 }
270273 Menu {
271274 ForEach(["read", "write", "admin"], id: \.self) { role in
272275 Button(role) { Task { await model.grant(user: grant.user, role: role) } }
@@ -277,8 +280,10 @@ struct RepoSettingsView: View {
277280 .disabled(model.working)
278281 }
279282 .swipeActions {
280 Button("Revoke", role: .destructive) {
281 revokingGrant = grant
283 if grant.isDirect {
284 Button("Revoke", role: .destructive) {
285 revokingGrant = grant
286 }
282287 }
283288 }
284289 }
@@ -307,7 +312,7 @@ struct RepoSettingsView: View {
307312 } header: {
308313 Text("Access")
309314 } footer: {
310 Text("Direct grants only. Ownership and org membership carry their own access.")
315 Text("Everyone who can reach this repository and how. Only a direct grant can be revoked here; a role change grants directly.")
311316 }
312317 }
313318
gitbayTests/RepoManagementTests.swift +11 −3
@@ -114,14 +114,22 @@ struct RepoSettingsViewModelTests {
114114 @Test func accessGrantsLoadSeparately() async throws {
115115 let (model, stub) = try await loadedModel()
116116 stub.enqueue(.init(status: 200, json: """
117 {"protocol_version":1,"data":[{"user":"alice","role":"write"},\
118 {"user":"bob","role":"read"}],"exit_code":0}
117 {"protocol_version":1,"data":[{"user":"alice","role":"write","source":"direct"},\
118 {"user":"bob","role":"read","source":"team"},{"user":"cmc","role":"admin"}],"exit_code":0}
119119 """, match: "argv=access"))
120120
121121 await model.loadGrants()
122122
123 #expect(model.grants?.map(\.user) == ["alice", "bob"])
123 #expect(model.grants?.map(\.user) == ["alice", "bob", "cmc"])
124124 #expect(model.grants?.first?.role == "write")
125 // v1.16.0 says where the access comes from; only a direct grant
126 // can be revoked here. An older server says nothing.
127 #expect(model.grants?[0].source == "direct")
128 #expect(model.grants?[0].isDirect == true)
129 #expect(model.grants?[1].source == "team")
130 #expect(model.grants?[1].isDirect == false)
131 #expect(model.grants?[2].source == nil)
132 #expect(model.grants?[2].isDirect == true)
125133 #expect(model.grantsError == nil)
126134 let read = try #require(stub.seen.last)
127135 #expect(read.url.query() == "argv=repo&argv=access&argv=list&argv=krz/gitbay")