gitbay/Admin/AdminRepos.swift

v1.14.1
gitbay-ios/gitbay/Admin/AdminRepos.swift history · blame · raw

155 lines · 5364 bytes

  1import Foundation
  2import Observation
  3
  4/// One row of `admin repo list`: every repository, with size and last push.
  5nonisolated struct AdminRepo: Decodable, Sendable, Hashable, Identifiable {
  6    let path: String
  7    let visibility: String
  8    let archived: Bool?
  9    let createdAt: Date
 10    let lastPush: Date?
 11    let bytes: Int64
 12    var id: String { path }
 13    var isArchived: Bool { archived ?? false }
 14    enum CodingKeys: String, CodingKey {
 15        case path, visibility, archived, bytes
 16        case createdAt = "created_at"
 17        case lastPush = "last_push"
 18    }
 19}
 20
 21/// The writes an instance admin makes on any repository, each audited.
 22nonisolated enum AdminRepoAction: Sendable, Hashable {
 23    case archive, unarchive, makePublic, makePrivate, reindex, delete
 24
 25    func argv(_ path: String) -> [String] {
 26        switch self {
 27        case .archive: ["admin", "repo", "archive", path]
 28        case .unarchive: ["admin", "repo", "unarchive", path]
 29        case .makePublic: ["admin", "repo", "visibility", path, "public"]
 30        case .makePrivate: ["admin", "repo", "visibility", path, "private"]
 31        case .reindex: ["admin", "symbols", "reindex", path]
 32        case .delete: ["admin", "repo", "delete", path, "--yes"]
 33        }
 34    }
 35}
 36
 37/// `admin repo list`, narrowed by owner and visibility, and the per-row writes.
 38@Observable
 39@MainActor
 40final class AdminReposViewModel {
 41
 42    let list: PagedListModel<AdminRepo>
 43    var owner = ""
 44    /// nil lists both.
 45    var visibility: String?
 46    private(set) var actionError: String?
 47    private(set) var notice: String?
 48    private(set) var working = false
 49
 50    private let client: GitbayClient
 51
 52    init(client: GitbayClient) {
 53        self.client = client
 54        list = PagedListModel(client: client, argv: ["admin", "repo", "list"], emptyMessage: "No repository matches.")
 55    }
 56
 57    var argv: [String] {
 58        var argv = ["admin", "repo", "list"]
 59        let owner = owner.trimmingCharacters(in: .whitespaces)
 60        if !owner.isEmpty { argv += ["--owner", owner] }
 61        if let visibility { argv += ["--visibility", visibility] }
 62        return argv
 63    }
 64
 65    func load() async {
 66        list.argv = argv
 67        await list.reload()
 68    }
 69
 70    func perform(_ action: AdminRepoAction, on repo: AdminRepo) async {
 71        working = true
 72        actionError = nil
 73        notice = nil
 74        defer { working = false }
 75        do {
 76            try await client.run(action.argv(repo.path))
 77            if action == .reindex { notice = "A rebuild of \(repo.path)'s symbol index is queued." }
 78            changed(repo, action)
 79        } catch GitbayError.notFound where action == .delete {
 80            // A failure is retried once; a delete that went through and
 81            // then failed answers the retry with not found.
 82            changed(repo, action)
 83        } catch let error as GitbayError {
 84            actionError = error.userFacingMessage
 85        } catch {
 86            actionError = GitbayError.transport(error).userFacingMessage
 87        }
 88        list.argv = argv
 89        await list.refresh()
 90    }
 91
 92    /// Screens showing the repository elsewhere reload; a rebuilt index
 93    /// changes nothing they draw.
 94    private func changed(_ repo: AdminRepo, _ action: AdminRepoAction) {
 95        guard action != .reindex else { return }
 96        NotificationCenter.default.post(name: .gitbayRepositoryChanged, object: repo.path)
 97    }
 98}
 99
100/// `admin org limits <org>`: an organization's repository and storage caps.
101@Observable
102@MainActor
103final class AdminOrgLimitsViewModel {
104
105    private(set) var limits: OwnerLimits?
106    private(set) var loadedOrg: String?
107    private(set) var actionError: String?
108    private(set) var working = false
109
110    private let client: GitbayClient
111
112    init(client: GitbayClient) {
113        self.client = client
114    }
115
116    /// Not a read-only command (it also sets caps), so it goes as one.
117    func load(org: String) async {
118        let org = org.trimmingCharacters(in: .whitespaces)
119        guard !org.isEmpty else { return }
120        working = true
121        actionError = nil
122        defer { working = false }
123        do {
124            limits = try await client.run(["admin", "org", "limits", org], as: OwnerLimits.self)
125            loadedOrg = org
126        } catch let error as GitbayError {
127            limits = nil
128            loadedOrg = nil
129            actionError = error.messageForTypedInput
130        } catch {
131            actionError = GitbayError.transport(error).userFacingMessage
132        }
133    }
134
135    func set(repos: String, megabytes: String) async -> Bool {
136        guard let org = loadedOrg else { return false }
137        guard let flags = OwnerLimits.flags(repos: repos, megabytes: megabytes, orgs: nil) else {
138            actionError = "A cap is a whole number, 0 for unlimited, or default for the instance's."
139            return false
140        }
141        guard !flags.isEmpty else { return true }
142        working = true
143        actionError = nil
144        defer { working = false }
145        do {
146            limits = try await client.run(["admin", "org", "limits", org] + flags, as: OwnerLimits.self)
147            return true
148        } catch let error as GitbayError {
149            actionError = error.messageForTypedInput
150        } catch {
151            actionError = GitbayError.transport(error).userFacingMessage
152        }
153        return false
154    }
155}