gitbay/MRs/MRModels.swift
312 lines · 12877 bytes
1import Foundation
2
3/// The MR below or above a stack member: `mr show` sends `{number, title}`.
4nonisolated struct StackRef: Decodable, Sendable, Hashable, Identifiable {
5 let number: Int64
6 let title: String
7
8 var id: Int64 { number }
9}
10
11/// One row of `mr list`, and the header half of `mr show`.
12nonisolated struct MergeRequest: Decodable, Sendable, Hashable, Identifiable {
13 let number: Int64
14 let title: String
15 let state: String
16 let author: String
17 /// "branch" for a same-repo MR, "owner/name:branch" cross-repo,
18 /// "" when the source is gone.
19 let source: String
20 let targetRef: String
21 let headSHA: String
22 let body: String?
23 let createdAt: Date
24 /// Open but not asking: does not merge, does not appear in a review queue.
25 let draft: Bool
26 let stackedOn: StackRef?
27
28 enum CodingKeys: String, CodingKey {
29 case number, title, state, author, source, body, draft
30 case targetRef = "target_ref"
31 case headSHA = "head_sha"
32 case createdAt = "created_at"
33 case stackedOn = "stacked_on"
34 }
35
36 init(from decoder: Decoder) throws {
37 let container = try decoder.container(keyedBy: CodingKeys.self)
38 number = try container.decode(Int64.self, forKey: .number)
39 title = try container.decode(String.self, forKey: .title)
40 state = try container.decode(String.self, forKey: .state)
41 author = try container.decode(String.self, forKey: .author)
42 source = try container.decode(String.self, forKey: .source)
43 targetRef = try container.decode(String.self, forKey: .targetRef)
44 headSHA = try container.decode(String.self, forKey: .headSHA)
45 body = try container.decodeIfPresent(String.self, forKey: .body)
46 createdAt = try container.decode(Date.self, forKey: .createdAt)
47 draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false
48 stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn)
49 }
50
51 var id: Int64 { number }
52 var isOpen: Bool { state == "open" }
53}
54
55/// One row of `mr revisions`: a head the merge request has had. `n`
56/// counts from the first push; `current` marks the head it has now.
57nonisolated struct MRRevision: Decodable, Sendable, Hashable, Identifiable {
58 let n: Int
59 let sha: String
60 let baseSHA: String?
61 let createdAt: String
62 let current: Bool?
63
64 enum CodingKeys: String, CodingKey {
65 case n, sha, current
66 case baseSHA = "base_sha"
67 case createdAt = "created_at"
68 }
69
70 var id: Int { n }
71 var isCurrent: Bool { current ?? false }
72}
73
74/// The whole of `mr show`.
75nonisolated struct MRDetail: Decodable, Sendable, Hashable {
76 let number: Int64
77 let title: String
78 let state: String
79 let author: String
80 let source: String
81 let targetRef: String
82 let headSHA: String
83 let body: String?
84 let bodyFormat: String?
85 let milestone: String?
86 let createdAt: Date
87 /// Set once the merge request is resolved. The actor is absent when
88 /// the account is gone, or the row was imported.
89 let mergedAt: Date?
90 let mergedBy: String?
91 let closedAt: Date?
92 let closedBy: String?
93 /// The merge request this one was closed in favour of. Omitted by
94 /// the server when there is none.
95 let supersededBy: Int64?
96 let checks: [Check]?
97 let checksCombined: String?
98 let unresolvedThreads: Int?
99 let commits: [MRCommit]?
100 let comments: [MRComment]?
101 let reviews: [Review]?
102 /// Open but not asking: does not merge, does not appear in a review queue.
103 let draft: Bool
104 let reviewRequests: [String]
105 let stackedOn: StackRef?
106 let stacked: [StackRef]
107 /// The merge gates, computed by the server the way `mr merge` will
108 /// judge them. Absent from a server older than v1.16.0.
109 let gates: Gates?
110
111 enum CodingKeys: String, CodingKey {
112 case number, title, state, author, source, body, milestone, checks, commits, comments, reviews, draft, stacked, gates
113 case bodyFormat = "body_format"
114 case targetRef = "target_ref"
115 case headSHA = "head_sha"
116 case createdAt = "created_at"
117 case mergedAt = "merged_at"
118 case mergedBy = "merged_by"
119 case closedAt = "closed_at"
120 case closedBy = "closed_by"
121 case supersededBy = "superseded_by"
122 case checksCombined = "checks_combined"
123 case unresolvedThreads = "unresolved_threads"
124 case reviewRequests = "review_requests"
125 case stackedOn = "stacked_on"
126 }
127
128 init(from decoder: Decoder) throws {
129 let container = try decoder.container(keyedBy: CodingKeys.self)
130 number = try container.decode(Int64.self, forKey: .number)
131 title = try container.decode(String.self, forKey: .title)
132 state = try container.decode(String.self, forKey: .state)
133 author = try container.decode(String.self, forKey: .author)
134 source = try container.decode(String.self, forKey: .source)
135 targetRef = try container.decode(String.self, forKey: .targetRef)
136 headSHA = try container.decode(String.self, forKey: .headSHA)
137 body = try container.decodeIfPresent(String.self, forKey: .body)
138 bodyFormat = try container.decodeIfPresent(String.self, forKey: .bodyFormat)
139 milestone = try container.decodeIfPresent(String.self, forKey: .milestone)
140 createdAt = try container.decode(Date.self, forKey: .createdAt)
141 mergedAt = try container.decodeIfPresent(Date.self, forKey: .mergedAt)
142 mergedBy = try container.decodeIfPresent(String.self, forKey: .mergedBy)
143 closedAt = try container.decodeIfPresent(Date.self, forKey: .closedAt)
144 closedBy = try container.decodeIfPresent(String.self, forKey: .closedBy)
145 supersededBy = try container.decodeIfPresent(Int64.self, forKey: .supersededBy)
146 checks = try container.decodeIfPresent([Check].self, forKey: .checks)
147 checksCombined = try container.decodeIfPresent(String.self, forKey: .checksCombined)
148 unresolvedThreads = try container.decodeIfPresent(Int.self, forKey: .unresolvedThreads)
149 commits = try container.decodeIfPresent([MRCommit].self, forKey: .commits)
150 comments = try container.decodeIfPresent([MRComment].self, forKey: .comments)
151 reviews = try container.decodeIfPresent([Review].self, forKey: .reviews)
152 draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false
153 reviewRequests = try container.decodeIfPresent([String].self, forKey: .reviewRequests) ?? []
154 stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn)
155 stacked = try container.decodeIfPresent([StackRef].self, forKey: .stacked) ?? []
156 gates = try container.decodeIfPresent(Gates.self, forKey: .gates)
157 }
158
159 var isOpen: Bool { state == "open" }
160
161 /// What stands between the merge request and a merge, all at once.
162 nonisolated struct Gates: Decodable, Sendable, Hashable {
163 let draft: Bool
164 let approvalsRequired: Int
165 /// Fresh approvals from reviewers who count.
166 let approvals: [String]
167 let changesRequested: [String]
168 let codeownersRequired: Bool
169 let ownersOutstanding: [Owners]
170 let resolvedRequired: Bool
171 let openThreads: Int
172 let checksRequired: Bool
173 /// Combined check status; nil when none reported.
174 let checks: String?
175 let fastForward: Bool
176 let unmet: [String]
177
178 var isMet: Bool { unmet.isEmpty }
179
180 enum CodingKeys: String, CodingKey {
181 case draft, approvals, checks, unmet
182 case approvalsRequired = "approvals_required"
183 case changesRequested = "changes_requested"
184 case codeownersRequired = "codeowners_required"
185 case ownersOutstanding = "owners_outstanding"
186 case resolvedRequired = "resolved_required"
187 case openThreads = "open_threads"
188 case checksRequired = "checks_required"
189 case fastForward = "fast_forward"
190 }
191
192 init(from decoder: Decoder) throws {
193 let c = try decoder.container(keyedBy: CodingKeys.self)
194 draft = try c.decodeIfPresent(Bool.self, forKey: .draft) ?? false
195 approvalsRequired = try c.decodeIfPresent(Int.self, forKey: .approvalsRequired) ?? 0
196 approvals = try c.decodeIfPresent([String].self, forKey: .approvals) ?? []
197 changesRequested = try c.decodeIfPresent([String].self, forKey: .changesRequested) ?? []
198 codeownersRequired = try c.decodeIfPresent(Bool.self, forKey: .codeownersRequired) ?? false
199 ownersOutstanding = try c.decodeIfPresent([Owners].self, forKey: .ownersOutstanding) ?? []
200 resolvedRequired = try c.decodeIfPresent(Bool.self, forKey: .resolvedRequired) ?? false
201 openThreads = try c.decodeIfPresent(Int.self, forKey: .openThreads) ?? 0
202 checksRequired = try c.decodeIfPresent(Bool.self, forKey: .checksRequired) ?? false
203 checks = try c.decodeIfPresent(String.self, forKey: .checks)
204 fastForward = try c.decodeIfPresent(Bool.self, forKey: .fastForward) ?? false
205 unmet = try c.decodeIfPresent([String].self, forKey: .unmet) ?? []
206 }
207
208 /// Files whose owners have not approved, and who those owners are.
209 nonisolated struct Owners: Decodable, Sendable, Hashable, Identifiable {
210 let files: [String]
211 let owners: [String]
212 var id: String { files.joined(separator: "\u{1f}") }
213 }
214 }
215
216 nonisolated struct Check: Decodable, Sendable, Hashable {
217 let context: String
218 let state: String
219 let url: String?
220 /// When the check last changed state.
221 let updatedAt: Date?
222 /// How long the build behind the check ran, as the server
223 /// formats it ("1m12s"). Only CI checks have a build to time.
224 let duration: String?
225
226 enum CodingKeys: String, CodingKey {
227 case context, state, url, duration
228 case updatedAt = "updated_at"
229 }
230 }
231
232 nonisolated struct MRCommit: Decodable, Sendable, Hashable, Identifiable {
233 let sha: String
234 let subject: String
235 var id: String { sha }
236 var shortSHA: String { String(sha.prefix(10)) }
237 }
238
239 nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable {
240 let author: String
241 let body: String
242 let bodyFormat: String?
243 let createdAt: Date
244
245 enum CodingKeys: String, CodingKey {
246 case author, body
247 case bodyFormat = "body_format"
248 case createdAt = "created_at"
249 }
250
251 var id: String { author + createdAt.timeIntervalSince1970.description + body }
252 }
253
254 nonisolated struct Review: Decodable, Sendable, Hashable, Identifiable {
255 let reviewer: String
256 let verdict: String
257 /// The review predates the current head — it approved older code.
258 let stale: Bool
259 /// Whether the merge gates count this verdict: a reader may review
260 /// a public merge request, but only a writer decides it. Absent
261 /// before v1.16.0, when only writers could review at all.
262 let counts: Bool
263 let createdAt: Date?
264
265 enum CodingKeys: String, CodingKey {
266 case reviewer, verdict, stale, counts
267 case createdAt = "created_at"
268 }
269
270 init(from decoder: Decoder) throws {
271 let c = try decoder.container(keyedBy: CodingKeys.self)
272 reviewer = try c.decode(String.self, forKey: .reviewer)
273 verdict = try c.decode(String.self, forKey: .verdict)
274 stale = try c.decodeIfPresent(Bool.self, forKey: .stale) ?? false
275 counts = try c.decodeIfPresent(Bool.self, forKey: .counts) ?? true
276 createdAt = try c.decodeIfPresent(Date.self, forKey: .createdAt)
277 }
278
279 var id: String { reviewer }
280 }
281}
282
283/// One review thread of `mr threads`.
284nonisolated struct ReviewThread: Decodable, Sendable, Hashable, Identifiable {
285 let id: Int64
286 let path: String
287 let side: String // new | old
288 let line: Int64
289 /// Anchored to an older head; the diff has moved under it.
290 let stale: Bool
291 let resolvedBy: String?
292 let comments: [Comment]
293
294 enum CodingKeys: String, CodingKey {
295 case id, path, side, line, stale, comments
296 case resolvedBy = "resolved_by"
297 }
298
299 var isResolved: Bool { resolvedBy != nil }
300
301 nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable {
302 let id: Int64
303 let author: String
304 let body: String
305 let createdAt: Date
306
307 enum CodingKeys: String, CodingKey {
308 case id, author, body
309 case createdAt = "created_at"
310 }
311 }
312}