gitbay/Admin/AdminRepos.swift
155 lines · 5364 bytes
9 symbols in this file
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}