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