summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-17 23:19:22 -0500
committerChristian Cleberg <[email protected]>2026-03-17 23:19:22 -0500
commit8f2057c53e9009c2529c9c4849c914666c0e4b40 (patch)
tree77b5a93625fe9ea98baf4ab0d137055524705dad /Hutch/Views/Repositories/RepositorySettingsViewModel.swift
parentd27726d8755a5631fbd2105f735811a95379c4ab (diff)
downloadhutch-8f2057c53e9009c2529c9c4849c914666c0e4b40.tar.gz
hutch-8f2057c53e9009c2529c9c4849c914666c0e4b40.tar.bz2
hutch-8f2057c53e9009c2529c9c4849c914666c0e4b40.zip
create privacy policy
Diffstat (limited to 'Hutch/Views/Repositories/RepositorySettingsViewModel.swift')
-rw-r--r--Hutch/Views/Repositories/RepositorySettingsViewModel.swift301
1 files changed, 301 insertions, 0 deletions
diff --git a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
new file mode 100644
index 0000000..bdae87f
--- /dev/null
+++ b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
@@ -0,0 +1,301 @@
+import Foundation
+
+// MARK: - Response types
+
+private struct UpdateRepoResponse: Decodable, Sendable {
+ let updateRepository: UpdatedRepo
+}
+
+private struct UpdatedRepo: Decodable, Sendable {
+ let id: Int
+ let rid: String
+ let name: String
+ let description: String?
+ let visibility: Visibility
+}
+
+private struct ACLResponse: Decodable, Sendable {
+ let repository: ACLRepository?
+}
+
+private struct ACLRepository: Decodable, Sendable {
+ let acls: ACLPage
+}
+
+private struct ACLPage: Decodable, Sendable {
+ let results: [ACLEntry]
+ let cursor: String?
+}
+
+private struct UpdateACLResponse: Decodable, Sendable {
+ let updateACL: ACLEntry
+}
+
+private struct DeleteACLResponse: Decodable, Sendable {
+ let deleteACL: DeletedACL
+}
+
+private struct DeletedACL: Decodable, Sendable {
+ let id: Int
+}
+
+private struct DeleteRepoResponse: Decodable, Sendable {
+ let deleteRepository: DeletedRepo
+}
+
+private struct DeletedRepo: Decodable, Sendable {
+ let id: Int
+}
+
+// MARK: - ACL Model
+
+struct ACLEntry: Decodable, Sendable, Identifiable {
+ let id: Int
+ let mode: String
+ let entity: Entity
+}
+
+// MARK: - View Model
+
+@Observable
+@MainActor
+final class RepositorySettingsViewModel {
+
+ let repositoryId: Int
+ let repositoryRid: String
+ private let client: SRHTClient
+
+ // MARK: - Info fields
+
+ var editedDescription: String
+ var editedVisibility: Visibility
+ var editedHead: String
+ var isSavingInfo = false
+
+ // MARK: - Rename fields
+
+ var editedName: String
+ var isRenaming = false
+
+ // MARK: - ACL state
+
+ private(set) var acls: [ACLEntry] = []
+ private(set) var isLoadingACLs = false
+ var newACLEntity = ""
+ var newACLMode = "RO"
+ var isAddingACL = false
+ var isDeletingACL = false
+
+ // MARK: - Delete state
+
+ var isDeleting = false
+
+ // MARK: - Branches (for HEAD picker)
+
+ var branches: [Reference]
+
+ // MARK: - Results
+
+ var error: String?
+ var updatedName: String?
+ var didDelete = false
+
+ init(
+ repository: RepositorySummary,
+ branches: [Reference],
+ client: SRHTClient
+ ) {
+ self.repositoryId = repository.id
+ self.repositoryRid = repository.rid
+ self.client = client
+ self.editedDescription = repository.description ?? ""
+ self.editedVisibility = repository.visibility
+ self.editedName = repository.name
+ self.branches = branches
+
+ // Extract branch name from HEAD reference
+ if let head = repository.head?.name {
+ self.editedHead = head.replacingOccurrences(of: "refs/heads/", with: "")
+ } else {
+ self.editedHead = "main"
+ }
+ }
+
+ // MARK: - Update Repository Info
+
+ private static let updateRepoMutation = """
+ mutation updateRepository($id: Int!, $input: RepoInput!) {
+ updateRepository(id: $id, input: $input) {
+ id rid name description visibility
+ }
+ }
+ """
+
+ func saveInfo() async {
+ isSavingInfo = true
+ defer { isSavingInfo = false }
+ error = nil
+
+ do {
+ let input: [String: any Sendable] = [
+ "description": editedDescription,
+ "visibility": editedVisibility.rawValue,
+ "HEAD": editedHead
+ ]
+ _ = try await client.execute(
+ service: .git,
+ query: Self.updateRepoMutation,
+ variables: ["id": repositoryId, "input": input],
+ responseType: UpdateRepoResponse.self
+ )
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ // MARK: - Rename
+
+ func rename() async {
+ isRenaming = true
+ defer { isRenaming = false }
+ error = nil
+
+ do {
+ let input: [String: any Sendable] = [
+ "name": editedName
+ ]
+ let result = try await client.execute(
+ service: .git,
+ query: Self.updateRepoMutation,
+ variables: ["id": repositoryId, "input": input],
+ responseType: UpdateRepoResponse.self
+ )
+ updatedName = result.updateRepository.name
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ // MARK: - ACLs
+
+ private static let aclsQuery = """
+ query acls($rid: ID!) {
+ repository(rid: $rid) {
+ acls {
+ 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 }
+ }
+ """
+
+ func loadACLs() async {
+ guard !isLoadingACLs else { return }
+ isLoadingACLs = true
+ defer { isLoadingACLs = false }
+
+ do {
+ let result = try await client.execute(
+ service: .git,
+ query: Self.aclsQuery,
+ variables: ["rid": repositoryRid],
+ responseType: ACLResponse.self
+ )
+ acls = result.repository?.acls.results ?? []
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ func addACL() async {
+ let entity = newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !entity.isEmpty else { return }
+ isAddingACL = true
+ defer { isAddingACL = false }
+ error = nil
+
+ do {
+ let result = try await client.execute(
+ service: .git,
+ query: Self.updateACLMutation,
+ variables: [
+ "repoId": repositoryId,
+ "mode": newACLMode,
+ "entity": entity
+ ],
+ responseType: UpdateACLResponse.self
+ )
+ // Replace existing entry or append
+ if let index = acls.firstIndex(where: { $0.id == result.updateACL.id }) {
+ acls[index] = result.updateACL
+ } else {
+ acls.append(result.updateACL)
+ }
+ newACLEntity = ""
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ func deleteACL(_ entry: ACLEntry) async {
+ isDeletingACL = true
+ defer { isDeletingACL = false }
+ error = nil
+
+ do {
+ _ = try await client.execute(
+ service: .git,
+ query: Self.deleteACLMutation,
+ variables: ["id": entry.id],
+ responseType: DeleteACLResponse.self
+ )
+ acls.removeAll { $0.id == entry.id }
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+
+ // MARK: - Delete Repository
+
+ private static let deleteRepoMutation = """
+ mutation deleteRepository($id: Int!) {
+ deleteRepository(id: $id) { id }
+ }
+ """
+
+ func deleteRepository() async {
+ isDeleting = true
+ defer { isDeleting = false }
+ error = nil
+
+ do {
+ _ = try await client.execute(
+ service: .git,
+ query: Self.deleteRepoMutation,
+ variables: ["id": repositoryId],
+ responseType: DeleteRepoResponse.self
+ )
+ didDelete = true
+ } catch {
+ self.error = error.localizedDescription
+ }
+ }
+}