gitbay/MRs/MRModels.swift
235 lines · 8890 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 let checks: [Check]?
94 let checksCombined: String?
95 let unresolvedThreads: Int?
96 let commits: [MRCommit]?
97 let comments: [MRComment]?
98 let reviews: [Review]?
99 /// Open but not asking: does not merge, does not appear in a review queue.
100 let draft: Bool
101 let reviewRequests: [String]
102 let stackedOn: StackRef?
103 let stacked: [StackRef]
104
105 enum CodingKeys: String, CodingKey {
106 case number, title, state, author, source, body, milestone, checks, commits, comments, reviews, draft, stacked
107 case bodyFormat = "body_format"
108 case targetRef = "target_ref"
109 case headSHA = "head_sha"
110 case createdAt = "created_at"
111 case mergedAt = "merged_at"
112 case mergedBy = "merged_by"
113 case closedAt = "closed_at"
114 case closedBy = "closed_by"
115 case checksCombined = "checks_combined"
116 case unresolvedThreads = "unresolved_threads"
117 case reviewRequests = "review_requests"
118 case stackedOn = "stacked_on"
119 }
120
121 init(from decoder: Decoder) throws {
122 let container = try decoder.container(keyedBy: CodingKeys.self)
123 number = try container.decode(Int64.self, forKey: .number)
124 title = try container.decode(String.self, forKey: .title)
125 state = try container.decode(String.self, forKey: .state)
126 author = try container.decode(String.self, forKey: .author)
127 source = try container.decode(String.self, forKey: .source)
128 targetRef = try container.decode(String.self, forKey: .targetRef)
129 headSHA = try container.decode(String.self, forKey: .headSHA)
130 body = try container.decodeIfPresent(String.self, forKey: .body)
131 bodyFormat = try container.decodeIfPresent(String.self, forKey: .bodyFormat)
132 milestone = try container.decodeIfPresent(String.self, forKey: .milestone)
133 createdAt = try container.decode(Date.self, forKey: .createdAt)
134 mergedAt = try container.decodeIfPresent(Date.self, forKey: .mergedAt)
135 mergedBy = try container.decodeIfPresent(String.self, forKey: .mergedBy)
136 closedAt = try container.decodeIfPresent(Date.self, forKey: .closedAt)
137 closedBy = try container.decodeIfPresent(String.self, forKey: .closedBy)
138 checks = try container.decodeIfPresent([Check].self, forKey: .checks)
139 checksCombined = try container.decodeIfPresent(String.self, forKey: .checksCombined)
140 unresolvedThreads = try container.decodeIfPresent(Int.self, forKey: .unresolvedThreads)
141 commits = try container.decodeIfPresent([MRCommit].self, forKey: .commits)
142 comments = try container.decodeIfPresent([MRComment].self, forKey: .comments)
143 reviews = try container.decodeIfPresent([Review].self, forKey: .reviews)
144 draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false
145 reviewRequests = try container.decodeIfPresent([String].self, forKey: .reviewRequests) ?? []
146 stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn)
147 stacked = try container.decodeIfPresent([StackRef].self, forKey: .stacked) ?? []
148 }
149
150 var isOpen: Bool { state == "open" }
151
152 nonisolated struct Check: Decodable, Sendable, Hashable {
153 let context: String
154 let state: String
155 let url: String?
156 /// When the check last changed state.
157 let updatedAt: Date?
158 /// How long the build behind the check ran, as the server
159 /// formats it ("1m12s"). Only CI checks have a build to time.
160 let duration: String?
161
162 enum CodingKeys: String, CodingKey {
163 case context, state, url, duration
164 case updatedAt = "updated_at"
165 }
166 }
167
168 nonisolated struct MRCommit: Decodable, Sendable, Hashable, Identifiable {
169 let sha: String
170 let subject: String
171 var id: String { sha }
172 var shortSHA: String { String(sha.prefix(10)) }
173 }
174
175 nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable {
176 let author: String
177 let body: String
178 let bodyFormat: String?
179 let createdAt: Date
180
181 enum CodingKeys: String, CodingKey {
182 case author, body
183 case bodyFormat = "body_format"
184 case createdAt = "created_at"
185 }
186
187 var id: String { author + createdAt.timeIntervalSince1970.description + body }
188 }
189
190 nonisolated struct Review: Decodable, Sendable, Hashable, Identifiable {
191 let reviewer: String
192 let verdict: String
193 /// The review predates the current head — it approved older code.
194 let stale: Bool
195 let createdAt: Date?
196
197 enum CodingKeys: String, CodingKey {
198 case reviewer, verdict, stale
199 case createdAt = "created_at"
200 }
201
202 var id: String { reviewer }
203 }
204}
205
206/// One review thread of `mr threads`.
207nonisolated struct ReviewThread: Decodable, Sendable, Hashable, Identifiable {
208 let id: Int64
209 let path: String
210 let side: String // new | old
211 let line: Int64
212 /// Anchored to an older head; the diff has moved under it.
213 let stale: Bool
214 let resolvedBy: String?
215 let comments: [Comment]
216
217 enum CodingKeys: String, CodingKey {
218 case id, path, side, line, stale, comments
219 case resolvedBy = "resolved_by"
220 }
221
222 var isResolved: Bool { resolvedBy != nil }
223
224 nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable {
225 let id: Int64
226 let author: String
227 let body: String
228 let createdAt: Date
229
230 enum CodingKeys: String, CodingKey {
231 case id, author, body
232 case createdAt = "created_at"
233 }
234 }
235}