import Foundation import Observation /// One row of `admin repo list`: every repository, with size and last push. nonisolated struct AdminRepo: Decodable, Sendable, Hashable, Identifiable { let path: String let visibility: String let archived: Bool? let createdAt: Date let lastPush: Date? let bytes: Int64 var id: String { path } var isArchived: Bool { archived ?? false } enum CodingKeys: String, CodingKey { case path, visibility, archived, bytes case createdAt = "created_at" case lastPush = "last_push" } } /// The writes an instance admin makes on any repository, each audited. nonisolated enum AdminRepoAction: Sendable, Hashable { case archive, unarchive, makePublic, makePrivate, reindex, delete func argv(_ path: String) -> [String] { switch self { case .archive: ["admin", "repo", "archive", path] case .unarchive: ["admin", "repo", "unarchive", path] case .makePublic: ["admin", "repo", "visibility", path, "public"] case .makePrivate: ["admin", "repo", "visibility", path, "private"] case .reindex: ["admin", "symbols", "reindex", path] case .delete: ["admin", "repo", "delete", path, "--yes"] } } } /// `admin repo list`, narrowed by owner and visibility, and the per-row writes. @Observable @MainActor final class AdminReposViewModel { let list: PagedListModel var owner = "" /// nil lists both. var visibility: String? private(set) var actionError: String? private(set) var notice: String? private(set) var working = false private let client: GitbayClient init(client: GitbayClient) { self.client = client list = PagedListModel(client: client, argv: ["admin", "repo", "list"], emptyMessage: "No repository matches.") } var argv: [String] { var argv = ["admin", "repo", "list"] let owner = owner.trimmingCharacters(in: .whitespaces) if !owner.isEmpty { argv += ["--owner", owner] } if let visibility { argv += ["--visibility", visibility] } return argv } func load() async { list.argv = argv await list.reload() } func perform(_ action: AdminRepoAction, on repo: AdminRepo) async { working = true actionError = nil notice = nil defer { working = false } do { try await client.run(action.argv(repo.path)) if action == .reindex { notice = "A rebuild of \(repo.path)'s symbol index is queued." } changed(repo, action) } catch GitbayError.notFound where action == .delete { // A failure is retried once; a delete that went through and // then failed answers the retry with not found. changed(repo, action) } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } list.argv = argv await list.refresh() } /// Screens showing the repository elsewhere reload; a rebuilt index /// changes nothing they draw. private func changed(_ repo: AdminRepo, _ action: AdminRepoAction) { guard action != .reindex else { return } NotificationCenter.default.post(name: .gitbayRepositoryChanged, object: repo.path) } } /// `admin org limits `: an organization's repository and storage caps. @Observable @MainActor final class AdminOrgLimitsViewModel { private(set) var limits: OwnerLimits? private(set) var loadedOrg: String? private(set) var actionError: String? private(set) var working = false private let client: GitbayClient init(client: GitbayClient) { self.client = client } /// Not a read-only command (it also sets caps), so it goes as one. func load(org: String) async { let org = org.trimmingCharacters(in: .whitespaces) guard !org.isEmpty else { return } working = true actionError = nil defer { working = false } do { limits = try await client.run(["admin", "org", "limits", org], as: OwnerLimits.self) loadedOrg = org } catch let error as GitbayError { limits = nil loadedOrg = nil actionError = error.messageForTypedInput } catch { actionError = GitbayError.transport(error).userFacingMessage } } func set(repos: String, megabytes: String) async -> Bool { guard let org = loadedOrg else { return false } guard let flags = OwnerLimits.flags(repos: repos, megabytes: megabytes, orgs: nil) else { actionError = "A cap is a whole number, 0 for unlimited, or default for the instance's." return false } guard !flags.isEmpty else { return true } working = true actionError = nil defer { working = false } do { limits = try await client.run(["admin", "org", "limits", org] + flags, as: OwnerLimits.self) return true } catch let error as GitbayError { actionError = error.messageForTypedInput } catch { actionError = GitbayError.transport(error).userFacingMessage } return false } }