gitbay/MRs/MRModels.swift
356 lines · 14817 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 /// Omitted by the server when there are none.
87 let labels: [String]
88 let createdAt: Date
89 /// Set once the merge request is resolved. The actor is absent when
90 /// the account is gone, or the row was imported.
91 let mergedAt: Date?
92 let mergedBy: String?
93 let closedAt: Date?
94 let closedBy: String?
95 /// The merge request this one was closed in favour of. Omitted by
96 /// the server when there is none.
97 let supersededBy: Int64?
98 let checks: [Check]?
99 let checksCombined: String?
100 let unresolvedThreads: Int?
101 let commits: [MRCommit]?
102 let reactions: [ReactionCount]?
103 let comments: [MRComment]?
104 let reviews: [Review]?
105 /// Open but not asking: does not merge, does not appear in a review queue.
106 let draft: Bool
107 let reviewRequests: [String]
108 let stackedOn: StackRef?
109 let stacked: [StackRef]
110 /// The merge gates, computed by the server the way `mr merge` will
111 /// judge them. Absent from a server older than v1.16.0.
112 let gates: Gates?
113 /// A merge queued with `mr merge --when-ready`, waiting on the gates.
114 let queued: Queued?
115
116 enum CodingKeys: String, CodingKey {
117 case number, title, state, author, source, body, milestone, labels, checks, commits, reactions, comments, reviews, draft, stacked, gates, queued
118 case bodyFormat = "body_format"
119 case targetRef = "target_ref"
120 case headSHA = "head_sha"
121 case createdAt = "created_at"
122 case mergedAt = "merged_at"
123 case mergedBy = "merged_by"
124 case closedAt = "closed_at"
125 case closedBy = "closed_by"
126 case supersededBy = "superseded_by"
127 case checksCombined = "checks_combined"
128 case unresolvedThreads = "unresolved_threads"
129 case reviewRequests = "review_requests"
130 case stackedOn = "stacked_on"
131 }
132
133 init(from decoder: Decoder) throws {
134 let container = try decoder.container(keyedBy: CodingKeys.self)
135 number = try container.decode(Int64.self, forKey: .number)
136 title = try container.decode(String.self, forKey: .title)
137 state = try container.decode(String.self, forKey: .state)
138 author = try container.decode(String.self, forKey: .author)
139 source = try container.decode(String.self, forKey: .source)
140 targetRef = try container.decode(String.self, forKey: .targetRef)
141 headSHA = try container.decode(String.self, forKey: .headSHA)
142 body = try container.decodeIfPresent(String.self, forKey: .body)
143 bodyFormat = try container.decodeIfPresent(String.self, forKey: .bodyFormat)
144 milestone = try container.decodeIfPresent(String.self, forKey: .milestone)
145 labels = try container.decodeIfPresent([String].self, forKey: .labels) ?? []
146 createdAt = try container.decode(Date.self, forKey: .createdAt)
147 mergedAt = try container.decodeIfPresent(Date.self, forKey: .mergedAt)
148 mergedBy = try container.decodeIfPresent(String.self, forKey: .mergedBy)
149 closedAt = try container.decodeIfPresent(Date.self, forKey: .closedAt)
150 closedBy = try container.decodeIfPresent(String.self, forKey: .closedBy)
151 supersededBy = try container.decodeIfPresent(Int64.self, forKey: .supersededBy)
152 checks = try container.decodeIfPresent([Check].self, forKey: .checks)
153 checksCombined = try container.decodeIfPresent(String.self, forKey: .checksCombined)
154 unresolvedThreads = try container.decodeIfPresent(Int.self, forKey: .unresolvedThreads)
155 commits = try container.decodeIfPresent([MRCommit].self, forKey: .commits)
156 reactions = try container.decodeIfPresent([ReactionCount].self, forKey: .reactions)
157 comments = try container.decodeIfPresent([MRComment].self, forKey: .comments)
158 reviews = try container.decodeIfPresent([Review].self, forKey: .reviews)
159 draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false
160 reviewRequests = try container.decodeIfPresent([String].self, forKey: .reviewRequests) ?? []
161 stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn)
162 stacked = try container.decodeIfPresent([StackRef].self, forKey: .stacked) ?? []
163 gates = try container.decodeIfPresent(Gates.self, forKey: .gates)
164 queued = try container.decodeIfPresent(Queued.self, forKey: .queued)
165 }
166
167 /// Who queued the merge, with which strategy, and why it has not
168 /// merged yet.
169 nonisolated struct Queued: Decodable, Sendable, Hashable {
170 let by: String
171 let strategy: String?
172 let reason: String?
173 let queuedAt: Date
174
175 enum CodingKeys: String, CodingKey {
176 case by, strategy, reason
177 case queuedAt = "queued_at"
178 }
179 }
180
181 var isOpen: Bool { state == "open" }
182
183 /// What stands between the merge request and a merge, all at once.
184 nonisolated struct Gates: Decodable, Sendable, Hashable {
185 let draft: Bool
186 let approvalsRequired: Int
187 /// Fresh approvals from reviewers who count.
188 let approvals: [String]
189 let changesRequested: [String]
190 let codeownersRequired: Bool
191 let ownersOutstanding: [Owners]
192 let resolvedRequired: Bool
193 let openThreads: Int
194 let checksRequired: Bool
195 /// Combined check status; nil when none reported.
196 let checks: String?
197 let fastForward: Bool
198 let unmet: [String]
199
200 var isMet: Bool { unmet.isEmpty }
201
202 enum CodingKeys: String, CodingKey {
203 case draft, approvals, checks, unmet
204 case approvalsRequired = "approvals_required"
205 case changesRequested = "changes_requested"
206 case codeownersRequired = "codeowners_required"
207 case ownersOutstanding = "owners_outstanding"
208 case resolvedRequired = "resolved_required"
209 case openThreads = "open_threads"
210 case checksRequired = "checks_required"
211 case fastForward = "fast_forward"
212 }
213
214 init(from decoder: Decoder) throws {
215 let c = try decoder.container(keyedBy: CodingKeys.self)
216 draft = try c.decodeIfPresent(Bool.self, forKey: .draft) ?? false
217 approvalsRequired = try c.decodeIfPresent(Int.self, forKey: .approvalsRequired) ?? 0
218 approvals = try c.decodeIfPresent([String].self, forKey: .approvals) ?? []
219 changesRequested = try c.decodeIfPresent([String].self, forKey: .changesRequested) ?? []
220 codeownersRequired = try c.decodeIfPresent(Bool.self, forKey: .codeownersRequired) ?? false
221 ownersOutstanding = try c.decodeIfPresent([Owners].self, forKey: .ownersOutstanding) ?? []
222 resolvedRequired = try c.decodeIfPresent(Bool.self, forKey: .resolvedRequired) ?? false
223 openThreads = try c.decodeIfPresent(Int.self, forKey: .openThreads) ?? 0
224 checksRequired = try c.decodeIfPresent(Bool.self, forKey: .checksRequired) ?? false
225 checks = try c.decodeIfPresent(String.self, forKey: .checks)
226 fastForward = try c.decodeIfPresent(Bool.self, forKey: .fastForward) ?? false
227 unmet = try c.decodeIfPresent([String].self, forKey: .unmet) ?? []
228 }
229
230 /// Files whose owners have not approved, and who those owners are.
231 nonisolated struct Owners: Decodable, Sendable, Hashable, Identifiable {
232 let files: [String]
233 let owners: [String]
234 var id: String { files.joined(separator: "\u{1f}") }
235 }
236 }
237
238 nonisolated struct Check: Decodable, Sendable, Hashable {
239 let context: String
240 let state: String
241 let url: String?
242 /// When the check last changed state.
243 let updatedAt: Date?
244 /// How long the build behind the check ran, as the server
245 /// formats it ("1m12s"). Only CI checks have a build to time.
246 let duration: String?
247
248 enum CodingKeys: String, CodingKey {
249 case context, state, url, duration
250 case updatedAt = "updated_at"
251 }
252 }
253
254 nonisolated struct MRCommit: Decodable, Sendable, Hashable, Identifiable {
255 let sha: String
256 let subject: String
257 var id: String { sha }
258 var shortSHA: String { String(sha.prefix(10)) }
259 }
260
261 nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable {
262 /// What `--comment` names. Absent from a server older than v1.39.0.
263 let commentID: Int64?
264 let author: String
265 let body: String
266 let bodyFormat: String?
267 let createdAt: Date
268 let reactions: [ReactionCount]?
269 /// "system" for a line the server posted (a retarget, a queued
270 /// merge), which takes no reactions (krz/gitbay#332).
271 let kind: String?
272
273 enum CodingKeys: String, CodingKey {
274 case author, body, reactions, kind
275 case commentID = "id"
276 case bodyFormat = "body_format"
277 case createdAt = "created_at"
278 }
279
280 /// A server without `kind` names the author "system" on those lines;
281 /// an account can carry that name too, so `kind` wins when present.
282 var isSystem: Bool { kind.map { $0 == "system" } ?? (author == "system") }
283
284 var id: String { author + createdAt.timeIntervalSince1970.description + body }
285 }
286
287 nonisolated struct Review: Decodable, Sendable, Hashable, Identifiable {
288 let reviewer: String
289 let verdict: String
290 /// The review predates the current head — it approved older code.
291 let stale: Bool
292 /// Whether the merge gates count this verdict: a reader may review
293 /// a public merge request, but only a writer decides it. Absent
294 /// before v1.16.0, when only writers could review at all.
295 let counts: Bool
296 let createdAt: Date?
297
298 enum CodingKeys: String, CodingKey {
299 case reviewer, verdict, stale, counts
300 case createdAt = "created_at"
301 }
302
303 init(from decoder: Decoder) throws {
304 let c = try decoder.container(keyedBy: CodingKeys.self)
305 reviewer = try c.decode(String.self, forKey: .reviewer)
306 verdict = try c.decode(String.self, forKey: .verdict)
307 stale = try c.decodeIfPresent(Bool.self, forKey: .stale) ?? false
308 counts = try c.decodeIfPresent(Bool.self, forKey: .counts) ?? true
309 createdAt = try c.decodeIfPresent(Date.self, forKey: .createdAt)
310 }
311
312 var id: String { reviewer }
313 }
314}
315
316/// One review thread of `mr threads`.
317nonisolated struct ReviewThread: Decodable, Sendable, Hashable, Identifiable {
318 let id: Int64
319 let path: String
320 let side: String // new | old
321 let line: Int64
322 /// The first line of a range; absent for a one-line thread.
323 let startLine: Int64?
324 /// Anchored to an older head; the diff has moved under it.
325 let stale: Bool
326 let resolvedBy: String?
327 /// What the thread's ```suggestion block proposes (v1.39.0).
328 let suggestion: Suggestion?
329 let comments: [Comment]
330
331 enum CodingKeys: String, CodingKey {
332 case id, path, side, line, stale, suggestion, comments
333 case startLine = "start_line"
334 case resolvedBy = "resolved_by"
335 }
336
337 /// `path:line`, or `path:start–line` for a range.
338 var anchor: String {
339 if let startLine, startLine < line { return "\(path):\(startLine)–\(line)" }
340 return "\(path):\(line)"
341 }
342
343 var isResolved: Bool { resolvedBy != nil }
344
345 nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable {
346 let id: Int64
347 let author: String
348 let body: String
349 let createdAt: Date
350
351 enum CodingKeys: String, CodingKey {
352 case id, author, body
353 case createdAt = "created_at"
354 }
355 }
356}