gitbay/Repos/RepoSettingsViewModel.swift

v1.0.6
gitbay-ios/gitbay/Repos/RepoSettingsViewModel.swift history · blame · raw

135 lines · 4642 bytes

  1import Foundation
  2import Observation
  3
  4/// The repo settings screen: `repo settings show` for the admin knobs,
  5/// `repo show` for description/topics/visibility, and one write command
  6/// per control. Non-admins get the server's refusal verbatim.
  7@Observable
  8@MainActor
  9final class RepoSettingsViewModel {
 10
 11    nonisolated struct Loaded: Sendable, Hashable {
 12        let settings: RepoSettings
 13        let detail: RepoDetail
 14    }
 15
 16    private(set) var state: LoadState<Loaded> = .loading
 17    private(set) var actionError: String?
 18    private(set) var working = false
 19
 20    /// `repo deps status`, exposed as an optional plus a separate error
 21    /// string rather than folded into one state: `nil` with no error means
 22    /// "not loaded yet", `nil` with an error means "the read failed". The
 23    /// settings screen must never render "checks are off" when it means
 24    /// "we could not ask".
 25    private(set) var deps: DepsStatus?
 26    private(set) var depsError: String?
 27
 28    private let client: GitbayClient
 29    let repoPath: String
 30
 31    init(client: GitbayClient, repoPath: String) {
 32        self.client = client
 33        self.repoPath = repoPath
 34    }
 35
 36    func load() async {
 37        do {
 38            async let settings = client.read(
 39                ["repo", "settings", "show", repoPath], as: RepoSettings.self)
 40            async let detail = client.read(["repo", "show", repoPath], as: RepoDetail.self)
 41            state = .loaded(Loaded(settings: try await settings, detail: try await detail))
 42        } catch {
 43            state = .from(error)
 44        }
 45    }
 46
 47    /// `repo deps status` — three words, unlike every other `repo` command
 48    /// in this file, which is exactly why it's easy to get wrong.
 49    func loadDeps() async {
 50        depsError = nil
 51        deps = nil
 52        do {
 53            deps = try await client.read(["repo", "deps", "status", repoPath], as: DepsStatus.self)
 54        } catch let error as GitbayError {
 55            depsError = error.userFacingMessage
 56        } catch {
 57            depsError = GitbayError.transport(error).userFacingMessage
 58        }
 59    }
 60
 61    /// `enable`/`disable` are two distinct commands, not one command with a
 62    /// flag. Reloads the status afterward so the report reflects the
 63    /// change; a refused write leaves `deps` untouched.
 64    func setDepsEnabled(_ on: Bool) async {
 65        await perform(["repo", "deps", on ? "enable" : "disable", repoPath], reload: loadDeps)
 66    }
 67
 68    // MARK: - Writes, one command per knob
 69
 70    func setDescription(_ text: String) async {
 71        await perform(["repo", "settings", "description", repoPath, text])
 72    }
 73
 74    func setWebsite(_ url: String) async {
 75        await perform(["repo", "settings", "website", repoPath, url])
 76    }
 77
 78    func setVisibility(_ visibility: String) async {
 79        await perform(["repo", "settings", "visibility", repoPath, visibility])
 80    }
 81
 82    func setGitDaemon(_ on: Bool) async {
 83        await perform(["repo", "settings", "git-daemon", repoPath, on ? "on" : "off"])
 84    }
 85
 86    func protectBranch(_ branch: String) async {
 87        await perform(["repo", "settings", "protect", repoPath, branch])
 88    }
 89
 90    func unprotectBranch(_ branch: String) async {
 91        await perform(["repo", "settings", "unprotect", repoPath, branch])
 92    }
 93
 94    func setRequireApprovals(_ count: Int) async {
 95        await perform(["repo", "settings", "require-approvals", repoPath, String(count)])
 96    }
 97
 98    func setRequireResolved(_ on: Bool) async {
 99        await perform(["repo", "settings", "require-resolved", repoPath, on ? "on" : "off"])
100    }
101
102    func setRequireChecks(_ on: Bool) async {
103        await perform(["repo", "settings", "require-checks", repoPath, on ? "on" : "off"])
104    }
105
106    func setRequireSigned(_ on: Bool) async {
107        await perform(["repo", "settings", "require-signed", repoPath, on ? "on" : "off"])
108    }
109
110    func addTopic(_ topic: String) async {
111        await perform(["repo", "topics", "add", repoPath, topic])
112    }
113
114    func removeTopic(_ topic: String) async {
115        await perform(["repo", "topics", "remove", repoPath, topic])
116    }
117
118    private func perform(_ argv: [String]) async {
119        await perform(argv, reload: load)
120    }
121
122    private func perform(_ argv: [String], reload: () async -> Void) async {
123        working = true
124        actionError = nil
125        defer { working = false }
126        do {
127            try await client.run(argv)
128            await reload()
129        } catch let error as GitbayError {
130            actionError = error.userFacingMessage
131        } catch {
132            actionError = GitbayError.transport(error).userFacingMessage
133        }
134    }
135}