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 { |
| 27 | 27 | var branches: [String] { protectedBranches ?? [] } |
| 28 | 28 | } |
| 29 | 29 | |
| 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. |
| 31 | 34 | nonisolated struct AccessGrant: Decodable, Sendable, Hashable, Identifiable { |
| 32 | 35 | let user: String |
| 33 | 36 | let role: String |
| 37 | let source: String? |
| 34 | 38 | var id: String { user } |
| 39 | var isDirect: Bool { source == nil || source == "direct" } |
| 35 | 40 | } |
gitbay/Views/Repos/RepoSettingsView.swift
+8 −3
| @@ -267,6 +267,9 @@ struct RepoSettingsView: View { |
| 267 | 267 | .font(.gbSans(.subheadline)) |
| 268 | 268 | } |
| 269 | 269 | Spacer() |
| 270 | if let source = grant.source, source != "direct" { |
| 271 | GBChip(source, .secondary) |
| 272 | } |
| 270 | 273 | Menu { |
| 271 | 274 | ForEach(["read", "write", "admin"], id: \.self) { role in |
| 272 | 275 | Button(role) { Task { await model.grant(user: grant.user, role: role) } } |
| @@ -277,8 +280,10 @@ struct RepoSettingsView: View { |
| 277 | 280 | .disabled(model.working) |
| 278 | 281 | } |
| 279 | 282 | .swipeActions { |
| 280 | | Button("Revoke", role: .destructive) { |
| 281 | | revokingGrant = grant |
| 283 | if grant.isDirect { |
| 284 | Button("Revoke", role: .destructive) { |
| 285 | revokingGrant = grant |
| 286 | } |
| 282 | 287 | } |
| 283 | 288 | } |
| 284 | 289 | } |
| @@ -307,7 +312,7 @@ struct RepoSettingsView: View { |
| 307 | 312 | } header: { |
| 308 | 313 | Text("Access") |
| 309 | 314 | } 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.") |
| 311 | 316 | } |
| 312 | 317 | } |
| 313 | 318 | |
gitbayTests/RepoManagementTests.swift
+11 −3
| @@ -114,14 +114,22 @@ struct RepoSettingsViewModelTests { |
| 114 | 114 | @Test func accessGrantsLoadSeparately() async throws { |
| 115 | 115 | let (model, stub) = try await loadedModel() |
| 116 | 116 | 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} |
| 119 | 119 | """, match: "argv=access")) |
| 120 | 120 | |
| 121 | 121 | await model.loadGrants() |
| 122 | 122 | |
| 123 | | #expect(model.grants?.map(\.user) == ["alice", "bob"]) |
| 123 | #expect(model.grants?.map(\.user) == ["alice", "bob", "cmc"]) |
| 124 | 124 | #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) |
| 125 | 133 | #expect(model.grantsError == nil) |
| 126 | 134 | let read = try #require(stub.seen.last) |
| 127 | 135 | #expect(read.url.query() == "argv=repo&argv=access&argv=list&argv=krz/gitbay") |