krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v2.9.1: Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift · raw

  1import Foundation
  2
  3private struct HgUpdateRepositoryResponse: Decodable, Sendable {
  4    let updateRepository: HgUpdatedRepository
  5}
  6
  7private struct HgUpdatedRepository: Decodable, Sendable {
  8    let id: Int
  9}
 10
 11private struct HgRepositoryInfoResponse: Decodable, Sendable {
 12    let repository: HgRepositoryInfo?
 13}
 14
 15private struct HgRepositoryInfo: Decodable, Sendable {
 16    let description: String?
 17    let visibility: Visibility
 18    let nonPublishing: Bool?
 19}
 20
 21private struct HgACLResponse: Decodable, Sendable {
 22    let repository: HgACLRepository?
 23}
 24
 25private struct HgACLRepository: Decodable, Sendable {
 26    let accessControlList: HgACLPage
 27}
 28
 29private struct HgACLPage: Decodable, Sendable {
 30    let results: [HgACLEntry]
 31    let cursor: String?
 32}
 33
 34private struct HgUpdateACLResponse: Decodable, Sendable {
 35    let updateACL: HgACLEntry
 36}
 37
 38private struct HgDeleteACLResponse: Decodable, Sendable {
 39    let deleteACL: HgDeletedACL
 40}
 41
 42private struct HgDeletedACL: Decodable, Sendable {
 43    let id: Int
 44}
 45
 46private struct HgDeleteRepositoryResponse: Decodable, Sendable {
 47    let deleteRepository: HgDeletedRepository
 48}
 49
 50private struct HgDeletedRepository: Decodable, Sendable {
 51    let id: Int
 52}
 53
 54struct HgACLEntry: Decodable, Sendable, Identifiable {
 55    let id: Int
 56    let mode: String
 57    let entity: Entity
 58}
 59
 60@Observable
 61@MainActor
 62final class HgRepositorySettingsViewModel {
 63    let repositoryId: Int
 64    let repositoryRid: String
 65    let repositoryName: String
 66    private let client: SRHTClient
 67
 68    var editedDescription: String
 69    var editedVisibility: Visibility
 70    var editedNonPublishing: Bool
 71    var isSavingInfo = false
 72
 73    private(set) var acls: [HgACLEntry] = []
 74    private(set) var isLoadingACLs = false
 75    var newACLEntity = ""
 76    var newACLMode = "RO"
 77    var isAddingACL = false
 78    var isDeletingACL = false
 79
 80    var histeditRevision = ""
 81    var isDeleting = false
 82    var didDelete = false
 83    var error: String?
 84
 85    init(repository: RepositorySummary, client: SRHTClient) {
 86        self.repositoryId = repository.id
 87        self.repositoryRid = repository.rid
 88        self.repositoryName = repository.name
 89        self.client = client
 90        self.editedDescription = repository.description ?? ""
 91        self.editedVisibility = repository.visibility
 92        self.editedNonPublishing = false
 93    }
 94
 95    private static let updateRepositoryMutation = """
 96    mutation updateRepository($id: Int!, $input: RepoInput!) {
 97        updateRepository(id: $id, input: $input) {
 98            id
 99        }
100    }
101    """
102
103    private static let accessControlListQuery = """
104    query hgAccessControlList($rid: ID!) {
105        repository(rid: $rid) {
106            accessControlList {
107                results {
108                    id
109                    mode
110                    entity { canonicalName }
111                }
112                cursor
113            }
114        }
115    }
116    """
117
118    private static let updateACLMutation = """
119    mutation updateACL($repoId: Int!, $mode: AccessMode!, $entity: String!) {
120        updateACL(repoId: $repoId, mode: $mode, entity: $entity) {
121            id
122            mode
123            entity { canonicalName }
124        }
125    }
126    """
127
128    private static let deleteACLMutation = """
129    mutation deleteACL($id: Int!) {
130        deleteACL(id: $id) { id }
131    }
132    """
133
134    private static let deleteRepositoryMutation = """
135    mutation deleteRepository($id: Int!) {
136        deleteRepository(id: $id) { id }
137    }
138    """
139
140    private static let repositoryInfoQuery = """
141    query hgRepositoryInfo($rid: ID!) {
142        repository(rid: $rid) {
143            description
144            visibility
145            nonPublishing
146        }
147    }
148    """
149
150    func loadRepositoryInfo() async {
151        error = nil
152
153        do {
154            let result = try await client.execute(
155                service: .hg,
156                query: Self.repositoryInfoQuery,
157                variables: ["rid": repositoryRid],
158                responseType: HgRepositoryInfoResponse.self
159            )
160
161            if let repository = result.repository {
162                editedDescription = repository.description ?? ""
163                editedVisibility = repository.visibility
164                editedNonPublishing = repository.nonPublishing ?? false
165            }
166        } catch {
167            self.error = error.userFacingMessage
168        }
169    }
170
171    func saveInfo() async -> Bool {
172        isSavingInfo = true
173        defer { isSavingInfo = false }
174        error = nil
175
176        do {
177            let input: [String: any Sendable] = [
178                "description": editedDescription,
179                "visibility": editedVisibility.rawValue,
180                "nonPublishing": editedNonPublishing
181            ]
182            _ = try await client.execute(
183                service: .hg,
184                query: Self.updateRepositoryMutation,
185                variables: ["id": repositoryId, "input": input],
186                responseType: HgUpdateRepositoryResponse.self
187            )
188            return true
189        } catch {
190            self.error = error.userFacingMessage
191            return false
192        }
193    }
194
195    func loadACLs() async {
196        guard !isLoadingACLs else { return }
197        isLoadingACLs = true
198        defer { isLoadingACLs = false }
199        error = nil
200
201        do {
202            let result = try await client.execute(
203                service: .hg,
204                query: Self.accessControlListQuery,
205                variables: ["rid": repositoryRid],
206                responseType: HgACLResponse.self
207            )
208            acls = result.repository?.accessControlList.results ?? []
209        } catch {
210            self.error = error.userFacingMessage
211        }
212    }
213
214    func addACL() async {
215        let rawEntity = newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines)
216        guard !rawEntity.isEmpty else { return }
217        let entity = hgCanonicalEntity(from: rawEntity)
218        isAddingACL = true
219        defer { isAddingACL = false }
220        error = nil
221
222        do {
223            let result = try await client.execute(
224                service: .hg,
225                query: Self.updateACLMutation,
226                variables: [
227                    "repoId": repositoryId,
228                    "mode": newACLMode,
229                    "entity": entity
230                ],
231                responseType: HgUpdateACLResponse.self
232            )
233            if let index = acls.firstIndex(where: { $0.id == result.updateACL.id }) {
234                acls[index] = result.updateACL
235            } else {
236                acls.append(result.updateACL)
237            }
238            newACLEntity = ""
239        } catch {
240            let message = error.localizedDescription
241            if message.localizedCaseInsensitiveContains("No such repository or user found") {
242                self.error = "That user is not available on hg.sr.ht yet. They need to create or activate an hg.sr.ht repository first."
243            } else {
244                self.error = error.userFacingMessage
245            }
246        }
247    }
248
249    private func hgCanonicalEntity(from input: String) -> String {
250        let username = input.hasPrefix("~") ? String(input.dropFirst()) : input
251        return "~\(username)"
252    }
253
254    func deleteACL(_ entry: HgACLEntry) async {
255        isDeletingACL = true
256        defer { isDeletingACL = false }
257        error = nil
258
259        do {
260            _ = try await client.execute(
261                service: .hg,
262                query: Self.deleteACLMutation,
263                variables: ["id": entry.id],
264                responseType: HgDeleteACLResponse.self
265            )
266            acls.removeAll { $0.id == entry.id }
267        } catch {
268            self.error = error.userFacingMessage
269        }
270    }
271
272    func deleteRepository() async {
273        isDeleting = true
274        defer { isDeleting = false }
275        error = nil
276
277        do {
278            _ = try await client.execute(
279                service: .hg,
280                query: Self.deleteRepositoryMutation,
281                variables: ["id": repositoryId],
282                responseType: HgDeleteRepositoryResponse.self
283            )
284            didDelete = true
285        } catch {
286            self.error = error.userFacingMessage
287        }
288    }
289}