gitbay/Repos/RepoModels.swift
212 lines · 7232 bytes
1import Foundation
2
3/// One row of `repo list`.
4nonisolated struct RepoSummary: Decodable, Sendable, Hashable, Identifiable {
5 let path: String
6 let visibility: String
7 let description: String?
8 let archived: Bool?
9 /// Present on `repo search` rows, absent on `repo list`.
10 let topics: [String]?
11
12 var id: String { path }
13 var isArchived: Bool { archived ?? false }
14 var isPrivate: Bool { visibility == "private" }
15
16 /// "krz/gitbay" → ("krz", "gitbay")
17 var owner: String { String(path.split(separator: "/").first ?? "") }
18 var name: String { String(path.split(separator: "/").last ?? "") }
19}
20
21/// `repo show <owner/name>`.
22nonisolated struct RepoDetail: Decodable, Sendable, Hashable {
23 let path: String
24 let description: String?
25 let website: String?
26 let visibility: String
27 let defaultBranch: String
28 let protectedBranches: [String]?
29 let archived: Bool?
30 let topics: [String]?
31 /// The caller's own watch state: `"watching"`, `"muted"`, or absent
32 /// for the default (neither).
33 let watch: String?
34 /// `omitempty` on a Bool, so absent means false, not unknown.
35 let bookmarked: Bool
36 /// Absent means either not a fork, or a fork whose parent the caller
37 /// cannot read — the server withholds it rather than confirm a
38 /// private parent exists. Nothing further may be inferred from nil.
39 let forkOf: String?
40
41 enum CodingKeys: String, CodingKey {
42 case path, description, website, visibility, archived, topics, watch, bookmarked
43 case defaultBranch = "default_branch"
44 case protectedBranches = "protected_branches"
45 case forkOf = "fork_of"
46 }
47
48 init(from decoder: any Decoder) throws {
49 let container = try decoder.container(keyedBy: CodingKeys.self)
50 path = try container.decode(String.self, forKey: .path)
51 description = try container.decodeIfPresent(String.self, forKey: .description)
52 website = try container.decodeIfPresent(String.self, forKey: .website)
53 visibility = try container.decode(String.self, forKey: .visibility)
54 defaultBranch = try container.decode(String.self, forKey: .defaultBranch)
55 protectedBranches = try container.decodeIfPresent([String].self, forKey: .protectedBranches)
56 archived = try container.decodeIfPresent(Bool.self, forKey: .archived)
57 topics = try container.decodeIfPresent([String].self, forKey: .topics)
58 watch = try container.decodeIfPresent(String.self, forKey: .watch)
59 bookmarked = try container.decodeIfPresent(Bool.self, forKey: .bookmarked) ?? false
60 forkOf = try container.decodeIfPresent(String.self, forKey: .forkOf)
61 }
62
63 var isArchived: Bool { archived ?? false }
64}
65
66/// The caller's watch/mute state on a repository, from `RepoDetail.watch`.
67/// `repo watch` sets `.watching`, `repo mute` sets `.muted`, and
68/// `repo unwatch` clears either back to `.default`, so `available` offers
69/// unwatch from both explicit states and never from the default.
70nonisolated enum WatchState: Sendable, Hashable {
71 case watching
72 case muted
73 case `default`
74
75 /// Unrecognized raw values (a future server addition) fall back to
76 /// `.default` rather than crashing or masquerading as watching.
77 init(_ raw: String?) {
78 switch raw {
79 case "watching": self = .watching
80 case "muted": self = .muted
81 default: self = .default
82 }
83 }
84
85 var available: [WatchAction] {
86 switch self {
87 case .watching: [.unwatch, .mute]
88 case .muted: [.watch, .unwatch]
89 case .default: [.watch, .mute]
90 }
91 }
92}
93
94/// A transition `WatchState.available` may offer.
95nonisolated enum WatchAction: Sendable, Hashable {
96 case watch
97 case unwatch
98 case mute
99}
100
101/// `repo refs <owner/name>` — the branches and tags the server knows.
102nonisolated struct RepoRefs: Decodable, Sendable, Hashable {
103 let branches: [RepoRef]
104 let tags: [RepoRef]
105}
106
107nonisolated struct RepoRef: Decodable, Sendable, Hashable, Identifiable {
108 let name: String
109 let sha: String
110 var id: String { name }
111}
112
113/// `repo tree` — one directory listing.
114nonisolated struct TreeListing: Decodable, Sendable, Hashable {
115 let path: String
116 let ref: String
117 let dir: String
118 let entries: [TreeEntry]
119}
120
121/// One entry of a tree. The sha is the git object id — content at a sha
122/// never changes, which is what makes caching on it sound.
123nonisolated struct TreeEntry: Decodable, Sendable, Hashable, Identifiable {
124 let name: String
125 let type: String // blob | tree
126 let mode: String
127 let sha: String
128 let size: Int64?
129
130 var id: String { sha + name }
131 var isDirectory: Bool { type == "tree" }
132 var isSymlink: Bool { mode == "120000" }
133}
134
135/// `repo cat` — one file. Exactly one of `content`/`base64` is set.
136nonisolated struct FileContent: Decodable, Sendable, Hashable {
137 let path: String
138 let ref: String
139 let file: String
140 let size: Int
141 let binary: Bool
142 let truncated: Bool?
143 let content: String?
144 let base64: String?
145
146 var isTruncated: Bool { truncated ?? false }
147
148 var data: Data? {
149 if let content { return Data(content.utf8) }
150 if let base64 { return Data(base64Encoded: base64) }
151 return nil
152 }
153}
154
155/// `repo log` — one commit, with its signature verdict.
156nonisolated struct Commit: Decodable, Sendable, Hashable, Identifiable {
157 let sha: String
158 let subject: String
159 let authorName: String
160 let authorEmail: String
161 let committerEmail: String?
162 let date: Date
163 let signature: Signature
164
165 enum CodingKeys: String, CodingKey {
166 case sha, subject, date, signature
167 case authorName = "author_name"
168 case authorEmail = "author_email"
169 case committerEmail = "committer_email"
170 }
171
172 var id: String { sha }
173 var shortSHA: String { String(sha.prefix(10)) }
174
175 nonisolated struct Signature: Decodable, Sendable, Hashable {
176 let state: SignatureState
177 let signer: String?
178 let keyFingerprint: String?
179
180 enum CodingKeys: String, CodingKey {
181 case state, signer
182 case keyFingerprint = "key_fingerprint"
183 }
184 }
185}
186
187/// The server's verdict on a commit signature; `internal/sig/verify.go`.
188/// Unknown future states decode as `.unrecognized` rather than failing the
189/// whole log.
190nonisolated enum SignatureState: Sendable, Hashable, Decodable {
191 case verified
192 case signedUnknownKey
193 case signedEmailMismatch
194 case signedKeyExpired
195 case signedKeyRevoked
196 case badSignature
197 case unsigned
198 case unrecognized(String)
199
200 init(from decoder: any Decoder) throws {
201 switch try decoder.singleValueContainer().decode(String.self) {
202 case "verified": self = .verified
203 case "signed_unknown_key": self = .signedUnknownKey
204 case "signed_email_mismatch": self = .signedEmailMismatch
205 case "signed_key_expired": self = .signedKeyExpired
206 case "signed_key_revoked": self = .signedKeyRevoked
207 case "bad_signature": self = .badSignature
208 case "unsigned": self = .unsigned
209 case let other: self = .unrecognized(other)
210 }
211 }
212}