summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift')
-rw-r--r--Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift287
1 files changed, 287 insertions, 0 deletions
diff --git a/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift
new file mode 100644
index 0000000..3e431f8
--- /dev/null
+++ b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift
@@ -0,0 +1,287 @@
+import Foundation
+
+private struct HgUpdateRepositoryResponse: Decodable, Sendable {
+ let updateRepository: HgUpdatedRepository
+}
+
+private struct HgUpdatedRepository: Decodable, Sendable {
+ let id: Int
+}
+
+private struct HgRepositoryInfoResponse: Decodable, Sendable {
+ let repository: HgRepositoryInfo?
+}
+
+private struct HgRepositoryInfo: Decodable, Sendable {
+ let description: String?
+ let visibility: Visibility
+ let nonPublishing: Bool?
+}
+
+private struct HgACLResponse: Decodable, Sendable {
+ let repository: HgACLRepository?
+}
+
+private struct HgACLRepository: Decodable, Sendable {
+ let accessControlList: HgACLPage
+}
+
+private struct HgACLPage: Decodable, Sendable {
+ let results: [HgACLEntry]
+ let cursor: String?
+}
+
+private struct HgUpdateACLResponse: Decodable, Sendable {
+ let updateACL: HgACLEntry
+}
+
+private struct HgDeleteACLResponse: Decodable, Sendable {
+ let deleteACL: HgDeletedACL
+}
+
+private struct HgDeletedACL: Decodable, Sendable {
+ let id: Int
+}
+
+private struct HgDeleteRepositoryResponse: Decodable, Sendable {
+ let deleteRepository: HgDeletedRepository
+}
+
+private struct HgDeletedRepository: Decodable, Sendable {
+ let id: Int
+}
+
+struct HgACLEntry: Decodable, Sendable, Identifiable {
+ let id: Int
+ let mode: String
+ let entity: Entity
+}
+
+@Observable
+@MainActor
+final class HgRepositorySettingsViewModel {
+ let repositoryId: Int
+ let repositoryRid: String
+ let repositoryName: String
+ private let client: SRHTClient
+
+ var editedDescription: String
+ var editedVisibility: Visibility
+ var editedNonPublishing: Bool
+ var isSavingInfo = false
+
+ private(set) var acls: [HgACLEntry] = []
+ private(set) var isLoadingACLs = false
+ var newACLEntity = ""
+ var newACLMode = "RO"
+ var isAddingACL = false
+ var isDeletingACL = false
+
+ var histeditRevision = ""
+ var isDeleting = false
+ var didDelete = false
+ var error: String?
+
+ init(repository: RepositorySummary, client: SRHTClient) {
+ self.repositoryId = repository.id
+ self.repositoryRid = repository.rid
+ self.repositoryName = repository.name
+ self.client = client
+ self.editedDescription = repository.description ?? ""
+ self.editedVisibility = repository.visibility
+ self.editedNonPublishing = false
+ }
+
+ private static let updateRepositoryMutation = """
+ mutation updateRepository($id: Int!, $input: RepoInput!) {
+ updateRepository(id: $id, input: $input) {
+ id
+ }
+ }
+ """
+
+ private static let accessControlListQuery = """
+ query hgAccessControlList($rid: ID!) {
+ repository(rid: $rid) {
+ accessControlList {
+ results {
+ id
+ mode
+ entity { canonicalName }
+ }
+ cursor
+ }
+ }
+ }
+ """
+
+ private static let updateACLMutation = """
+ mutation updateACL($repoId: Int!, $mode: AccessMode!, $entity: String!) {
+ updateACL(repoId: $repoId, mode: $mode, entity: $entity) {
+ id
+ mode
+ entity { canonicalName }
+ }
+ }
+ """
+
+ private static let deleteACLMutation = """
+ mutation deleteACL($id: Int!) {
+ deleteACL(id: $id) { id }
+ }
+ """
+
+ private static let deleteRepositoryMutation = """
+ mutation deleteRepository($id: Int!) {
+ deleteRepository(id: $id) { id }
+ }
+ """
+
+ private static let repositoryInfoQuery = """
+ query hgRepositoryInfo($rid: ID!) {
+ repository(rid: $rid) {
+ description
+ visibility
+ nonPublishing
+ }
+ }
+ """
+
+ func loadRepositoryInfo() async {
+ error = nil
+
+ do {
+ let result = try await client.execute(
+ service: .hg,
+ query: Self.repositoryInfoQuery,
+ variables: ["rid": repositoryRid],
+ responseType: HgRepositoryInfoResponse.self
+ )
+
+ if let repository = result.repository {
+ editedDescription = repository.description ?? ""
+ editedVisibility = repository.visibility
+ editedNonPublishing = repository.nonPublishing ?? false
+ }
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ func saveInfo() async {
+ isSavingInfo = true
+ defer { isSavingInfo = false }
+ error = nil
+
+ do {
+ let input: [String: any Sendable] = [
+ "description": editedDescription,
+ "visibility": editedVisibility.rawValue,
+ "nonPublishing": editedNonPublishing
+ ]
+ _ = try await client.execute(
+ service: .hg,
+ query: Self.updateRepositoryMutation,
+ variables: ["id": repositoryId, "input": input],
+ responseType: HgUpdateRepositoryResponse.self
+ )
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ func loadACLs() async {
+ guard !isLoadingACLs else { return }
+ isLoadingACLs = true
+ defer { isLoadingACLs = false }
+ error = nil
+
+ do {
+ let result = try await client.execute(
+ service: .hg,
+ query: Self.accessControlListQuery,
+ variables: ["rid": repositoryRid],
+ responseType: HgACLResponse.self
+ )
+ acls = result.repository?.accessControlList.results ?? []
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ func addACL() async {
+ let rawEntity = newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !rawEntity.isEmpty else { return }
+ let entity = hgCanonicalEntity(from: rawEntity)
+ isAddingACL = true
+ defer { isAddingACL = false }
+ error = nil
+
+ do {
+ let result = try await client.execute(
+ service: .hg,
+ query: Self.updateACLMutation,
+ variables: [
+ "repoId": repositoryId,
+ "mode": newACLMode,
+ "entity": entity
+ ],
+ responseType: HgUpdateACLResponse.self
+ )
+ if let index = acls.firstIndex(where: { $0.id == result.updateACL.id }) {
+ acls[index] = result.updateACL
+ } else {
+ acls.append(result.updateACL)
+ }
+ newACLEntity = ""
+ } catch {
+ let message = error.localizedDescription
+ if message.localizedCaseInsensitiveContains("No such repository or user found") {
+ self.error = "That user is not available on hg.sr.ht yet. They need to create or activate an hg.sr.ht repository first."
+ } else {
+ self.error = message
+ }
+ }
+ }
+
+ private func hgCanonicalEntity(from input: String) -> String {
+ let username = input.hasPrefix("~") ? String(input.dropFirst()) : input
+ return "~\(username)"
+ }
+
+ func deleteACL(_ entry: HgACLEntry) async {
+ isDeletingACL = true
+ defer { isDeletingACL = false }
+ error = nil
+
+ do {
+ _ = try await client.execute(
+ service: .hg,
+ query: Self.deleteACLMutation,
+ variables: ["id": entry.id],
+ responseType: HgDeleteACLResponse.self
+ )
+ acls.removeAll { $0.id == entry.id }
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ func deleteRepository() async {
+ isDeleting = true
+ defer { isDeleting = false }
+ error = nil
+
+ do {
+ _ = try await client.execute(
+ service: .hg,
+ query: Self.deleteRepositoryMutation,
+ variables: ["id": repositoryId],
+ responseType: HgDeleteRepositoryResponse.self
+ )
+ didDelete = true
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+}