import Foundation import Observation /// The repo settings screen: `repo settings show` for the admin knobs, /// `repo show` for description/topics/visibility, and one write command /// per control. Non-admins get the server's refusal verbatim. @Observable @MainActor final class RepoSettingsViewModel { nonisolated struct Loaded: Sendable, Hashable { let settings: RepoSettings let detail: RepoDetail } private(set) var state: LoadState = .loading private(set) var actionError: String? private(set) var working = false /// `repo deps status`, exposed as an optional plus a separate error /// string rather than folded into one state: `nil` with no error means /// "not loaded yet", `nil` with an error means "the read failed". The /// settings screen must never render "checks are off" when it means /// "we could not ask". private(set) var deps: DepsStatus? private(set) var depsError: String? /// `repo access list`, with the same nil-plus-error shape as `deps`: /// "not loaded" and "could not ask" must not read as "nobody granted". private(set) var grants: [AccessGrant]? private(set) var grantsError: String? private let client: GitbayClient let repoPath: String init(client: GitbayClient, repoPath: String) { self.client = client self.repoPath = repoPath } func load() async { do { async let settings = client.read( ["repo", "settings", "show", repoPath], as: RepoSettings.self) async let detail = client.read(["repo", "show", repoPath], as: RepoDetail.self) state = .loaded(Loaded(settings: try await settings, detail: try await detail)) } catch { state = .from(error) } } /// `repo deps status` — three words, unlike every other `repo` command /// in this file, which is exactly why it's easy to get wrong. func loadDeps() async { depsError = nil deps = nil do { deps = try await client.read(["repo", "deps", "status", repoPath], as: DepsStatus.self) } catch let error as GitbayError { depsError = error.userFacingMessage } catch { depsError = GitbayError.transport(error).userFacingMessage } } /// `enable`/`disable` are two distinct commands, not one command with a /// flag. Reloads the status afterward so the report reflects the /// change; a refused write leaves `deps` untouched. func setDepsEnabled(_ on: Bool) async { await perform(["repo", "deps", on ? "enable" : "disable", repoPath], reload: loadDeps) } // MARK: - Writes, one command per knob func setDescription(_ text: String) async { await perform(["repo", "settings", "description", repoPath, text]) } func setWebsite(_ url: String) async { await perform(["repo", "settings", "website", repoPath, url]) } func setVisibility(_ visibility: String) async { await perform(["repo", "settings", "visibility", repoPath, visibility]) } func setGitDaemon(_ on: Bool) async { await perform(["repo", "settings", "git-daemon", repoPath, on ? "on" : "off"]) } func protectBranch(_ branch: String) async { await perform(["repo", "settings", "protect", repoPath, branch]) } func unprotectBranch(_ branch: String) async { await perform(["repo", "settings", "unprotect", repoPath, branch]) } func setRequireApprovals(_ count: Int) async { await perform(["repo", "settings", "require-approvals", repoPath, String(count)]) } func setRequireResolved(_ on: Bool) async { await perform(["repo", "settings", "require-resolved", repoPath, on ? "on" : "off"]) } func setRequireChecks(_ on: Bool) async { await perform(["repo", "settings", "require-checks", repoPath, on ? "on" : "off"]) } func setRequireSigned(_ on: Bool) async { await perform(["repo", "settings", "require-signed", repoPath, on ? "on" : "off"]) } func setRequireCodeowners(_ on: Bool) async { await perform(["repo", "settings", "require-codeowners", repoPath, on ? "on" : "off"]) } func loadGrants() async { grantsError = nil grants = nil do { grants = try await client.readList(["repo", "access", "list", repoPath], of: AccessGrant.self) } catch let error as GitbayError { grantsError = error.userFacingMessage } catch { grantsError = GitbayError.transport(error).userFacingMessage } } /// `repo access grant read|write|admin` — granting again /// changes an existing user's role. func grant(user: String, role: String) async { let user = user.trimmingCharacters(in: .whitespaces) guard !user.isEmpty else { return } await perform(["repo", "access", "grant", repoPath, user, role], reload: loadGrants) } func revoke(user: String) async { await perform(["repo", "access", "revoke", repoPath, user], reload: loadGrants) } func addTopic(_ topic: String) async { await perform(["repo", "topics", "add", repoPath, topic]) } func removeTopic(_ topic: String) async { await perform(["repo", "topics", "remove", repoPath, topic]) } private func perform(_ argv: [String]) async { await perform(argv, reload: load) } private func perform(_ argv: [String], reload: () async -> Void) async { working = true actionError = nil defer { working = false } do { try await client.run(argv) await reload() } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } }